1
0
Files
jecnarozvrh/scrape/utils/parseAbsence.js
2025-12-20 20:52:08 +01:00

175 lines
5.1 KiB
JavaScript

/*
* 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.
*/
const LAST_HOUR = 10;
// -------------------------------
// Helpers
// -------------------------------
export const cleanInput = (input) => (input ?? "").trim().replace(/\s+/g, " ");
export const isTeacherToken = (t) => /^[A-Za-z]+$/.test(t);
export const parseSpec = (spec) => {
if (!spec) return null;
let m;
// Handle "6,7"
if ((m = spec.match(/^(\d+),(\d+)$/))) {
const from = Number(m[1]);
const to = Number(m[2]);
return from >= 1 && to >= from && to <= LAST_HOUR
? { kind: "range", value: { from, to } }
: null;
}
if ((m = spec.match(/^(\d+)\+$/))) {
const from = Number(m[1]);
return from >= 1 && from <= LAST_HOUR
? { kind: "range", value: { from, to: LAST_HOUR } }
: null;
}
if ((m = spec.match(/^(\d+)-(\d+)$/))) {
const from = Number(m[1]);
const to = Number(m[2]);
return from >= 1 && to >= from && to <= LAST_HOUR
? { kind: "range", value: { from, to } }
: null;
}
if ((m = spec.match(/^(\d+)$/))) {
const hour = Number(m[1]);
return hour >= 1 && hour <= LAST_HOUR
? { kind: "single", value: hour }
: null;
}
return null;
};
export const resolveTeacher = (teacherCode, teacherMap = {}) => ({
code: teacherCode,
name: teacherMap?.[teacherCode.toLowerCase()] ?? null,
});
const makeResult = (teacherCode, spec, teacherMap) => {
const { name } = resolveTeacher(teacherCode, teacherMap);
const type = spec ? (spec.kind === "range" ? "range" : "single") : "wholeDay";
const hours = spec ? spec.value : null;
return { teacher: name, teacherCode: teacherCode.toLowerCase(), type, hours };
};
// -------------------------------
// Teacher list processing (modular)
// -------------------------------
const processTeacherList = (teacherListStr, spec, teacherMap) => {
let results = [];
const teachers = teacherListStr.split(/[,;]\s*/).filter(Boolean);
if (teacherListStr.includes(";")) {
teachers.forEach((t, i) => {
const resSpec = i === teachers.length - 1 ? spec : null;
results.push(makeResult(t, resSpec, teacherMap));
});
} else {
results = teachers.map((t) => makeResult(t, spec, teacherMap));
}
return results;
};
// -------------------------------
// Main parser
// -------------------------------
export default function parseAbsence(input, teacherMap = {}) {
const s = cleanInput(input);
if (!s) return [];
const results = [];
const consumed = [];
const markConsumed = (start, end) => consumed.push([start, end]);
const isConsumed = (i) => consumed.some(([a, b]) => i >= a && i < b);
const teacherListThenSpecRe =
/([A-Za-z]+(?:[,;]\s?[A-Za-z]+)*)(?:\s*)(\d+(?:\+|-\d+|,\d+)?)(?![A-Za-z])/g;
let m;
while ((m = teacherListThenSpecRe.exec(s)) !== null) {
const matchStart = m.index;
const matchEnd = teacherListThenSpecRe.lastIndex;
if (isConsumed(matchStart)) continue;
const [_, teacherListStr, specStr] = m;
const spec = parseSpec(specStr);
if (!spec) continue;
results.push(...processTeacherList(teacherListStr, spec, teacherMap));
markConsumed(matchStart, matchEnd);
}
const teacherExkRe = /([A-Za-z]+)-exk/gi;
while ((m = teacherExkRe.exec(s)) !== null) {
const matchStart = m.index;
const matchEnd = teacherExkRe.lastIndex;
if (isConsumed(matchStart)) continue;
const teacherCode = m[1];
const { name } = resolveTeacher(teacherCode, teacherMap);
results.push({
teacher: name,
teacherCode: teacherCode.toLowerCase(),
type: "exkurze",
hours: null,
});
markConsumed(matchStart, matchEnd);
}
// Standalone teachers → whole day
const teacherOnlyRe = /([A-Za-z]+(?:[,;]\s?[A-Za-z]+)*)/g;
while ((m = teacherOnlyRe.exec(s)) !== null) {
const matchStart = m.index;
const matchEnd = teacherOnlyRe.lastIndex;
if (isConsumed(matchStart)) continue;
const tList = m[1].split(/[,;]\s*/).filter(Boolean);
tList.forEach((t) => {
if (isTeacherToken(t)) results.push(makeResult(t, null, teacherMap));
else
results.push({
type: "invalid",
teacher: null,
teacherCode: null,
hours: null,
original: t,
});
});
markConsumed(matchStart, matchEnd);
}
// 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
results.push({
type: "invalid",
teacher: null,
teacherCode: null,
hours: null,
original: m[1],
});
}
return results;
}