Skip to content

Commit c12fbd0

Browse files
thomasyzy7dawangk
andauthored
Ty/scrum 52 timetable generation (#81)
Co-authored-by: dawangk <[email protected]>
1 parent 8bba298 commit c12fbd0

File tree

10 files changed

+764
-2
lines changed

10 files changed

+764
-2
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { describe, expect, it, test } from "@jest/globals";
2+
3+
import { createOffering, canInsert } from "../src/utils/generatorHelpers";
4+
import { Offering } from "../src/types/generatorTypes";
5+
6+
describe("canInsert function", () => {
7+
const offering1: Offering = createOffering({
8+
id: 1,
9+
course_id: 101,
10+
day: "MO",
11+
start: "09:00:00",
12+
end: "10:00:00",
13+
});
14+
const offering2: Offering = createOffering({
15+
id: 2,
16+
course_id: 102,
17+
day: "MO",
18+
start: "10:00:00",
19+
end: "11:00:00",
20+
});
21+
const offering3: Offering = createOffering({
22+
id: 3,
23+
course_id: 103,
24+
day: "MO",
25+
start: "11:00:00",
26+
end: "12:00:00",
27+
});
28+
29+
it("should return true if there is no overlap with existing offerings", async () => {
30+
const toInsert: Offering = createOffering({
31+
id: 4,
32+
course_id: 104,
33+
day: "MO",
34+
start: "12:00:00",
35+
end: "13:00:00",
36+
});
37+
const curList: Offering[] = [offering1, offering2, offering3];
38+
39+
const result = await canInsert(toInsert, curList);
40+
41+
expect(result).toBe(true); // No overlap, should return true
42+
});
43+
44+
it("should return false if there is an overlap with an existing offering", async () => {
45+
const toInsert: Offering = createOffering({
46+
id: 4,
47+
course_id: 104,
48+
day: "MO",
49+
start: "09:30:00",
50+
end: "10:30:00",
51+
});
52+
const curList: Offering[] = [offering1, offering2, offering3];
53+
54+
const result = await canInsert(toInsert, curList);
55+
56+
expect(result).toBe(false); // There is an overlap with offering1, should return false
57+
});
58+
59+
it("should return true if the new offering starts after the last one ends", async () => {
60+
const toInsert: Offering = createOffering({
61+
id: 4,
62+
course_id: 104,
63+
day: "MO",
64+
start: "13:00:00",
65+
end: "14:00:00",
66+
});
67+
const curList: Offering[] = [offering1, offering2, offering3];
68+
69+
const result = await canInsert(toInsert, curList);
70+
71+
expect(result).toBe(true); // No overlap, should return true
72+
});
73+
74+
it("should return true if the new offering ends before the first one starts", async () => {
75+
const toInsert: Offering = createOffering({
76+
id: 4,
77+
course_id: 104,
78+
day: "MO",
79+
start: "07:00:00",
80+
end: "08:00:00",
81+
});
82+
const curList: Offering[] = [offering1, offering2, offering3];
83+
84+
const result = await canInsert(toInsert, curList);
85+
86+
expect(result).toBe(true); // No overlap, should return true
87+
});
88+
89+
it("should return false if the new offering is completely inside an existing one", async () => {
90+
const toInsert: Offering = createOffering({
91+
id: 4,
92+
course_id: 104,
93+
day: "MO",
94+
start: "09:30:00",
95+
end: "09:45:00",
96+
});
97+
const curList: Offering[] = [offering1, offering2, offering3];
98+
99+
const result = await canInsert(toInsert, curList);
100+
101+
expect(result).toBe(false); // Overlaps with offering1, should return false
102+
});
103+
104+
it("should return true if the day is different (no overlap)", async () => {
105+
const toInsert: Offering = createOffering({
106+
id: 4,
107+
course_id: 104,
108+
day: "TU",
109+
start: "09:00:00",
110+
end: "10:00:00",
111+
});
112+
const curList: Offering[] = [offering1, offering2, offering3];
113+
114+
const result = await canInsert(toInsert, curList);
115+
116+
expect(result).toBe(true); // Different day, no overlap
117+
});
118+
});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { describe, expect, it, test } from "@jest/globals";
2+
3+
import {
4+
createOffering,
5+
getFrequencyTable,
6+
} from "../src/utils/generatorHelpers";
7+
import { Offering } from "../src/types/generatorTypes";
8+
9+
describe("getFrequencyTable", () => {
10+
test("should return a frequency map of days", () => {
11+
const offering1: Offering = createOffering({
12+
id: 1,
13+
course_id: 101,
14+
day: "MO",
15+
start: "09:00:00",
16+
end: "10:00:00",
17+
});
18+
const offering2: Offering = createOffering({
19+
id: 2,
20+
course_id: 102,
21+
day: "TU",
22+
start: "10:00:00",
23+
end: "11:00:00",
24+
});
25+
const offering3: Offering = createOffering({
26+
id: 3,
27+
course_id: 103,
28+
day: "TU",
29+
start: "11:00:00",
30+
end: "12:00:00",
31+
});
32+
const offering4: Offering = createOffering({
33+
id: 4,
34+
course_id: 104,
35+
day: "MO",
36+
start: "11:00:00",
37+
end: "12:00:00",
38+
});
39+
const offering5: Offering = createOffering({
40+
id: 5,
41+
course_id: 105,
42+
day: "WE",
43+
start: "11:00:00",
44+
end: "12:00:00",
45+
});
46+
const offering6: Offering = createOffering({
47+
id: 6,
48+
course_id: 106,
49+
day: "WE",
50+
start: "11:00:00",
51+
end: "12:00:00",
52+
});
53+
const offering7: Offering = createOffering({
54+
id: 7,
55+
course_id: 107,
56+
day: "WE",
57+
start: "11:00:00",
58+
end: "12:00:00",
59+
});
60+
61+
const result = getFrequencyTable([
62+
offering1,
63+
offering2,
64+
offering3,
65+
offering4,
66+
offering5,
67+
offering6,
68+
offering7,
69+
]);
70+
71+
expect(result.get("MO")).toBe(2);
72+
expect(result.get("TU")).toBe(2);
73+
expect(result.get("WE")).toBe(3);
74+
expect(result.get("TH")).toBeUndefined(); // Day not in data
75+
expect(result.get("FR")).toBeUndefined(); // Day not in data
76+
expect(result.size).toBe(3);
77+
});
78+
79+
test("should return an empty map for an empty array", () => {
80+
const result = getFrequencyTable([]);
81+
expect(result.size).toBe(0);
82+
});
83+
});
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { describe, expect, it, test } from "@jest/globals";
2+
3+
import { createOffering, isValidOffering } from "../src/utils/generatorHelpers";
4+
import {
5+
Offering,
6+
Restriction,
7+
RestrictionType,
8+
} from "../src/types/generatorTypes";
9+
10+
describe("isValidOffering", () => {
11+
const sampleOffering: Offering = createOffering({
12+
id: 1,
13+
course_id: 101,
14+
day: "MO",
15+
start: "10:00:00",
16+
end: "11:00:00",
17+
});
18+
19+
test("should allow offering if there are no restrictions", () => {
20+
expect(isValidOffering(sampleOffering, [])).toBe(true);
21+
});
22+
23+
test("should allow offering if all restrictions are disabled", () => {
24+
const restrictions: Restriction[] = [
25+
{
26+
type: RestrictionType.RestrictBefore,
27+
days: ["MO"],
28+
startTime: "",
29+
endTime: "09:00:00",
30+
disabled: true,
31+
numDays: 0,
32+
},
33+
];
34+
expect(isValidOffering(sampleOffering, restrictions)).toBe(true);
35+
});
36+
37+
test("should reject offering if it starts before restriction start time", () => {
38+
const restrictions: Restriction[] = [
39+
{
40+
type: RestrictionType.RestrictBefore,
41+
days: ["MO"],
42+
startTime: "",
43+
endTime: "11:00:00",
44+
disabled: false,
45+
numDays: 0,
46+
},
47+
];
48+
expect(isValidOffering(sampleOffering, restrictions)).toBe(false);
49+
});
50+
51+
test("should reject offering if it ends after restriction end time", () => {
52+
const restrictions: Restriction[] = [
53+
{
54+
type: RestrictionType.RestrictAfter,
55+
days: ["MO"],
56+
startTime: "10:30:00",
57+
endTime: "",
58+
disabled: false,
59+
numDays: 0,
60+
},
61+
];
62+
expect(isValidOffering(sampleOffering, restrictions)).toBe(false);
63+
});
64+
65+
test("should reject offering if it is within restricted time range", () => {
66+
const restrictions: Restriction[] = [
67+
{
68+
type: RestrictionType.RestrictBetween,
69+
days: ["MO"],
70+
startTime: "09:00:00",
71+
endTime: "12:00:00",
72+
disabled: false,
73+
numDays: 0,
74+
},
75+
];
76+
expect(isValidOffering(sampleOffering, restrictions)).toBe(false);
77+
});
78+
79+
test("should reject offering if the day is restricted", () => {
80+
const restrictions: Restriction[] = [
81+
{
82+
type: RestrictionType.RestrictDay,
83+
days: ["MO"],
84+
startTime: "",
85+
endTime: "",
86+
disabled: false,
87+
numDays: 0,
88+
},
89+
];
90+
expect(isValidOffering(sampleOffering, restrictions)).toBe(false);
91+
});
92+
93+
test("should allow offering if the day is not restricted", () => {
94+
const restrictions: Restriction[] = [
95+
{
96+
type: RestrictionType.RestrictDay,
97+
days: ["TU"],
98+
startTime: "",
99+
endTime: "",
100+
disabled: false,
101+
numDays: 0,
102+
},
103+
];
104+
expect(isValidOffering(sampleOffering, restrictions)).toBe(true);
105+
});
106+
});
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import exp from "constants";
2+
import { Request, Response } from "express";
3+
4+
import {
5+
Offering,
6+
OfferingList,
7+
GroupedOfferingList,
8+
} from "../types/generatorTypes";
9+
import {
10+
getMaxDays,
11+
groupOfferings,
12+
getValidOfferings,
13+
categorizeValidOfferings,
14+
trim,
15+
} from "../utils/generatorHelpers";
16+
import getOfferings from "../services/getOfferings";
17+
import { getValidSchedules } from "../services/getValidSchedules";
18+
19+
import asyncHandler from "../middleware/asyncHandler"; // Middleware to handle async route handlers
20+
21+
// Express route handler to generate timetables based on user input
22+
export default {
23+
generateTimetable: asyncHandler(async (req: Request, res: Response) => {
24+
try {
25+
// Extract event details and course information from the request
26+
const { semester, courses, restrictions } = req.body;
27+
const courseOfferingsList: OfferingList[] = [];
28+
const validCourseOfferingsList: GroupedOfferingList[] = [];
29+
const maxdays = await getMaxDays(restrictions);
30+
const validSchedules: Offering[][] = [];
31+
// Fetch offerings for each course
32+
for (const course of courses) {
33+
const { id } = course;
34+
courseOfferingsList.push({
35+
course_id: id,
36+
offerings: (await getOfferings(id, semester)) ?? [],
37+
});
38+
}
39+
40+
const groupedOfferingsList: GroupedOfferingList[] =
41+
await groupOfferings(courseOfferingsList);
42+
43+
// console.log(JSON.stringify(groupedOfferingsList, null, 2));
44+
45+
// Filter out invalid offerings based on the restrictions
46+
for (const { course_id, groups } of groupedOfferingsList) {
47+
validCourseOfferingsList.push({
48+
course_id: course_id,
49+
groups: await getValidOfferings(groups, restrictions),
50+
});
51+
}
52+
53+
const categorizedOfferings = await categorizeValidOfferings(
54+
validCourseOfferingsList,
55+
);
56+
57+
// console.log(typeof categorizedOfferings);
58+
// console.log(JSON.stringify(categorizedOfferings, null, 2));
59+
60+
// Generate valid schedules for the given courses and restrictions
61+
await getValidSchedules(
62+
validSchedules,
63+
categorizedOfferings,
64+
[],
65+
0,
66+
categorizedOfferings.length,
67+
maxdays,
68+
);
69+
70+
// Return error if no valid schedules are found
71+
if (validSchedules.length === 0) {
72+
return res.status(404).json({ error: "No valid schedules found." });
73+
}
74+
// Return the valid schedules
75+
return res.status(200).json({
76+
amount: validSchedules.length,
77+
schedules: trim(validSchedules),
78+
});
79+
} catch (error) {
80+
// Catch any error and return the error message
81+
const errorMessage =
82+
error instanceof Error ? error.message : "An unknown error occurred";
83+
return res.status(500).send({ error: errorMessage });
84+
}
85+
}),
86+
};

0 commit comments

Comments
 (0)