feat: enhance lesson plan handling with new parser and color-coded room assignments

This commit is contained in:
2026-10-23 00:33:13 +03:00
parent c0487e4c2a
commit 02e6404731
11 changed files with 121 additions and 18 deletions

View File

@@ -12,6 +12,7 @@ import {
weekday,
weekIndexOf,
} from "../shared/weeks.js";
import { planFor as planForWeek } from "../shared/lessons.js";
let index = null,
building = null;
@@ -87,10 +88,8 @@ export function peopleAndRooms(lesson, year) {
};
}
const inWeeks = (ranges, n) => !ranges || (n != null && ranges.some(([a, b]) => n >= a && (b === null || n <= b)));
/** The lesson's plan entries for a week `{n, color}`: by its number, its colour or both. */
const planFor = (entry, week) =>
week && entry.plan?.length ? entry.plan.filter((p) => inWeeks(p.weeks, week.n) && (!p.color || p.color === week.color)) : [];
const planFor = (entry, week) => planForWeek(entry.plan, week);
/**
* Rooms a lesson uses on a date: a dated move wins, then the room for that week ("1-9н. 502н",

View File

@@ -10,7 +10,7 @@ import { parseWeekRanges, parseTextDates, DATE_LIST, weekday } from "../shared/w
import { roomMoves, onlyPlace, weekPlan, partsByWeek } from "./rooms.js";
import { placeNote, lessonPlace, placeOf } from "./places.js";
const run = promisify(execFile);
export const PARSER_VERSION = 14;
export const PARSER_VERSION = 15;
const days = [
"понедельник",
"вторник",

View File

@@ -51,8 +51,9 @@ export function roomMoves(text, year) {
return { moves, rest };
}
// A week mark opening a part of a line: "1-9н.", "с 10н", "14-17н.", "7,9,11н.".
const WEEK_MARK = /(?<=^|[\s,;])(?:(?:с|со)\s*)?\d{1,2}(?:\s*[-–,]\s*\d{1,2})*\s*н(?:ед)?(?![а-яё])\.?/giu;
// A week mark opening a part of a line: "1-9н.", "с 10н", "14-17н.", "7,9,11н.". Not a room
// with its week's colour: "32н-з.н." is room 32Н in green weeks.
const WEEK_MARK = /(?<=^|[\s,;])(?:(?:с|со)\s*)?\d{1,2}(?:\s*[-–,]\s*\d{1,2})*\s*н(?:ед)?(?![а-яё])(?!\.?\s*[-–]?\s*[бз](?:ел)?\.?\s*н)\.?/giu;
/** A line cut where each week mark begins: "1-13н-332, 14-17н.-502н" → two parts. */
export function partsByWeek(text) {
@@ -63,6 +64,8 @@ export function partsByWeek(text) {
const PART_MARK = /^(?:(?:с|со)\s*)?\d{1,2}(?:\s*[-–,]\s*\d{1,2})*\s*н(?:ед)?\.?/iu;
// "бел.н.504н/зел.н.524н", "б.н.301Г/з.н.317Д": a room for white weeks and one for green ones.
const BY_COLOR = /б(?:ел)?\.?\s*н(?:ед)?\.?\s*([^/\s]+)\s*\/\s*з(?:ел)?\.?\s*н(?:ед)?\.?\s*([^\s,;]+)/iu;
// "ауд.533н-б.н.,420н-з.н.": the colour after each room.
const ROOM_COLOR = new RegExp(`(${ROOM_TEXT})\\s*[-–]?\\s*(б|з)(?:ел)?\\.?\\s*н(?:ед)?\\.?(?![а-яё])`, "giu");
const roomsIn = (text) =>
tokens(text.replace(PERSON, " ").replace(/\([^)]*\)/g, " "))
@@ -83,9 +86,15 @@ export function weekPlan(text) {
const rest = part.slice(mark.length);
const teachers = [...rest.matchAll(PERSON)].map((m) => personName(m[0]));
const color = rest.match(BY_COLOR);
const byRoom = [...rest.matchAll(ROOM_COLOR)];
if (color) {
plan.push({ weeks, color: "white", rooms: roomsIn(color[1]), teachers });
plan.push({ weeks, color: "green", rooms: roomsIn(color[2]), teachers });
} else if (byRoom.length) {
for (const [name, letter] of [["white", "б"], ["green", "з"]]) {
const rooms = byRoom.filter((m) => m[2].toLowerCase() === letter).map((m) => roomKey(m[1]));
if (rooms.length) plan.push({ weeks, color: name, rooms, teachers });
}
} else if (mark) {
const rooms = roomsIn(rest);
if (rooms.length || teachers.length) plan.push({ weeks, color: null, rooms, teachers });
@@ -106,6 +115,7 @@ export function onlyPlace(text) {
const left = (text || "")
.replace(PERSON, " ")
.replace(BY_COLOR, " ")
.replace(ROOM_COLOR, " ")
.replace(/(?:(?:с|со)\s*)?\d{1,2}(?:\s*[-–,]\s*\d{1,2})*\s*н(?:ед)?\.?/giu, " ")
.replace(/ауд\.?|а\./giu, " ")
.replace(/[А-ЯA-Z]\d-\d{2}|\d{2,4}\s?[А-Яа-я]{0,2}(?![а-яё])/gu, " ");

View File

@@ -4,6 +4,8 @@ import { request, MIN_REFRESH } from "./source.js";
import { DATA } from "./paths.js";
const data = DATA + "parsed/";
// The downloaded Word files: a new parser reads them again without asking the university.
const originals = DATA + "documents/";
// When each document was last downloaded, and the downloads under way: a forced refresh
// downloads a document at most once per MIN_REFRESH, and simultaneous ones share a download.
const fetchedAt = new Map(),
@@ -16,11 +18,12 @@ export async function scheduleFor(file, { refresh = false } = {}) {
if (!refresh)
try {
const cached = JSON.parse(await readFile(cache, "utf8"));
if (
cached.schemaVersion === PARSER_VERSION &&
cached.source.modified === file.modified
)
return cached;
if (cached.source.modified === file.modified) {
if (cached.schemaVersion === PARSER_VERSION) return cached;
// The same file read by an older parser: parse the saved copy again.
const saved = await readFile(originals + file.id).catch(() => null);
if (saved) return parse(file, saved, cache);
}
} catch {}
if (!loading.has(file.id))
loading.set(
@@ -33,6 +36,12 @@ export async function scheduleFor(file, { refresh = false } = {}) {
async function download(file, cache) {
const buffer = await request(file.path);
fetchedAt.set(file.id, Date.now());
await mkdir(originals, { recursive: true });
await writeFile(originals + file.id, buffer);
return parse(file, buffer, cache);
}
async function parse(file, buffer, cache) {
const parsed = await parseWord(buffer, {
name: file.name,
url: file.url,