diff --git a/src/schedule-generator/README.md b/src/schedule-generator/README.md index 76e4f7286..3f7b39717 100644 --- a/src/schedule-generator/README.md +++ b/src/schedule-generator/README.md @@ -1,6 +1,6 @@ # schedule-generator -This folder contains everything necessary to the functionality of the schedule-generation _algorithm_, for the new semesterly schedule-generation feature launching SP24. +This folder contains everything necessary to the functionality of the schedule-generation _algorithm_, for the new semesterly schedule-generation feature launching SP24. This is for use by the components in the `src/components/ScheduleGenerate` folder. Below is a detailed file-by-file breakdown: @@ -8,30 +8,63 @@ Below is a detailed file-by-file breakdown: This is essentially what is imported and used by the frontend. It takes in a request (type `GeneratorRequest`, see `generator-request.ts`) and outputs a meaningful value of type `GeneratedScheduleOutput` which can be used to see what courses fulfill what requirements and are at what times. -Currently the algorithm functions by randomly shuffling potential courses the user inputted, and then generating the first valid schedule that can be constructed through iteration through this randomly shuffled list. +Currently the algorithm functions by randomly shuffling potential courses the user inputted, and then generating the first valid schedule that can be constructed by iterating through this randomly shuffled list. A "valid schedule" is one that is under the credit limit and does not duplicate courses. Valid schedules are constructed by iterating through the shuffled list of courses and adding them to the schedule if: + +- they do not fulfill a requirement that has already been fulfilled +- they are not some duplicate of a course already in the generated schedule +- they do not push the schedule over the credit limit +- the course is offered in the upcoming semester +- there is enough time to get to (one of) the course's offered timeslots given the current schedule and including a 15-minute gap between classes + +If a course is added to the schedule, the requirement it fulfills is marked as fulfilled and the course is marked as taken, and the current number of credits is alos updated. The algorithm continues until the credit limit is reached or all courses have been iterated through. + +Please note that the frontend generates five schedules at once, for paging through. This is done by calling the algorithm five times, but with a different random seed, as JavaScript's `Random` library uses system time. However, especially for smaller problems, this means that some generated schedules might be the same — a potential avenue for improvement down the line would be to have a somewhat more sophisticated algorithm that allows for the generation of `n` unique solutions, though `n` number of iterations). + +There are a couple of specificities that one should be aware of in relation to the algorithm's functionality: + +- a `Set` is used to guarantee that no course is duplicated + - if a user wants to take a course twice (e.g. CS 4999), they should make two separate requirement groups and add the course to each group +- another `Set` is used to guarantee that no _requirement_ is fulfilled multiple times + - previously there was no cap on this, but now we guarantee that an outputted schedule has <= 1 fulfillment of each requirement + - if a user wants to fulfill the same requirement multiple times (e.g. take multiple liberal studies, potentially), then they should follow the procedure above of duplicating requirement groups + +Please note that GitHub Actions may complain about some `console.log` functions inside of `algorithm.ts`. This can be safely ignored — these `console.log`s are necessary and only called in the context of the `prettyPrintSchedule` function in a backend test. ## `course-unit.ts` -A `CourseUnit` is a class that represents a single course _in a single semester and "meta-timeslot"_. Example: +A `CourseUnit` is a class that represents a single course. -Say we have one lecture L and two discussions D1 and D2. Then there would be generated: +As shown in `testing.ts`, constructing a `CourseUnit` is somewhat involved due to all the associated frontend parameters, especially for the PDF generator. These include color, time, offered semesters, etc. ```typescript -Course(name=L, timeslots=[LectureTimeslot, D1Timeslot], ...) -Course(name=L, timeslots=[LectureTimeslot, D2Timeslot], ...) +new Course( + L, + '#FFFFF', + 3, + [ + { + daysOfTheWeek: ['Monday', 'Wednesday'], + start: '10:00 AM', + end: '11:30 AM' + } + ], + ['Fall', 'Spring'], + [coreClass, techElective] +); ``` -This is done to aid in the random-shuffling algorithm as well as overlap-checking mechanism (in `algorithm.ts`). - -A `Course` has associated with it 1-7 days of the week (hence why we need multiple `Course`s for a single class representation, as some may meet on different days — this is the easiest way to represent this). - ## `generator-request.ts` Just a narrow wrapper around the information you send to `algorithm.ts` for schedule generation. It stores the user's inputted courses to fulfill requirements, desired requirements to be fulfilled, the name of the semester, and a maximum amount of credits. ## `requirement.ts` -A super-simple class that just stores the "`name`" of a class, as well as the type of the requirement (e.g. "College") and its typeValue (e.g. "CS"). +A simple class that just stores the "`name`" of a class, as well as the `type` of the requirement (e.g. `"College"`) and its `typeValue` (e.g. `"CS"`). + +These parameters are required to: + +- track which requirements are being fulfilled +- pass to the PDF downloader enough information to generate tables with requirement fulfillment information — see `src/tools/export-plan/pdf-schedule-generator.ts` ## `testing.ts` diff --git a/src/schedule-generator/algorithm.ts b/src/schedule-generator/algorithm.ts index dc693ba4f..ec944ff37 100644 --- a/src/schedule-generator/algorithm.ts +++ b/src/schedule-generator/algorithm.ts @@ -2,6 +2,14 @@ import Course, { Timeslot } from './course-unit'; import GeneratorRequest from './generator-request'; import Requirement from './requirement'; +/** + * The output of the schedule generator. + * + * @param semester The semester for which the schedule is generated. + * @param schedule A map of courses to their respective timeslots. + * @param fulfilledRequirements A map of course codes to the requirements they fulfill. + * @param totalCredits The total number of credits in the schedule. + */ export type GeneratedScheduleOutput = { semester: string; schedule: Map; @@ -10,29 +18,47 @@ export type GeneratedScheduleOutput = { totalCredits: number; }; +/** + * A class (based off of Java OOP architecture) that generates a valid semester schedule based on a + * list of courses and their respective timeslots, requirements, and other information. + */ export default class ScheduleGenerator { + /** + * A directly-accessible method that generates a schedule for some desired semester given + * a list of courses, requirement info, and a credit limit. + * + * @param request An instance of a class containing the necessary information to generate a schedule. + * @returns The generated schedule. + */ static generateSchedule(request: GeneratorRequest): GeneratedScheduleOutput { const { classes, semester } = request; let { creditLimit } = request; const schedule: Map = new Map(); - const fulfilledRequirements: Map = new Map(); // used for checking no course duplicates + const fulfilledRequirementsByCourse: Map = new Map(); // used for checking no course duplicates const actualFulfilledRequirements: Set = new Set(); // used for checking no requirement duplicates // Randomly shuffle the list of available courses classes.sort(() => Math.random() - 0.5); + // Randomly shuffle the course timeslots for more variability + classes.forEach(course => { + course.timeslots.sort(() => Math.random() - 0.5); + }); + let totalCredits = 0; classes.forEach(course => { if (course.offeredSemesters.includes(semester)) { - let performAdditionFlag = true; + let performAdditionFlag = true; // whether we can use this course or not + // onlyCourseRequirement serves to doubly-ensure that only one requirement is being mapped + // to each course (because the courses are being dragged under single requirement groups) const onlyCourseRequirement = course.requirements[0].name ?? 'nonsense-requirement'; // New logic: must be free for *all* time slots. if ( actualFulfilledRequirements.has(onlyCourseRequirement) || - fulfilledRequirements.has(course.code) || + fulfilledRequirementsByCourse.has(course.code) || creditLimit - course.credits < 0 ) { performAdditionFlag = false; @@ -54,7 +80,7 @@ export default class ScheduleGenerator { ScheduleGenerator.addToSchedule(schedule, course, course.timeslots); creditLimit -= course.credits; totalCredits += course.credits; - fulfilledRequirements.set(course.code, course.requirements); + fulfilledRequirementsByCourse.set(course.code, course.requirements); for (const requirement of course.requirements) { actualFulfilledRequirements.add(requirement.name); } @@ -62,9 +88,20 @@ export default class ScheduleGenerator { } }); - return { semester, schedule, fulfilledRequirements, totalCredits }; + return { + semester, + schedule, + fulfilledRequirements: fulfilledRequirementsByCourse, + totalCredits + }; } + /** + * A helper static function that console.logs a pretty-printed text version of the schedule. + * Useful for debugging and testing. + * + * @param output The output of the schedule generator. + */ static prettyPrintSchedule(output: GeneratedScheduleOutput): void { console.log('************************'); console.log(`Generated Schedule for ${output.semester}:`); @@ -92,12 +129,19 @@ export default class ScheduleGenerator { console.log(`Total Credits in the Schedule: ${output.totalCredits}`); } + /** + * A helper function to check if a timeslot is occupied in the schedule. + * + * @param schedule Information about the currently built-up schedule + * @param timeslot The timeslot to check for overlap + * @returns Whether the timeslot is occupied or not + */ private static isTimeslotOccupied( schedule: Map, timeslot: Timeslot ): boolean { // Check for overlap. - const gap = 15 * 60 * 1000; // 15 minutes in milliseconds + const gap = 15 * 60 * 1000; // 15 minutes in milliseconds; need a 15 min gap for walking const timeslotCopy = { ...timeslot }; if (!timeslotCopy.start.includes(' ')) { @@ -137,6 +181,14 @@ export default class ScheduleGenerator { return false; } + /** + * A helper function that adds to our map of courses to timeslots some new + * course and its respective timeslots. + * + * @param schedule The schedule to add the course to + * @param course The course to add + * @param timeslots The timeslots to add + */ private static addToSchedule( schedule: Map, course: Course, diff --git a/src/schedule-generator/course-unit.ts b/src/schedule-generator/course-unit.ts index 34b1dc020..a8730bb92 100644 --- a/src/schedule-generator/course-unit.ts +++ b/src/schedule-generator/course-unit.ts @@ -1,5 +1,8 @@ import Requirement from './requirement'; +/** + * Represents a potential day (or days) in the week that a course might be offered. + */ export type DayOfTheWeek = | 'Monday' | 'Tuesday' @@ -9,15 +12,23 @@ export type DayOfTheWeek = | 'Saturday' | 'Sunday'; +/** + * Represents a potential time slot for a course. + */ export type Timeslot = { daysOfTheWeek: DayOfTheWeek[]; // e.g. ['Monday', 'Wednesday', 'Friday'] start: string; // e.g. '10:00 AM' end: string; // e.g. '11:30 AM' }; +/** + * Represents a course that can be taken by a student, as 'massaged' into a + * format that is more easily consumed by the frontend (and in particular, the + * PDF downloader). + */ export type CourseForFrontend = { - title: string; // title - code: string; // math 2940 + title: string; // e.g. Linear Algebra for Engineers + code: string; // e.g. MATH 2940 color: string; courseCredits: number; fulfilledReq: Requirement; @@ -26,6 +37,16 @@ export type CourseForFrontend = { timeEnd: string; }; +/** + * Represents a course that can be taken by a student. + * + * @param code The course code (e.g. MATH 2940). + * @param color The color of the course (for the PDF generator). + * @param credits The number of credits the course is worth. + * @param timeslots The timeslots that the course is offered. + * @param offeredSemesters The semesters in which the course is offered. + * @param requirements The requirements that the course fulfills. + */ export default class Course { code: string; @@ -36,15 +57,6 @@ export default class Course { /* *all* of these have to be available for the course to be scheduled. will usually just be one, but if there is e.g. a lab or a discussion then it could be two - for now, just going to have multiple copies of the course. - - say we have one lecture L and two discussions D1 and D2. - then there would be generated: - - Course(code=L, timeslots=[LectureTimeslot, D1Timeslot]) - Course(code=L, timeslots=[LectureTimeslot, D2Timeslot]) - - then the algorithm will just choose one of these to schedule */ timeslots: Timeslot[]; @@ -68,6 +80,11 @@ export default class Course { this.requirements = requirements; } + /** + * A helper function that returns a string representation of the course. + * + * @returns A string representation of the course. + */ toString(): string { return `${this.code}: ------------------- diff --git a/src/schedule-generator/generator-request.ts b/src/schedule-generator/generator-request.ts index de9797788..ed807e59b 100644 --- a/src/schedule-generator/generator-request.ts +++ b/src/schedule-generator/generator-request.ts @@ -1,6 +1,17 @@ import Course from './course-unit'; import Requirement from './requirement'; +/** + * Represents a request to the schedule generator, containing all of the information + * necessary to construct a valid schedule according to user specification in the + * /build page. + * + * @param classes The list of courses that the student is willing to have fulfill requirements. + * @param requirements The list of requirements that the student wants to fulfill. + * @param creditLimit The maximum number of credits that the student is willing to take. + * @param semester The semester for which the schedule is being generated. + * @returns A new instance of the GeneratorRequest class that contains this data collated. + */ export default class GeneratorRequest { classes: Course[]; diff --git a/src/schedule-generator/requirement.ts b/src/schedule-generator/requirement.ts index 55c39ed74..65e23952c 100644 --- a/src/schedule-generator/requirement.ts +++ b/src/schedule-generator/requirement.ts @@ -1,5 +1,13 @@ +/** + * This class represents a requirement for a college, major, minor, grad school, or the university. + * It is one of the requirements that a student indicates they want to fulfill on the build page. + * + * @param name The name of the requirement (e.g. Mathematics). + * @param forType The type of requirement (e.g. College, Major, Minor, Grad, Uni). + * @param typeValue The specific 'subtype' of the requirement, for the PDF generator, e.g. 'CoE'. + */ export default class Requirement { - name: string; // effectively name + name: string; for: 'College' | 'Major' | 'Minor' | 'Grad' | 'Uni'; diff --git a/src/schedule-generator/testing.ts b/src/schedule-generator/testing.ts index d4f130bab..108256587 100644 --- a/src/schedule-generator/testing.ts +++ b/src/schedule-generator/testing.ts @@ -3,7 +3,13 @@ import Course from './course-unit'; import GeneratorRequest from './generator-request'; import ScheduleGenerator from './algorithm'; +/** + * A helper class used for backend / ts-node-only testing of the schedule generator algorithm. + */ class Testing { + /** + * A helper function that runs the schedule generator algorithm with some test data. + */ public static main(): void { // Create some requirements const techElective = new Requirement('Technical Elective', 'College', 'CoE'); @@ -20,8 +26,8 @@ class Testing { { daysOfTheWeek: ['Monday', 'Wednesday'], start: '10:00 AM', - end: '11:30 AM', - }, + end: '11:30 AM' + } ], ['Fall', 'Spring'], [coreClass, techElective] @@ -35,13 +41,13 @@ class Testing { { daysOfTheWeek: ['Tuesday', 'Thursday'], start: '11:30 AM', - end: '1:00 PM', + end: '1:00 PM' }, { daysOfTheWeek: ['Friday'], start: '3:00 PM', - end: '5:30 PM', - }, + end: '5:30 PM' + } ], ['Fall', 'Spring'], [coreClass, techElective] @@ -55,13 +61,13 @@ class Testing { { daysOfTheWeek: ['Monday', 'Wednesday'], start: '10:00 AM', - end: '11:30 AM', + end: '11:30 AM' }, { daysOfTheWeek: ['Thursday'], start: '11:30 AM', - end: '1:00 PM', - }, + end: '1:00 PM' + } ], ['Fall'], [techElective] @@ -75,13 +81,13 @@ class Testing { { daysOfTheWeek: ['Tuesday', 'Thursday'], start: '12:20 PM', - end: '1:10 PM', + end: '1:10 PM' }, { daysOfTheWeek: ['Monday', 'Friday'], start: '2:30 PM', - end: '4:30 PM', - }, + end: '4:30 PM' + } ], ['Fall', 'Spring'], [engrdReq, probabilityReq] @@ -95,8 +101,8 @@ class Testing { { daysOfTheWeek: ['Sunday'], start: '7:00 AM', - end: '11:00 AM', - }, + end: '11:00 AM' + } ], ['Fall', 'Spring'], [coreClass] @@ -110,8 +116,8 @@ class Testing { { daysOfTheWeek: ['Monday', 'Saturday'], start: '4:30 PM', - end: '6:00 PM', - }, + end: '6:00 PM' + } ], ['Spring'], [techElective] @@ -125,13 +131,13 @@ class Testing { { daysOfTheWeek: ['Tuesday'], start: '1:00 PM', - end: '2:30 PM', + end: '2:30 PM' }, { daysOfTheWeek: ['Monday', 'Friday'], start: '12:00 PM', - end: '3:00 PM', - }, + end: '3:00 PM' + } ], ['Fall', 'Spring'], [techElective] @@ -145,8 +151,8 @@ class Testing { { daysOfTheWeek: ['Monday', 'Wednesday', 'Friday'], start: '10:00 AM', - end: '11:30 AM', - }, + end: '11:30 AM' + } ], ['Spring'], [coreClass, probabilityReq] @@ -160,13 +166,13 @@ class Testing { { daysOfTheWeek: ['Monday', 'Tuesday', 'Wednesday'], start: '11:30 AM', - end: '1:00 PM', + end: '1:00 PM' }, { daysOfTheWeek: ['Thursday', 'Friday'], start: '1:00 PM', - end: '2:30 PM', - }, + end: '2:30 PM' + } ], ['Fall'], [engrdReq, techElective] @@ -181,13 +187,13 @@ class Testing { { daysOfTheWeek: ['Monday', 'Wednesday', 'Friday'], start: '8:55 AM', - end: '9:45 AM', + end: '9:45 AM' }, { daysOfTheWeek: ['Tuesday', 'Thursday'], start: '11:40 AM', - end: '12:30 PM', - }, + end: '12:30 PM' + } ], ['Spring'], [techElective] @@ -202,13 +208,13 @@ class Testing { { daysOfTheWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], start: '8:55 AM', - end: '9:45 AM', + end: '9:45 AM' }, { daysOfTheWeek: ['Saturday'], start: '11:40 AM', - end: '12:30 PM', - }, + end: '12:30 PM' + } ], ['Fall'], [techElective, probabilityReq, engrdReq, coreClass] @@ -225,14 +231,15 @@ class Testing { randomCourse2, randomCourse3, randomCourse4, - randomCourse5, + randomCourse5 ]; const requirements: Requirement[] = [ techElective, coreClass, probabilityReq, - engrdReq, // Assuming you also want to include this in the requirements as mentioned, though not explicitly added in the original Java code testing request. + engrdReq // Assuming you also want to include this in the requirements as mentioned, + // though not explicitly added in the original Java code testing request. ]; // Create request