initial commit
This commit is contained in:
105
shared/lessons.js
Normal file
105
shared/lessons.js
Normal file
@@ -0,0 +1,105 @@
|
||||
// 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", "Аудитория"],
|
||||
["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";
|
||||
}
|
||||
/** 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 }));
|
||||
}
|
||||
Reference in New Issue
Block a user