education.timetable-clash@1.0.0
impl/typescript.ts
2,196 bytes · the TypeScript implementation · view raw
import { type TimetableClash, type TimetableSession } from "./education_timetable_clash_types.ts";
function shared(a: readonly string[], b: readonly string[]): string[] {
const out: string[] = [];
for (const x of a) {
if (b.includes(x) && !out.includes(x)) out.push(x);
}
return out;
}
/**
* Every pair of sessions that overlap on the same day and share a room,
* teacher or group. Times are half-open: a session ending at 600 does not
* clash with one starting at 600.
*/
export function timetableClashes(sessions: readonly TimetableSession[]): readonly TimetableClash[] {
const ids = new Set<string>();
for (const s of sessions) {
if (ids.has(s.id)) throw new RangeError(`session id "${s.id}" appears twice`);
ids.add(s.id);
if (!Number.isInteger(s.day) || s.day < 1) {
throw new RangeError(`day must be a whole number of 1 or more, received ${s.day} for "${s.id}"`);
}
if (!Number.isInteger(s.start) || s.start < 0 || s.start > 1439) {
throw new RangeError(`start must be a whole minute from 0 to 1439, received ${s.start} for "${s.id}"`);
}
if (!Number.isInteger(s.end) || s.end <= s.start || s.end > 1440) {
throw new RangeError(`end must be a whole minute after start and no later than 1440, received ${s.end} for "${s.id}"`);
}
}
const found: { clash: TimetableClash; i: number; j: number }[] = [];
for (let i = 0; i < sessions.length; i++) {
for (let j = i + 1; j < sessions.length; j++) {
const a = sessions[i];
const b = sessions[j];
if (a.day !== b.day) continue;
const start = Math.max(a.start, b.start);
const end = Math.min(a.end, b.end);
if (start >= end) continue;
const rooms = shared(a.rooms, b.rooms);
const teachers = shared(a.teachers, b.teachers);
const groups = shared(a.groups, b.groups);
if (rooms.length + teachers.length + groups.length === 0) continue;
found.push({ clash: { first: a.id, second: b.id, day: a.day, start, end, rooms, teachers, groups }, i, j });
}
}
found.sort((x, y) => x.clash.day - y.clash.day || x.clash.start - y.clash.start || x.i - y.i || x.j - y.j);
return found.map((f) => f.clash);
}