-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathrequirement-graph-builder.ts
145 lines (136 loc) · 5.94 KB
/
requirement-graph-builder.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import RequirementFulfillmentGraph, { CourseWithUniqueId } from './requirement-graph';
interface CourseForRequirementGraph extends CourseWithUniqueId {
readonly courseId: number;
}
export type BuildRequirementFulfillmentGraphParameters<
Requirement extends string,
Course extends CourseForRequirementGraph
> = {
/**
* A list of applicable requirements in the system. e.g. if the user is CS major
* in COE, then the list should contain all university requirements, COE requirements, and CS major
* requirements.
*/
readonly requirements: readonly Requirement[];
/** A list of courses user inputted into course plan, regardless of semesters. */
readonly userCourses: readonly Course[];
/**
* Some requirements might have several different ways to fulfill them. This is a map from
* toggleable requirements to a list of course IDs that could be applied to the requirement under
* the user's choice.
* (Note: it has to be course id because we want to match course id against eligible course id list.)
*/
readonly userChoiceOnFulfillmentStrategy: Readonly<Record<Requirement, readonly number[]>>;
/**
* The mapping from course's unique ID to requirement override opt-in and opt-out options.
* It describes how the user wants to use a course to override requirements.
* This handles AP/IB overrides, as well as general overrides. This can encode double counting
* elimination as opt-out.
* The granularity of optIn/optOut being slot-specific requires the actual
* slot computation to be handled in the frontend computation. When building the
* graph, only optIn/optOut choices are relevant (to add the extra edges).
*/
readonly userChoiceOnRequirementOverrides: {
readonly [uniqueId: string]: {
// Including both acknowledged and arbitrary opt-in.
readonly optIn: readonly Requirement[];
readonly optOut: readonly Requirement[];
};
};
/**
* Naively give a list of courses ID that can satisfy a requirement. Most of the time this function
* should just return the pre-computed eligible course id list. For requirements have multiple
* fulfillment strategies, it will return the union of all pre-computed course list.
*/
readonly getAllCoursesThatCanPotentiallySatisfyRequirement: (
requirement: Requirement
) => readonly number[];
};
export const buildRequirementFulfillmentGraph = <
Requirement extends string,
Course extends CourseForRequirementGraph
>({
requirements,
userCourses,
userChoiceOnFulfillmentStrategy,
userChoiceOnRequirementOverrides,
getAllCoursesThatCanPotentiallySatisfyRequirement,
}: BuildRequirementFulfillmentGraphParameters<Requirement, Course>): RequirementFulfillmentGraph<
Requirement,
Course
> => {
const graph = new RequirementFulfillmentGraph<Requirement, Course>();
const userCourseCourseIDToCourseMap = new Map<number, Course[]>();
userCourses.forEach(course => {
let existing = userCourseCourseIDToCourseMap.get(course.courseId);
if (existing == null) {
existing = [];
userCourseCourseIDToCourseMap.set(course.courseId, existing);
}
existing.push(course);
});
// Phase 1:
// Building a rough graph by naively connecting requirements and courses based on
// `getAllCoursesThatCanPotentiallySatisfyRequirement`.
requirements.forEach(requirement => {
graph.addRequirementNode(requirement);
getAllCoursesThatCanPotentiallySatisfyRequirement(requirement).forEach(courseId => {
(userCourseCourseIDToCourseMap.get(courseId) || []).forEach(userCourse =>
graph.addEdge(requirement, userCourse)
);
});
});
// Phase 2: Respect user's choices on fulfillment strategies.
Object.entries<readonly number[]>(userChoiceOnFulfillmentStrategy).forEach(
([key, coursesOfChosenFulfillmentStrategy]) => {
const correspondingRequirement = key as Requirement;
const coursesToKeepSet = new Set(coursesOfChosenFulfillmentStrategy);
graph
.getConnectedCoursesFromRequirement(correspondingRequirement)
.forEach(connectedCourse => {
if (!coursesToKeepSet.has(connectedCourse.courseId)) {
graph.removeEdge(correspondingRequirement, connectedCourse);
}
});
}
);
// Phase 3: Respect user's choices on opt-in/opt-out.
userCourses.forEach(course => {
const { uniqueId } = course;
// typeof uniqueId === 'string' means it's AP/IB equivalent course.
// uniqueId < 0 means it's swim test.
// User never gets to make a choice about these courses, so it will never appear in the choices.
// Therefore, removing those edges will nullify all these credits.
if (typeof uniqueId === 'string' || uniqueId < 0) return;
const userChoiceOnOptInOptOutCourse = userChoiceOnRequirementOverrides[uniqueId];
if (userChoiceOnOptInOptOutCourse == null) return;
userChoiceOnOptInOptOutCourse.optIn.forEach(optedInRequirement => {
graph.addEdge(optedInRequirement, course);
});
userChoiceOnOptInOptOutCourse.optOut.forEach(optedOutRequirement => {
graph.removeEdge(optedOutRequirement, course);
});
});
// Phase MAX_INT: PROFIT!
return graph;
};
export const removeIllegalEdgesFromRequirementFulfillmentGraph = <
Requirement extends string,
Course extends CourseForRequirementGraph
>(
graph: RequirementFulfillmentGraph<Requirement, Course>,
allowDoubleCounting: (requirement: Requirement) => boolean
): ReadonlySet<string | number> => {
const doubleCountedCourseUniqueIDSet = new Set<string | number>();
graph.getAllCourses().forEach(course => {
const requirementsThatDoesNotAllowDoubleCounting = graph
.getConnectedRequirementsFromCourse(course)
.filter(it => !allowDoubleCounting(it));
if (requirementsThatDoesNotAllowDoubleCounting.length > 1) {
// Illegal double counting
requirementsThatDoesNotAllowDoubleCounting.forEach(r => graph.removeEdge(r, course));
doubleCountedCourseUniqueIDSet.add(course.uniqueId);
}
});
return doubleCountedCourseUniqueIDSet;
};