/**
 * Code for data mapping, cleanup and validation
 */ import { uniq , trim , groupBy , values } from 'lodash' ; import { decode } from 'he' ; import { VenueLesson } from '../types/venues' ; import { DayText , LessonType , ModuleCode , SemesterData , SemesterDataCondensed , } from '../types/modules' ; import { ModuleAliases } from '../types/mapper' ; export const ZWSP = '\u200b' ; /**
 * Converts a word to a word-regex pair. The regex matches whole words.
 * This creates the regex beforehand so they are not created repeatedly in the
 * loop in titleize.
 */ const getWordRegex = ( word ) : [ string , RegExp ] => [ word , new RegExp ( `\\b${ word }\\b` , 'gi' ) ] ; /**
 * NUS specific title case function that accounts for school names, etc.
 */ const minorWords = [ 'and' , 'or' , 'the' , 'a' , 'an' , 'to' , 'of' , 'in' ] . map ( getWordRegex ) ; const abbreviations = [ 'NUS' , 'MIT' , 'IP' , 'NA' ] . map ( getWordRegex ) ; export function titleize ( string ) { let capitalized = string . toLowerCase ( ) . replace ( /(?:^|\s\(?|-|\/)\S/g , ( char ) => char . toUpperCase ( ) ) ; minorWords . forEach ( ( [ word , regex ] ) => { capitalized = capitalized . replace ( regex , ( match , index ) => ( index === 0 ? match : word ) ) ; } ) ; abbreviations . forEach ( ( [ abbr , regex ] ) => { capitalized = capitalized . replace ( regex , abbr ) ; } ) ; return capitalized ; } export function decodeHTMLEntities ( string ) { return decode ( string ) ; } /**
 * Remove keys with empty values, null or strings like 'nil', 'none'
 * Mutates the input object
 */ const trimChars = ' \t\n.,!?/-_' ; const nullStrings = new Set ( [ '' , 'nil' , 'na' , 'n/a' , 'null' , 'none' ] ) ; export function removeEmptyValues < T > ( object , keys ) { keys . forEach ( ( key ) => { const value = object [ key ] ; if ( ! value ) delete object [ key ] ; if ( typeof value === 'string' && nullStrings . has ( trim ( value , trimChars ) . toLowerCase ( ) ) ) { delete object [ key ] ; } } ) ; return object ; } /**
 * Trim given values if they are strings
 * Mutates the input object
 */ export function trimValues < T extends Record < string , any > > ( object , keys ) { for ( const key of keys ) { const value = object [ key ] ; if ( typeof value === 'string' ) { object [ key ] = value . trim ( ) ; } } return object ; } /**
 * Find modules which have lessons that fall on the exact same times
 */ export function getDuplicateModules ( classes ) { const lessonsByTime = values ( groupBy ( classes , ( lesson ) => [ lesson . startTime , lesson . endTime , lesson . weeks , lesson . day ] ) ) ; for ( const lessons of lessonsByTime ) { if ( lessons . length > 1 ) { const moduleCodes = uniq ( lessons . map ( ( lesson ) => lesson . moduleCode ) ) ; if ( uniq ( moduleCodes ) . length > 1 ) return moduleCodes ; } } return [ ] ; } /**
 * Remove duplicated lessons given by the modules array and merge their module
 * codes
 */ export function mergeModules ( classes , modules ) { const mergedModuleCode = modules . join ( `/${ ZWSP }` ) ; return classes . filter ( ( lesson ) => modules . indexOf ( lesson . moduleCode ) < 1 ) . map ( ( lesson ) => { if ( lesson . moduleCode !== modules [ 0 ] ) return lesson ; return { ... lesson , moduleCode : mergedModuleCode , } ; } ) ; } export function mergeDualCodedModules ( classes ) { let mergedModules = classes ; let duplicateModules = getDuplicateModules ( mergedModules ) ; const aliases = { } ; while ( duplicateModules . length ) { for ( const moduleCode of duplicateModules ) { if ( ! aliases [ moduleCode ] ) aliases [ moduleCode ] = new Set ( ) ; duplicateModules . filter ( ( moduleToAdd ) => moduleToAdd !== moduleCode ) . forEach ( ( moduleToAdd ) => aliases [ moduleCode ] . add ( moduleToAdd ) ) ; } mergedModules = mergeModules ( mergedModules , duplicateModules ) ; duplicateModules = getDuplicateModules ( mergedModules ) ; } return { lessons : mergedModules , aliases } ; } export const dayTextMap = { '1' : 'Monday' , '2' : 'Tuesday' , '3' : 'Wednesday' , '4' : 'Thursday' , '5' : 'Friday' , '6' : 'Saturday' , } ; export const unrecognizedLessonTypes = { '4' : 'Tutorial Type 4' , '5' : 'Tutorial Type 5' , '6' : 'Tutorial Type 6' , '7' : 'Tutorial Type 7' , '8' : 'Tutorial Type 8' , '9' : 'Tutorial Type 9' , A : 'Supervision of Academic Exercise' , O : 'Others' , V : 'Lecture On Demand' , I : 'Independent Study Module' , C : 'Bedside Tutorial' , M : 'Ensemble Teaching' , J : 'Mini-Project' , } ; export const activityLessonType = { B : 'Laboratory' , L : 'Lecture' , D : 'Design Lecture' , R : 'Recitation' , P : 'Packaged Lecture' , X : 'Packaged Tutorial' , W : 'Workshop' , E : 'Seminar-Style Module Class' , S : 'Sectional Teaching' , T : 'Tutorial' , '2' : 'Tutorial Type 2' , '3' : 'Tutorial Type 3' , ... unrecognizedLessonTypes , } ; /**
 * In COVID-19 times, there were many classes that had the same venues (E-Learn_A,
 * E-Learn_B, E-Learn_C). This confused our algorithm to merge dual-coded modules
 * and created lots of false positives for module aliases. This avoid-list specifies
 * the names of the modules that should not be aliased with each other.
 *
 * See https://github.com/nusmodifications/nusmods/pull/3322
 */ export const modulesToAvoidMerging = new Set < string > ( [ 'Discrete Structures' , 'Programming Methodology' , 'Programming Methodology II' , 'Data Structures and Algorithms' , 'Graduate Research Seminar' , ] ) ; export function isModuleOffered ( module ) { return module . semesterData && module . semesterData . length > 0 ; }