125 lines
6.2 KiB
JavaScript
125 lines
6.2 KiB
JavaScript
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);
|
||
}
|