Files
psu-schedule/server/rooms.js

125 lines
6.2 KiB
JavaScript
Raw Permalink 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 } from "../shared/weeks.js";
// Rooms and teachers as Word cells write them, shared by the parser, the directory and the
// diagnostics: room tokens, dated moves ("26.10.2026-333") and plans by week ("1-9н. 502н,
// с 10н. – 359", "бел.н.504н/зел.н.524н", "2-9н.-Клос О.Ю., 10-14н.-Конторов В.Э.").
export const PERSON = /[А-ЯЁ][а-яё]+(?:-[А-ЯЁ][а-яё]+)?\s+[А-ЯЁ]\.\s*[А-ЯЁ]\./gu;
export const personName = (s) => s.replace(/\s+/g, " ").replace(/\.\s+/g, ".").trim();
// "129", "207Г", "202МК", "504Н", and the "В0-03" style. Rooms have at least two digits.
export const ROOM_TOKEN = /[А-ЯA-Z]\d-\d{2}|\d{2,4}\/\d{2,4}|\d{2,4}[А-ЯA-Z]{0,2}/gu;
const ROOM_TEXT = "[А-ЯA-Z]\\d-\\d{2}|\\d{2,4}(?:\\s?(?!и(?![а-яё]))[А-Яа-я]{1,2}(?![а-яё]))?";
// "(13.10-ауд.510н)", "14.10. -ауд. 333", "07.09. ауд. 414", "07.10-203мк", "26.10.2026-333":
// on that date the lesson moves.
export const ROOM_MOVE = new RegExp(
`(\\d{1,2})\\.(\\d{1,2})(?:\\.(\\d{4}|\\d{2})(?!\\d))?\\.?\\s*(?:[-–]\\s*(?:ауд\\.?|а\\.)?|(?:ауд\\.?|а\\.))\\s*(${ROOM_TEXT})`,
"giu",
);
// Word files mix Latin look-alikes into room numbers ("12C" with a Latin C).
const LATIN = { A: "А", B: "В", C: "С", E: "Е", H: "Н", K: "К", M: "М", O: "О", P: "Р", T: "Т", X: "Х" };
export const roomKey = (s) =>
(s || "")
.replace(/\s+/g, "")
.toUpperCase()
.replace(/[ABCEHKMOPTX]/g, (c) => LATIN[c])
.replace(/\.$/, "");
export const tokens = (text) =>
(text || "")
.toUpperCase()
// "210 мк" → "210МК"; "129 и 130" stays two rooms.
.replace(/(\d{2,4})\s+(?!И(?![А-ЯЁ]))([А-ЯЁ]{1,2})(?![А-ЯЁ])/gu, "$1$2")
.match(ROOM_TOKEN) || [];
const validDate = (d, m) => Number(d) >= 1 && Number(d) <= 31 && Number(m) >= 1 && Number(m) <= 12;
const isoFor = (day, month, year, explicit) => {
const m = Number(month),
y = explicit ? Number(explicit.length === 2 ? `20${explicit}` : explicit) : m >= 8 ? Number(year) : Number(year) + 1;
return `${y}-${String(m).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
};
/** Dated moves in a text and the text without them: `{moves: [{date, room}], rest}`. */
export function roomMoves(text, year) {
const moves = [];
const rest = (text || "").replace(ROOM_MOVE, (match, d, m, y, room) => {
if (!validDate(d, m)) return match;
if (year || y) moves.push({ date: isoFor(d, m, year, y), room: roomKey(room) });
return " ";
}).replace(/\(\s*\)/g, " ");
// "402н (13.10-ауд.510н)" leaves "402н ( )": the brackets go with the move.
return { moves, rest };
}
// 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) {
const starts = [...text.matchAll(WEEK_MARK)].map((m) => m.index);
if (!starts.length || starts[0] > 0) starts.unshift(0);
return starts.map((start, i) => text.slice(start, starts[i + 1]).trim()).filter(Boolean);
}
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, " "))
.map(roomKey)
.filter((r) => !/ДО$/.test(r));
/**
* Rooms and teachers that depend on the week, from a line of a cell: entries
* `{weeks: [[a, b]] | null, color: "white" | "green" | null, rooms, teachers}`. Empty when the
* line names none of them by week.
*/
export function weekPlan(text) {
const plan = [];
for (const part of partsByWeek(text || "")) {
const mark = part.match(PART_MARK)?.[0] || "";
const weeks = mark ? parseWeekRanges(mark) : null;
if (mark && !weeks) continue;
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 });
}
}
// "2-9н.-Клос О.Ю., 10-14н.-Конторов В.Э. 319Д": teachers by week, then the room of them all.
const last = plan.at(-1);
if (plan.length > 1 && last.rooms.length && plan.slice(0, -1).every((p) => !p.rooms.length && p.teachers.length && !p.color))
for (const p of plan.slice(0, -1)) p.rooms = [...last.rooms];
return plan;
}
/**
* Whether a line names only rooms, teachers and weeks, with no subject: "1-9н. 502н",
* "с 10н. – 533н", "2-9н.-Клос О.Ю., 10-14н.-Конторов В.Э. 319Д". It continues the lesson above.
*/
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, " ");
// A word, or an abbreviation of a subject ("РБ"): not only a place.
return !/[А-Яа-яЁё]{3,}|[А-ЯЁ]{2,}/u.test(left);
}