feat(parser): update group parsing to handle special cases and improve regex patterns
feat(parser): enhance details extraction for lessons, including handling of subgroups and room moves feat(timetable): integrate room and teacher retrieval based on week context test(parser): add tests for group parsing with special characters and room move scenarios test(formats): implement tests for various lesson formats and their expected outputs feat(rooms): introduce room management functions for handling moves and week-based plans
This commit is contained in:
114
server/rooms.js
Normal file
114
server/rooms.js
Normal file
@@ -0,0 +1,114 @@
|
||||
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н.".
|
||||
const WEEK_MARK = /(?<=^|[\s,;])(?:(?:с|со)\s*)?\d{1,2}(?:\s*[-–,]\s*\d{1,2})*\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;
|
||||
|
||||
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);
|
||||
if (color) {
|
||||
plan.push({ weeks, color: "white", rooms: roomsIn(color[1]), teachers });
|
||||
plan.push({ weeks, color: "green", rooms: roomsIn(color[2]), 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(/(?:(?:с|со)\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);
|
||||
}
|
||||
Reference in New Issue
Block a user