import * as R from 'ramda' ; import romanify from 'romanify' ; import { ModuleCode } from '../../types/modules' ; import { AND_OR_REGEX , MODULE_REGEX , OPERATORS , OPERATORS_REGEX } from './constants' ; /**
 * Normalizes different formats with same semantic meanings.
 * E.g. x & y -> x and y
 *
 * The following code depends heavily on regex and the replace function.
 * It is recommended that you read the following before proceeding:
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace
 */ const moduleWithDelimiterRegex = /[A-Z]{2,3}\W[0-9]{4}(?:[A-Z]|[A-Z]R)?/g ; function removeSpaceFromModule ( string ) { return string . replace ( moduleWithDelimiterRegex , R . replace ( /\W/ , '' ) ) ; } const leftConjoinedRegex = /\b([^\s()]{6,9})(and|or)\b/gi ; const rightConjoinedRegex = /\b(and|or)([^\s()]{6,9})\b/gi ; function fixOperatorTypos ( string ) { return string . replace ( leftConjoinedRegex , ( match , p1 , p2 ) => ( MODULE_REGEX . test ( p1 ) ? `${ p1 } ${ p2 }` : match ) ) . replace ( rightConjoinedRegex , ( match , p1 , p2 ) => MODULE_REGEX . test ( p2 ) ? `${ p1 } ${ p2 }` : match ) ; } const modulePostFixRegex = /([A-Z]{2,3}[0-9]{4})(\/[A-Z]|[A-Z]R)+\b/g ; function insertPostFixAsStandalone ( string ) { return string . replace ( modulePostFixRegex , ( match , module , ... args ) => { const p = args . slice ( 0 , - 2 ) ; const modules = [ module , ... p . map ( ( postfix ) => `${ module }${ postfix . slice ( 1 ) }` ) , ] ; return modules . join ( OPERATORS . or ) ; } ) ; } function convertCommas ( oxfordString ) { const string = oxfordString . replace ( /,\s*and\b/gi , OPERATORS . and ) ; if ( ! string . includes ( ',' ) ) return string ; const hasAndOperators = string . includes ( OPERATORS . and ) ; const hasOrOperators = string . includes ( OPERATORS . or ) ; return hasAndOperators && ! hasOrOperators ? string . replace ( /,/g , OPERATORS . and ) : string . replace ( /,/g , OPERATORS . or ) ; } function convertToNumerals ( number , string ) { const alphabet = 'abcdefghijklmnopqrstuvwxyz' [ ( number - 1 ) % 26 ] ; const roman = romanify ( number ) . toLowerCase ( ) ; const romanNumeralRegex = new RegExp ( `\\(?(${ roman }|${ alphabet })\\)` ) ; if ( romanNumeralRegex . test ( string ) ) { const replaced = string . replace ( romanNumeralRegex , `(${ number })` ) ; return convertToNumerals ( number + 1 , replaced ) ; } return string ; } function fixBrackets ( string ) { return R . match ( /\(/g , string ) . length === R . match ( /\)/g , string ) . length ? string : string . replace ( /(?:\(?\b(\d+)\))/g , ( match , digit ) => `(${ digit })` ) ; } const moduleTitlesRegex = /([^\s()]+)\b[\s]+(?:and|or)[\s]+([^\s()]+)\b/g ; function removeModuleTitles ( string ) { const result = string . replace ( moduleTitlesRegex , ( match , p1 , p2 ) => { if ( AND_OR_REGEX . test ( p1 ) || AND_OR_REGEX . test ( p2 ) || MODULE_REGEX . test ( p1 ) || MODULE_REGEX . test ( p2 ) ) { return match ; } return '' ; } ) ; if ( result !== string ) { return removeModuleTitles ( result ) ; } return result ; } const gceRegex = /((GCE\s+)?[‘'"]?[AO][’'"]?(\s+|-)(level)\s*(\w+\s*)?)((and|or)\s+)?/gi ; const jcSubjectsRegex = /(H[1-3]\s+([A-Z]\w+\s*)+)((and|or)\s+)?/g ; function removeGCEPrerequisites ( string ) { const trailingAndOrRegex = /(( and| or)\s*)(?:\)?)$/g ; return string . replace ( gceRegex , '' ) . replace ( jcSubjectsRegex , '' ) . replace ( trailingAndOrRegex , ( match , p ) => match . replace ( p , '' ) ) ; } const replaceBrackets = ( str ) => str . replace ( /[{[<]/g , '(' ) . replace ( /[}\]>]/g , ')' ) ; const replaceOperators = ( str ) => str . replace ( /[|/]/g , OPERATORS . or ) . replace ( /[;&]/g , OPERATORS . and ) . replace ( / plus /g , OPERATORS . and ) . replace ( OPERATORS_REGEX , R . toLower ) ; const normalizeWhitespace = ( str ) => str . replace ( /\s+/g , ' ' ) ; export const normalize = R . pipe ( removeSpaceFromModule , fixOperatorTypos , insertPostFixAsStandalone , convertCommas , R . curry ( convertToNumerals ) ( 1 ) , replaceBrackets , fixBrackets , replaceOperators , R . compose ( removeModuleTitles , removeGCEPrerequisites ) , normalizeWhitespace ) ; export default function normalizeString ( string , moduleCode ) { const moduleWithoutPostfix = moduleCode . slice ( 0 , R . findLastIndex ( R . test ( /\d/ ) , ( moduleCode as unknown ) as string [ ] ) + 1 ) ; const moduleRegex = new RegExp ( `\\b${ moduleWithoutPostfix }(?:[A-Z]|[A-Z]R)?\\b` , 'g' ) ; const preprocessed = string . replace ( moduleRegex , '' ) ; return normalize ( preprocessed ) ; }