import { test } from "node:test"; import assert from "node:assert/strict"; import { groupsIn, alternatives, entries, details, leadingTime, parallel, audienceOf, clean } from "../server/parser.js"; import { placeOf } from "../server/places.js"; import { roomMoves, weekPlan, onlyPlace } from "../server/rooms.js"; import { peopleAndRooms, roomsOn, teachersOn, movedOn } from "../server/directory.js"; // Cell formats found by the catalog's diagnostics (October 2026). test("Группы с направлением в скобках: 24-ИЯ (РА)и-1 … и-6 — разные группы", () => { assert.deepEqual(groupsIn("24-ИЯ (РА)и-1 | 24-ИЯ (РА)и-2 | 24-ИЯ (РА)и-3"), ["24-ИЯ (РА)и-1", "24-ИЯ (РА)и-2", "24-ИЯ (РА)и-3"]); assert.deepEqual(groupsIn("25 ИЯ (РА)и-1"), ["25-ИЯ (РА)и-1"]); assert.deepEqual(groupsIn("24-РГФи-1 26-ИТ-2"), ["24-РГФи-1", "26-ИТ-2"]); }); test("Перенос аудитории на дату, и с годом: «412н 26.10.2026-333»", () => { assert.deepEqual(roomMoves("Типология (л.) Полякова Г.В. 412н 26.10.2026-333", 2026).moves, [{ date: "2026-10-26", room: "333" }]); assert.deepEqual(roomMoves("Язык (п.з.) Горбач М.Н. 510н\n28.10.2026-504н", 2026).moves, [{ date: "2026-10-28", room: "504Н" }]); assert.deepEqual(roomMoves("ауд. 432 14.10. -ауд. 333", 2026).moves, [{ date: "2026-10-14", room: "333" }]); // The parser keeps the usual room; the move goes to that date only. const lesson = { raw: "Типология (л.) Полякова Г.В. 412н 26.10.2026-333", room: "412н", teacher: "Полякова Г.В." }; const entry = peopleAndRooms(lesson, "2026"); assert.deepEqual(roomsOn(entry, "2026-10-26"), ["333"]); assert.equal(movedOn(entry, "2026-10-26"), true); assert.deepEqual(roomsOn(entry, "2026-10-19"), ["412Н"]); assert.equal(movedOn(entry, "2026-10-19"), false); }); test("Строка с аудиторией по неделям продолжает занятие, а не становится новым", () => { const [one, ...rest] = entries("Язык профессионального общения (англ) (п.з.) Шабунина О.А.\n1-9н. 502н\nс 10н -359"); assert.equal(rest.length, 0); assert.equal(details(one.main).subject, "Язык профессионального общения (англ)"); assert.deepEqual(one.plan, [ { weeks: [[1, 9]], color: null, rooms: ["502Н"], teachers: [] }, { weeks: [[10, null]], color: null, rooms: ["359"], teachers: [] }, ]); const entry = peopleAndRooms({ raw: one.text, room: "", teacher: "Шабунина О.А.", plan: one.plan }, "2026"); assert.deepEqual(roomsOn(entry, "2026-10-20", { n: 8, color: "green" }), ["502Н"]); assert.deepEqual(roomsOn(entry, "2026-11-03", { n: 10, color: "green" }), ["359"]); }); test("Преподаватель по неделям и аудитория по цвету недели", () => { const teachers = weekPlan("2-9н.-Клос О.Ю., 10-14н.-Конторов В.Э. 319Д"); // The room at the end is the room of both parts. assert.deepEqual(teachers, [ { weeks: [[2, 9]], color: null, rooms: ["319Д"], teachers: ["Клос О.Ю."] }, { weeks: [[10, 14]], color: null, rooms: ["319Д"], teachers: ["Конторов В.Э."] }, ]); const entry = peopleAndRooms({ raw: "2-14н. История литературы (п.з.)\n2-9н.-Клос О.Ю., 10-14н.-Конторов В.Э. 319Д", room: "", teacher: "", plan: teachers }, "2026"); assert.deepEqual(teachersOn(entry, { n: 3, color: "white" }), ["Клос О.Ю."]); assert.deepEqual(teachersOn(entry, { n: 12, color: "green" }), ["Конторов В.Э."]); const [lesson] = entries("с 10н. Дискурсивная практика (рус) (п.з.) Булах А.С.\nбел.н.504н/зел.н.524н"); const byColor = peopleAndRooms({ raw: lesson.text, room: "", teacher: "", plan: lesson.plan }, "2026"); assert.deepEqual(roomsOn(byColor, "2026-11-10", { n: 11, color: "white" }), ["504Н"]); assert.deepEqual(roomsOn(byColor, "2026-11-17", { n: 12, color: "green" }), ["524Н"]); // The colour after each room (25-ДО): "ауд.533н-б.н.,420н-з.н.". const [after] = entries("Теоретические основы воспитания и развития речи детей раннего возраста (п.з.) Савейко В.Л. ауд.533н-б.н.,420н-з.н."); assert.deepEqual(after.plan, [ { weeks: null, color: "white", rooms: ["533Н"], teachers: ["Савейко В.Л."] }, { weeks: null, color: "green", rooms: ["420Н"], teachers: ["Савейко В.Л."] }, ]); const roomAfter = peopleAndRooms({ raw: after.text, room: "533н", teacher: "Савейко В.Л.", plan: after.plan }, "2026"); assert.deepEqual(roomsOn(roomAfter, "2026-10-30", { n: 9, color: "white" }), ["533Н"]); assert.deepEqual(roomsOn(roomAfter, "2026-10-23", { n: 8, color: "green" }), ["420Н"]); assert.deepEqual(weekPlan("ауд. 420н з.н., 533н б.н.").map((p) => [p.color, p.rooms]), [["white", ["533Н"]], ["green", ["420Н"]]]); assert.equal(onlyPlace("533н-б.н.,420н-з.н."), true); // "32н" before its colour is a room, not week 32. assert.deepEqual(weekPlan("Коррекционная дошкольная педагогика (л.) Остапчук С.В. ауд.64н -б.н., 32н-з.н.").map((p) => [p.color, p.rooms]), [["white", ["64Н"]], ["green", ["32Н"]]]); assert.equal(onlyPlace("1-9н. 502н"), true); assert.equal(onlyPlace("13,15 н. РБ (л.з) Сивацкий Д.С.209н"), false); }); test("Строка с видом и неделями без предмета — тот же предмет в другие недели", () => { const list = entries("2-6н. Потребительское право (л.) Савицкая К.Д. ауд. А1-06\n7,9,11н. – (п.з.) Савицкая К.Д. ауд. А1-06 8,12,14н. – (п.з.) Савицкая К.Д. ауд. А1-06"); assert.deepEqual(list.map((e) => [details(e.main).condition, details(e.main).subject, details(e.main).type]), [ ["2-6н.", "Потребительское право", "Лекция"], ["7,9,11н.", "Потребительское право", "Практика"], ["8,12,14н.", "Потребительское право", "Практика"], ]); assert.equal(details(list[0].main).room, "А1-06"); }); test("Дисциплины по выбору: заголовок, предмет перед неделями, виды по неделям", () => { const read = (raw) => entries(raw).map((e) => ((d) => `${d.condition} ${d.subject} · ${d.type}`)(details(e.main))); assert.deepEqual(read("Дисциплины по выбору: 1-16 н. Логика (л./п.з.) Магалинский И.В. ауд. С0-05\nСоциология 1-10 н. (л.) Шидловский С.О. 11-16 н. (п.з.) Лапацуева А.С.С0-03"), [ "1-16 н. Логика · Лекция и практика", "1-10 н. Социология · Лекция", "11-16 н. Социология · Практика", ]); assert.deepEqual(read("Дисциплина по выбору: Социология ауд. С0-03\n1-10 н. (л.) Шидловский С.О.\n11-18 н. (п.з.) Лапацуева А.С."), [ "1-10 н. Социология · Лекция", "11-18 н. Социология · Практика", ]); assert.deepEqual(read("Дисциплина сцециализации: 4-14 н. Преступления в сфере информационных технологий (л.) Чижик Е.В. ауд. В0-05"), [ "4-14 н. Преступления в сфере информационных технологий · Лекция", ]); assert.deepEqual(read("1-11н. - л., 12-14 – п.з. Защита населения Дмитриченко Н.С. ауд. 210м/к"), [ "1-11н. Защита населения · Лекция", "12-14н. Защита населения · Практика", ]); }); test("Время в начале ячейки: интервал с дефисом, заголовок со временем", () => { assert.deepEqual(leadingTime("17:00-17:20-информационный час-Клос О.Ю. 319Д").starts, ["17:00"]); assert.equal(leadingTime("17:00-17:20-информационный час").end, "17:20"); assert.deepEqual(alternatives("18.35+20.10 Новополоцк:\n1-5 н. Судебно-следственная психология (л.) Колесникова Е.В. ауд. 246"), [ "18.35+20.10 1-5 н. Судебно-следственная психология (л.) Колесникова Е.В. ауд. 246", ]); }); test("Подгруппа в тексте ячейки, точка вместо занятия, заголовок заглавными", () => { const own = details("12н.2п/гр. Экономика организаций (л.з.) Лисиченок Е.П. ауд.331"); assert.equal(own.subject, "Экономика организаций"); assert.equal(own.ownSubgroup, 2); assert.deepEqual(entries("."), []); assert.equal(details("ВВЕДЕНИЕ В ВЫСШЕЕ ОБРАЗОВАНИЕ\nг. Полоцк, Полоцкий коллегиум\n26-ГЕО, 26-ИТ-1 Ректорский вход\n26-ИТ-2, 26-СТ обзорная экскурсия").subject, "ВВЕДЕНИЕ В ВЫСШЕЕ ОБРАЗОВАНИЕ"); // A two-line subject stays whole. assert.equal(details("Интеллектуальная собственность - драйвер инновационного развития\nна пространстве Евразии").subject.includes("Евразии"), true); }); test("Немецкий или французский: два занятия в одной ячейке на всю группу", () => { const cell = "Немецкий язык (п.з) – Найверт Ю.О. 208А,\nФранцузский язык (п.з.) – Глобенко М.В. 336Е"; const list = entries(cell); assert.deepEqual(list.map((e) => details(e.main).room), ["208А", "336Е"]); assert.deepEqual(list.map((e) => details(e.main).teacher), ["Найверт Ю.О.", "Глобенко М.В."]); assert.deepEqual(list[0].choice, ["Немецкий язык", "Французский язык"]); // A room on its own line stays with its lesson. assert.deepEqual( parallel("Теория языка (англ.) (л.) (кроме иностр.студ.)-Чеботарева Е.М.305Г\nИстория языка (англ.) (л) (для иностр.студ.) Третьяк З.И.\n321Д").at(-1), "История языка (англ.) (л) (для иностр.студ.) Третьяк З.И.\n321Д", ); // A lesson with its teacher on the next line is one lesson. assert.equal(entries("Русский язык как иностранный (п.з.)\nНемирская В.А. 305Г").length, 1); // The comma before the next week's lesson is no part of the room. assert.equal(details(entries("2-13н. Белорусский язык (п.з.) - Третьяк З.И. 331Е,\n2-13н. Стилистика (п.з.) – Коротких А.В. 301Г")[0].main).room, "331Е"); }); test("Пары для иностранных студентов и для остальных", () => { assert.equal(audienceOf("История языка (п.з.) (кроме иностр.студ.) Ковшик О.С. 305Г"), "local"); assert.equal(audienceOf("Русский язык как иностранный (п.з.) Немирская В.А. 305Г"), "foreign"); assert.equal(audienceOf("Современный разговорный иностранный язык (на базе второго иностранного языка)"), null); // Russian for foreign students while the others have physical education, in every spelling. const pe = (text) => entries(text).map((e) => [details(e.main).subject, e.audience]); assert.deepEqual(pe("Ф и з и ч е с к а я к у л ь т у р а\nРусский язык как иностранный (для иностранных студентов) Семченко Е.Д. ауд.313н"), [ ["Ф и з и ч е с к а я к у л ь т у р а", "local"], ["Русский язык как иностранный (для иностранных студентов)", "foreign"], ]); assert.deepEqual(pe("со 2 нед. Русский язык как иностранный (п.з.) Немирская В.А. 331Е со 2 нед. ФИЗИЧЕСКАЯ КУЛЬТУРА").map((p) => p[1]), ["foreign", "local"]); assert.equal(details(entries("Русский язык как иностранный (п.з.) Немирская В.А. 331Е ФИЗИЧЕСКАЯ КУЛЬТУРА")[0].main).room, "331Е"); // The town or the teacher on a line of its own is no lesson for the others. assert.equal(entries("17.00 Новополоцк\nРусский язык как иностранный (п.з.)\nКириллова В.С. ауд. 537н").length, 1); assert.equal(entries("Русский язык как иностранный (для иностранных студентов)\nХаткевич Т.В. ауд. 323н").length, 1); }); test("Вид занятия после преподавателя: фамилия не входит в название", () => { const after = details("Русский язык как иностранный Козыренко А.Н.(п.з.) ауд.100"); assert.equal(after.subject, "Русский язык как иностранный"); assert.equal(after.type, "Практика"); assert.equal(details("Русский язык как иностранный\nХаткевич Т.В. (п.з.) ауд.504н").subject, "Русский язык как иностранный"); // No subject at all: the cell's text stays visible. assert.equal(details("(л.з.) Алексеева Т.А. ауд. 33н").subject, "(л.з.) Алексеева Т.А. ауд. 33н"); }); test("Разрядка, «со 2 по 14 нед.», город с середины дня", () => { // Letters spaced out for show, words apart by several spaces. assert.equal(clean("Ф и з и ч е с к а я к у л ь т у р а"), "Физическая культура"); assert.equal(clean("З а н я т и я в П о л о ц к е"), "Занятия в Полоцке"); assert.equal(clean("со 2 по 14 нед. М о д у л ь « М а й н о р »"), "2-14 нед. Модуль «Майнор»"); // Initials and short words stay as they are. assert.equal(clean("Борун Е.Н. ауд. 100"), "Борун Е.Н. ауд. 100"); // "Занятия в Полоцке" in the 13:40 row: the morning is not in Polotsk. const places = [{ day: 5, date: null, groups: ["25-ИТ-1"], subgroups: [], place: "Полоцк", from: "13:40" }]; const at = (start) => placeOf({ day: 5, date: null, start, groups: ["25-ИТ-1"], subgroups: [] }, places, () => []); assert.equal(at("08:30"), null); assert.equal(at("15:20"), "Полоцк"); }); test("Старое имя разделённой группы: понятная ошибка, другое написание — та же группа", async () => { const { timetable } = await import("../server/timetable.js"); const schedule = { groups: ["24-ИЯ (РА)и-1", "24-ИЯ (РА)и-2"], lessons: [], notes: [], period: "Осенний семестр", source: {} }; const file = { id: "doc", name: "24-ИЯ (РА)и-1_24-ИЯ (РА)и-2.docx" }; assert.throws( () => timetable({ file, schedule, calendar: null, group: "24-ИЯ(РА)", from: "2026-10-19" }), (e) => e.code === "group_not_found" && /разделена: 24-ИЯ \(РА\)и-1, 24-ИЯ \(РА\)и-2/.test(e.message), ); assert.equal(timetable({ file, schedule, calendar: null, group: "24-ИЯ(РА)и-1", from: "2026-10-19" }).group, "24-ИЯ (РА)и-1"); });