Files
psu-schedule/shared/lessons.js

116 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Lesson identity and schedule comparison, shared by the browser and the tests.
// Lesson ids depend on the table row, so they change whenever the university
// republishes a file; these keys describe what a student would call "the same lesson".
const norm = (s) => (s || "").toLowerCase().replace(/\s+/g, " ").trim();
const subgroupOf = (lesson, group) =>
lesson.subgroups?.find((s) => s.group === group)?.label || "";
/** Same day, time, subject, row colour and subgroup: survives republishing. */
export function lessonKey(lesson, group) {
return [
lesson.date || lesson.day,
lesson.start,
norm(lesson.subject),
lesson.variant,
subgroupOf(lesson, group),
norm(lesson.condition),
].join("|");
}
// Looser identity for pairing a lesson whose time or week mark changed.
const looseKey = (lesson, group) =>
[norm(lesson.subject), lesson.variant, subgroupOf(lesson, group), norm(lesson.type)].join("|");
const FIELDS = [
["start", "Начало"],
["end", "Окончание"],
["day", "День"],
["date", "Дата"],
["room", "Аудитория"],
// "Полоцк": the town a cell or the day's note names (parser 14).
["place", "Город"],
["teacher", "Преподаватель"],
["condition", "Недели"],
["type", "Вид занятия"],
];
/**
* What changed for one group between two versions of the same document.
* Returns { added, removed, changed } where changed items list field differences.
*/
export function diffSchedules(before, after, group) {
const pick = (s) => (s?.lessons || []).filter((l) => l.groups.includes(group));
const removed = [],
added = [],
changed = [];
const old = new Map();
for (const lesson of pick(before)) {
const key = lessonKey(lesson, group);
old.set(key, [...(old.get(key) || []), lesson]);
}
const unmatched = [];
for (const lesson of pick(after)) {
const key = lessonKey(lesson, group),
list = old.get(key);
if (list?.length) {
const was = list.shift();
const fields = differences(was, lesson);
if (fields.length) changed.push({ before: was, after: lesson, fields });
} else unmatched.push(lesson);
}
const left = [...old.values()].flat();
// Pair the rest by subject on the same day first, then by subject alone (a lesson moved to another day).
for (const sameDay of [true, false])
for (const lesson of [...unmatched]) {
const index = left.findIndex(
(l) =>
looseKey(l, group) === looseKey(lesson, group) &&
(!sameDay || (l.date || l.day) === (lesson.date || lesson.day)),
);
if (index < 0) continue;
const [was] = left.splice(index, 1);
unmatched.splice(unmatched.indexOf(lesson), 1);
changed.push({ before: was, after: lesson, fields: differences(was, lesson) });
}
added.push(...unmatched);
removed.push(...left);
const order = (l) => `${l.date || ""}${l.day}${l.start}`;
added.sort((a, b) => order(a).localeCompare(order(b)));
removed.sort((a, b) => order(a).localeCompare(order(b)));
changed.sort((a, b) => order(a.after).localeCompare(order(b.after)));
return { added, removed, changed };
}
function differences(before, after) {
return FIELDS.filter(([field]) => (before[field] ?? "") !== (after[field] ?? "")).map(
([field, label]) => ({ field, label, from: before[field] ?? "", to: after[field] ?? "" }),
);
}
export const changeCount = (diff) =>
diff ? diff.added.length + diff.removed.length + diff.changed.length : 0;
/** Applies the student's own choices: their subgroup and the lessons they hid. */
export function personalFilter(lesson, group, prefs) {
const own = subgroupOf(lesson, group);
if (prefs?.subgroup && own && own !== prefs.subgroup) return "other-subgroup";
if (prefs?.hidden?.includes(lessonKey(lesson, group))) return "hidden";
return "shown";
}
const inWeeks = (ranges, n) => !ranges || (n != null && ranges.some(([a, b]) => n >= a && (b === null || n <= b)));
/**
* A lesson's rooms and teachers by week ("1-9н. 502н", "бел.н.504н/зел.н.524н",
* "ауд.533н-б.н.,420н-з.н."): the plan entries for a week `{n, color}`, by its number, its colour
* or both. Shared by the server's timetable and the browser.
*/
export const planFor = (plan, week) =>
week && plan?.length ? plan.filter((p) => inWeeks(p.weeks, week.n) && (!p.color || p.color === week.color)) : [];
/** Subgroup labels present for a group, in column order. */
export function subgroupsOf(schedule, group) {
const labels = new Set();
for (const lesson of schedule?.lessons || [])
if (lesson.groups.includes(group)) {
const label = subgroupOf(lesson, group);
if (label) labels.add(label);
}
return [...labels].sort((a, b) => a.localeCompare(b, "ru", { numeric: true }));
}