/* * 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); // 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; 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); } // 2. Teachers with "-exk" suffix 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); } // --------------------------------------------------------- // 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; const matchEnd = teacherOnlyRe.lastIndex; if (isConsumed(matchStart)) continue; 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({ type: "invalid", teacher: null, teacherCode: null, hours: null, original: t, }); }); markConsumed(matchStart, matchEnd); } // 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; results.push({ type: "invalid", teacher: null, teacherCode: null, hours: null, original: m[1], }); } return results; }