This commit is contained in:
Generated
+1231
-8
File diff suppressed because it is too large
Load Diff
+4
-2
@@ -7,7 +7,8 @@
|
||||
"type": "module",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"test": "tsx tests/test.ts",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"start": "concurrently \"NODE_ENV=development tsx server.ts\" \"NODE_ENV=development tsx cron-runner.ts\"",
|
||||
"build": "tsc && cd web && hugo --gc --minify && cd ../viewer && npm run build",
|
||||
"build-noweb": "tsc",
|
||||
@@ -46,6 +47,7 @@
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.1.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Jakub Žitník
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
const API_BASE_URL = process.env.ANNOUNCEMENT_API || "http://localhost:3000";
|
||||
|
||||
export type Announcement = {
|
||||
|
||||
+4
-5
@@ -39,7 +39,7 @@ interface ResolvedDay {
|
||||
/**
|
||||
* Read theme colors from the workbook
|
||||
*/
|
||||
async function getThemeColors(filePath: string): Promise<ThemeColors | null> {
|
||||
export async function getThemeColors(filePath: string): Promise<ThemeColors | null> {
|
||||
const data = fs.readFileSync(filePath);
|
||||
const zip = await JSZip.loadAsync(data);
|
||||
|
||||
@@ -164,10 +164,9 @@ export default async function parseV3(workbook: Workbook, downloadedFilePath: st
|
||||
// ────────────────────────────────────────────────────────────
|
||||
//
|
||||
|
||||
function getUpcomingSheets(workbook: ExcelJS.Workbook): ResolvedDay[] {
|
||||
export function getUpcomingSheets(workbook: ExcelJS.Workbook, today: Date = new Date()): ResolvedDay[] {
|
||||
const dateRegex = /^(pondělí|úterý|středa|čtvrtek|pátek|po|út|ut|st|čt|ct|pa|pá)\s+(\d{1,2})\.+\s*(\d{1,2})\.+\s*(\d{4}|\d{2})/i;
|
||||
|
||||
const today = new Date();
|
||||
const todayMidnight = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
|
||||
const result: ResolvedDay[] = [];
|
||||
@@ -190,7 +189,7 @@ function getUpcomingSheets(workbook: ExcelJS.Workbook): ResolvedDay[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
function groupSheetsByDate(items: ResolvedDay[]) {
|
||||
export function groupSheetsByDate(items: ResolvedDay[]) {
|
||||
const map: Record<string, Worksheet[]> = {};
|
||||
|
||||
for (const item of items) {
|
||||
@@ -214,7 +213,7 @@ function groupSheetsByDate(items: ResolvedDay[]) {
|
||||
// ────────────────────────────────────────────────────────────
|
||||
//
|
||||
|
||||
function extractDaySchedule(sheet: Worksheet, teacherMap: Record<string, string>, themeColors: ThemeColors | null, flags: Flag[]) {
|
||||
export function extractDaySchedule(sheet: Worksheet, teacherMap: Record<string, string>, themeColors: ThemeColors | null, flags: Flag[]) {
|
||||
return {
|
||||
changes: extractClassChanges(sheet, themeColors, flags),
|
||||
absence: extractAbsence(sheet, teacherMap),
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,353 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import fs from "fs";
|
||||
import parseAbsence from "../scrape/utils/parseAbsence.js";
|
||||
|
||||
const teachermap: Record<string, string> = JSON.parse(
|
||||
fs.readFileSync("./tests/content/teachermap.json", "utf8"),
|
||||
);
|
||||
|
||||
const cases: [string, any[]][] = [
|
||||
[
|
||||
"Me",
|
||||
[
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "wholeDay",
|
||||
hours: null,
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"ad",
|
||||
[
|
||||
{
|
||||
teacher: "Bc. Daniel Adámek",
|
||||
teacherCode: "ad",
|
||||
type: "wholeDay",
|
||||
hours: null,
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"me ad",
|
||||
[
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "wholeDay",
|
||||
hours: null,
|
||||
},
|
||||
{
|
||||
teacher: "Bc. Daniel Adámek",
|
||||
teacherCode: "ad",
|
||||
type: "wholeDay",
|
||||
hours: null,
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"me 3",
|
||||
[
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "single",
|
||||
hours: 3,
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"ad 1",
|
||||
[
|
||||
{
|
||||
teacher: "Bc. Daniel Adámek",
|
||||
teacherCode: "ad",
|
||||
type: "single",
|
||||
hours: 1,
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"me 2-4",
|
||||
[
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "range",
|
||||
hours: { from: 2, to: 4 },
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"ad 5,6",
|
||||
[
|
||||
{
|
||||
teacher: "Bc. Daniel Adámek",
|
||||
teacherCode: "ad",
|
||||
type: "range",
|
||||
hours: { from: 5, to: 6 },
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"me 7+",
|
||||
[
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "range",
|
||||
hours: { from: 7, to: 10 },
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"me,ad 3-5",
|
||||
[
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "range",
|
||||
hours: { from: 3, to: 5 },
|
||||
},
|
||||
{
|
||||
teacher: "Bc. Daniel Adámek",
|
||||
teacherCode: "ad",
|
||||
type: "range",
|
||||
hours: { from: 3, to: 5 },
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"me;ad 4",
|
||||
[
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "wholeDay",
|
||||
hours: null,
|
||||
},
|
||||
{
|
||||
teacher: "Bc. Daniel Adámek",
|
||||
teacherCode: "ad",
|
||||
type: "single",
|
||||
hours: 4,
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"me;ad;bo 2-3",
|
||||
[
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "wholeDay",
|
||||
hours: null,
|
||||
},
|
||||
{
|
||||
teacher: "Bc. Daniel Adámek",
|
||||
teacherCode: "ad",
|
||||
type: "wholeDay",
|
||||
hours: null,
|
||||
},
|
||||
{
|
||||
teacher: "Ing. Anna Bodnárová",
|
||||
teacherCode: "bo",
|
||||
type: "range",
|
||||
hours: { from: 2, to: 3 },
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"me 2 ad 4-5",
|
||||
[
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "single",
|
||||
hours: 2,
|
||||
},
|
||||
{
|
||||
teacher: "Bc. Daniel Adámek",
|
||||
teacherCode: "ad",
|
||||
type: "range",
|
||||
hours: { from: 4, to: 5 },
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"3",
|
||||
[
|
||||
{
|
||||
type: "invalid",
|
||||
teacher: null,
|
||||
teacherCode: null,
|
||||
hours: null,
|
||||
original: "3",
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"2-4",
|
||||
[
|
||||
{
|
||||
type: "invalid",
|
||||
teacher: null,
|
||||
teacherCode: null,
|
||||
hours: null,
|
||||
original: "2-4",
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"me Xx 3",
|
||||
[
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "wholeDay",
|
||||
hours: null,
|
||||
},
|
||||
{
|
||||
teacher: null,
|
||||
teacherCode: "xx",
|
||||
type: "single",
|
||||
hours: 3,
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"me,ad;bo 1-2 ad 5+",
|
||||
[
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "wholeDay",
|
||||
hours: null,
|
||||
},
|
||||
{
|
||||
teacher: "Bc. Daniel Adámek",
|
||||
teacherCode: "ad",
|
||||
type: "wholeDay",
|
||||
hours: null,
|
||||
},
|
||||
{
|
||||
teacher: "Ing. Anna Bodnárová",
|
||||
teacherCode: "bo",
|
||||
type: "range",
|
||||
hours: { from: 1, to: 2 },
|
||||
},
|
||||
{
|
||||
teacher: "Bc. Daniel Adámek",
|
||||
teacherCode: "ad",
|
||||
type: "range",
|
||||
hours: { from: 5, to: 10 },
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"Me-exk",
|
||||
[
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "exkurze",
|
||||
hours: null,
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"za Vn zastupuje Jk",
|
||||
[
|
||||
{
|
||||
teacher: "Ing. Zdeněk Vondra",
|
||||
teacherCode: "vn",
|
||||
type: "zastoupen",
|
||||
hours: null,
|
||||
zastupuje: {
|
||||
teacher: "David Janoušek",
|
||||
teacherCode: "jk",
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"Vc 1. h",
|
||||
[
|
||||
{
|
||||
teacher: "Ing. Antonín Vobecký",
|
||||
teacherCode: "vc",
|
||||
type: "single",
|
||||
hours: 1,
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"Sv-5-exk",
|
||||
[
|
||||
{
|
||||
teacher: "Ing. Jan Šváb",
|
||||
teacherCode: "sv",
|
||||
type: "exkurze",
|
||||
hours: { from: 5, to: 10 },
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"Ex-exk. 3+",
|
||||
[
|
||||
{
|
||||
teacher: "Ing. Jana Exnerová",
|
||||
teacherCode: "ex",
|
||||
type: "exkurze",
|
||||
hours: { from: 3, to: 10 },
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"Ex-exk.3+",
|
||||
[
|
||||
{
|
||||
teacher: "Ing. Jana Exnerová",
|
||||
teacherCode: "ex",
|
||||
type: "exkurze",
|
||||
hours: { from: 3, to: 10 },
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"Ex-exk. 3",
|
||||
[
|
||||
{
|
||||
teacher: "Ing. Jana Exnerová",
|
||||
teacherCode: "ex",
|
||||
type: "exkurze",
|
||||
hours: 3,
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"Su 4 - 6",
|
||||
[
|
||||
{
|
||||
teacher: "MUDr. Kristina Studénková",
|
||||
teacherCode: "su",
|
||||
type: "range",
|
||||
hours: { from: 4, to: 6 },
|
||||
},
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
function expectSameItems(actual: any[], expected: any[]) {
|
||||
expect(actual).toHaveLength(expected.length);
|
||||
for (const item of expected) {
|
||||
expect(actual).toContainEqual(item);
|
||||
}
|
||||
}
|
||||
|
||||
describe("parseAbsence", () => {
|
||||
it.each(cases)("parseAbsence(%s) → %j", (input, expected) => {
|
||||
expectSameItems(parseAbsence(input, teachermap), expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
import { describe, it, expect, beforeAll } from "vitest";
|
||||
import fs from "fs";
|
||||
import ExcelJS from "exceljs";
|
||||
import {
|
||||
getThemeColors,
|
||||
getUpcomingSheets,
|
||||
groupSheetsByDate,
|
||||
extractDaySchedule,
|
||||
} from "../scrape/parse/v3.js";
|
||||
|
||||
const XLSX_PATH = "tests/content/1.xlsx";
|
||||
const TEACHERMAP_PATH = "tests/content/teachermap.json";
|
||||
|
||||
let workbook: ExcelJS.Workbook;
|
||||
let teachermap: Record<string, string>;
|
||||
|
||||
beforeAll(async () => {
|
||||
workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.readFile(XLSX_PATH);
|
||||
teachermap = JSON.parse(fs.readFileSync(TEACHERMAP_PATH, "utf8"));
|
||||
});
|
||||
|
||||
describe("sheet filtering", () => {
|
||||
it("includes sheets on or after the injected date", () => {
|
||||
const injected = new Date(2026, 5, 2); // June 2, 2026
|
||||
const sheets = getUpcomingSheets(workbook, injected);
|
||||
|
||||
expect(sheets.length).toBeGreaterThanOrEqual(1);
|
||||
expect(sheets.map((s) => s.dateKey)).toContain("2026-06-02");
|
||||
});
|
||||
|
||||
it("excludes sheets before the injected date", () => {
|
||||
const injected = new Date(2026, 5, 2);
|
||||
const sheets = getUpcomingSheets(workbook, injected);
|
||||
|
||||
for (const s of sheets) {
|
||||
const [y, m, d] = s.dateKey.split("-").map(Number);
|
||||
expect(new Date(y, m - 1, d).getTime()).toBeGreaterThanOrEqual(
|
||||
injected.getTime(),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("full day parsing", () => {
|
||||
let result: ReturnType<typeof extractDaySchedule>;
|
||||
let sheet: ExcelJS.Worksheet;
|
||||
let themeColors: Awaited<ReturnType<typeof getThemeColors>>;
|
||||
|
||||
beforeAll(async () => {
|
||||
const injected = new Date(2026, 5, 2);
|
||||
const upcoming = getUpcomingSheets(workbook, injected);
|
||||
const resolved = groupSheetsByDate(upcoming);
|
||||
const day = resolved.find((d) => d.dateKey === "2026-06-02")!;
|
||||
sheet = day.sheet;
|
||||
themeColors = await getThemeColors(XLSX_PATH);
|
||||
result = extractDaySchedule(sheet, teachermap, themeColors, []);
|
||||
});
|
||||
|
||||
describe("changes", () => {
|
||||
it("is a non-empty record of class → lessons", () => {
|
||||
const keys = Object.keys(result.changes);
|
||||
expect(keys.length).toBeGreaterThan(0);
|
||||
|
||||
for (const [cls, lessons] of Object.entries(result.changes)) {
|
||||
expect(cls).toMatch(/^[AEC][0-4][a-c]?$/);
|
||||
expect(lessons).toHaveLength(10);
|
||||
for (const lesson of lessons) {
|
||||
if (lesson === null) continue;
|
||||
expect(lesson).toHaveProperty("text");
|
||||
expect(typeof lesson.text).toBe("string");
|
||||
expect(lesson).toHaveProperty("backgroundColor");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("includes A1a with known lesson at hour 5", () => {
|
||||
const a1a = result.changes["A1a"];
|
||||
expect(a1a).toBeDefined();
|
||||
const hour5 = a1a[4];
|
||||
expect(hour5).not.toBeNull();
|
||||
expect(hour5!.text).toContain("ZE");
|
||||
});
|
||||
});
|
||||
|
||||
describe("absence", () => {
|
||||
it("is an array of parsed absence entries", () => {
|
||||
expect(Array.isArray(result.absence)).toBe(true);
|
||||
expect(result.absence.length).toBeGreaterThan(0);
|
||||
|
||||
for (const entry of result.absence) {
|
||||
expect(entry).toHaveProperty("teacher");
|
||||
expect(entry).toHaveProperty("teacherCode");
|
||||
expect(entry).toHaveProperty("type");
|
||||
expect(entry).toHaveProperty("hours");
|
||||
}
|
||||
});
|
||||
|
||||
it("contains a known absence entry", () => {
|
||||
const su = result.absence.find((a: any) => a.teacherCode === "su");
|
||||
expect(su).toBeDefined();
|
||||
expect(su!.teacher).toContain("Studénková");
|
||||
});
|
||||
});
|
||||
|
||||
describe("takesPlace", () => {
|
||||
it("is a non-empty string", () => {
|
||||
expect(typeof result.takesPlace).toBe("string");
|
||||
expect(result.takesPlace.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("reservedRooms", () => {
|
||||
it("is an array of 10 entries", () => {
|
||||
expect(result.reservedRooms).toHaveLength(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
-352
@@ -1,352 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Jakub Žitník
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
import fs from "fs";
|
||||
import parseAbsence from "../scrape/utils/parseAbsence.js";
|
||||
|
||||
const teachermap: Record<string, string> = JSON.parse(fs.readFileSync("./tests/teachermap.json", "utf8"));
|
||||
let passedAll = true;
|
||||
|
||||
test("Me", [
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "wholeDay",
|
||||
hours: null,
|
||||
},
|
||||
]);
|
||||
|
||||
test("ad", [
|
||||
{
|
||||
teacher: "Bc. Daniel Adámek",
|
||||
teacherCode: "ad",
|
||||
type: "wholeDay",
|
||||
hours: null,
|
||||
},
|
||||
]);
|
||||
|
||||
test("me ad", [
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "wholeDay",
|
||||
hours: null,
|
||||
},
|
||||
{
|
||||
teacher: "Bc. Daniel Adámek",
|
||||
teacherCode: "ad",
|
||||
type: "wholeDay",
|
||||
hours: null,
|
||||
},
|
||||
]);
|
||||
|
||||
test("me 3", [
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "single",
|
||||
hours: 3,
|
||||
},
|
||||
]);
|
||||
|
||||
test("ad 1", [
|
||||
{
|
||||
teacher: "Bc. Daniel Adámek",
|
||||
teacherCode: "ad",
|
||||
type: "single",
|
||||
hours: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
test("me 2-4", [
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "range",
|
||||
hours: { from: 2, to: 4 },
|
||||
},
|
||||
]);
|
||||
|
||||
test("ad 5,6", [
|
||||
{
|
||||
teacher: "Bc. Daniel Adámek",
|
||||
teacherCode: "ad",
|
||||
type: "range",
|
||||
hours: { from: 5, to: 6 },
|
||||
},
|
||||
]);
|
||||
|
||||
test("me 7+", [
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "range",
|
||||
hours: { from: 7, to: 10 },
|
||||
},
|
||||
]);
|
||||
|
||||
test("me,ad 3-5", [
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "range",
|
||||
hours: { from: 3, to: 5 },
|
||||
},
|
||||
{
|
||||
teacher: "Bc. Daniel Adámek",
|
||||
teacherCode: "ad",
|
||||
type: "range",
|
||||
hours: { from: 3, to: 5 },
|
||||
},
|
||||
]);
|
||||
|
||||
test("me;ad 4", [
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "wholeDay",
|
||||
hours: null,
|
||||
},
|
||||
{
|
||||
teacher: "Bc. Daniel Adámek",
|
||||
teacherCode: "ad",
|
||||
type: "single",
|
||||
hours: 4,
|
||||
},
|
||||
]);
|
||||
|
||||
test("me;ad;bo 2-3", [
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "wholeDay",
|
||||
hours: null,
|
||||
},
|
||||
{
|
||||
teacher: "Bc. Daniel Adámek",
|
||||
teacherCode: "ad",
|
||||
type: "wholeDay",
|
||||
hours: null,
|
||||
},
|
||||
{
|
||||
teacher: "Ing. Anna Bodnárová",
|
||||
teacherCode: "bo",
|
||||
type: "range",
|
||||
hours: { from: 2, to: 3 },
|
||||
},
|
||||
]);
|
||||
|
||||
test("me 2 ad 4-5", [
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "single",
|
||||
hours: 2,
|
||||
},
|
||||
{
|
||||
teacher: "Bc. Daniel Adámek",
|
||||
teacherCode: "ad",
|
||||
type: "range",
|
||||
hours: { from: 4, to: 5 },
|
||||
},
|
||||
]);
|
||||
|
||||
test("3", [
|
||||
{
|
||||
type: "invalid",
|
||||
teacher: null,
|
||||
teacherCode: null,
|
||||
hours: null,
|
||||
original: "3",
|
||||
},
|
||||
]);
|
||||
|
||||
test("2-4", [
|
||||
{
|
||||
type: "invalid",
|
||||
teacher: null,
|
||||
teacherCode: null,
|
||||
hours: null,
|
||||
original: "2-4",
|
||||
},
|
||||
]);
|
||||
|
||||
test("me Xx 3", [
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "wholeDay",
|
||||
hours: null,
|
||||
},
|
||||
{ teacher: null, teacherCode: "xx", type: "single", hours: 3 },
|
||||
]);
|
||||
|
||||
test("me,ad;bo 1-2 ad 5+", [
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "wholeDay",
|
||||
hours: null,
|
||||
},
|
||||
{
|
||||
teacher: "Bc. Daniel Adámek",
|
||||
teacherCode: "ad",
|
||||
type: "wholeDay",
|
||||
hours: null,
|
||||
},
|
||||
{
|
||||
teacher: "Ing. Anna Bodnárová",
|
||||
teacherCode: "bo",
|
||||
type: "range",
|
||||
hours: { from: 1, to: 2 },
|
||||
},
|
||||
{
|
||||
teacher: "Bc. Daniel Adámek",
|
||||
teacherCode: "ad",
|
||||
type: "range",
|
||||
hours: { from: 5, to: 10 },
|
||||
},
|
||||
]);
|
||||
|
||||
test("Me-exk", [
|
||||
{
|
||||
teacher: "Michaela Meitnerová",
|
||||
teacherCode: "me",
|
||||
type: "exkurze",
|
||||
hours: null,
|
||||
},
|
||||
]);
|
||||
|
||||
test("za Vn zastupuje Jk", [
|
||||
{
|
||||
teacher: "Ing. Zdeněk Vondra",
|
||||
teacherCode: "vn",
|
||||
type: "zastoupen",
|
||||
hours: null,
|
||||
zastupuje: {
|
||||
teacher: "David Janoušek",
|
||||
teacherCode: "jk",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
test("Vc 1. h", [
|
||||
{
|
||||
teacher: "Ing. Antonín Vobecký",
|
||||
teacherCode: "vc",
|
||||
type: "single",
|
||||
hours: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
test("Sv-5-exk", [
|
||||
{
|
||||
teacher: "Ing. Jan Šváb",
|
||||
teacherCode: "sv",
|
||||
type: "exkurze",
|
||||
hours: { from: 5, to: 10 },
|
||||
},
|
||||
]);
|
||||
|
||||
test("Ex-exk. 3+", [
|
||||
{
|
||||
teacher: "Ing. Jana Exnerová",
|
||||
teacherCode: "ex",
|
||||
type: "exkurze",
|
||||
hours: { from: 3, to: 10 },
|
||||
},
|
||||
]);
|
||||
|
||||
test("Ex-exk.3+", [
|
||||
{
|
||||
teacher: "Ing. Jana Exnerová",
|
||||
teacherCode: "ex",
|
||||
type: "exkurze",
|
||||
hours: { from: 3, to: 10 },
|
||||
},
|
||||
]);
|
||||
|
||||
test("Ex-exk. 3", [
|
||||
{
|
||||
teacher: "Ing. Jana Exnerová",
|
||||
teacherCode: "ex",
|
||||
type: "exkurze",
|
||||
hours: 3,
|
||||
},
|
||||
]);
|
||||
|
||||
test("Su 4 - 6", [
|
||||
{
|
||||
teacher: "MUDr. Kristina Studénková",
|
||||
teacherCode: "su",
|
||||
type: "range",
|
||||
hours: {from: 4, to: 6},
|
||||
}
|
||||
]);
|
||||
|
||||
function test(input: string, expectedOutput: any[]) {
|
||||
const res = parseAbsence(input, teachermap);
|
||||
|
||||
if (!deepEqual(res, expectedOutput)) {
|
||||
passedAll = false;
|
||||
console.error("ERROR for input: " + input);
|
||||
console.log(JSON.stringify(res, null, 2));
|
||||
console.log(JSON.stringify(expectedOutput, null, 2));
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
function deepEqual(a: any, b: any): boolean {
|
||||
if (a === b) return true;
|
||||
|
||||
if (
|
||||
typeof a !== "object" ||
|
||||
a === null ||
|
||||
typeof b !== "object" ||
|
||||
b === null
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle arrays (ignore order)
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) return false;
|
||||
|
||||
const used = new Array(b.length).fill(false);
|
||||
|
||||
return a.every((itemA) =>
|
||||
b.some((itemB, i) => {
|
||||
if (used[i]) return false; // don't reuse elements
|
||||
if (deepEqual(itemA, itemB)) {
|
||||
used[i] = true; // mark element as used
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
||||
|
||||
const keysA = Object.keys(a);
|
||||
const keysB = Object.keys(b);
|
||||
|
||||
if (keysA.length !== keysB.length) return false;
|
||||
|
||||
return keysA.every((key) => keysB.includes(key) && deepEqual(a[key], b[key]));
|
||||
}
|
||||
|
||||
if (passedAll) {
|
||||
console.log("All tests passed");
|
||||
}
|
||||
Reference in New Issue
Block a user