Skip to content

Commit ee1ae62

Browse files
committed
First draft of timetable generation
Testing and tweaking needed.
1 parent e8b4e3e commit ee1ae62

File tree

2 files changed

+267
-4
lines changed

2 files changed

+267
-4
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import {describe, expect, it, test} from '@jest/globals';
2+
3+
import {canInsert, createOffering, Offering} from '../src/controllers/generatorController';
4+
5+
describe('canInsert function', () => {
6+
const offering1: Offering = createOffering(
7+
{id: 1, course_id: 101, day: 'MO', start: '09:00:00', end: '10:00:00'});
8+
const offering2: Offering = createOffering(
9+
{id: 2, course_id: 102, day: 'MO', start: '10:00:00', end: '11:00:00'});
10+
const offering3: Offering = createOffering(
11+
{id: 3, course_id: 103, day: 'MO', start: '11:00:00', end: '12:00:00'});
12+
13+
it('should return true if there is no overlap with existing offerings',
14+
async () => {
15+
const toInsert: Offering = createOffering({
16+
id: 4,
17+
course_id: 104,
18+
day: 'MO',
19+
start: '12:00:00',
20+
end: '13:00:00'
21+
});
22+
const curList: Offering[] = [offering1, offering2, offering3];
23+
24+
const result = await canInsert(toInsert, curList);
25+
26+
expect(result).toBe(true); // No overlap, should return true
27+
});
28+
29+
it('should return false if there is an overlap with an existing offering',
30+
async () => {
31+
const toInsert: Offering = createOffering({
32+
id: 4,
33+
course_id: 104,
34+
day: 'MO',
35+
start: '09:30:00',
36+
end: '10:30:00'
37+
});
38+
const curList: Offering[] = [offering1, offering2, offering3];
39+
40+
const result = await canInsert(toInsert, curList);
41+
42+
expect(result).toBe(
43+
false); // There is an overlap with offering1, should return false
44+
});
45+
46+
it('should return true if the new offering starts after the last one ends',
47+
async () => {
48+
const toInsert: Offering = createOffering({
49+
id: 4,
50+
course_id: 104,
51+
day: 'MO',
52+
start: '13:00:00',
53+
end: '14:00:00'
54+
});
55+
const curList: Offering[] = [offering1, offering2, offering3];
56+
57+
const result = await canInsert(toInsert, curList);
58+
59+
expect(result).toBe(true); // No overlap, should return true
60+
});
61+
62+
it('should return true if the new offering ends before the first one starts',
63+
async () => {
64+
const toInsert: Offering = createOffering({
65+
id: 4,
66+
course_id: 104,
67+
day: 'MO',
68+
start: '07:00:00',
69+
end: '08:00:00'
70+
});
71+
const curList: Offering[] = [offering1, offering2, offering3];
72+
73+
const result = await canInsert(toInsert, curList);
74+
75+
expect(result).toBe(true); // No overlap, should return true
76+
});
77+
78+
it('should return false if the new offering is completely inside an existing one',
79+
async () => {
80+
const toInsert: Offering = createOffering({
81+
id: 4,
82+
course_id: 104,
83+
day: 'MO',
84+
start: '09:30:00',
85+
end: '09:45:00'
86+
});
87+
const curList: Offering[] = [offering1, offering2, offering3];
88+
89+
const result = await canInsert(toInsert, curList);
90+
91+
expect(result).toBe(
92+
false); // Overlaps with offering1, should return false
93+
});
94+
95+
it('should return true if the day is different (no overlap)', async () => {
96+
const toInsert: Offering = createOffering(
97+
{id: 4, course_id: 104, day: 'TU', start: '09:00:00', end: '10:00:00'});
98+
const curList: Offering[] = [offering1, offering2, offering3];
99+
100+
const result = await canInsert(toInsert, curList);
101+
102+
expect(result).toBe(true); // Different day, no overlap
103+
});
104+
});

course-matrix/backend/src/controllers/generatorController.ts

Lines changed: 163 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,66 @@ import {Request, Response} from 'express';
33
import {supabase} from '../db/setupDb';
44
import asyncHandler from '../middleware/asyncHandler';
55

6-
async function getOfferings(course_id: number, semester: string) {
6+
export interface Offering {
7+
id: number;
8+
course_id: number;
9+
meeting_section: string;
10+
offering: string;
11+
day: string;
12+
start: string;
13+
end: string;
14+
location: string;
15+
current: number;
16+
max: number;
17+
is_waitlisted: boolean;
18+
delivery_mode: string;
19+
instructor: string;
20+
notes: string;
21+
code: string;
22+
}
23+
24+
export function createOffering(overrides: Partial<Offering> = {}): Offering {
25+
return {
26+
id: overrides.id ?? -1,
27+
course_id: overrides.course_id ?? -1,
28+
meeting_section: overrides.meeting_section ?? 'No Section',
29+
offering: overrides.offering ?? 'No Offering',
30+
day: overrides.day ?? 'N/A',
31+
start: overrides.start ?? '00:00:00',
32+
end: overrides.end ?? '00:00:00',
33+
location: overrides.location ?? 'No Room',
34+
current: overrides.current ?? -1,
35+
max: overrides.max ?? -1,
36+
is_waitlisted: overrides.is_waitlisted ?? false,
37+
delivery_mode: overrides.delivery_mode ?? 'N/A',
38+
instructor: overrides.instructor ?? 'N/A',
39+
notes: overrides.notes ?? 'N/A',
40+
code: overrides.code ?? 'N/A',
41+
};
42+
}
43+
44+
export enum RestrictionType {
45+
RestrictBefore = 'Restrict Before',
46+
RestrictAfter = 'Restrict After',
47+
RestrictBetween = 'Restrict Between',
48+
RestrictDay = 'Restrict Day',
49+
RestrictDaysOff = 'Days Off'
50+
}
51+
52+
export interface Restriction {
53+
type: RestrictionType;
54+
days: string[];
55+
startTime: string;
56+
endTime: string;
57+
disabled: boolean;
58+
}
59+
60+
export interface OfferingList {
61+
course_id: number;
62+
offerings: Offering[];
63+
}
64+
65+
export async function getOfferings(course_id: number, semester: string) {
766
let {data: offeringData, error: offeringError} = await supabase
867
.schema('course')
968
.from('offerings')
@@ -29,23 +88,123 @@ async function getOfferings(course_id: number, semester: string) {
2988
}
3089

3190

91+
92+
export const filterValidOfferings =
93+
(offerings: Offering[], f: (x: Offering) => boolean):
94+
Offering[] => {
95+
return offerings.filter(f);
96+
}
97+
98+
export function isValidOffering(
99+
offering: Offering, restrictions: Restriction[]) {
100+
for (const restriction of restrictions) {
101+
if (restriction.disabled) continue;
102+
103+
switch (restriction.type) {
104+
case RestrictionType.RestrictBefore:
105+
if (offering.start < restriction.startTime) return false;
106+
break;
107+
108+
case RestrictionType.RestrictAfter:
109+
if (offering.end > restriction.endTime) return false;
110+
break;
111+
112+
case RestrictionType.RestrictBetween:
113+
if (offering.start >= restriction.startTime &&
114+
offering.end <= restriction.endTime) {
115+
return false;
116+
}
117+
break;
118+
case RestrictionType.RestrictDay:
119+
if (restriction.days.includes(offering.day)) {
120+
return false;
121+
}
122+
break;
123+
}
124+
}
125+
return true;
126+
}
127+
128+
export async function getValidOfferings(
129+
offerings: Offering[], restrictions: Restriction[]) {
130+
return filterValidOfferings(offerings, x => isValidOffering(x, restrictions));
131+
}
132+
133+
export async function canInsert(toInsert: Offering, curList: Offering[]) {
134+
for (const offering of curList) {
135+
if (offering.day == toInsert.day) {
136+
if (offering.start < toInsert.end && toInsert.start < offering.end) {
137+
return false;
138+
}
139+
}
140+
}
141+
142+
return true;
143+
}
144+
145+
export async function getValidSchedules(
146+
courseOfferingsList: OfferingList[], curList: Offering[], cur: number,
147+
len: number):
148+
Promise<Offering[][]> {
149+
if (cur == len) return [curList];
150+
151+
const validSchedules: Offering[][] = [];
152+
153+
const offeringsForCourse = courseOfferingsList[cur];
154+
155+
for (const offering of offeringsForCourse.offerings) {
156+
if (await canInsert(offering, curList)) {
157+
curList.push(offering);
158+
// Recursively call the function for the next course
159+
const res: Offering[][] = await getValidSchedules(
160+
courseOfferingsList, curList, cur + 1, len);
161+
162+
// If a valid schedule is found, return it
163+
validSchedules.push(...res);
164+
165+
// If no valid schedule is found, pop the offering and continue
166+
curList.pop();
167+
}
168+
}
169+
170+
// If no valid schedule is found for this course, return null
171+
return validSchedules;
172+
}
173+
32174
export default {
33175
generateTimetable: asyncHandler(async (req: Request, res: Response) => {
34176
try {
35177
// Retrieve event properties from the request body
36178
const {name, date, semester, search, courses, restrictions} = req.body;
37-
const courseOfferingsList = [];
179+
const courseOfferingsList: OfferingList[] = [];
180+
const validCourseOfferingsList: OfferingList[] = [];
38181
// Retrieve the authenticated user
39182
const user_id = (req as any).user.id;
40183

41184
// extracting offerings from each course
42185
for (const course of courses) {
43186
const {id, code, name} = course;
44187
courseOfferingsList.push(
45-
{course_id: id, offerings: await getOfferings(id, semester)});
188+
{course_id: id, offerings: await getOfferings(id, semester) ?? []});
46189
}
190+
courseOfferingsList.forEach(
191+
course => console.log(JSON.stringify(course, null, 2)));
47192

48-
return res.status(200).json(courseOfferingsList);
193+
// filter out invalid course entries
194+
for (const {course_id, offerings} of courseOfferingsList) {
195+
validCourseOfferingsList.push({
196+
course_id: course_id,
197+
offerings: await getValidOfferings(offerings ?? [], restrictions)
198+
});
199+
}
200+
201+
const validSchedules = await getValidSchedules(
202+
validCourseOfferingsList, [], 0, validCourseOfferingsList.length);
203+
204+
if (validSchedules.length === 0) {
205+
return res.status(404).json({error: 'No valid schedules found.'});
206+
}
207+
return res.status(200).json({validSchedules});
49208
} catch (error) {
50209
return res.status(500).send({error});
51210
}

0 commit comments

Comments
 (0)