@@ -7,7 +7,10 @@ import {
77 generateCombinationsOptimized ,
88 MAX_RESULTS ,
99} from "../generateCombinations" ;
10- import { meetingTimesToBinaryMask , hasConflictInSchedule } from "../binaryMeetingTime" ;
10+ import {
11+ meetingTimesToBinaryMask ,
12+ hasConflictInSchedule ,
13+ } from "../binaryMeetingTime" ;
1114import { createMockSection } from "./mocks" ;
1215
1316// ---------------------------------------------------------------------------
@@ -38,19 +41,35 @@ function toIdSets(schedules: SectionWithCourse[][]): Set<number>[] {
3841// Fixtures — non-conflicting time slots
3942// ---------------------------------------------------------------------------
4043
41- const S_MON_8 = createMockSection ( 1 , [ { days : [ 1 ] , startTime : 800 , endTime : 900 } ] ) ;
42- const S_MON_10 = createMockSection ( 2 , [ { days : [ 1 ] , startTime : 1000 , endTime : 1100 } ] ) ;
43- const S_MON_12 = createMockSection ( 3 , [ { days : [ 1 ] , startTime : 1200 , endTime : 1300 } ] ) ;
44- const S_TUE_8 = createMockSection ( 4 , [ { days : [ 2 ] , startTime : 800 , endTime : 900 } ] ) ;
45- const S_TUE_10 = createMockSection ( 5 , [ { days : [ 2 ] , startTime : 1000 , endTime : 1100 } ] ) ;
46- const S_WED_8 = createMockSection ( 6 , [ { days : [ 3 ] , startTime : 800 , endTime : 900 } ] ) ;
47- const S_WED_10 = createMockSection ( 7 , [ { days : [ 3 ] , startTime : 1000 , endTime : 1100 } ] ) ;
44+ const S_MON_8 = createMockSection ( 1 , [
45+ { days : [ 1 ] , startTime : 800 , endTime : 900 } ,
46+ ] ) ;
47+ const S_MON_10 = createMockSection ( 2 , [
48+ { days : [ 1 ] , startTime : 1000 , endTime : 1100 } ,
49+ ] ) ;
50+ const S_MON_12 = createMockSection ( 3 , [
51+ { days : [ 1 ] , startTime : 1200 , endTime : 1300 } ,
52+ ] ) ;
53+ const S_TUE_8 = createMockSection ( 4 , [
54+ { days : [ 2 ] , startTime : 800 , endTime : 900 } ,
55+ ] ) ;
56+ const S_TUE_10 = createMockSection ( 5 , [
57+ { days : [ 2 ] , startTime : 1000 , endTime : 1100 } ,
58+ ] ) ;
59+ const S_WED_8 = createMockSection ( 6 , [
60+ { days : [ 3 ] , startTime : 800 , endTime : 900 } ,
61+ ] ) ;
62+ const S_WED_10 = createMockSection ( 7 , [
63+ { days : [ 3 ] , startTime : 1000 , endTime : 1100 } ,
64+ ] ) ;
4865// Conflicts with S_MON_8 (same day/time)
49- const S_MON_8_B = createMockSection ( 8 , [ { days : [ 1 ] , startTime : 800 , endTime : 900 } ] ) ;
66+ const S_MON_8_B = createMockSection ( 8 , [
67+ { days : [ 1 ] , startTime : 800 , endTime : 900 } ,
68+ ] ) ;
5069// Multi-meeting: MWF lecture + TR lab
5170const S_MWF_LECTURE_TR_LAB = createMockSection ( 9 , [
52- { days : [ 1 , 3 , 5 ] , startTime : 900 , endTime : 950 } , // MWF 9-9:50
53- { days : [ 2 , 4 ] , startTime : 1100 , endTime : 1150 } , // TR 11-11:50
71+ { days : [ 1 , 3 , 5 ] , startTime : 900 , endTime : 950 } , // MWF 9-9:50
72+ { days : [ 2 , 4 ] , startTime : 1100 , endTime : 1150 } , // TR 11-11:50
5473] ) ;
5574// Conflicts with lecture block
5675const S_MON_OVERLAP_LECTURE = createMockSection ( 10 , [
@@ -90,7 +109,11 @@ describe("addOptionalCourses — basic correctness", () => {
90109 assert . equal ( results . length , 2 ) ;
91110 const idSets = toIdSets ( results ) ;
92111 assert . ok ( idSets . some ( ( s ) => s . size === 1 && s . has ( S_MON_8 . id ) ) ) ;
93- assert . ok ( idSets . some ( ( s ) => s . size === 2 && s . has ( S_MON_8 . id ) && s . has ( S_TUE_10 . id ) ) ) ;
112+ assert . ok (
113+ idSets . some (
114+ ( s ) => s . size === 2 && s . has ( S_MON_8 . id ) && s . has ( S_TUE_10 . id ) ,
115+ ) ,
116+ ) ;
94117 } ) ;
95118
96119 test ( "one optional course, always conflicts → returns only base schedule" , ( ) => {
@@ -148,7 +171,10 @@ describe("addOptionalCourses — basic correctness", () => {
148171
149172 test ( "no output schedules contain time conflicts" , ( ) => {
150173 const base = [ S_MON_8 , S_TUE_8 ] ;
151- const optionals = [ [ S_MON_10 , S_MON_8_B ] , [ S_WED_10 , S_WED_8 ] ] ;
174+ const optionals = [
175+ [ S_MON_10 , S_MON_8_B ] ,
176+ [ S_WED_10 , S_WED_8 ] ,
177+ ] ;
152178 const results = addOptionalCourses (
153179 base ,
154180 combinedMask ( base ) ,
@@ -211,7 +237,7 @@ describe("addOptionalCourses — multiple meeting times per section", () => {
211237 // S_MWF_LECTURE_TR_LAB occupies MWF@9 and TR@11. The optional below occupies MWF@9 too.
212238 const conflictingMulti = createMockSection ( 20 , [
213239 { days : [ 1 , 3 , 5 ] , startTime : 900 , endTime : 950 } , // conflicts with lecture
214- { days : [ 6 ] , startTime : 800 , endTime : 900 } , // Saturday — no conflict
240+ { days : [ 6 ] , startTime : 800 , endTime : 900 } , // Saturday — no conflict
215241 ] ) ;
216242 const optionals = [ [ conflictingMulti ] ] ;
217243 const results = addOptionalCourses (
@@ -243,7 +269,10 @@ describe("addOptionalCourses — numCourses filtering", () => {
243269 1 , // numCourses = 1, base already has 1
244270 ) ;
245271 assert . equal ( results . length , 1 ) ;
246- assert . deepEqual ( results [ 0 ] . map ( ( s ) => s . id ) , [ S_MON_8 . id ] ) ;
272+ assert . deepEqual (
273+ results [ 0 ] . map ( ( s ) => s . id ) ,
274+ [ S_MON_8 . id ] ,
275+ ) ;
247276 } ) ;
248277
249278 test ( "numCourses = base + 1 → only schedules with exactly one optional added" , ( ) => {
@@ -325,7 +354,14 @@ describe("addOptionalCourses — numCourses filtering", () => {
325354describe ( "addOptionalCourses — maxResults cap" , ( ) => {
326355 test ( "respects maxResults = 1" , ( ) => {
327356 const optionals = [ [ S_MON_8 ] , [ S_TUE_8 ] , [ S_WED_8 ] ] ;
328- const results = addOptionalCourses ( [ ] , BigInt ( 0 ) , optionals , buildMasks ( optionals ) , undefined , 1 ) ;
357+ const results = addOptionalCourses (
358+ [ ] ,
359+ BigInt ( 0 ) ,
360+ optionals ,
361+ buildMasks ( optionals ) ,
362+ undefined ,
363+ 1 ,
364+ ) ;
329365 assert . equal ( results . length , 1 ) ;
330366 } ) ;
331367
@@ -337,14 +373,33 @@ describe("addOptionalCourses — maxResults cap", () => {
337373 [ S_WED_8 ] ,
338374 [ createMockSection ( 50 , [ { days : [ 4 ] , startTime : 800 , endTime : 900 } ] ) ] ,
339375 ] ;
340- const results = addOptionalCourses ( [ ] , BigInt ( 0 ) , optionals , buildMasks ( optionals ) , undefined , 5 ) ;
376+ const results = addOptionalCourses (
377+ [ ] ,
378+ BigInt ( 0 ) ,
379+ optionals ,
380+ buildMasks ( optionals ) ,
381+ undefined ,
382+ 5 ,
383+ ) ;
341384 assert . equal ( results . length , 5 ) ;
342385 } ) ;
343386
344387 test ( "maxResults larger than total results → returns all" , ( ) => {
345388 const optionals = [ [ S_MON_10 ] , [ S_WED_10 ] ] ;
346- const uncapped = addOptionalCourses ( [ ] , BigInt ( 0 ) , optionals , buildMasks ( optionals ) ) ;
347- const capped = addOptionalCourses ( [ ] , BigInt ( 0 ) , optionals , buildMasks ( optionals ) , undefined , 100 ) ;
389+ const uncapped = addOptionalCourses (
390+ [ ] ,
391+ BigInt ( 0 ) ,
392+ optionals ,
393+ buildMasks ( optionals ) ,
394+ ) ;
395+ const capped = addOptionalCourses (
396+ [ ] ,
397+ BigInt ( 0 ) ,
398+ optionals ,
399+ buildMasks ( optionals ) ,
400+ undefined ,
401+ 100 ,
402+ ) ;
348403 assert . equal ( capped . length , uncapped . length ) ;
349404 } ) ;
350405} ) ;
@@ -372,7 +427,12 @@ describe("addOptionalCourses — push/pop isolation", () => {
372427 test ( "base schedule array is not mutated by the call" , ( ) => {
373428 const base = [ S_MON_8 ] ;
374429 const optionals = [ [ S_TUE_10 ] ] ;
375- addOptionalCourses ( base , combinedMask ( base ) , optionals , buildMasks ( optionals ) ) ;
430+ addOptionalCourses (
431+ base ,
432+ combinedMask ( base ) ,
433+ optionals ,
434+ buildMasks ( optionals ) ,
435+ ) ;
376436 assert . equal ( base . length , 1 ) ;
377437 } ) ;
378438} ) ;
@@ -385,12 +445,15 @@ describe("addOptionalCourses — push/pop isolation", () => {
385445describe ( "locked + optional courses integration" , ( ) => {
386446 test ( "locked courses generate valid base schedules, each extended with optionals" , ( ) => {
387447 // Two locked courses, each with 2 sections
388- const lockedCourseA = [ S_MON_8 , S_MON_10 ] ; // A sections
389- const lockedCourseB = [ S_TUE_8 , S_WED_8 ] ; // B sections (all non-conflicting with A)
390- const lockedSchedules = generateCombinationsOptimized ( [ lockedCourseA , lockedCourseB ] ) ;
448+ const lockedCourseA = [ S_MON_8 , S_MON_10 ] ; // A sections
449+ const lockedCourseB = [ S_TUE_8 , S_WED_8 ] ; // B sections (all non-conflicting with A)
450+ const lockedSchedules = generateCombinationsOptimized ( [
451+ lockedCourseA ,
452+ lockedCourseB ,
453+ ] ) ;
391454
392455 const optionals = [ [ S_WED_10 , S_MON_12 ] ] ;
393- const optMasks = buildMasks ( optionals ) ;
456+ const optMasks = buildMasks ( optionals ) ;
394457
395458 const all : SectionWithCourse [ ] [ ] = [ ] ;
396459 for ( const { schedule, mask } of lockedSchedules ) {
@@ -402,10 +465,13 @@ describe("locked + optional courses integration", () => {
402465 assert . ok ( all . length > 0 ) ;
403466 assert . ok ( all . every ( ( s ) => ! hasConflictInSchedule ( s ) ) ) ;
404467 // Every result must contain both locked sections
405- assert . ok ( all . every ( ( s ) =>
406- lockedCourseA . some ( ( a ) => s . includes ( a ) ) &&
407- lockedCourseB . some ( ( b ) => s . includes ( b ) ) ,
408- ) ) ;
468+ assert . ok (
469+ all . every (
470+ ( s ) =>
471+ lockedCourseA . some ( ( a ) => s . includes ( a ) ) &&
472+ lockedCourseB . some ( ( b ) => s . includes ( b ) ) ,
473+ ) ,
474+ ) ;
409475 } ) ;
410476
411477 test ( "MAX_RESULTS is respected across the combined locked+optional loop" , ( ) => {
@@ -415,13 +481,22 @@ describe("locked + optional courses integration", () => {
415481 ] ;
416482 const lockedSchedules = generateCombinationsOptimized ( lockedCourses ) ;
417483 const optionals = [ [ S_WED_8 , S_WED_10 ] ] ;
418- const optMasks = buildMasks ( optionals ) ;
484+ const optMasks = buildMasks ( optionals ) ;
419485
420486 const all : SectionWithCourse [ ] [ ] = [ ] ;
421487 for ( const { schedule, mask } of lockedSchedules ) {
422488 if ( all . length >= MAX_RESULTS ) break ;
423489 const remaining = MAX_RESULTS - all . length ;
424- all . push ( ...addOptionalCourses ( schedule , mask , optionals , optMasks , undefined , remaining ) ) ;
490+ all . push (
491+ ...addOptionalCourses (
492+ schedule ,
493+ mask ,
494+ optionals ,
495+ optMasks ,
496+ undefined ,
497+ remaining ,
498+ ) ,
499+ ) ;
425500 }
426501
427502 assert . ok ( all . length <= MAX_RESULTS ) ;
@@ -432,12 +507,14 @@ describe("locked + optional courses integration", () => {
432507 const lockedCourses = [ [ S_MON_8 ] ] ;
433508 const lockedSchedules = generateCombinationsOptimized ( lockedCourses ) ;
434509 const optionals = [ [ S_TUE_8 ] , [ S_WED_8 ] ] ;
435- const optMasks = buildMasks ( optionals ) ;
510+ const optMasks = buildMasks ( optionals ) ;
436511
437512 // numCourses = 2 → only schedules with exactly 1 optional added
438513 const results : SectionWithCourse [ ] [ ] = [ ] ;
439514 for ( const { schedule, mask } of lockedSchedules ) {
440- results . push ( ...addOptionalCourses ( schedule , mask , optionals , optMasks , 2 ) ) ;
515+ results . push (
516+ ...addOptionalCourses ( schedule , mask , optionals , optMasks , 2 ) ,
517+ ) ;
441518 }
442519
443520 assert . ok ( results . every ( ( s ) => s . length === 2 ) ) ;
@@ -452,7 +529,12 @@ describe("addOptionalCourses — optional course with no sections", () => {
452529 test ( "empty optional course is silently skipped (no crash)" , ( ) => {
453530 const base = [ S_MON_8 ] ;
454531 const optionals : SectionWithCourse [ ] [ ] = [ [ ] ] ; // one optional course, zero sections
455- const results = addOptionalCourses ( base , combinedMask ( base ) , optionals , buildMasks ( optionals ) ) ;
532+ const results = addOptionalCourses (
533+ base ,
534+ combinedMask ( base ) ,
535+ optionals ,
536+ buildMasks ( optionals ) ,
537+ ) ;
456538 // The only choice for the empty course is skip → returns [base]
457539 assert . equal ( results . length , 1 ) ;
458540 assert . deepEqual ( results [ 0 ] , base ) ;
@@ -461,7 +543,12 @@ describe("addOptionalCourses — optional course with no sections", () => {
461543 test ( "empty optional among non-empty optionals" , ( ) => {
462544 const base = [ S_MON_8 ] ;
463545 const optionals : SectionWithCourse [ ] [ ] = [ [ ] , [ S_TUE_10 ] ] ;
464- const results = addOptionalCourses ( base , combinedMask ( base ) , optionals , buildMasks ( optionals ) ) ;
546+ const results = addOptionalCourses (
547+ base ,
548+ combinedMask ( base ) ,
549+ optionals ,
550+ buildMasks ( optionals ) ,
551+ ) ;
465552 // Empty course: skip only. TUE_10 course: skip or include → 2 results
466553 assert . equal ( results . length , 2 ) ;
467554 assert . ok ( results . every ( ( s ) => ! hasConflictInSchedule ( s ) ) ) ;
@@ -476,19 +563,33 @@ describe("addOptionalCourses — exact result IDs", () => {
476563 test ( "base + one optional: exact section IDs in each result" , ( ) => {
477564 const base = [ S_MON_8 ] ;
478565 const optionals = [ [ S_TUE_10 ] ] ;
479- const results = addOptionalCourses ( base , combinedMask ( base ) , optionals , buildMasks ( optionals ) ) ;
566+ const results = addOptionalCourses (
567+ base ,
568+ combinedMask ( base ) ,
569+ optionals ,
570+ buildMasks ( optionals ) ,
571+ ) ;
480572 const idSets = toIdSets ( results ) ;
481573 // Result 1: just base
482574 assert . ok ( idSets . some ( ( s ) => s . size === 1 && s . has ( S_MON_8 . id ) ) ) ;
483575 // Result 2: base + TUE_10
484- assert . ok ( idSets . some ( ( s ) => s . size === 2 && s . has ( S_MON_8 . id ) && s . has ( S_TUE_10 . id ) ) ) ;
576+ assert . ok (
577+ idSets . some (
578+ ( s ) => s . size === 2 && s . has ( S_MON_8 . id ) && s . has ( S_TUE_10 . id ) ,
579+ ) ,
580+ ) ;
485581 } ) ;
486582
487583 test ( "one optional with two sections: only the non-conflicting section appears" , ( ) => {
488584 const base = [ S_MON_8 ] ;
489585 // S_MON_8_B conflicts with base; S_TUE_10 does not
490586 const optionals = [ [ S_MON_8_B , S_TUE_10 ] ] ;
491- const results = addOptionalCourses ( base , combinedMask ( base ) , optionals , buildMasks ( optionals ) ) ;
587+ const results = addOptionalCourses (
588+ base ,
589+ combinedMask ( base ) ,
590+ optionals ,
591+ buildMasks ( optionals ) ,
592+ ) ;
492593 const idSets = toIdSets ( results ) ;
493594 // No result should contain S_MON_8_B
494595 assert . ok ( idSets . every ( ( s ) => ! s . has ( S_MON_8_B . id ) ) ) ;
@@ -499,11 +600,17 @@ describe("addOptionalCourses — exact result IDs", () => {
499600 test ( "two optionals: all 4 exact combinations present" , ( ) => {
500601 const base = [ S_MON_8 ] ;
501602 const optionals = [ [ S_TUE_10 ] , [ S_WED_8 ] ] ;
502- const results = addOptionalCourses ( base , combinedMask ( base ) , optionals , buildMasks ( optionals ) ) ;
603+ const results = addOptionalCourses (
604+ base ,
605+ combinedMask ( base ) ,
606+ optionals ,
607+ buildMasks ( optionals ) ,
608+ ) ;
503609 const idSets = toIdSets ( results ) ;
504610 assert . equal ( idSets . length , 4 ) ;
505611 // Exact 4 combinations
506- const has = ( ids : number [ ] ) => idSets . some ( ( s ) => s . size === ids . length && ids . every ( ( id ) => s . has ( id ) ) ) ;
612+ const has = ( ids : number [ ] ) =>
613+ idSets . some ( ( s ) => s . size === ids . length && ids . every ( ( id ) => s . has ( id ) ) ) ;
507614 assert . ok ( has ( [ S_MON_8 . id ] ) ) ;
508615 assert . ok ( has ( [ S_MON_8 . id , S_TUE_10 . id ] ) ) ;
509616 assert . ok ( has ( [ S_MON_8 . id , S_WED_8 . id ] ) ) ;
@@ -519,13 +626,13 @@ describe("generateCombinationsOptimized — capped results are a valid subset",
519626 test ( "every schedule in the capped result also appears in the uncapped result" , ( ) => {
520627 // Small enough that uncapped is tractable
521628 const sectionsByCourse = [
522- [ S_MON_8 , S_MON_10 , S_MON_12 ] ,
523- [ S_TUE_8 , S_TUE_10 ] ,
524- [ S_WED_8 , S_WED_10 ] ,
629+ [ S_MON_8 , S_MON_10 , S_MON_12 ] ,
630+ [ S_TUE_8 , S_TUE_10 ] ,
631+ [ S_WED_8 , S_WED_10 ] ,
525632 ] ;
526633
527634 const uncapped = generateCombinationsOptimized ( sectionsByCourse ) ;
528- const capped = generateCombinationsOptimized ( sectionsByCourse , 3 ) ;
635+ const capped = generateCombinationsOptimized ( sectionsByCourse , 3 ) ;
529636
530637 assert . ok ( capped . length <= 3 ) ;
531638 assert . ok ( capped . length <= uncapped . length ) ;
@@ -539,7 +646,10 @@ describe("generateCombinationsOptimized — capped results are a valid subset",
539646 for ( const id of ids ) if ( ! u . has ( id ) ) return false ;
540647 return true ;
541648 } ) ;
542- assert . ok ( found , `capped schedule [${ [ ...ids ] } ] not found in uncapped results` ) ;
649+ assert . ok (
650+ found ,
651+ `capped schedule [${ [ ...ids ] } ] not found in uncapped results` ,
652+ ) ;
543653 }
544654 } ) ;
545655
@@ -549,6 +659,8 @@ describe("generateCombinationsOptimized — capped results are a valid subset",
549659 [ S_WED_8 , S_WED_10 ] ,
550660 ] ;
551661 const results = generateCombinationsOptimized ( sectionsByCourse , 5 ) ;
552- assert . ok ( results . every ( ( { schedule } ) => ! hasConflictInSchedule ( schedule ) ) ) ;
662+ assert . ok (
663+ results . every ( ( { schedule } ) => ! hasConflictInSchedule ( schedule ) ) ,
664+ ) ;
553665 } ) ;
554666} ) ;
0 commit comments