Skip to content

Commit 9fcde30

Browse files
committed
Update ES query skeleton
1 parent 19f1480 commit 9fcde30

File tree

1 file changed

+48
-8
lines changed

1 file changed

+48
-8
lines changed

website/src/views/modules/ModuleFinderSidebar.tsx

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ import {
1010
import { Filter } from 'react-feather';
1111
import { State as StoreState } from 'types/state';
1212

13-
import { attributeDescription, NUSModuleAttributes, Semester, Semesters } from 'types/modules';
13+
import {
14+
attributeDescription,
15+
Module,
16+
NUSModuleAttributes,
17+
Semester,
18+
Semesters,
19+
} from 'types/modules';
1420
import { RefinementItem } from 'types/views';
1521

1622
import SideMenu, { OPEN_MENU_LABEL } from 'views/components/SideMenu';
@@ -27,6 +33,11 @@ import config from 'config';
2733
import styles from './ModuleFinderSidebar.scss';
2834
import ChecklistFilter, { FilterItem } from '../components/filters/ChecklistFilter';
2935

36+
type ExamTiming = {
37+
start: string;
38+
duration: number;
39+
};
40+
3041
const RESET_FILTER_OPTIONS = { filter: true };
3142

3243
const STATIC_EXAM_FILTER_ITEMS: FilterItem[] = [
@@ -50,7 +61,28 @@ const STATIC_EXAM_FILTER_ITEMS: FilterItem[] = [
5061
},
5162
];
5263

53-
function getExamClashFilter(semester: Semester, examDates: string[]): FilterItem {
64+
function getExamClashFilter(semester: Semester, examTimings: ExamTiming[]): FilterItem {
65+
// @param startTime is an ISO string in UTC timezone
66+
const getEndTime = (startTime: string, duration: number): string => {
67+
const endTime = new Date(startTime);
68+
endTime.setMinutes(endTime.getMinutes() + duration);
69+
return endTime.toISOString();
70+
};
71+
// Map each exam to an Elasticsearch range query.
72+
// Exam2 clashes with exam1 when (exam2.start < exam1.end) && (exam2.end > exam1.start)
73+
const clashRanges = examTimings.map((exam) => ({
74+
bool: {
75+
must: {
76+
range: {
77+
'semesterData.examDate': {
78+
gte: exam.start, // TODO find a way to subtract semesterData.duration
79+
lt: getEndTime(exam.start, exam.duration),
80+
},
81+
},
82+
},
83+
},
84+
}));
85+
5486
return {
5587
key: `no-exam-clash-${semester}`,
5688
label: `No Exam Clash (${config.shortSemesterNames[semester]})`,
@@ -60,8 +92,8 @@ function getExamClashFilter(semester: Semester, examDates: string[]): FilterItem
6092
nested: {
6193
path: 'semesterData',
6294
query: {
63-
terms: {
64-
'semesterData.examDate': examDates,
95+
bool: {
96+
must_not: clashRanges,
6597
},
6698
},
6799
},
@@ -83,10 +115,18 @@ const ModuleFinderSidebar: React.FC = () => {
83115
const examClashFilters = Semesters.map((semester): FilterItem | null => {
84116
const timetable = getSemesterTimetable(semester);
85117
const modules = getSemesterModules(timetable, allModules);
86-
const examDates = modules
87-
.map((module) => getModuleSemesterData(module, semester)?.examDate)
88-
.filter(notNull);
89-
return examDates.length ? getExamClashFilter(semester, examDates) : null;
118+
// Filter for modules with non-empty exam timings, and map them to new ExamTiming objects
119+
const examTimings = modules.reduce<ExamTiming[]>((result: ExamTiming[], mod: Module) => {
120+
const data = getModuleSemesterData(mod, semester);
121+
if (data?.examDate && data?.examDuration) {
122+
result.push({
123+
start: data.examDate,
124+
duration: data.examDuration,
125+
})
126+
}
127+
return result;
128+
}, []);
129+
return examTimings.length ? getExamClashFilter(semester, examTimings) : null;
90130
}).filter(notNull);
91131
return [...STATIC_EXAM_FILTER_ITEMS, ...examClashFilters];
92132
}, [getSemesterTimetable, allModules]);

0 commit comments

Comments
 (0)