Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update exam clash detection in the Module search page #3678

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 63 additions & 7 deletions website/src/views/modules/ModuleFinderSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import {
import { Filter } from 'react-feather';
import { State as StoreState } from 'types/state';

import { attributeDescription, NUSModuleAttributes, Semester, Semesters } from 'types/modules';
import {
attributeDescription,
Module,
NUSModuleAttributes,
Semester,
Semesters,
} from 'types/modules';
import { RefinementItem } from 'types/views';

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

type ExamTiming = {
start: string;
duration: number;
};

const RESET_FILTER_OPTIONS = { filter: true };

const STATIC_EXAM_FILTER_ITEMS: FilterItem[] = [
Expand All @@ -50,7 +61,42 @@ const STATIC_EXAM_FILTER_ITEMS: FilterItem[] = [
},
];

function getExamClashFilter(semester: Semester, examDates: string[]): FilterItem {
function getExamClashFilter(semester: Semester, examTimings: ExamTiming[]): FilterItem {
// @param startTime is an ISO string in UTC timezone
const getEndTime = (startTime: string, duration: number): string => {
const endTime = new Date(startTime);
endTime.setMinutes(endTime.getMinutes() + duration);
return endTime.toISOString();
};
// For each exam1, map it to an Elasticsearch query that will return True
// if another exam clashes with exam1. For example, exam2 clashes (i.e. overlaps)
// with exam1 iff (exam2.start < exam1.end) && (exam2.end > exam1.start)
const clashRanges = examTimings.map((exam1) => ({
bool: {
must: [
{
range: {
'semesterData.examDate': {
lt: getEndTime(exam1.start, exam1.duration),
},
},
},
{
script: {
script: {
source: `doc.containsKey('semesterData.examDate') &&
doc.containsKey('semesterData.examDuration') &&
doc['semesterData.examDate'].value.plusMinutes(doc['semesterData.examDuration'].value).isAfter(ZonedDateTime.parse(params.exam1start))`,
params: {
exam1start: exam1.start,
},
},
},
},
],
},
}));

return {
key: `no-exam-clash-${semester}`,
label: `No Exam Clash (${config.shortSemesterNames[semester]})`,
Expand All @@ -60,8 +106,8 @@ function getExamClashFilter(semester: Semester, examDates: string[]): FilterItem
nested: {
path: 'semesterData',
query: {
terms: {
'semesterData.examDate': examDates,
bool: {
must_not: clashRanges,
},
},
},
Expand All @@ -83,10 +129,20 @@ const ModuleFinderSidebar: React.FC = () => {
const examClashFilters = Semesters.map((semester): FilterItem | null => {
const timetable = getSemesterTimetable(semester);
const modules = getSemesterModules(timetable, allModules);
const examDates = modules
.map((module) => getModuleSemesterData(module, semester)?.examDate)
const examTimings: ExamTiming[] = modules
.map((mod) => {
// Filter for modules with non-empty exam timings, and map them to new ExamTiming objects
const data = getModuleSemesterData(mod, semester);
if (data?.examDate && data?.examDuration) {
return {
start: data.examDate,
duration: data.examDuration,
};
}
return null;
})
.filter(notNull);
return examDates.length ? getExamClashFilter(semester, examDates) : null;
return examTimings.length ? getExamClashFilter(semester, examTimings) : null;
}).filter(notNull);
return [...STATIC_EXAM_FILTER_ITEMS, ...examClashFilters];
}, [getSemesterTimetable, allModules]);
Expand Down