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 = /(? 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, }; }