diff --git a/solving/solver_stage.py b/solving/solver_stage.py index 7613717..8804c29 100644 --- a/solving/solver_stage.py +++ b/solving/solver_stage.py @@ -604,8 +604,20 @@ UMLAUT_STEMS = { "jaehrl": "jährl", "kuerzest": "kürzest", "laenge": "länge", "staerk": "stärk", "hoeh": "höh", "fruehest": "frühest", "spaetest": "spätest", "baell": "bäll", "heisst": "heißt", "betraeg": "beträg", "ausfuehr": "ausführ", "abzueg": "abzüg", "enthaelt": "enthält", "tatsaechl": "tatsächl", + # Wortstämme aus 252 gespeicherten Läufen; greifen auch in Komposita („Gesamtstueckzahl") und Beugungen. + # Jeder Stamm ist gegen /usr/share/dict/ngerman und american-english geprüft: er steht in keinem + # korrekt geschriebenen Wort („loes" fiel heraus: aloes, floes). + "taet": "tät", "stueck": "stück", "fuehr": "führ", "traeg": "träg", "vollstaend": "vollständ", + "zulaess": "zuläss", "loesbar": "lösbar", "erloes": "erlös", "schoepf": "schöpf", "gebuehr": "gebühr", + "noetig": "nötig", "faell": "fäll", "haelt": "hält", "waehr": "währ", "waechs": "wächs", "waere": "wäre", + "laeuft": "läuft", "schluess": "schlüss", "duerf": "dürf", "kaeuf": "käuf", "wuerd": "würd", + "gueltig": "gültig", "gehoer": "gehör", "drueck": "drück", "uebrig": "übrig", "haeng": "häng", + "naemlich": "nämlich", "lueck": "lück", "saetz": "sätz", "plaen": "plän", "aermst": "ärmst", + "spaet": "spät", "frueh": "früh", "aender": "änder", "zaehl": "zähl", "verhaeltnis": "verhältnis", + "ergaenz": "ergänz", "begruend": "begründ", "pruef": "prüf", "rueck": "rück", "maessig": "mäßig", "vermoeg": "vermög", } _STEM_PATTERN = re.compile("|".join(UMLAUT_STEMS), re.IGNORECASE) +_CAMEL_CASE = re.compile(r"[a-zäöüß][A-ZÄÖÜ]") # KeinFruehNachSpaet _UMLAUT_WORD = re.compile(r"\b\w*[äöüÄÖÜß]\w*\b") @@ -635,17 +647,24 @@ def umlaut_map(aufgabe: str, *weitere: str) -> dict: def fix_transliterations(text: str, extra: dict | None = None) -> str: """'guenstigste' -> 'günstigste': erst Wörter aus dem Lauf (`umlaut_map`), dann die feste Stammliste. - Nur für Lesertexte. Bezeichner und Restriktionsnamen bleiben ASCII – der Namensabgleich hängt daran. + Nur für Lesertexte. Bezeichner und Restriktionsnamen bleiben ASCII – der Namensabgleich hängt daran; + auch in Tabellenzellen bleibt ein Wort mit `_` oder Binnenmajuskel unverändert (vorher wurde mal berichtigt, mal nicht). """ def gross_wie(vorlage, richtig): return richtig[:1].upper() + richtig[1:] if vorlage[:1].isupper() else richtig - text = text or "" - if extra: - muster = re.compile(r"\b(?:" + "|".join(sorted(map(re.escape, extra), key=len, reverse=True)) + r")\b", - re.IGNORECASE) - text = muster.sub(lambda t: gross_wie(t.group(0), extra[t.group(0).lower()]), text) - return _STEM_PATTERN.sub(lambda t: gross_wie(t.group(0), UMLAUT_STEMS[t.group(0).lower()]), text) + muster = (re.compile("^(?:" + "|".join(sorted(map(re.escape, extra), key=len, reverse=True)) + ")$", re.IGNORECASE) + if extra else None) + + def wort(t): + w = t.group(0) + if "_" in w or _CAMEL_CASE.search(w): # Bezeichner (Restriktionsnamen in Tabellenzellen) bleiben + return w + if muster and muster.match(w): + return gross_wie(w, extra[w.lower()]) + return _STEM_PATTERN.sub(lambda m: gross_wie(m.group(0), UMLAUT_STEMS[m.group(0).lower()]), w) + + return re.sub(r"\w+", wort, text or "") def check_text_quality(run, summary: str = "") -> list: diff --git a/tests/test_result_tables.py b/tests/test_result_tables.py index 0dace43..ed3da8b 100644 --- a/tests/test_result_tables.py +++ b/tests/test_result_tables.py @@ -293,3 +293,19 @@ def test_numbers_and_words_are_not_mistaken_for_units(): from solving.solver_stage import check_answer_units run = _run_with_answer("Teil 2 zeigt: x1 = 6, 3 Maschinen, Stufe 2a, am Tag 3 sind alle 5 Aufträge erledigt.") assert check_answer_units(run, "Plane die Aufträge.") == [] + + +def test_repair_reaches_compounds_and_inflections(): + # beobachtet in Portfolio Nr. 2 und nur_formeln (17.09.2026), Stämme gegen ngerman geprüft + from solving.solver_stage import fix_transliterations + text = "Aufgefuehrt: Beitraege, Jahresvolatilitaet, Gesamtstueckzahl, zulaessige Kardinalitaet, vollstaendig." + assert fix_transliterations(text) == ("Aufgeführt: Beiträge, Jahresvolatilität, Gesamtstückzahl, " + "zulässige Kardinalität, vollständig.") + + +def test_repair_leaves_correct_words_and_identifiers_alone(): + from solving.solver_stage import fix_transliterations + richtig = "Die neue Dauer, Quelle, aktuell, Kostenkoeffizient, Bauer, streuen, blue." + assert fix_transliterations(richtig) == richtig + namen = "Bindend: Spaet_zu_Frueh_Verbot und KeinFruehNachSpaet sowie Kapazitaet_M1." + assert fix_transliterations(namen) == namen