import { mapValues , values , pick , mergeWith , sortBy } from 'lodash' ; import { diff } from 'deep-diff' ; import { Task } from '../types/tasks' ; import { ModuleAliases , ModuleWithoutTree , SemesterModule , SemesterModuleData , } from '../types/mapper' ; import { Module , ModuleCode , ModuleCondensed , ModuleInformation } from '../types/modules' ; import type { MPEModule } from '../types/mpe' ; import BaseTask from './BaseTask' ; import config from '../config' ; import { Logger } from '../services/logger' ; import generatePrereqTree from '../services/requisite-tree' ; import { union } from '../utils/set' ; import { isModuleOffered } from '../utils/data' ; import { isModuleInMPE } from '../utils/mpe' ; interface Input { semesterData : SemesterModuleData [ ] [ ] ; aliases : ModuleAliases [ ] ; } type Output = Module [ ] ; /**
 * Merge aliases and convert set to array
 */ export function mergeAliases ( aliases ) { const merged = ( mergeWith as Function ) ( ... aliases , ( src , dest ) => { if ( src && dest ) return union ( src , dest ) ; } ) ; return mapValues ( merged , ( set ) => Array . from ( set ) ) ; } /**
 * Check that the module info match between the previous semester's module info
 * and the next semester's info. Discrepancy would mean that the module's data
 * has been updated between semesters, which would be a source of error because
 * we assume that the data does not differ, and when it does showing the latest
 * semester's data is correct.
 *
 * This function returns null if the two agree, otherwise it returns an object
 * containing the keys that don't.
 */ export function moduleDataCheck ( module , semesterModule ) { const { semesterData , aliases , ... existing } = module ; const difference = diff ( existing , semesterModule ) ; if ( ! difference ) return null ; const keys = difference . map ( ( edit ) : string => edit . path && edit . path [ 0 ] ) ; return { left : pick ( module , keys ) , right : pick ( semesterModule , keys ) , } ; } /**
 * Combine modules from multiple semesters into one
 */ export function combineModules ( semesters , aliases , logger ) { const modules = { } ; for ( const semesterModules of semesters ) { for ( const semesterModule of semesterModules ) { const { moduleCode , semesterData } = semesterModule ; const module = { ... semesterModule . module , aliases : aliases [ moduleCode ] , semesterData : semesterData ? [ semesterData ] : [ ] , } ; const existingData = modules [ moduleCode ] ; if ( existingData ) { const difference = moduleDataCheck ( existingData , semesterModule . module ) ; if ( difference ) { logger . warn ( difference , 'Module with different module info between semesters' ) ; } module . semesterData . unshift ( ... existingData . semesterData ) ; } modules [ moduleCode ] = module ; } } return values ( modules ) ; } const getModuleCondensed = ( { moduleCode , title , semesterData , } ) : ModuleCondensed => ( { moduleCode , title , semesters : semesterData . map ( ( semester ) => semester . semester ) , } ) ; const getModuleMPEParticipation = ( { title , moduleCode , moduleCredit , attributes , } ) : MPEModule => ( { title , moduleCode , moduleCredit , inS1MPE : attributes ?. mpes1 , inS2MPE : attributes ?. mpes2 , } ) ; const getModuleInfo = ( { moduleCode , title , description , moduleCredit , department , faculty , workload , prerequisite , preclusion , corequisite , semesterData , attributes , } ) : ModuleInformation => ( { moduleCode , title , description , moduleCredit , department , faculty , workload , prerequisite , preclusion , corequisite , attributes , semesterData : semesterData . map ( ( { semester , examDate , examDuration , covidZones } ) => ( { semester , examDate , examDuration , covidZones , } ) ) , } ) ; /**
 * Collect semester data from multiple semesters together
 *
 * Output:
 *  - modules/<ModuleCode>.json
 *  - moduleInformation.json // DEPRECATED. TODO: Remove after AY19/20 starts
 *  - moduleInfo.json
 *  - moduleList.json
 */ export default class CollateModules extends BaseTask implements Task < Input , Output > { academicYear : string ; get name ( ) { return `Collating modules for ${ this . academicYear }` ; } constructor ( academicYear = config . academicYear ) { super ( academicYear ) ; this . academicYear = academicYear ; this . logger = this . rootLogger . child ( { task : CollateModules . name , year : academicYear , } ) ; } async run ( input ) { this . logger . info ( `Collating modules for ${ this . academicYear }` ) ; const { semesterData , aliases } = input ; const combinedAliases = mergeAliases ( aliases ) ; const modulesWithoutTree = combineModules ( semesterData , combinedAliases , this . logger ) ; const unsortedModules = await generatePrereqTree ( modulesWithoutTree ) ; const modules = sortBy ( unsortedModules , ( module ) => module . moduleCode ) ; this . logger . info ( `Collated ${ modules . length } modules` ) ; await Promise . all ( modules . map ( ( module ) => this . io . module ( module . moduleCode , module ) ) ) ; const moduleCondensed = modules . filter ( isModuleOffered ) . map ( getModuleCondensed ) ; const mpeModules = modules . filter ( isModuleOffered ) . filter ( isModuleInMPE ) . map ( getModuleMPEParticipation ) ; const moduleInfo = modules . map ( getModuleInfo ) ; await Promise . all ( [ this . io . moduleList ( moduleCondensed ) , this . io . mpeModules ( mpeModules ) , this . io . moduleInfo ( moduleInfo ) , this . io . moduleAliases ( combinedAliases ) , ] ) ; const moduleInformation = moduleInfo . filter ( isModuleOffered ) ; await this . io . moduleInformation ( moduleInformation ) ; return values ( modules ) ; } }