|
1 | 1 | import exp from "constants"; |
2 | 2 | import { Request, Response } from "express"; |
3 | 3 |
|
4 | | -import { supabase } from "../db/setupDb"; // Supabase instance for database interactions |
5 | | -import asyncHandler from "../middleware/asyncHandler"; // Middleware to handle async route handlers |
6 | | - |
7 | | -// Interface to define the structure of an Offering |
8 | | -export interface Offering { |
9 | | - id: number; |
10 | | - course_id: number; |
11 | | - meeting_section: string; |
12 | | - offering: string; |
13 | | - day: string; |
14 | | - start: string; |
15 | | - end: string; |
16 | | - location: string; |
17 | | - current: number; |
18 | | - max: number; |
19 | | - is_waitlisted: boolean; |
20 | | - delivery_mode: string; |
21 | | - instructor: string; |
22 | | - notes: string; |
23 | | - code: string; |
24 | | -} |
25 | | - |
26 | | -// Utility function to create an Offering object with optional overrides |
27 | | -export function createOffering(overrides: Partial<Offering> = {}): Offering { |
28 | | - return { |
29 | | - id: overrides.id ?? -1, |
30 | | - course_id: overrides.course_id ?? -1, |
31 | | - meeting_section: overrides.meeting_section ?? "No Section", |
32 | | - offering: overrides.offering ?? "No Offering", |
33 | | - day: overrides.day ?? "N/A", |
34 | | - start: overrides.start ?? "00:00:00", |
35 | | - end: overrides.end ?? "00:00:00", |
36 | | - location: overrides.location ?? "No Room", |
37 | | - current: overrides.current ?? -1, |
38 | | - max: overrides.max ?? -1, |
39 | | - is_waitlisted: overrides.is_waitlisted ?? false, |
40 | | - delivery_mode: overrides.delivery_mode ?? "N/A", |
41 | | - instructor: overrides.instructor ?? "N/A", |
42 | | - notes: overrides.notes ?? "N/A", |
43 | | - code: overrides.code ?? "N/A", |
44 | | - }; |
45 | | -} |
46 | | - |
47 | | -// Enum to define different types of restrictions for offerings |
48 | | -export enum RestrictionType { |
49 | | - RestrictBefore = "Restrict Before", |
50 | | - RestrictAfter = "Restrict After", |
51 | | - RestrictBetween = "Restrict Between", |
52 | | - RestrictDay = "Restrict Day", |
53 | | - RestrictDaysOff = "Days Off", |
54 | | -} |
55 | | - |
56 | | -// Interface for the restriction object |
57 | | -export interface Restriction { |
58 | | - type: RestrictionType; |
59 | | - days: string[]; |
60 | | - startTime: string; |
61 | | - endTime: string; |
62 | | - disabled: boolean; |
63 | | - numDays: number; |
64 | | -} |
65 | | - |
66 | | -// Interface for organizing offerings with the same meeting_section together |
67 | | -export interface GroupedOfferingList { |
68 | | - course_id: number; |
69 | | - groups: Record<string, Offering[]>; |
70 | | -} |
71 | | - |
72 | | -// Interface for organizing offerings by course ID |
73 | | -export interface OfferingList { |
74 | | - course_id: number; |
75 | | - offerings: Offering[]; |
76 | | -} |
77 | | - |
78 | | -// Interface for organizing offerings by course ID and the category of the |
79 | | -// course (LEC, TUT, PRA) |
80 | | -export interface CategorizedOfferingList { |
81 | | - course_id: number; |
82 | | - category: "LEC" | "TUT" | "PRA"; |
83 | | - offerings: Record<string, Offering[]>; |
84 | | -} |
85 | | - |
86 | | -// Function to fetch offerings from the database for a given course and semester |
87 | | -export async function getOfferings(course_id: number, semester: string) { |
88 | | - let { data: offeringData, error: offeringError } = await supabase |
89 | | - .schema("course") |
90 | | - .from("offerings") |
91 | | - .select( |
92 | | - ` |
93 | | - id, |
94 | | - course_id, |
95 | | - meeting_section, |
96 | | - offering, |
97 | | - day, |
98 | | - start, |
99 | | - end, |
100 | | - location, |
101 | | - current, |
102 | | - max, |
103 | | - is_waitlisted, |
104 | | - delivery_mode, |
105 | | - instructor, |
106 | | - notes, |
107 | | - code |
108 | | - `, |
109 | | - ) |
110 | | - .eq("course_id", course_id) |
111 | | - .eq("offering", semester); |
112 | | - |
113 | | - return offeringData; |
114 | | -} |
115 | | - |
116 | | -// Function to group offerings with the same meeting section together |
117 | | -export async function groupOfferings(courseOfferingsList: OfferingList[]) { |
118 | | - const groupedOfferingsList: GroupedOfferingList[] = []; |
119 | | - for (const offering of courseOfferingsList) { |
120 | | - const groupedOfferings: GroupedOfferingList = { |
121 | | - course_id: offering.course_id, |
122 | | - groups: {}, |
123 | | - }; |
124 | | - offering.offerings.forEach((offering) => { |
125 | | - if (!groupedOfferings.groups[offering.meeting_section]) { |
126 | | - groupedOfferings.groups[offering.meeting_section] = []; |
127 | | - } |
128 | | - groupedOfferings.groups[offering.meeting_section].push(offering); |
129 | | - }); |
130 | | - groupedOfferingsList.push(groupedOfferings); |
131 | | - } |
132 | | - |
133 | | - return groupedOfferingsList; |
134 | | -} |
135 | | - |
136 | | -// Function to get the maximum number of days allowed based on restrictions |
137 | | -export async function getMaxDays(restrictions: Restriction[]) { |
138 | | - for (const restriction of restrictions) { |
139 | | - if (restriction.disabled) continue; |
140 | | - if (restriction.type == RestrictionType.RestrictDaysOff) { |
141 | | - return 5 - restriction.numDays; // Subtract the restricted days from the total days |
142 | | - } |
143 | | - } |
144 | | - return 5; // Default to 5 days if no restrictions |
145 | | -} |
| 4 | +import {Offering, OfferingList, GroupedOfferingList} from "../types/generatorTypes" |
| 5 | +import {getMaxDays, groupOfferings, getValidOfferings, categorizeValidOfferings, trim} from "../utils/generatorHelpers" |
| 6 | +import getOfferings from "../services/getOfferings"; |
| 7 | +import { getValidSchedules } from "../services/getValidSchedules"; |
146 | 8 |
|
147 | | -// Function to check if an offering satisfies the restrictions |
148 | | -export function isValidOffering( |
149 | | - offering: Offering, |
150 | | - restrictions: Restriction[], |
151 | | -) { |
152 | | - for (const restriction of restrictions) { |
153 | | - if (restriction.disabled) continue; |
154 | | - if (!restriction.days.includes(offering.day)) continue; |
155 | | - // Check based on the restriction type |
156 | | - switch (restriction.type) { |
157 | | - case RestrictionType.RestrictBefore: |
158 | | - if (offering.start < restriction.endTime) return false; |
159 | | - break; |
160 | | - |
161 | | - case RestrictionType.RestrictAfter: |
162 | | - console.log("===="); |
163 | | - console.log(offering.end); |
164 | | - console.log(restriction.endTime); |
165 | | - if (offering.end > restriction.startTime) return false; |
166 | | - break; |
167 | | - |
168 | | - case RestrictionType.RestrictBetween: |
169 | | - if ( |
170 | | - offering.start < restriction.endTime && |
171 | | - restriction.startTime < offering.end |
172 | | - ) { |
173 | | - return false; |
174 | | - } |
175 | | - break; |
176 | | - |
177 | | - case RestrictionType.RestrictDay: |
178 | | - if (restriction.days.includes(offering.day)) { |
179 | | - return false; |
180 | | - } |
181 | | - break; |
182 | | - } |
183 | | - } |
184 | | - |
185 | | - console.log(offering); |
186 | | - return true; |
187 | | -} |
188 | | - |
189 | | -// Function to get valid offerings by filtering them based on the restrictions |
190 | | -export async function getValidOfferings( |
191 | | - groups: Record<string, Offering[]>, |
192 | | - restrictions: Restriction[], |
193 | | -) { |
194 | | - const validGroups: Record<string, Offering[]> = {}; |
195 | | - |
196 | | - // Loop through each group in the groups object |
197 | | - for (const [groupKey, offerings] of Object.entries(groups)) { |
198 | | - // Check if all offerings in the group are valid |
199 | | - const allValid = offerings.every((offering) => |
200 | | - isValidOffering(offering, restrictions), |
201 | | - ); |
202 | | - |
203 | | - // Only add the group to validGroups if all offerings are valid |
204 | | - if (allValid) { |
205 | | - validGroups[groupKey] = offerings; |
206 | | - } |
207 | | - } |
208 | | - |
209 | | - // Return the object with valid groups |
210 | | - return validGroups; |
211 | | -} |
212 | | - |
213 | | -// Function to categorize offerings into lectures, tutorials, and practicals |
214 | | -export async function categorizeValidOfferings( |
215 | | - offerings: GroupedOfferingList[], |
216 | | -) { |
217 | | - const lst: CategorizedOfferingList[] = []; |
218 | | - |
219 | | - for (const offering of offerings) { |
220 | | - const lectures: CategorizedOfferingList = { |
221 | | - course_id: offering.course_id, |
222 | | - category: "LEC", |
223 | | - offerings: {}, |
224 | | - }; |
225 | | - const tutorials: CategorizedOfferingList = { |
226 | | - course_id: offering.course_id, |
227 | | - category: "TUT", |
228 | | - offerings: {}, |
229 | | - }; |
230 | | - const practicals: CategorizedOfferingList = { |
231 | | - course_id: offering.course_id, |
232 | | - category: "PRA", |
233 | | - offerings: {}, |
234 | | - }; |
235 | | - |
236 | | - for (const [meeting_section, offerings] of Object.entries( |
237 | | - offering.groups, |
238 | | - )) { |
239 | | - if (meeting_section && meeting_section.startsWith("PRA")) { |
240 | | - practicals.offerings[meeting_section] = offerings; |
241 | | - } else if (meeting_section && meeting_section.startsWith("TUT")) { |
242 | | - tutorials.offerings[meeting_section] = offerings; |
243 | | - } else { |
244 | | - lectures.offerings[meeting_section] = offerings; |
245 | | - } |
246 | | - } |
247 | | - |
248 | | - for (const x of [lectures, practicals, tutorials]) { |
249 | | - if (Object.keys(x.offerings).length > 0) { |
250 | | - lst.push(x); |
251 | | - } |
252 | | - } |
253 | | - } |
254 | | - return lst; |
255 | | -} |
256 | | - |
257 | | -// Function to check if an offering can be inserted into the current list of |
258 | | -// offerings without conflicts |
259 | | -export async function canInsert(toInsert: Offering, curList: Offering[]) { |
260 | | - for (const offering of curList) { |
261 | | - if (offering.day == toInsert.day) { |
262 | | - if (offering.start < toInsert.end && toInsert.start < offering.end) { |
263 | | - return false; // Check if the time overlaps |
264 | | - } |
265 | | - } |
266 | | - } |
267 | | - |
268 | | - return true; // No conflict found |
269 | | -} |
270 | | - |
271 | | -// Function to check if an ever offerings in toInstList can be inserted into |
272 | | -// the current list of offerings without conflicts |
273 | | -export async function canInsertList( |
274 | | - toInsertList: Offering[], |
275 | | - curList: Offering[], |
276 | | -) { |
277 | | - console.log(toInsertList); |
278 | | - return toInsertList.every((x) => canInsert(x, curList)); |
279 | | -} |
280 | | - |
281 | | -// Function to generate a frequency table of days from a list of offerings |
282 | | -export function getFrequencyTable(arr: Offering[]): Map<string, number> { |
283 | | - const freqMap = new Map<string, number>(); |
284 | | - |
285 | | - for (const item of arr) { |
286 | | - const count = freqMap.get(item.day) || 0; |
287 | | - freqMap.set(item.day, count + 1); |
288 | | - } |
289 | | - return freqMap; |
290 | | -} |
291 | | - |
292 | | -// Function to generate all valid schedules based on offerings and restrictions |
293 | | - |
294 | | -export async function getValidSchedules( |
295 | | - validSchedules: Offering[][], |
296 | | - courseOfferingsList: CategorizedOfferingList[], |
297 | | - curList: Offering[], |
298 | | - cur: number, |
299 | | - len: number, |
300 | | - maxdays: number, |
301 | | -) { |
302 | | - // Base case: if all courses have been considered |
303 | | - if (cur == len) { |
304 | | - const freq: Map<string, number> = getFrequencyTable(curList); |
305 | | - |
306 | | - // If the number of unique days is within the allowed limit, add the current |
307 | | - // schedule to the list |
308 | | - if (freq.size <= maxdays) { |
309 | | - validSchedules.push([...curList]); // Push a copy of the current list |
310 | | - } |
311 | | - return; |
312 | | - } |
313 | | - |
314 | | - const offeringsForCourse = courseOfferingsList[cur]; |
315 | | - |
316 | | - // Recursively attempt to add offerings for the current course |
317 | | - for (const [groupKey, offerings] of Object.entries( |
318 | | - offeringsForCourse.offerings, |
319 | | - )) { |
320 | | - if (await canInsertList(offerings, curList)) { |
321 | | - const count = offerings.length; |
322 | | - curList.push(...offerings); // Add offering to the current list |
323 | | - |
324 | | - // Recursively generate schedules for the next course |
325 | | - await getValidSchedules( |
326 | | - validSchedules, |
327 | | - courseOfferingsList, |
328 | | - curList, |
329 | | - cur + 1, |
330 | | - len, |
331 | | - maxdays, |
332 | | - ); |
333 | | - |
334 | | - // Backtrack: remove the last offering if no valid schedule was found |
335 | | - for (let i = 0; i < count; i++) curList.pop(); |
336 | | - } |
337 | | - } |
338 | | -} |
339 | | - |
340 | | -// Trims the list of scheules to only return 10 random schedule if there is more |
341 | | -// than 10 available options. |
342 | | -export function trim(schedules: Offering[][]) { |
343 | | - if (schedules.length <= 10) return schedules; |
344 | | - const num = schedules.length; |
345 | | - |
346 | | - const uniqueNumbers = new Set<number>(); |
347 | | - while (uniqueNumbers.size < 10) { |
348 | | - uniqueNumbers.add(Math.floor(Math.random() * num)); |
349 | | - } |
350 | | - // console.log(uniqueNumbers); |
351 | | - const trim_schedule: Offering[][] = []; |
352 | | - for (const value of uniqueNumbers) trim_schedule.push(schedules[value]); |
353 | | - |
354 | | - return trim_schedule; |
355 | | -} |
| 9 | +import asyncHandler from "../middleware/asyncHandler"; // Middleware to handle async route handlers |
356 | 10 |
|
357 | 11 | // Express route handler to generate timetables based on user input |
358 | 12 | export default { |
359 | 13 | generateTimetable: asyncHandler(async (req: Request, res: Response) => { |
360 | 14 | try { |
361 | 15 | // Extract event details and course information from the request |
362 | | - const { name, date, semester, search, courses, restrictions } = req.body; |
| 16 | + const {semester, courses, restrictions } = req.body; |
363 | 17 | const courseOfferingsList: OfferingList[] = []; |
364 | 18 | const validCourseOfferingsList: GroupedOfferingList[] = []; |
365 | 19 | const maxdays = await getMaxDays(restrictions); |
|
0 commit comments