From 5f7bffd6e4b7f68a1e0f190ecce3338bc33c103d Mon Sep 17 00:00:00 2001 From: Zakhar Timoshenko Date: Mon, 19 Oct 2026 19:36:19 +0300 Subject: [PATCH] Refactor code structure for improved readability and maintainability --- README.md | 20 ++- .../org/xtimms/psu/schedule/data/Api.kt | 32 ++-- ios/PSUSchedule/API.swift | 28 +++- server/calendar.js | 3 +- server/errors.js | 58 +++++++ server/index.js | 58 +++---- server/limits.js | 6 +- server/parser.js | 144 ++++++++++++++++-- server/source.js | 16 +- server/timetable.js | 38 +++-- test/errors.test.js | 27 ++++ test/fixtures/isf-credits-26-arh.docx | Bin 0 -> 33174 bytes test/fixtures/itzs-summer-2026.docx | Bin 0 -> 48235 bytes test/fixtures/itzs-winter-2024.docx | Bin 0 -> 54108 bytes test/limits.test.js | 8 +- test/parser.test.js | 76 +++++++++ test/timetable.test.js | 25 +++ 17 files changed, 454 insertions(+), 85 deletions(-) create mode 100644 server/errors.js create mode 100644 test/errors.test.js create mode 100644 test/fixtures/isf-credits-26-arh.docx create mode 100644 test/fixtures/itzs-summer-2026.docx create mode 100644 test/fixtures/itzs-winter-2024.docx diff --git a/README.md b/README.md index 61dcafd..3e2f1f0 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,9 @@ Vite работает на http://127.0.0.1:5173 и проксирует `/api` Каталог и папка года перечитываются раз в 15 минут, таблица недель и звонки — раз в 6 часов, распознанный документ — при изменении даты файла или версии парсера. Устаревшая копия отвечает сразу, а перечитывание идёт в фоне. Ждать приходится только при первом запуске, в начале нового учебного года и при «Обновить». «Обновить» (`?refresh=1`) принудительно перечитывает всё, но обращается к университету не чаще раза в 2 минуты (таблицу недель — раз в 10 минут), иначе отвечает только что прочитанной копией. После сбоя источника фоновые попытки тоже ждут 2 минуты. Если источник недоступен, используются сохранённые копии с явной пометкой. Фоновая синхронизация при закрытом приложении не реализована. -Ответы API сжимаются (gzip или brotli; семестр группы — около 200 КБ JSON, в gzip — около 5 КБ) и отдаются с `Cache-Control: no-cache` и `ETag`. Клиент хранит ответ, но каждый раз спрашивает заново. Если ничего не изменилось, приходит пустой `304`. Android-клиент делает это сам через дисковый кэш OkHttp. Импорт Word ограничен: два разбора одновременно, ещё четыре ждут, остальным — `429`; с одного адреса — до 10 файлов за 10 минут. За прокси (Caddy, туннель) адрес клиента берётся из `X-Forwarded-For`, если прокси находится в локальной или частной сети. +Ответы API сжимаются (gzip или brotli; семестр группы — около 200 КБ JSON, в gzip — около 5 КБ) и отдаются с `Cache-Control: no-cache` и `ETag`. Клиент хранит ответ, но каждый раз спрашивает заново. Если ничего не изменилось, приходит пустой `304`. Android-клиент делает это сам через дисковый кэш OkHttp. Импорт Word ограничен: два разбора одновременно, ещё четыре ждут, остальным — `429`; с одного адреса — до 10 файлов за 10 минут. За прокси (Caddy, туннель) адрес клиента берётся из `X-Forwarded-For`, если прокси находится в локальной или частной сети. Для `/api/timetable` ETag считается до любой работы — из версии документа, парсера и сервера, таблицы недель, параметров запроса и даты, — поэтому неизменённое расписание отвечает `304` примерно за миллисекунду, не открывая документ. + +Ошибки приходят как `{ "error": "текст для показа", "code": "…" }`. Клиенты опираются на `code`, а не на текст: `bad_request` (400), `unknown_endpoint`, `document_not_found`, `group_not_found` (404), `file_too_large` (413), `unreadable_file` (422), `too_many_requests` и `busy` (429), `source_unavailable` и `source_invalid` (502, каталог университета не ответил или не распознан), `source_timeout` (504), `internal` (500). Полный список — в `server/errors.js`. Android и iOS при `source_unavailable`, `source_timeout` и `busy` оставляют на экране сохранённую копию и показывают «Повторить». `GET /api/health` без обращения к сети показывает выбранный учебный год, возраст каталога, найденные файлы недель и звонков, семестры и предупреждения: папка нового года не найдена, сегодняшняя дата вне таблицы недель, источник не обновляется. @@ -63,7 +65,20 @@ npm run sync ## Границы парсера -Основной парсер читает XML и сетку таблиц `.docx`, включая горизонтальные и вертикальные объединения ячеек. Поддерживаются проверенные дневные шаблоны ФЭФ, ФИТ, ФКНЭ, ИСФ и ГФ, а также заочные установочные сессии с отдельными колонками предмета, аудитории и преподавателя. Это не универсальный парсер любых документов Word: без заголовков групп, дней и времени импорт возвращает понятную ошибку. +Основной парсер читает XML и сетку таблиц `.docx`, включая горизонтальные и вертикальные объединения ячеек. Поддерживаются проверенные дневные шаблоны ФЭФ, ФИТ, ФКНЭ, ИСФ и ГФ, заочные установочные сессии с отдельными колонками предмета, аудитории и преподавателя, а также заочные лабораторно-экзаменационные сессии. + +**Лабораторно-экзаменационные сессии заочников** (проверено на зимней сессии 23-ИТзс 2024 года и летней 2026 года, `test/fixtures/itzs-*.docx`) устроены иначе: +- **Группа.** Указана только в заголовке («группа 23-ИТзс»). +- **Недели.** В таблице две–три недели стоят рядом блоками столбцов «Д.н. — Время — Дисциплина», у каждого дня своя дата. Сессия поэтому не зависит от таблицы недель и показывается по своим датам даже в прошлом учебном году. API отдаёт её границы в поле `semester` с названием «Лабораторно-экзаменационная сессия». +- **Подгруппы.** Если под одним заголовком дисциплины столбец разделён, левая часть — подгруппа 1, правая — подгруппа 2, даже при неровном делении. +- **Типы и предмет.** Распознаются установочные лекции и занятия (`уст.л.`, `уст.пз.`), экзамены, зачёты, консультации и защита курсовой работы. Предмет берётся из кавычек («…по учебной дисциплине «Базы данных»»). +- **Время.** Если у экзамена указано только начало («8.30»), конец берётся по расписанию звонков. + +**Расписания зачётов** (папка «Расписание зачетов», проверено на 23-, 24- и 26-АРХ ИСФ, `test/fixtures/isf-credits-26-arh.docx`) — таблица «Дата — Время — группа»: +- **Дата и время.** В ячейке дня дата стоит перед днём недели («27.10.2026 вторник»), пустая ячейка продолжает день выше. Указано одно время — начало, конец берётся по расписанию звонков. Такое одиночное время теперь принимается и в других таблицах: прежде в документах «Введение в высшее образование» и в установочных сессиях пропадали строки вроде «Торжественное собрание 10.30». +- **Тип записи.** Строка без пометки считается зачётом, «Консультация по учебной дисциплине…» — консультацией. Все они попадают в обратный отсчёт сессии. + +Это не универсальный парсер любых документов Word: без заголовков групп, дней и времени импорт возвращает понятную ошибку. **Даты занятий рассчитываются по университетской таблице «Расписание белых и зеленых недель»** из корня каталога: неделя 1 начинается 01.09.2026 (вторник), нечётные недели белые, чётные зелёные. Пометка в белой или зелёной строке относится только к неделям этого цвета: `1-13н` в белой строке — недели 1, 3, …, 13, а в объединённой строке — каждая неделя. `с 3н.` означает «до конца таблицы». Текстовые даты вроде «15 октября» привязываются к своему дню. Нераспознанные пометки (`2,6-12-16н`) не скрываются, а помечаются для сверки с Word; исходная пометка всегда видна. Разбор пометок общий для сервера и браузера (`shared/weeks.js`), поэтому старые сохранённые расписания тоже получают даты. Вне периода таблицы приложение показывает все строки Word. Номера подгрупп соответствуют столбцам внутри группы; именованных подгрупп в этих шаблонах нет. @@ -173,6 +188,7 @@ SIMCTL_CHILD_PSU_API_URL=http://localhost:3010/ xcrun simctl launch booted org.x - `server/parser.js` — таблицы `.docx`, условия и преобразование `.doc`. - `server/index.js` — API и выдача собранной PWA. - `server/limits.js` — ограничения импорта: число одновременных разборов и запросов с одного адреса. +- `server/errors.js` — коды ошибок API и их HTTP-статусы. - `ios/Shared/` — модели, даты в часовом поясе Минска, расчёт пар и общее хранилище; общие для приложения и виджетов. - `ios/PSUSchedule/` — приложение iOS: `AppModel.swift` (состояние и загрузка), `API.swift`, `Reminders.swift`, экраны в `Views/`. - `ios/Widgets/` — виджеты WidgetKit. diff --git a/android/app/src/main/kotlin/org/xtimms/psu/schedule/data/Api.kt b/android/app/src/main/kotlin/org/xtimms/psu/schedule/data/Api.kt index f25ad6e..9de5e49 100644 --- a/android/app/src/main/kotlin/org/xtimms/psu/schedule/data/Api.kt +++ b/android/app/src/main/kotlin/org/xtimms/psu/schedule/data/Api.kt @@ -15,7 +15,15 @@ import java.io.File import java.io.IOException import java.util.concurrent.TimeUnit -class ApiException(message: String, val offline: Boolean = false) : Exception(message) +/** + * An error ready to show. [code] is the server's machine-readable reason (server/errors.js); + * [offline] means the data could not be fetched — no network, or the university's catalog + * is down — so a saved copy stays on screen and "Повторить" makes sense. + */ +class ApiException(message: String, val offline: Boolean = false, val code: String? = null) : Exception(message) + +/** Server codes after which trying again later can help. */ +private val SOURCE_DOWN = setOf("source_unavailable", "source_timeout", "busy") val json = Json { ignoreUnknownKeys = true @@ -44,14 +52,20 @@ class Api(baseUrl: String, cacheDir: File? = null) { client.newCall(Request.Builder().url(url).build()).execute().use { response -> val text = response.body?.string().orEmpty() if (!response.isSuccessful) { - val message = runCatching { - json.parseToJsonElement(text).jsonObject["error"]?.jsonPrimitive?.content - }.getOrNull() - // The server's catch-all 404 means it runs an older version without this endpoint. - if (response.code == 404 && path.startsWith("api/") && message == "Адрес не найден.") throw ApiException( + val body = runCatching { json.parseToJsonElement(text).jsonObject }.getOrNull() + val message = body?.get("error")?.jsonPrimitive?.content + // Servers before API version 5 send no code; their catch-all 404 has this text. + val code = body?.get("code")?.jsonPrimitive?.content + ?: if (response.code == 404 && message == "Адрес не найден.") "unknown_endpoint" else null + if (code == "unknown_endpoint") throw ApiException( "Сервер расписания не знает запрос /$path — на нём старая версия. Обновите сервер.", + code = code, + ) + throw ApiException( + message ?: "Сервер ответил ошибкой ${response.code}.", + offline = code in SOURCE_DOWN, + code = code, ) - throw ApiException(message ?: "Сервер ответил ошибкой ${response.code}.") } text } @@ -70,12 +84,12 @@ class Api(baseUrl: String, cacheDir: File? = null) { return call() } catch (e: IOException) { android.util.Log.w("Api", "GET $url failed (attempt ${attempt + 1})", e) - if (attempt == RETRIES - 1) throw ApiException(OFFLINE, offline = true) + if (attempt == RETRIES - 1) throw ApiException(OFFLINE, offline = true, code = "offline") delay(wait) wait *= 3 } } - throw ApiException(OFFLINE, offline = true) + throw ApiException(OFFLINE, offline = true, code = "offline") } private companion object { diff --git a/ios/PSUSchedule/API.swift b/ios/PSUSchedule/API.swift index 32087c5..9d0ba28 100644 --- a/ios/PSUSchedule/API.swift +++ b/ios/PSUSchedule/API.swift @@ -1,10 +1,13 @@ import Foundation import OSLog -/// An error with a message ready to show; `offline` when the server could not be reached. +/// An error with a message ready to show. `code` is the server's machine-readable reason +/// (server/errors.js); `offline` means the data could not be fetched — no network, or the +/// university's catalog is down — so the saved copy stays and "Повторить" makes sense. struct APIError: LocalizedError { var message: String var offline = false + var code: String? var errorDescription: String? { message } } @@ -25,6 +28,8 @@ struct API: Sendable { private static let log = Logger(subsystem: "org.xtimms.psu.schedule", category: "API") private static let offline = "Нет связи с сервером расписания." + /// Server codes after which trying again later can help. + private static let sourceDown: Set = ["source_unavailable", "source_timeout", "busy"] private static let retries = 3 // The server answers "no-cache" with an ETag: URLSession keeps the answer and asks again @@ -43,14 +48,21 @@ struct API: Sendable { if !items.isEmpty { components.queryItems = items } let url = components.url! let (data, response) = try await withRetry(url) - guard let http = response as? HTTPURLResponse else { throw APIError(message: Self.offline, offline: true) } + guard let http = response as? HTTPURLResponse else { throw APIError(message: Self.offline, offline: true, code: "offline") } guard (200..<300).contains(http.statusCode) else { - let message = (try? JSONSerialization.jsonObject(with: data) as? [String: Any])?["error"] as? String - // The server's catch-all 404 means it runs an older version without this endpoint. - if http.statusCode == 404 && message == "Адрес не найден." { - throw APIError(message: "Сервер расписания не знает запрос /\(path) — на нём старая версия. Обновите сервер.") + let body = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] + let message = body?["error"] as? String + // Servers before API version 5 send no code; their catch-all 404 has this text. + let code = body?["code"] as? String + ?? (http.statusCode == 404 && message == "Адрес не найден." ? "unknown_endpoint" : nil) + if code == "unknown_endpoint" { + throw APIError(message: "Сервер расписания не знает запрос /\(path) — на нём старая версия. Обновите сервер.", code: code) } - throw APIError(message: message ?? "Сервер ответил ошибкой \(http.statusCode).") + throw APIError( + message: message ?? "Сервер ответил ошибкой \(http.statusCode).", + offline: code.map(Self.sourceDown.contains) ?? false, + code: code + ) } do { return try JSONDecoder().decode(T.self, from: data) @@ -74,7 +86,7 @@ struct API: Sendable { wait *= 3 } } - throw APIError(message: Self.offline, offline: true) + throw APIError(message: Self.offline, offline: true, code: "offline") } /// The whole current semester; a server without range=semester answers the from/days window. diff --git a/server/calendar.js b/server/calendar.js index fcc6aba..6beb81f 100644 --- a/server/calendar.js +++ b/server/calendar.js @@ -1,4 +1,5 @@ import { readDocx } from "./parser.js"; +import { apiError } from "./errors.js"; const toIso = (d, m, y) => `${y}-${m.padStart(2, "0")}-${d.padStart(2, "0")}`; @@ -31,7 +32,7 @@ export function parseWeeksDocx(buffer) { } weeks.sort((a, b) => a.start.localeCompare(b.start)); if (!weeks.length) - throw new Error("Таблица недель не распознана."); + throw apiError(502, "source_invalid", "Таблица недель не распознана."); return weeks; } diff --git a/server/errors.js b/server/errors.js new file mode 100644 index 0000000..f9c0591 --- /dev/null +++ b/server/errors.js @@ -0,0 +1,58 @@ +/** + * Errors the API reports to clients: an HTTP status, a stable machine-readable `code` and a + * message ready to show. Clients branch on the code, never on the wording of the message. + * + * 400 bad_request the request itself is wrong (missing or malformed parameters) + * 404 unknown_endpoint no such API address: the server is older than the client + * 404 document_not_found the document left the catalog + * 404 group_not_found the document has no such group + * 413 file_too_large an import over 10 MB + * 422 unreadable_file an import that is not a readable Word table + * 429 too_many_requests the client sends too much; Retry-After says when to come back + * 429 busy the server is parsing other imports + * 502 source_unavailable the university catalog did not answer + * 502 source_invalid the catalog answered, but its folders or tables are not recognised + * 504 source_timeout the university catalog answered too slowly + * 500 internal a fault of the server itself + */ +export class ApiError extends Error { + constructor(status, code, message) { + super(message); + this.status = status; + this.code = code; + } +} + +export const apiError = (status, code, message) => new ApiError(status, code, message); + +const SOURCE_DOWN = "Каталог университета сейчас не отвечает. Попробуйте позже."; +const SOURCE_SLOW = "Каталог университета отвечает слишком долго. Попробуйте позже."; + +/** + * A failed download from the university as an API error: curl's exit code 28 (or our own + * kill on the time limit) is a timeout, any other failure means the source is unavailable. + */ +export function sourceError(error) { + if (error instanceof ApiError) return error; + const timedOut = error?.code === 28 || error?.killed || error?.signal === "SIGTERM"; + return timedOut + ? new ApiError(504, "source_timeout", SOURCE_SLOW) + : new ApiError(502, "source_unavailable", SOURCE_DOWN); +} + +/** Status, code and message for any error that reached the error handler. */ +export function describe(error) { + if (error instanceof ApiError) + return { status: error.status, code: error.code, message: error.message }; + if (error?.code === "LIMIT_FILE_SIZE") + return { + status: 413, + code: "file_too_large", + message: "Файл больше 10 МБ. Выберите документ меньшего размера.", + }; + return { + status: 500, + code: "internal", + message: "Не удалось загрузить расписание. Проверьте подключение и попробуйте ещё раз.", + }; +} diff --git a/server/index.js b/server/index.js index b838d59..74bf1b2 100644 --- a/server/index.js +++ b/server/index.js @@ -5,6 +5,8 @@ import multer from "multer"; import { parseWord, PARSER_VERSION } from "./parser.js"; import { scheduleFor } from "./schedules.js"; import { rateLimit, semaphore } from "./limits.js"; +import { apiError, describe } from "./errors.js"; +import { createHash } from "node:crypto"; import { timetable } from "./timetable.js"; import { getIndex, @@ -39,8 +41,9 @@ app.use("/api", (_req, res, next) => { }); // Raised whenever an endpoint is added or changed, so a deployment is easy to verify: // 2 — /api/timetable for the Android client; 3 — its range=semester; -// 4 — compression, ETag revalidation, throttled refresh and import limits. -const API_VERSION = 4; +// 4 — compression, ETag revalidation, throttled refresh and import limits; +// 5 — error codes (`code` beside `error`, see server/errors.js), timetable 304 before any work. +const API_VERSION = 5; // Liveness plus what the server currently reads from the university, without network calls. app.get("/api/health", async (_req, res) => { @@ -54,7 +57,7 @@ app.get("/api/health", async (_req, res) => { }); }); const isDate = (s) => /^\d{4}-\d{2}-\d{2}$/.test(s || ""); -const badRequest = (res, error) => res.status(400).json({ error }); +const badRequest = (res, error) => res.status(400).json({ error, code: "bad_request" }); // University-wide lookups over every catalog document. app.get("/api/directory/search", async (req, res, next) => { try { @@ -112,16 +115,28 @@ app.get("/api/timetable", async (req, res, next) => { if (!doc || !group) return badRequest(res, "Укажите документ (doc) и группу (group)."); if (!isDate(from)) return badRequest(res, "Дата должна быть в формате ГГГГ-ММ-ДД."); const file = await documentFor(doc); + const calendar = await directoryCalendar(); + const range = req.query.range === "semester" ? "semester" : undefined; + const today = todayIso(); + // The answer follows only from these: the document's version, the parser and the server + // that read it, the week table, the request and the date. An unchanged one comes back + // as an empty 304 before the document is opened or a single day is computed. + const etag = 'W/"tt-' + createHash("sha1") + .update([API_VERSION, PARSER_VERSION, file.id, file.modified, calendar?.updatedAt, group, from, req.query.days, range, today].join("|")) + .digest("base64url") + .slice(0, 22) + '"'; + res.set("ETag", etag); + if (req.fresh) return res.status(304).end(); res.json( timetable({ file, schedule: await scheduleFor(file), - calendar: await directoryCalendar(), + calendar, group, from, days: req.query.days, - range: req.query.range === "semester" ? "semester" : undefined, - today: todayIso(), + range, + today, }), ); } catch (e) { @@ -194,39 +209,30 @@ const upload = multer({ app.post("/api/import", importLimit, upload.single("file"), async (req, res, next) => { try { if (!req.file) - return res.status(400).json({ error: "Выберите файл Word." }); + return badRequest(res, "Выберите файл Word."); // Multer decodes multipart names as latin1 by default. const name = Buffer.from(req.file.originalname, "latin1").toString("utf8"); if (!/\.docx?$/i.test(name)) - return res - .status(400) - .json({ error: "Поддерживаются файлы .docx и .doc." }); + return badRequest(res, "Поддерживаются файлы .docx и .doc."); res.json(await parsing(() => parseWord(req.file.buffer, { name, uploaded: true }))); } catch (e) { - // A file that cannot be read is the client's problem (422); a busy server keeps its 429. - e.status ??= 422; - next(e); + // A file that cannot be read is the client's problem; a busy server keeps its 429. + next(e.status || e.code === "LIMIT_FILE_SIZE" ? e : apiError(422, "unreadable_file", e.message)); } }); const dist = fileURLToPath(new URL("../dist/", import.meta.url)); app.use(express.static(dist, { maxAge: 0 })); app.get("/{*path}", (req, res, next) => req.path.startsWith("/api/") - ? res.status(404).json({ error: "Адрес не найден." }) + ? res.status(404).json({ error: "Адрес не найден.", code: "unknown_endpoint" }) : res.sendFile(dist + "index.html", (err) => err && next(err)), ); -app.use((error, _req, res, _next) => { - console.error(error.message); - res - .status(error.code === "LIMIT_FILE_SIZE" ? 413 : error.status || 503) - .json({ - error: - error.code === "LIMIT_FILE_SIZE" - ? "Файл больше 10 МБ. Выберите документ меньшего размера." - : [404, 422, 429].includes(error.status) - ? error.message - : "Не удалось загрузить расписание. Проверьте подключение и попробуйте ещё раз.", - }); +app.use((error, req, res, _next) => { + const { status, code, message } = describe(error); + // Faults of the server keep their stack; expected errors need a line. + if (status >= 500 && code === "internal") console.error(req.method, req.originalUrl, error); + else console.error(`${req.method} ${req.originalUrl} → ${status} ${code}: ${error.message}`); + res.status(status).json({ error: message, code }); }); const port = Number(process.env.PORT || 3001); app.listen(port, process.env.HOST || "127.0.0.1", () => { diff --git a/server/limits.js b/server/limits.js index 0de19fd..df2722c 100644 --- a/server/limits.js +++ b/server/limits.js @@ -1,3 +1,5 @@ +import { apiError } from "./errors.js"; + /** * Limits for the expensive endpoints: parsing an uploaded Word file can take a LibreOffice * conversion and seconds of CPU, so one client must not be able to occupy the server. @@ -15,7 +17,7 @@ export function rateLimit({ max, windowMs, message }) { windows.set(req.ip, w); if (w.count <= max) return next(); res.set("Retry-After", String(Math.ceil((w.start + windowMs - now) / 1000))); - res.status(429).json({ error: message }); + res.status(429).json({ error: message, code: "too_many_requests" }); }; } @@ -29,7 +31,7 @@ export function semaphore({ limit, queue, message }) { return async function run(task) { if (active < limit) active++; else if (waiting.length < queue) await new Promise((resolve) => waiting.push(resolve)); - else throw Object.assign(new Error(message), { status: 429 }); + else throw apiError(429, "busy", message); try { return await task(); } finally { diff --git a/server/parser.js b/server/parser.js index 90a2929..8b65f6c 100644 --- a/server/parser.js +++ b/server/parser.js @@ -8,7 +8,7 @@ import { join } from "node:path"; import { createHash } from "node:crypto"; import { parseWeekRanges, parseTextDates } from "../shared/weeks.js"; const run = promisify(execFile); -export const PARSER_VERSION = 8; +export const PARSER_VERSION = 10; const days = [ "понедельник", "вторник", @@ -62,11 +62,14 @@ export function details(raw) { let rest = raw.slice(condition.length).trim(); // "Экзамен. Финансы …" or "Зачёт: …": the label is the kind of session event, not the subject. const leading = rest.match( - /^(экзамен|зач[её]т|консультация)(?![а-яё])[\s.:–—-]*/i, + /^(экзамен|зач[её]т|консультация|защита\s+курсов(?:ой|ого)\s+(?:работы|проекта))(?![а-яё])[\s.:–—-]*/i, ); if (leading) rest = rest.slice(leading[0].length); + // Correspondence sessions: "Экзамен по учебной дисциплине «Базы данных» Скуковская А.А. ауд.219Д". + const named = rest.match(/^по\s+(?:учебной\s+)?дисциплине\s*«([^»]+)»\s*/i); + if (named) rest = named[1].trim() + " " + rest.slice(named[0].length); const kind = rest.match( - /\((л\.?\s*з\.?|п\.?\s*з\.?|л\.?|лаб\.?|пр\.?|лек\.?|сем\.?|экз\.?|зач\.?|конс\.?)\)/i, + /\((уст\.?\s*л\.?|уст\.?\s*п\.?\s*з\.?|л\.?\s*з\.?|п\.?\s*з\.?|л\.?|лаб\.?|пр\.?|лек\.?|сем\.?|экз\.?|зач\.?|конс\.?)\)/i, ); // Whole words only: "консультирования" in a subject name is not a consultation. const sessionWord = (leading?.[1] || @@ -75,15 +78,19 @@ export function details(raw) { const person = rest.match(/[А-ЯЁ][а-яё-]+\s+[А-ЯЁ]\.\s*[А-ЯЁ]\./u); const room = rest.match(/(?:ауд\.?\s*)([\d]+\s*[А-Яа-я]?(?:\s*\([^)]*\))?)/i) || - rest.match(/\b(\d{2,3}\s*[А-Яа-я]?)\s*$/u); - let subject = rest - .slice(0, kind?.index ?? person?.index ?? room?.index ?? rest.length) + rest.match(/\b(\d{2,3}\s*[А-Яа-я]?)\s*$/u) || + // A room with a name instead of a number: "ауд.Медиа". + rest.match(/(?:ауд\.\s*)([А-ЯЁ][а-яё]{2,})(?![а-яё])/u); + let subject = (named ? named[1].trim() : rest + .slice(0, kind?.index ?? person?.index ?? room?.index ?? rest.length)) .trim() .replace(/[–—,\-]+$/, "") .trim(); if (!subject) subject = rest; + const defence = /^защита/i.test(leading?.[1] || ""); const token = kind?.[1].replace(/[.\s]/g, "").toLowerCase() || + (defence ? "защ" : undefined) || (sessionWord.startsWith("экз") ? "экз" : sessionWord.startsWith("зач") @@ -96,7 +103,13 @@ export function details(raw) { teacher: person?.[0] || "", room: room?.[1].trim().replace(/\s+/g, "") || "", type: - token === "лз" || token === "лаб" + token === "устл" + ? "Установочная лекция" + : token === "устпз" + ? "Установочное занятие" + : token === "защ" + ? "Защита курсовой работы" + : token === "лз" || token === "лаб" ? "Лабораторная" : token === "пз" || token === "пр" ? "Практика" @@ -205,6 +218,38 @@ export function readDocx(buffer) { .filter((s) => s && !/декан|начальник|_{3}|^утверждаю/i.test(s)), }; } +/** + * Laboratory-examination sessions of correspondence students: the group is named only in the + * title ("группа 23-ИТзс") and the table holds several weeks side by side. Each block of + * columns is "Д.н." (the weekday with its date), "Время" and "Дисциплина, преподаватель, + * аудитория", whose columns split in two where the subgroups have separate laboratory classes. + */ +function weekBlocks(rows) { + for (let i = 0; i < Math.min(rows.length, 4); i++) { + const cells = rows[i]; + const heads = cells.filter((c) => /^(д\.?\s*н\.?|дни|день)$/i.test(c.text.trim())); + if (!heads.length) continue; + const blocks = heads.map((head, n) => { + const next = heads[n + 1]?.col ?? Infinity; + const time = cells.find((c) => c.col > head.col && c.col < next && /^время/i.test(c.text)); + const subject = cells.find( + (c) => c.col > (time?.col ?? next) && c.col < next && /дисциплин/i.test(c.text), + ); + return time && subject + ? { day: head.col, time: time.col, from: subject.col, to: subject.col + subject.span } + : null; + }); + if (blocks.every(Boolean)) return { header: i, blocks }; + } + return null; +} +/** "8.30" alone: an exam's start without an end. */ +function startOnly(value) { + const m = value.trim().match(/^([0-2]?\d)[.:]([0-5]\d)$/); + return m ? { start: `${m[1].padStart(2, "0")}:${m[2]}`, end: null, original: value } : null; +} +const isoDate = (m) => `${m[3]}-${m[2].padStart(2, "0")}-${m[1].padStart(2, "0")}`; + export function parseDocx(buffer, source = {}) { const document = readDocx(buffer); const lessons = [], @@ -223,6 +268,8 @@ export function parseDocx(buffer, source = {}) { : null); let seenRows = 0, skippedRows = 0; + // "Расписание зачетов": every row without its own label is a credit. + const credits = /расписание\s+зач[её]тов/i.test(document.text); for (const { rows } of document.tables) { let ranges = [], header = -1; @@ -243,7 +290,64 @@ export function parseDocx(buffer, source = {}) { break; } } - if (header < 0) continue; + if (header < 0) { + const layout = weekBlocks(rows); + const named = groupsIn(document.text.match(/групп[аы]\s+([^\n]+)/i)?.[1] || ""); + if (!layout || !named.length) continue; + named.forEach((g) => groups.add(g)); + for (const block of layout.blocks) { + let day = null, + date = null, + time = null; + for (let i = layout.header + 1; i < rows.length; i++) { + const row = rows[i]; + const dayText = row.find((c) => c.col === block.day)?.text.toLowerCase() || ""; + const weekday = days.findIndex((d) => dayText.startsWith(d)); + if (weekday >= 0) { + day = weekday + 1; + const m = dayText.match(/(\d{1,2})[.](\d{1,2})[.](\d{4})/); + date = m ? isoDate(m) : null; + time = null; + } + if (!day) continue; + // A merged time cell keeps the time above; an empty one ends it. + const timeCell = row.find((c) => c.col === block.time); + if (timeCell?.text) time = timesIn(timeCell.text) || startOnly(timeCell.text); + else if (timeCell?.merge !== "continue") time = null; + if (!time) continue; + seenRows++; + let count = 0; + for (const cell of row) { + if (cell.col < block.from || cell.col >= block.to) continue; + if (cell.merge === "continue" || !cell.text) continue; + // The whole width is the group; the left or right part is one subgroup. + const whole = cell.col <= block.from && cell.col + cell.span >= block.to; + const label = `Подгруппа ${cell.col <= block.from ? 1 : 2}`; + lessons.push({ + id: createHash("sha256") + .update([source.name, i, cell.col, cell.text].join("|")) + .digest("hex") + .slice(0, 16), + day, + date, + pair: null, + ...time, + ...details(cell.text), + groups: named, + subgroups: whole ? [] : named.map((group) => ({ group, label })), + variant: "all", + weekRanges: null, + dates: [], + sourceColor: null, + raw: cell.text, + }); + count++; + } + if (!count) skippedRows++; + } + } + continue; + } ranges.forEach((r) => groups.add(r.group)); let day = null, date = null, @@ -265,12 +369,14 @@ export function parseDocx(buffer, source = {}) { } for (let i = header + 1; i < rows.length; i++) { const row = rows[i]; - const dayCell = row.find((c) => - days.some((d) => c.text.toLowerCase().startsWith(d)), - ); + // "Понедельник 12.01.2026", or date first as in credit schedules: "27.10.2026 ⏎ вторник". + const weekdayOf = (text) => { + const t = text.toLowerCase().replace(/^\d{1,2}[.]\d{1,2}[.]\d{4}\s*(г\.?)?\s*/, ""); + return days.findIndex((d) => t.startsWith(d)) + 1; + }; + const dayCell = row.find((c) => c.col < ranges[0].col && weekdayOf(c.text)); if (dayCell) { - day = - days.findIndex((d) => dayCell.text.toLowerCase().startsWith(d)) + 1; + day = weekdayOf(dayCell.text); const m = dayCell.text.match(/(\d{1,2})[.](\d{1,2})[.](\d{4})/); date = m ? `${m[3]}-${m[2].padStart(2, "0")}-${m[1].padStart(2, "0")}` @@ -279,11 +385,12 @@ export function parseDocx(buffer, source = {}) { pair = null; } if (!day) continue; + // Credits and exams give only the start ("8.30"); the bell table supplies the end. const timeCell = row.find( - (c) => c.col < ranges[0].col && timesIn(c.text), + (c) => c.col < ranges[0].col && (timesIn(c.text) || startOnly(c.text)), ); if (timeCell) { - time = timesIn(timeCell.text); + time = timesIn(timeCell.text) || startOnly(timeCell.text); timeCol = timeCell.col; pair = session ? row.find((c) => c.col === 1)?.text : null; } @@ -302,6 +409,7 @@ export function parseDocx(buffer, source = {}) { rows[i + 1]?.find((c) => c.col === cell.col)?.merge === "continue"; for (const alternative of alternatives(raw)) { const data = details(alternative); + if (credits && data.type === "Занятие") data.type = "Зачёт"; const dateCandidate = alternative.match( /^(\d{2})[.](\d{2})(?:[.](\d{4}))?\s/, ); @@ -481,7 +589,11 @@ export function parseDocx(buffer, source = {}) { emptyRows: skippedRows, }, format: "docx", - period: /экзаменационн/i.test(document.text) + period: /лабораторно-экзаменационн/i.test(document.text) + ? "Лабораторно-экзаменационная сессия" + : credits + ? "Зачёты" + : /экзаменационн/i.test(document.text) ? "Экзаменационная сессия" : /установочн/i.test(document.text) ? "Установочная сессия" diff --git a/server/source.js b/server/source.js index f13b16d..090ce6a 100644 --- a/server/source.js +++ b/server/source.js @@ -5,6 +5,7 @@ import { readFile, writeFile, mkdir } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import { createHash } from "node:crypto"; import { parseWeeksDocx, parseBellsDocx } from "./calendar.js"; +import { apiError, sourceError } from "./errors.js"; import { academicStartYear, assignSemesters, @@ -68,6 +69,8 @@ export async function request(path, method = "GET") { encoding: "buffer", maxBuffer: 11 * 1024 * 1024, timeout: 28000, + }).catch((error) => { + throw sourceError(error); }); return stdout; } @@ -164,7 +167,7 @@ async function discover(refresh = false) { root.filter((e) => e.folder).map((e) => nameOf(e.path)), ); if (!year) - throw new Error("В каталоге университета нет папки учебного года."); + throw apiError(502, "source_invalid", "В каталоге университета нет папки учебного года."); const yearRoot = SHARE + encodeURIComponent(year.name) + "/"; const yearEntries = decodeListing( (await request(yearRoot, "PROPFIND")).toString(), @@ -187,7 +190,8 @@ async function crawl() { files = [], count = 0; while (queue.length) { - if (count + queue.length > 180) throw new Error("Каталог слишком большой."); + if (count + queue.length > 180) + throw apiError(502, "source_invalid", "Каталог университета слишком большой."); const batch = queue.splice(0, 5); count += batch.length; const results = await Promise.all( @@ -278,9 +282,7 @@ export async function documentFor(id) { const catalog = await getCatalog(); const file = catalog.files.find((f) => f.id === id); if (!file) { - const error = new Error("Документ не найден в каталоге. Обновите список."); - error.status = 404; - throw error; + throw apiError(404, "document_not_found", "Документ не найден в каталоге. Обновите список."); } return file; } @@ -296,7 +298,7 @@ export function mergeWeekTables(tables) { async function loadCalendar() { const { references } = await discover(true); if (!references.weeks.length) - throw new Error("Таблица белых и зелёных недель не найдена в каталоге."); + throw apiError(502, "source_invalid", "Таблица белых и зелёных недель не найдена в каталоге."); const tables = []; for (const file of references.weeks) try { @@ -305,7 +307,7 @@ async function loadCalendar() { // One damaged copy must not hide a readable newer or older one. console.error(`${file.name}: ${error.message}`); } - if (!tables.length) throw new Error("Таблица недель не распознана."); + if (!tables.length) throw apiError(502, "source_invalid", "Таблица недель не распознана."); let bells = []; if (references.bells) try { diff --git a/server/timetable.js b/server/timetable.js index 1eecb9b..66eb838 100644 --- a/server/timetable.js +++ b/server/timetable.js @@ -3,6 +3,7 @@ import { semesterView, addDays, weekday, weekIndexOf } from "../shared/weeks.js"; import { lessonKey, subgroupsOf } from "../shared/lessons.js"; import { occurrence, peopleAndRooms, roomsOn } from "./directory.js"; +import { apiError } from "./errors.js"; const MAX_DAYS = 62; // A whole semester with its session fits easily; the cap only guards against abuse. @@ -38,22 +39,35 @@ function lessonOut(lesson, group, people, date, status, calendar) { */ export function timetable({ file, schedule, calendar, group, from, days, range, today }) { if (!schedule.groups.includes(group)) { - const error = new Error(`В документе нет группы ${group}.`); - error.status = 404; - throw error; + throw apiError(404, "group_not_found", `В документе нет группы ${group}.`); } let count = Math.min(Math.max(Number(days) || 7, 1), MAX_DAYS); + const lessons = schedule.lessons.filter((l) => l.groups.includes(group)); + // A session schedule printed with a date on every day needs no week table: its own dates + // say when each lesson takes place, even in a year the table no longer covers. + const sessionDates = lessons.every((l) => l.date || l.dates?.length) + ? lessons.flatMap((l) => (l.date ? [l.date] : l.dates)).sort() + : []; + const session = sessionDates.length + ? { + id: `session-${sessionDates[0]}`, + label: schedule.period || "Сессия", + start: sessionDates[0], + end: sessionDates.at(-1), + } + : null; // range=semester: every day of the semester `today` falls in, so a client can keep - // the whole term offline. Without a known semester it keeps the plain from/days window. + // the whole term offline; a session schedule keeps its own span. Without either it + // keeps the plain from/days window. if (range === "semester") { const current = semesterView(calendar, schedule, today || from); - if (current) { - from = current.semester.start; - const span = Math.round((Date.parse(current.semester.end) - Date.parse(from)) / DAY_MS) + 1; - count = Math.min(Math.max(span, 1), MAX_SEMESTER_DAYS); + const span = current?.semester || session; + if (span) { + from = span.start; + const length = Math.round((Date.parse(span.end) - Date.parse(from)) / DAY_MS) + 1; + count = Math.min(Math.max(length, 1), MAX_SEMESTER_DAYS); } } - const lessons = schedule.lessons.filter((l) => l.groups.includes(group)); const entries = lessons.map((lesson) => ({ doc: { id: file.id, schedule }, lesson, @@ -66,7 +80,7 @@ export function timetable({ file, schedule, calendar, group, from, days, range, const date = addDays(from, i); const week = view ? view.weeks[weekIndexOf(view, date)] || null : null; const dayLessons = []; - if (view) + if (view || session) for (const entry of entries) { const status = occurrence(entry, date, calendar, views); if (status === "yes" || status === "maybe") @@ -99,8 +113,8 @@ export function timetable({ file, schedule, calendar, group, from, days, range, modified: file.modified || schedule.source.modified || null, }, period: schedule.period, - semester: view?.semester || null, - note: view + semester: view?.semester || session, + note: view || session ? null : "Семестр этого расписания не найден в таблице недель: даты не рассчитаны, смотрите строки Word (weekly).", subgroups: subgroupsOf(schedule, group), diff --git a/test/errors.test.js b/test/errors.test.js new file mode 100644 index 0000000..aad4171 --- /dev/null +++ b/test/errors.test.js @@ -0,0 +1,27 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { apiError, describe, sourceError } from "../server/errors.js"; + +test("Ошибки API: код и статус для клиентов", () => { + assert.deepEqual(describe(apiError(404, "group_not_found", "В документе нет группы 1-А.")), { + status: 404, + code: "group_not_found", + message: "В документе нет группы 1-А.", + }); + assert.deepEqual(describe({ code: "LIMIT_FILE_SIZE" }).code, "file_too_large"); + // Faults of the server itself never show their technical text. + const internal = describe(new TypeError("Cannot read properties of undefined")); + assert.equal(internal.status, 500); + assert.equal(internal.code, "internal"); + assert.doesNotMatch(internal.message, /undefined/); +}); + +test("Ошибки источника: таймаут curl отдельно от недоступности", () => { + assert.equal(sourceError({ code: 28 }).code, "source_timeout"); + assert.equal(sourceError({ code: 28 }).status, 504); + assert.equal(sourceError({ killed: true, signal: "SIGTERM" }).code, "source_timeout"); + assert.equal(sourceError({ code: 6 }).code, "source_unavailable"); + assert.equal(sourceError({ code: 22 }).status, 502); + const own = apiError(502, "source_invalid", "Таблица недель не распознана."); + assert.equal(sourceError(own), own); +}); diff --git a/test/fixtures/isf-credits-26-arh.docx b/test/fixtures/isf-credits-26-arh.docx new file mode 100644 index 0000000000000000000000000000000000000000..5486c609f52f22f505fd10ab60afb6631178027f GIT binary patch literal 33174 zcmeEtQ*dt4)@95S+b6c||M3j zdf9W1G3VN2>?JP+3Wf><0R#mE1VjjQhn^Fb4GaXt1q}p*0t5x3C1hvoY+~!Ir|e;G z;-o|8Ze#tk01Sj84+!LY{r?~TjUzCTA|ur=fEaom_(gc3l2Mf#&hof3tN>2!zG+K# zx#wHQ-E`UQ23Yv1m}#kXN*X^g@y5r^z>D12x=6gy8R)gzfo%{{gAMQoCJplD0 z(c;xx+yI5nt!`QCdyNjmIGeUHdEz94EFhM$u@F(LHWd7}}h4l|{r0zovo#rllA=-jbjK3E6 z!D>xVm|ztzI}f=qhlzch@Fohse&u)y=Cjs zrmk4ZEP}J<)B7*MHz2>hK!N1{H&&;s_=10a&+LAa7WSLfdX6U6PV{vDxc+}O|2L-a ze|hyvfQ%F<+ux@_qM|H(cn}_;FpyaQqnG-xU`yH^%zjuZo1H=-xk{^hGG1OG5 z6OleV!#Q3p$q=-^x1$Yh0tIB@bPNMxjLVQ7Owp$mx z?_A~{myP8K=)iCdog^CAATSx3{~c;LtQc_nmoXJZ5lTc96rWu)+xFqP(OhFB9?_D( zW9yihk-U-TII<*yAd{T{E~DlyYx0rP=7BDv<*V|T`MxSRF;1f!^xx5Cz3qs@1_=o0 zcM1^D_YKI+&e53O*v`ns=DQRAqXk~*Y{u;mCHV3G{0d@_n+Ig`F}1 zRU!d|*gpbiJIRxJaH~zNnZ8$D+P(KbEpom@Lug~93PS@^Sc$Y1j ziBom_=5iT9x@Nf2f!y<^8%4CEsuxNeK5p{wP7vcdXij{1XO(EK}NPuM`Fl|}| z#iha|B~BM!N#}GxGd^jvw1i&V9C8-KI1qy9C!*~Z^ia?fD&4SHm7tgRv#~QMRNUJ9 zmO;&SCLdS^06Wp*In-@=2P!8+3A5JL21e5g(fNb#d`LMk(~Eq6*Z1K)=JkH$ZE{d&do&NmlP#I@VSXT$DC7)d;U3->mD;aAoCGqWc3V z1v)5I1Af3)9z6$rsdy0#Z6L{!oU#F-j_JfGkbEnJJMAnW7rQJ(92Bm{p+ zz8H&Uc1S2uwywxx_-VMRAXjIIzJv&OiV2yZ{#>?eoRva;ownACwbvS-t=y4-Sfnp+ zNfKvUSWs}ZsF;~jaBfUc;%uPiwe`Z5c3RWhiT*+!8l|%0D7l%@(UXH0*pltcs^7gi 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 Ja{Be`{{hn@xYqyx literal 0 HcmV?d00001 diff --git a/test/fixtures/itzs-summer-2026.docx b/test/fixtures/itzs-summer-2026.docx new file mode 100644 index 0000000000000000000000000000000000000000..faa9127ecbd2acf62ba52629673e9ff4d653010b GIT binary patch literal 48235 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 NeF45loojyn^*@P;jkEv& literal 0 HcmV?d00001 diff --git a/test/fixtures/itzs-winter-2024.docx b/test/fixtures/itzs-winter-2024.docx new file mode 100644 index 0000000000000000000000000000000000000000..18b12844bc35dca33ef8ce18f500e9623677e172 GIT binary patch literal 54108 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 S0v7_{A3iWIfO7D!cmD@Ag=HxK literal 0 HcmV?d00001 diff --git a/test/limits.test.js b/test/limits.test.js index c38c690..d009fd1 100644 --- a/test/limits.test.js +++ b/test/limits.test.js @@ -14,7 +14,7 @@ test("Импорт: не больше двух разборов сразу, ли active--; }); const running = [task(), task(), task()]; - await assert.rejects(task(), (e) => e.status === 429 && e.message === "Занято"); + await assert.rejects(task(), (e) => e.status === 429 && e.code === "busy" && e.message === "Занято"); // Each finished task hands its slot to the one waiting. while (release.length) { release.shift()(); @@ -47,6 +47,10 @@ test("Импорт: лимит запросов считается по адре }; assert.ok(call("10.0.0.1").passed); assert.ok(call("10.0.0.1").passed); - assert.deepEqual(call("10.0.0.1"), { status: 429, body: { error: "Подождите" }, passed: false }); + assert.deepEqual(call("10.0.0.1"), { + status: 429, + body: { error: "Подождите", code: "too_many_requests" }, + passed: false, + }); assert.ok(call("10.0.0.2").passed); }); diff --git a/test/parser.test.js b/test/parser.test.js index bd4e966..5ecbd9f 100644 --- a/test/parser.test.js +++ b/test/parser.test.js @@ -160,3 +160,79 @@ test("DAV: метаданные вложенных документов и то assert.equal(file.course, "4 курс"); assert.equal(file.name, "23-ЛГ_23-БА.docx"); }); + +// Laboratory-examination sessions of correspondence students (23-ИТзс): weeks side by side, +// the group named only in the title, a date on every day. +const itzs = (name) => parseDocx(fixture(name), { name: `${name}.docx` }); +const at = (s, date, start) => + s.lessons + .filter((l) => l.date === date && l.start === start) + .map((l) => [l.type, l.subject, l.teacher, l.room, l.subgroups.map((g) => g.label).join()]); + +test("Заочная сессия: группа из заголовка, недели рядом, даты из Word", () => { + const s = itzs("itzs-winter-2024"); + assert.deepEqual(s.groups, ["23-ИТзс"]); + assert.equal(s.period, "Лабораторно-экзаменационная сессия"); + // Every filled discipline cell of the three weeks is one lesson. + assert.equal(s.lessons.length, 36); + assert.ok(s.lessons.every((l) => l.date >= "2024-01-08" && l.date <= "2024-01-28")); + // Two columns under one heading are the subgroups' laboratory classes. + assert.deepEqual(at(s, "2024-01-08", "17:00"), [ + ["Лабораторная", "Основы информационных технологий и компьютерной графики", "Михнович О.В.", "217Д", "Подгруппа 1"], + ["Лабораторная", "Основы алгоритмизации и программирования", "Конаплева Г.Ф.", "218Д", "Подгруппа 2"], + ]); + assert.deepEqual(at(s, "2024-01-15", "17:00"), [ + ["Установочная лекция", "Математический анализ", "Забелендик О.Н.", "220Д", ""], + ]); + // "Экзамен по учебной дисциплине «…»": the subject from the quotes, a room with a name. + assert.deepEqual(at(s, "2024-01-18", "08:30"), [ + ["Экзамен", "Основы алгоритмизации и программирования", "Чертков В.М.", "Медиа", ""], + ]); + assert.deepEqual(at(s, "2024-01-16", "13:40"), [ + ["Защита курсовой работы", "Основы алгоритмизации и программирования", "Чертков В.М.", "254В", ""], + ]); +}); + +test("Заочная сессия: неровное деление столбцов и экзамен без конца", () => { + const s = itzs("itzs-summer-2026"); + assert.deepEqual(s.groups, ["23-ИТзс"]); + assert.equal(s.lessons.length, 58); + // The third week's heading spans three columns, split 2+1 or 1+2: left and right subgroup. + assert.deepEqual(at(s, "2026-06-24", "08:30").map((l) => [l[1], l[4]]), [ + ["Методы и алгоритмы принятия", "Подгруппа 1"], + ["Распределенные вычисления", "Подгруппа 2"], + ]); + // "8.30" alone: the exam starts then; its end comes from the bell table. + const exam = s.lessons.find((l) => l.date === "2026-06-18" && l.type === "Экзамен"); + assert.deepEqual([exam.start, exam.end, exam.subject], ["08:30", null, "Базы данных"]); + assert.equal(s.lessons.filter((l) => l.type === "Консультация").length, 4); +}); + +test("Расписание зачетов: дата перед днём недели, одно время, зачёты по умолчанию", () => { + const s = parseDocx(fixture("isf-credits-26-arh"), { name: "26-АРХ.docx" }); + assert.deepEqual(s.groups, ["26-АРХ"]); + assert.equal(s.period, "Зачёты"); + assert.equal(s.lessons.length, 9); + const row = (l) => [l.date, l.start, l.end, l.type, l.subject, l.subgroups.map((g) => g.label).join()]; + const sorted = [...s.lessons].sort((a, b) => (a.date + a.start).localeCompare(b.date + b.start)); + assert.deepEqual(sorted.slice(0, 2).map(row), [ + ["2026-10-27", "17:00", null, "Консультация", "Введение в архитектурное проектирование", ""], + ["2026-10-31", "08:30", null, "Зачёт", "Введение в архитектурное проектирование", ""], + ]); + // An empty date cell continues the day above: the second consultation on 18 December. + assert.deepEqual( + s.lessons.filter((l) => l.date === "2026-12-18").map((l) => [l.start, l.subject]), + [ + ["08:30", "Проектирование небольшого архитектурного сооружения"], + ["15:20", "Рисунок"], + ], + ); + // Two columns under the group: each subgroup takes its credit with its own teacher. + assert.deepEqual( + s.lessons.filter((l) => l.date === "2026-12-22").map((l) => [l.teacher, l.room, l.subgroups[0]?.label]), + [ + ["Кузякова О.Д.", "361", "Подгруппа 1"], + ["Пискун А.А.", "370", "Подгруппа 2"], + ], + ); +}); diff --git a/test/timetable.test.js b/test/timetable.test.js index a9ea828..11810d9 100644 --- a/test/timetable.test.js +++ b/test/timetable.test.js @@ -58,3 +58,28 @@ test("range=semester: все дни текущего семестра от пе assert.equal(t.days.at(-1).date, t.semester.end); assert.ok(t.days.length > 62, "больше обычного предела в 62 дня"); }); + +test("Сессия с датами в Word не зависит от таблицы недель", () => { + // June 2026 lies outside the 2026/27 week table, yet every lesson has its own date. + const session = parseDocx(fixture("itzs-summer-2026"), { name: "23-ИТзс.docx" }); + const t = timetable({ + file: { id: "itzs", name: "23-ИТзс.docx" }, + schedule: session, + calendar, + group: "23-ИТзс", + from: "2026-10-19", + range: "semester", + today: "2026-10-19", + }); + assert.deepEqual(t.semester, { + id: "session-2026-06-08", + label: "Лабораторно-экзаменационная сессия", + start: "2026-06-08", + end: "2026-06-27", + }); + assert.equal(t.note, null); + assert.equal(t.days.length, 20); + assert.equal(t.days.reduce((n, d) => n + d.lessons.length, 0), 58); + const exam = t.days.find((d) => d.date === "2026-06-18").lessons.find((l) => l.type === "Экзамен"); + assert.deepEqual([exam.start, exam.end, exam.pair], ["08:30", "09:55", 1]); +});