Files
psu-schedule/server/diagnostics.js
Zakhar Timoshenko 297091cb0e feat: Add Settings and Subjects screens with theme reveal functionality
- Implemented SettingsScreen for user preferences including theme selection, reminders, and notifications.
- Created SubjectsScreen to display subjects with upcoming lessons and associated details.
- Introduced ThemeReveal for a smooth transition between light and dark themes.
- Added support for managing notes and tasks related to subjects.
- Enhanced UI components for better user experience and accessibility.
2026-10-24 02:47:37 +03:00

122 lines
6.5 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.
import { parseWeekRanges, weekday, DATE_LIST } from "../shared/weeks.js";
import { roomMoves } from "./rooms.js";
// The parser never guesses: a cell it cannot read becomes a lesson "every week" or keeps its
// whole text as the subject. These checks find such cells after parsing, so a new format or a
// typo in Word shows up as soon as the document is published instead of when a student notices.
// "мая" is a month, "Майнор" is not.
const MONTHS = /(?<![а-яё])(?:(?:январ|феврал|март|апрел|июн|июл|август|сентябр|октябр|ноябр|декабр)[а-яё]*|ма[йя](?![а-яё]))/iu;
// The longest semester has about twenty weeks; "27н." is a typo, not a week.
const LAST_WEEK = 24;
const message = {
weeks_unreadable: "Пометка недель не распознана: занятие показывается каждую неделю.",
weeks_out_of_range: "Номер недели больше, чем недель в семестре.",
weeks_like_date: "Пометка недель похожа на дату с опечаткой.",
dates_unread: "В ячейке есть даты, но ни одна не распознана: занятие показывается каждую неделю.",
date_weekday: "Дата приходится на другой день недели, чем строка таблицы.",
date_out_of_year: "Дата вне учебного года документа.",
weeks_partly: "Пометка недель прочитана не целиком: часть недель попала в название предмета.",
date_in_subject: "Дата в начале ячейки не распознана: занятие показывается каждую неделю.",
time_in_subject: "Время в начале ячейки не распознано: занятие стоит во время пары, а время попало в название.",
subject_missing: "Название предмета не выделено.",
time_invalid: "Время пары не распознано или выглядит неправдоподобно.",
group_empty: "В заголовке есть группа, но ни одного её занятия.",
};
/** Problems found in one parsed document: `{code, message, groups, day, start, raw, …}`. */
export function diagnose(schedule) {
const found = [];
const add = (code, lesson, extra = {}) =>
found.push({
code,
message: message[code],
...(lesson && {
// Word repeats a group in some headers ("24-ИЯ (РА)и-1_…и-2"): once is enough.
groups: [...new Set(lesson.groups)],
day: lesson.day,
start: lesson.start || null,
raw: lesson.raw,
}),
...extra,
});
const year = Number(schedule.academicYear) || null;
for (const lesson of schedule.lessons) {
const raw = lesson.raw || "";
const dated = Boolean(lesson.date) || lesson.dates?.length > 0;
if (lesson.condition && !dated) {
const ranges = lesson.weekRanges ?? parseWeekRanges(lesson.condition);
// "19,11, 03.12, 17.12": a date typed with a comma reads as weeks 19 and 11.
if (!/н/i.test(lesson.condition) && DATE_LIST.test(raw.slice(lesson.condition.length - 1)))
add("weeks_like_date", lesson, { condition: lesson.condition });
else if (!ranges) add("weeks_unreadable", lesson, { condition: lesson.condition });
else if (ranges.some(([a, b]) => Math.max(a, b ?? a) > LAST_WEEK))
add("weeks_out_of_range", lesson, { condition: lesson.condition });
DATE_LIST.lastIndex = 0;
}
// A move to another room on a date ("412н 26.10.2026-333") is read, not a lesson date.
const undated = roomMoves(raw).rest;
if (!dated && !lesson.condition && (MONTHS.test(undated) || /\b\d{1,2}\.\d{2}\.\d{4}\b/.test(undated)))
add("dates_unread", lesson);
for (const date of lesson.date ? [lesson.date] : lesson.dates || []) {
if (weekday(date) !== lesson.day) add("date_weekday", lesson, { date });
if (year && (date < `${year}-08-01` || date > `${year + 1}-08-31`))
add("date_out_of_year", lesson, { date });
}
const subject = (lesson.subject || "").trim();
const lead = subject.match(/^(\d{1,2})([.:])(\d{2})(\.?)(?:\s*[-–]\s*\d{1,2}[.:]\d{2})?/);
if (/^[;,]?\s*\d{1,2}(?:\s*[-–,;]\s*\d{1,2})*\s*н/i.test(subject)) add("weeks_partly", lesson, { subject });
else if (lead) {
const [whole, a, sep, b, dot] = lead;
// "3.12" or "24.11." is a date, "9.00", "17.00" or "11:10-11:30" a time.
const date = sep === "." && whole === `${a}.${b}${dot}` && Number(b) >= 1 && Number(b) <= 12 && (dot || Number(a) < 7 || Number(a) > 23);
add(date ? "date_in_subject" : "time_in_subject", lesson, { subject });
} else if (!subject || /^[\d.,;:–-]/.test(subject) || (subject === raw.trim() && raw.length > 120))
add("subject_missing", lesson, { subject });
const start = lesson.start || "";
if (!/^\d{2}:\d{2}$/.test(start) || start < "07:00" || start > "21:00" || (lesson.end && lesson.end <= start))
add("time_invalid", lesson, { end: lesson.end || null });
}
for (const group of schedule.groups)
if (!schedule.lessons.some((l) => l.groups.includes(group))) add("group_empty", null, { groups: [group] });
return found;
}
const reports = new WeakMap();
/**
* Every catalog document's problems, newest index only: computed once per index build.
* Filters: `group` (as printed, "23-АРХ"), `doc` (catalog id) and `code`.
*/
export function catalogDiagnostics(idx, { group = "", doc = "", code = "" } = {}) {
if (!reports.has(idx))
reports.set(
idx,
idx.docs.map(({ id, file, schedule }) => ({
doc: { id, name: file.name, faculty: file.faculty || schedule.source.faculty || null, url: file.url || null },
problems: diagnose(schedule),
})),
);
const key = group.replace(/\s+/g, "").toLowerCase();
const items = reports
.get(idx)
.filter((r) => !doc || r.doc.id === doc)
.map((r) => ({
...r,
problems: r.problems.filter(
(p) => (!code || p.code === code) && (!key || p.groups?.some((g) => g.toLowerCase() === key)),
),
}))
.filter((r) => r.problems.length);
const counts = {};
for (const r of items) for (const p of r.problems) counts[p.code] = (counts[p.code] || 0) + 1;
return {
builtAt: idx.builtAt,
documents: idx.docs.length,
problems: items.reduce((n, r) => n + r.problems.length, 0),
counts,
failed: doc || group || code ? [] : idx.failed,
items,
};
}