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; 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, type, hours }; }; // ------------------------------- // Teacher list processing (modular) // ------------------------------- const processTeacherList = (teacherListStr, spec, teacherMap) => { let results = []; if (teacherListStr.includes(",")) { // Comma = spec applies to all const teachers = teacherListStr.split(/\s*,\s*/).filter(Boolean); results = teachers.map((t) => makeResult(t, spec, teacherMap)); } else if (teacherListStr.includes(";")) { // Semicolon = spec applies only to last, others = whole day const teachers = teacherListStr.split(/\s*;\s*/).filter(Boolean); teachers.forEach((t, i) => { const resSpec = i === teachers.length - 1 ? spec : null; results.push(makeResult(t, resSpec, teacherMap)); }); } else { // Single teacher results.push(makeResult(teacherListStr, 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); // Regex: teacher-list [+ optional spec] const teacherListThenSpecRe = /([A-Za-z]+(?:[,;]\s?[A-Za-z]+)*)(?:\s*)?(\d+(?:\+|-\d+)?)/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); } // 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+)?)\b/g; while ((m = specOnlyRe.exec(s)) !== null) { const matchStart = m.index; if (isConsumed(matchStart)) continue; results.push({ type: "invalid", teacher: null, teacherCode: null, hours: null, original: m[1], }); } return results; }