116 lines
4.7 KiB
JavaScript
116 lines
4.7 KiB
JavaScript
// 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 }));
|
||
}
|