/** * Fragen über ihre Nummer nachschlagen. * * ## Warum das ein eigenes Modul ist * * Weil es eine eigene Fehlerquelle ist. Der Katalog führt **zwei** * Nummernformate nebeneinander, und beide haben ihre Tücken: * * * Kapitel I nummeriert abschnittsweise: `1.01` bis `2.150`, als Kennung * `I.1-01` bis `I.2-150`. * * Kapitel II, III und IV zählen jeweils bei `01` neu, als Kennung `II-01`, * `III-01`, `IV-01`. * * Daraus folgen zwei Dinge, die man leicht falsch macht: * * **Führende Nullen.** Wer `2.5` tippt, meint Frage `2.05`. Eine * Präfixsuche lieferte ihm still die Fragen `2.50` bis `2.59` – also das * Falsche statt nichts. Deshalb werden beide Seiten gleich normalisiert: * führende Nullen der Laufnummer fallen weg, hier wie dort. * * **Mehrdeutigkeit.** Von 437 verschiedenen Nummern sind **89 mehrdeutig** – * eine bloße `45` gibt es in Kapitel II, III und IV. Die Anwendung wählt * nicht heimlich eine aus, sondern zeigt alle drei nebeneinander, jede mit * ihrer Kapitelzeile. * * ## Ausdrücklich keine Teilzeichenkettensuche * * Die Nummern der Kapitel II bis IV sind zweistellig aufgefüllt. Eine * Teilzeichenkettensuche nach `05` höbe sechs Fragen über jeden Texttreffer. * Ein Nummerntreffer entsteht nur bei Gleichheit nach Normalisierung. */ import type { Katalog } from './katalog'; /** Höflichkeitsfloskeln, die vor einer Nummer stehen können. */ const VORSPANN = /^(frage|nr\.?|nummer)\s*/u; /** `2.5`, `2,05`, `2.150` – die Zählweise des Kapitels I. */ const MIT_ABSCHNITT = /^([1-9])[.,]0*(\d{1,3})$/u; /** `45`, `05`, `5` – die Zählweise der Kapitel II bis IV. */ const BLOSSE_NUMMER = /^0*(\d{1,3})$/u; /** `I.2-150`, `II-45`, `ii 45` – die vollständige Kennung. */ const KENNUNG = /^(i[.,]([1-5])|ii|iii|iv)[-– ]0*(\d{1,3})$/u; /** * Bringt eine Eingabe auf einen Nachschlageschlüssel. * * Liefert `null`, wenn die Eingabe nicht wie eine Nummer aussieht – dann * bleibt es bei der Textsuche. */ export function nummerSchluessel(roh: string): string | null { const t = roh.trim().toLowerCase().replace(/\s+/gu, ' ').replace(VORSPANN, '').trim(); const kennung = KENNUNG.exec(t); if (kennung !== null) { const kapitel = kennung[2] === undefined ? (kennung[1] ?? '') : `i.${kennung[2]}`; return `kennung:${kapitel}-${kennung[3] ?? ''}`; } const mitAbschnitt = MIT_ABSCHNITT.exec(t); if (mitAbschnitt !== null) { return `nummer:${mitAbschnitt[1] ?? ''}.${mitAbschnitt[2] ?? ''}`; } const bloss = BLOSSE_NUMMER.exec(t); if (bloss !== null) { return `nummer:${bloss[1] ?? ''}`; } return null; } /** Der Schlüssel, unter dem eine Frage selbst zu finden ist. */ function schluesselDerFrage(kennung: string, nummer: string): readonly string[] { const aus: string[] = []; // Kennung: „I.2-150“ → „kennung:i.2-150“, „II-45“ → „kennung:ii-45“. const teile = /^(i\.[1-5]|ii|iii|iv)-0*(\d{1,3})$/u.exec(kennung.toLowerCase()); if (teile !== null) { aus.push(`kennung:${teile[1] ?? ''}-${teile[2] ?? ''}`); } // Amtliche Nummer: „1.01“ → „nummer:1.1“, „45“ → „nummer:45“. const mitAbschnitt = MIT_ABSCHNITT.exec(nummer); if (mitAbschnitt !== null) { aus.push(`nummer:${mitAbschnitt[1] ?? ''}.${mitAbschnitt[2] ?? ''}`); } else { const bloss = BLOSSE_NUMMER.exec(nummer); if (bloss !== null) { aus.push(`nummer:${bloss[1] ?? ''}`); } } return aus; } export interface Nummernindex { readonly nach: ReadonlyMap; } export function nummernindexBauen(katalog: Katalog): Nummernindex { const nach = new Map(); for (const frage of katalog.fragen) { for (const schluessel of schluesselDerFrage(frage.id, frage.amtliche_nummer)) { const liste = nach.get(schluessel) ?? []; liste.push(frage.id); nach.set(schluessel, liste); } } return { nach }; } /** * Die Fragen zu einer Nummerneingabe – in Katalogreihenfolge. * * Leere Liste heißt: Die Eingabe sah nicht wie eine Nummer aus oder es gibt * die Nummer nicht. Beides ist kein Fehler; die Textsuche läuft ohnehin * darunter weiter. */ export function nummernTreffer(index: Nummernindex, eingabe: string): readonly string[] { const schluessel = nummerSchluessel(eingabe); if (schluessel === null) { return []; } return index.nach.get(schluessel) ?? []; }