fix: Umlaut-Berichtigung erfasst Komposita und Beugungen, Bezeichner bleiben

Wortstämme aus 252 gespeicherten Läufen, jeder gegen ngerman/american-english
geprüft; Wörter mit _ oder Binnenmajuskel gelten als Bezeichner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-09-17 14:26:50 +02:00
commit dd7206aee5
2 changed files with 42 additions and 7 deletions

View file

@ -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:

View file

@ -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