From 6b383b1af4309ef6f6a36192d24017057e265030 Mon Sep 17 00:00:00 2001 From: jzitnik-dev Date: Sun, 4 Jan 2026 14:02:10 +0100 Subject: [PATCH] feat: v2 Support of teacher absence format: "za Vn zastupuje Jk" --- package-lock.json | 5 ++--- scrape/parse.js | 4 ++-- scrape/parse/{v1.js => v1_v2.js} | 28 ++++++++++++++++++++--- scrape/scraper.js | 2 +- scrape/utils/parseAbsence.js | 38 +++++++++++++++++++++++++++++--- server.js | 18 +++++++++++++++ tests/test.js | 12 ++++++++++ 7 files changed, 95 insertions(+), 12 deletions(-) rename scrape/parse/{v1.js => v1_v2.js} (90%) diff --git a/package-lock.json b/package-lock.json index f8d9974..7eed502 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "": { "name": "jecnarozvrh", "version": "1.0.0", - "license": "ISC", + "license": "GPL-3.0-only", "dependencies": { "body-parser": "^2.2.0", "cheerio": "^1.1.2", @@ -947,8 +947,7 @@ "version": "0.0.1452169", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1452169.tgz", "integrity": "sha512-FOFDVMGrAUNp0dDKsAU1TorWJUx2JOU1k9xdgBKKJF3IBh/Uhl2yswG5r3TEAOrCiGY2QRp1e6LVDQrCsTKO4g==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/dom-serializer": { "version": "2.0.0", diff --git a/scrape/parse.js b/scrape/parse.js index 561b138..a4bb495 100644 --- a/scrape/parse.js +++ b/scrape/parse.js @@ -12,8 +12,8 @@ * GNU General Public License for more details. */ -import parseV1 from "./parse/v1.js"; +import parseV1V2 from "./parse/v1_v2.js"; export default async function parseThisShit(downloadedFilePath) { - await parseV1(downloadedFilePath) + await parseV1V2(downloadedFilePath) } diff --git a/scrape/parse/v1.js b/scrape/parse/v1_v2.js similarity index 90% rename from scrape/parse/v1.js rename to scrape/parse/v1_v2.js index 0aa621d..35bfec2 100644 --- a/scrape/parse/v1.js +++ b/scrape/parse/v1_v2.js @@ -17,7 +17,7 @@ import fs from "fs" import parseAbsence from "../utils/parseAbsence.js" import parseTeachers from "../utils/parseTeachers.js" -export default async function parseV1(downloadedFilePath) { +export default async function parseV1V2(downloadedFilePath) { const workbook = new ExcelJS.Workbook(); await workbook.xlsx.readFile(downloadedFilePath); const teacherMap = await parseTeachers(); @@ -216,7 +216,29 @@ export default async function parseV1(downloadedFilePath) { } } - fs.writeFileSync("db/v1.json", JSON.stringify(data, null, 2)); + fs.writeFileSync("db/v2.json", JSON.stringify(data, null, 2)); + + // Modify the data for v1 + const copy = JSON.parse(JSON.stringify(data)); + + copy.schedule.forEach(day => { + if (!Array.isArray(day.ABSENCE)) return; + + day.ABSENCE = day.ABSENCE.map(old => { + if (old.type === "zastoupen") { + console.log("pepa") + return { + teacher: old.teacher, + teacherCode: old.teacherCode, + type: "wholeDay", + hours: null + }; + } + return old; + }); + }); + + fs.writeFileSync("db/v1.json", JSON.stringify(copy, null, 2)) } -//parseThisShit("downloads/table.xlsx") +parseV1V2("db/current.xlsx") diff --git a/scrape/scraper.js b/scrape/scraper.js index 1f932bb..c69a0e5 100644 --- a/scrape/scraper.js +++ b/scrape/scraper.js @@ -67,7 +67,7 @@ async function handleError(page, err) { let browser, page; try { browser = await puppeteer.launch({ - headless: 'new', + headless: false, userDataDir: VOLUME_PATH, args: ['--no-sandbox', '--disable-setuid-sandbox'] }); diff --git a/scrape/utils/parseAbsence.js b/scrape/utils/parseAbsence.js index 5d9f3ca..4a14569 100644 --- a/scrape/utils/parseAbsence.js +++ b/scrape/utils/parseAbsence.js @@ -98,6 +98,7 @@ export default function parseAbsence(input, teacherMap = {}) { const markConsumed = (start, end) => consumed.push([start, end]); const isConsumed = (i) => consumed.some(([a, b]) => i >= a && i < b); + // 1. Teachers with specific hours (e.g. "Ab 1-4") const teacherListThenSpecRe = /([A-Za-z]+(?:[,;]\s?[A-Za-z]+)*)(?:\s*)(\d+(?:\+|-\d+|,\d+)?)(?![A-Za-z])/g; @@ -115,6 +116,7 @@ export default function parseAbsence(input, teacherMap = {}) { markConsumed(matchStart, matchEnd); } + // 2. Teachers with "-exk" suffix const teacherExkRe = /([A-Za-z]+)-exk/gi; while ((m = teacherExkRe.exec(s)) !== null) { const matchStart = m.index; @@ -132,7 +134,34 @@ export default function parseAbsence(input, teacherMap = {}) { markConsumed(matchStart, matchEnd); } - // Standalone teachers → whole day + // --------------------------------------------------------- + // 3. Substitution Pattern: "za Vn zastupuje Jk" + // --------------------------------------------------------- + const substitutionRe = /za\s+([A-Za-z]+)\s+zastupuje\s+([A-Za-z]+)/gi; + while ((m = substitutionRe.exec(s)) !== null) { + const matchStart = m.index; + const matchEnd = substitutionRe.lastIndex; + if (isConsumed(matchStart)) continue; + + const missingCode = m[1]; + const substituteCode = m[2]; + + const missingResolved = resolveTeacher(missingCode, teacherMap); + const subResolved = resolveTeacher(substituteCode, teacherMap); + + results.push({ + teacher: missingResolved.name, + teacherCode: missingResolved.code.toLowerCase(), + type: "zastoupen", + zastupuje: { + teacher: subResolved.name, + teacherCode: subResolved.code.toLowerCase() + } + }); + + markConsumed(matchStart, matchEnd); + } + const teacherOnlyRe = /([A-Za-z]+(?:[,;]\s?[A-Za-z]+)*)/g; while ((m = teacherOnlyRe.exec(s)) !== null) { const matchStart = m.index; @@ -141,6 +170,9 @@ export default function parseAbsence(input, teacherMap = {}) { const tList = m[1].split(/[,;]\s*/).filter(Boolean); tList.forEach((t) => { + const lowerT = t.toLowerCase(); + if (lowerT === 'za' || lowerT === 'zastupuje') return; + if (isTeacherToken(t)) results.push(makeResult(t, null, teacherMap)); else results.push({ @@ -154,12 +186,12 @@ export default function parseAbsence(input, teacherMap = {}) { markConsumed(matchStart, matchEnd); } - // Bare specs without teacher → invalid + // 5. Bare specs without teacher → invalid const specOnlyRe = /\b(\d+(?:\+|-\d+|,\d+)?)\b/g; while ((m = specOnlyRe.exec(s)) !== null) { const matchStart = m.index; if (isConsumed(matchStart)) continue; - if (m[1].trim() == "exk") continue; // Exkurze, will be implemented later + if (m[1].trim() == "exk") continue; results.push({ type: "invalid", diff --git a/server.js b/server.js index 282c981..9cb573f 100644 --- a/server.js +++ b/server.js @@ -19,6 +19,7 @@ import fs from "fs/promises"; import { getCurrentInterval } from "./scheduleRules.js"; import bodyParser from "body-parser"; +const VERSIONS = ["v1", "v2"]; const PORT = process.env.PORT || 3000; globalThis.File = class File {}; @@ -50,6 +51,23 @@ app.get('/versioned/v1', async (_, res) => { res.json(data); }); +VERSIONS.forEach((version) => { + app.get(`/versioned/${version}`, async (_, res) => { + try { + const filePath = path.join(process.cwd(), "db", `${version}.json`); + const dataStr = await fs.readFile(filePath, "utf8"); + const data = JSON.parse(dataStr); + + data.status.currentUpdateSchedule = getCurrentInterval(); + + res.json(data); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to load version data" }); + } + }); +}); + app.get("/status", async (_, res) => { const dataStr = await fs.readFile(path.resolve("./volume/customState.json"), {encoding: "utf8"}); const data = JSON.parse(dataStr); diff --git a/tests/test.js b/tests/test.js index 8728b5f..419e96d 100644 --- a/tests/test.js +++ b/tests/test.js @@ -228,6 +228,18 @@ test("Me-exk", [ } ]) +test("za Vn zastupuje Jk", [ + { + teacher: "Ing. Zdeněk Vondra", + teacherCode: "vn", + type: "zastoupen", + zastupuje: { + teacher: "David Janoušek", + teacherCode: "jk" + }, + } +]) + function test(input, expectedOutput) { const res = parseAbsence(input, teachermap);