refactor: VF-Linienzug-Journal-Grammatik in lib/vf_linienzug_grammatik.py gebuendelt

lib/vf_journal_export.py (Journal.dekodiere) und lib/vf_spec_export.py
(SpecLeser) implementierten denselben Tokenwalk (Praeambel, Sektions-
schleife, Glied-Dispatch, VF-Einheit-Unterschleife) sowie dieselben
Code-Tabellen (SEITE/VARIANTE/VERTEILUNG/IM_VF/ENDPUNKT/ENDE2/ENDE3/
GEFAELLE/GF_LAENGE_MODUS, MENU_START/MENU_START_AKTUELL/MENU_WEITER,
Grammatik) unabhaengig voneinander - Aenderungen an der Grammatik (wie
im vorigen Commit) mussten von Hand synchron gehalten werden und sind
bereits einmal auseinandergelaufen.

Neuer gemeinsamer Parser-Kern (Kern/lies_journal) plus Sink-Schnittstelle
(SpecSink in vf_spec_export.py, ProtokollSink in vf_journal_export.py)
ersetzt die doppelte Implementierung; Code-Tabellen/Grammatik-Objekte
leben jetzt nur noch in lib/vf_linienzug_grammatik.py. Journal behaelt
nur noch die reine XDATA/DXF-Zerlegung, SpecSchreiber (Spec -> Journal,
Gegenstueck zu Lisp/vf_spec.lsp) bleibt unveraendert in vf_spec_export.py.

Zusaetzlich: pruefe_flachen_freitext schlaegt jetzt hart fehl, wenn
generierter Freitext (quelle/beschreibung) ein rohes Anfuehrungszeichen
enthaelt - der Lisp-seitige Flach-JSON-Leser (ssg-cfg-parse-value) kennt
keine Escapes und wuerde daran die Zeichenkette (und damit die gesamte
restliche Datei) falsch zerlegen.

Byte-identischer Output gegen die Baseline geprueft (beide CLIs, echte
Eingaben); pytest tests/test_vf_spec.py + test_vf_journal_grammatik.py
unveraendert: 2 bekannte, vorbestehende Fehlschlaege, 38 bestanden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Samer Ayadi
2026-09-14 11:59:22 +02:00
co-authored by Claude Sonnet 5
parent 1bdecf0678
commit 6d15c6be13
3 changed files with 1023 additions and 610 deletions
+182 -316
View File
@@ -24,8 +24,10 @@ sonst waere jeder spec-gebaute Nachbau eine andere Kette als das Original.
Der Schreiber ist das Vorbild fuer vfl-spec->journal in Lisp/vf_spec.lsp;
beide muessen dieselbe Tokenfolge liefern.
Die Grammatik selbst (welche Frage in welcher Reihenfolge) steckt in
lib/vf_journal_export.py (Journal.dekodiere) und ist dort kommentiert.
Die Grammatik selbst (welche Frage in welcher Reihenfolge) steht EINMAL in
lib/vf_linienzug_grammatik.py und wird von hier (Spec) wie von
lib/vf_journal_export.py (Protokoll) ueber denselben Parser-Kern benutzt -
nur mit unterschiedlicher Sink.
Aufruf (erzeugt die Spec-Testdaten aus dem Protokoll der echten Ketten):
@@ -41,94 +43,20 @@ import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from vf_journal_export import arrays_einzeilig # noqa: E402
# Die Code-Tabellen, die Grammatik-Auspraegungen und der Parser-Kern stehen
# EINMAL im gemeinsamen Modul - hier nur noch importiert, nicht nachgebaut.
# Ein Teil davon wird hier selbst nicht mehr benutzt, sondern nur
# weitergereicht (vse.MENU_START, vse.GLIEDER, vse.GRAMMATIK_AKTUELL ... in
# tests/ und in Aufrufern) - die oeffentliche Flaeche bleibt unveraendert.
from vf_linienzug_grammatik import ( # noqa: E402,F401
ENDE2, ENDE3, ENDPUNKT, GEFAELLE, GF_LAENGE_MODUS, GLIEDER,
GRAMMATIK_AKTUELL, GRAMMATIK_AUFZEICHNUNG, Grammatik, IM_VF, MENU_START,
MENU_START_AKTUELL, MENU_WEITER, SEITE, VARIANTE, VERTEILUNG, VF_GLIEDER,
Sink, code_aus_ja, code_aus_wert, lies_journal, pruefe_flachen_freitext)
PREFIX = "[vf_spec_export]"
# ============================================================
# Code-Tabellen: Menue-Antwort <-> Domaenenwert
# ============================================================
# Links der Journal-Code, rechts der Wert in der Spec. Die Spec kennt KEINE
# Codes - sie sind eine Eigenschaft des Dialogs, nicht der Anlage. Nur die
# AS-/ES-Winkel und die Bogen-/Kurvenwinkel stehen im Journal als Wert und
# bleiben darum Werte.
SEITE = {"1": "links", "2": "rechts"}
VARIANTE = {"1": "aussen", "2": "innen"}
GEFAELLE = {"1": "hoehe", "2": "winkel"}
# Vorwahl "Wie soll die Laenge der Gefaellestrecke bestimmt werden?"
# (vfl-menu in vf-linienzug-modus, wahl="Linie-GF"). Die innere Wahl im
# Steigend-Sonderfall (Endpunkt/Hoehe) nutzt dieselbe Tabelle - dort sind
# nur "1"/"2" gueltig, "steigend" kommt dort nie vor.
GF_LAENGE_MODUS = {"1": "endpunkt", "2": "hoehe", "3": "steigend"}
VERTEILUNG = {"1": "haelfte", "2": "einlauf"}
IM_VF = {"1": "horizontal", "2": "vario-kurve", "3": "auf-ab"}
ENDPUNKT = {"1": "motorstation", "2": "weiter", "3": "kettenende"}
ENDE3 = {"1": "ja-mit-es", "2": "ja-ohne-es", "3": "nein"}
ENDE2 = {"1": "ja-mit-es", "2": "nein"}
# Glied-Typ -> Menue-Code, getrennt nach Position in der Kette. Der Code
# haengt am Frame: die ERSTE Sektion laeuft ohne Frame und bekommt 4 Optionen
# (kein GF-Bogen - es gibt noch keine Richtung, an die er anschliessen
# koennte), jede spaetere 5. Siehe vf-linienzug-modus.
#
# WICHTIG (2026-09-12): vf-linienzug-modus wurde SPAETER um "GF-Bogen"/
# "Vario-Kurve" auch am Kettenanfang erweitert (6 statt 4 Optionen dort,
# "Fortsetzung einer bestehenden Teilkette" - Referenzrichtung kommt dann aus
# einem KS_AUS am Startpunkt). Lisp/vf_spec.lsp's *vsp-menu-start* wurde
# entsprechend auf die NEUE 6er-Zuordnung umgestellt, WEIL vsp-journal seine
# Tokens durch den AKTUELLEN (lebenden) vf-linienzug-modus REPLAYT - der
# Code muss dort zur JETZT gueltigen Menue-Reihenfolge passen.
#
# Diese Python-Tabelle bleibt bewusst auf der ALTEN 4er-Zuordnung: sie
# decodiert tests/testdata/hm_recformat.json - eine EINGEFRORENE Aufzeichnung
# einer echten Kundenzeichnung, die zu einem Zeitpunkt VOR dieser Erweiterung
# entstand (ihr roher Code fuer "Linie-GF" am Kettenanfang ist "1", nicht
# "3"). Wuerde diese Tabelle auf die neue Zuordnung umgestellt, koennte
# SpecReader die vorhandene Aufzeichnung nicht mehr korrekt lesen (siehe
# Testfall test_verlustfrei in tests/test_vf_spec.py). Die Tokenfolgen von
# LISP (vsp-journal, GEGEN den lebenden Bau-Ablauf) und Python (hier, GEGEN
# die eingefrorene Aufzeichnung) sind fuer "Linie-GF" am Kettenanfang darum
# ab dieser Erweiterung bewusst NICHT mehr identisch - eine unvermeidbare
# Folge davon, dass sich die Menue-Struktur nach der Aufzeichnung geaendert
# hat. Wird hm_recformat.json je aus einer NEUEN Aufzeichnung (mit dem
# aktuellen 6-Optionen-Ablauf) neu erzeugt, muesste diese Tabelle mitziehen.
MENU_START = {"Linie-GF": "1", "Linie-VF": "2", "Horizontal-VF": "3",
"Linie": "4"}
MENU_WEITER = {"GF-Bogen": "1", "Linie-GF": "2", "Linie-VF": "3",
"Horizontal-VF": "4", "Linie": "5"}
GLIEDER = sorted(set(MENU_WEITER) | {"ES"})
# Glieder, die eine VF-Einheit eroeffnen, und ob ihr erster Koerper
# horizontal ist (winkel1 = 0 in vfl-vf-einheit: nur dann laufen die
# Separator-/Endpunkt-Fragen VOR der Laengenberechnung).
VF_GLIEDER = {"Horizontal-VF": True, "Linie-VF": False, "Linie": False}
def _um(tabelle, code, was):
if code not in tabelle:
raise ValueError("%s: unbekannter Code %r (erlaubt: %s)"
% (was, code, sorted(tabelle)))
return tabelle[code]
def _code(tabelle, wert, was):
fuer = dict((v, k) for k, v in tabelle.items())
if wert not in fuer:
raise ValueError("%s: unbekannter Wert %r (erlaubt: %s)"
% (was, wert, sorted(fuer)))
return fuer[wert]
def _ja(code, was):
return _um({"1": True, "2": False}, code, was)
def _ja_code(wert):
return "1" if wert else "2"
# ============================================================
# Tokens
# ============================================================
@@ -208,217 +136,110 @@ def erste_abweichung(a, b):
# ============================================================
# Leser: Journal -> Spec
# ============================================================
# Den Ablauf (welche Frage wann) laeuft der gemeinsame Kern in
# lib/vf_linienzug_grammatik.py. Hier steht nur noch, WOHIN die gemeldeten
# Werte wandern: in eine verschachtelte Spec aus OrderedDicts.
class SpecLeser(object):
"""Tokens in eine Spec uebersetzen. Folgt derselben Frage-Reihenfolge wie
Journal.dekodiere in lib/vf_journal_export.py, legt aber Domaenenwerte ab
statt Protokollzeilen."""
class SpecSink(Sink):
"""Baut aus den Meldungen des Parser-Kerns die Spec.
def __init__(self, tok):
self.tok = list(tok)
self.pos = 0
self.erster_step = True
self.erster_dl = True
Die Reihenfolge der Schluessel ist bewusst die des Frageablaufs - das
flache Spec-JSON wird in genau dieser Reihenfolge geschrieben und ist so
neben der Aufzeichnung lesbar.
"""
# --- Tokenzugriff ---
def __init__(self):
self.spec = collections.OrderedDict()
self.start = None
self.sektionen = []
self.sek = None
self.vf = None
self.sub = None
# Wohin die naechsten Felder gehoeren (Kopf / Sektion / VF-Einheit /
# Sub-Knoten). Der Kern sagt mit den Struktur-Meldungen an, wann sich
# das Ziel aendert - hier wird nichts geraten.
self.ziel = None
def art(self):
return self.tok[self.pos][0] if self.pos < len(self.tok) else None
def ergebnis(self):
self.spec["start"] = self.start
self.spec["sektionen"] = self.sektionen
return self.spec
def label(self):
return self.tok[self.pos][1] if self.pos < len(self.tok) else None
# --- Struktur ---
def nimm(self, erwartet):
if self.pos >= len(self.tok):
raise ValueError("Journal endet vor %s" % erwartet)
art, wert = self.tok[self.pos]
if art != erwartet:
raise ValueError("Token %d: %s erwartet, %s:%s gefunden"
% (self.pos, erwartet, art, wert))
self.pos += 1
return wert
def praeambel_start(self):
self.start = collections.OrderedDict()
self.ziel = self.start
def real(self):
return float(self.nimm("REAL"))
def praeambel_ende(self):
self.ziel = None
def ganz(self):
return int(self.nimm("INT"))
def sektion_start(self, label):
self.sek = collections.OrderedDict([("glied", label)])
self.ziel = self.sek
def opt_winkel_idx(self, ziel):
"""Die Winkelwahl (vfl-waehle-winkel) steht nur im Journal, wenn
mehrere Kandidatenwinkel gueltig waren - sonst faellt die Frage weg."""
if self.art() == "INT":
ziel["winkel_idx"] = self.ganz()
def sektion_ende(self):
self.sektionen.append(self.sek)
self.sek = None
self.ziel = None
def linie(self, ziel):
"""Segmentlaenge, und NUR beim allerersten Segment der Kette die
gesnappte Fahrtrichtung (vfl-in-abstand journalisiert sie nur bei
freier Richtungswahl)."""
ziel["dl"] = float(self.nimm("DL"))
if self.erster_dl and self.art() == "REAL":
ziel["hz"] = self.real()
self.erster_dl = False
def vf_start(self):
self.vf = collections.OrderedDict()
self.ziel = self.vf
# --- Ablauf ---
def vf_ende(self):
# "subs" wird erst beim ersten Sub-Knoten angelegt, damit es in der
# Reihenfolge hinter gf_verteilung/sep_vor/sep_nach steht.
self.vf.setdefault("subs", [])
self.sek["vf"] = self.vf
self.vf = None
self.ziel = self.sek
def lies(self):
spec = collections.OrderedDict()
spec["start"] = self.praeambel()
spec["sektionen"] = self.sektionen()
if self.pos != len(self.tok):
raise ValueError("nicht alle Tokens verbraucht (%d von %d)"
% (self.pos, len(self.tok)))
return spec
def sub_start(self, sub):
self.sub = collections.OrderedDict([("sub", sub)])
self.vf.setdefault("subs", [])
self.ziel = self.sub
def praeambel(self):
start = collections.OrderedDict()
start["punkt"] = [float(v) for v in self.nimm("PT").split(",")]
start["hoehe"] = self.real()
start["as"] = _ja(self.nimm("STR"), "AS-Element setzen")
if start["as"]:
start["as_winkel"] = self.nimm("STR") # Wert, kein Code
start["as_seite"] = _um(SEITE, self.nimm("STR"), "AS-Seite")
return start
def sub_ende(self):
self.vf["subs"].append(self.sub)
self.sub = None
self.ziel = self.vf
def sektionen(self):
raus = []
while self.pos < len(self.tok):
label = self.nimm("STEP")
if label == "ES":
raus.append(self.es_glied())
continue
if label == "Vario-Kurve":
raise ValueError("Vario-Kurve-Marker ausserhalb einer "
"VF-Einheit (Token %d)" % (self.pos - 1))
sek = collections.OrderedDict()
sek["glied"] = label
tabelle = MENU_START if self.erster_step else MENU_WEITER
code = self.nimm("STR")
soll = tabelle.get(label)
if soll is None:
raise ValueError("Glied %r ist an dieser Stelle nicht "
"waehlbar (Token %d)" % (label, self.pos - 1))
if code != soll:
raise ValueError("Glied %r erwartet Menue-Code %r, Journal "
"hat %r (Token %d)"
% (label, soll, code, self.pos - 1))
self.erster_step = False
self.glied(label, sek)
raus.append(sek)
return raus
# --- Werte ---
def es_glied(self):
sek = collections.OrderedDict()
sek["glied"] = "ES"
sek["winkel"] = self.nimm("STR") # Wert, kein Code
sek["seite"] = _um(SEITE, self.nimm("STR"), "ES-Seite")
return sek
def punkt_abs(self, punkt):
self.ziel["punkt"] = punkt
def glied(self, label, sek):
if label == "GF-Bogen":
sek["winkel"] = self.ganz()
sek["seite"] = _um(SEITE, self.nimm("STR"), "GF-Bogen-Seite")
elif label == "Linie-GF":
self.linie(sek)
sek["gefaelle"] = _um(GEFAELLE, self.nimm("STR"), "Gefaelle")
if sek["gefaelle"] == "winkel":
sek["winkel"] = self.real()
else:
sek["hoehe"] = self.real()
sek["ende"] = _um(ENDE3, self.nimm("STR"), "Kettenende")
elif label == "Linie-VF":
self.linie(sek)
sek["hoehe"] = self.real()
self.opt_winkel_idx(sek)
sek["vf"] = self.vf_einheit(auto_ende=False, erst_hor=False)
elif label == "Horizontal-VF":
self.linie(sek)
sek["vf"] = self.vf_einheit(auto_ende=False, erst_hor=True)
elif label == "Linie":
self.linie(sek)
sek["hoehe"] = self.real()
self.opt_winkel_idx(sek)
# vfl-segment-entscheidung waehlt selbst zwischen reiner
# Gefaellestrecke und VF-Einheit. Am Journal erkennbar: eine
# VF-Einheit beginnt mit der GF-Verteilungsfrage (STR), eine
# Gefaellestrecke geht direkt zum ES-Glied (STEP) oder endet.
if self.art() == "STR":
sek["segment_typ"] = "VF"
sek["vf"] = self.vf_einheit(auto_ende=True, erst_hor=False)
else:
sek["segment_typ"] = "GF"
else:
raise ValueError("unbekanntes Glied: %r" % label)
def linie(self, dl, hz, frage):
self.ziel["dl"] = dl
if hz is not None:
self.ziel["hz"] = hz
def vf_einheit(self, auto_ende, erst_hor):
vf = collections.OrderedDict()
vf["gf_verteilung"] = _um(VERTEILUNG, self.nimm("STR"),
"GF-Verteilung")
if erst_hor:
# Erster Koerper horizontal: vfl-baue-horizontal-koerper stellt
# die Separator-Fragen VOR der Laengenberechnung. Die
# Endpunkt-Antwort danach gehoert schon zum naechsten Knoten und
# wird darum unten in der Schleife gelesen.
vf["sep_vor"] = _ja(self.nimm("STR"), "Separator vor")
vf["sep_nach"] = _ja(self.nimm("STR"), "Separator nach")
vf["subs"] = []
ziel_ende = False
es_gewuenscht = True
while True:
was = _um(ENDPUNKT, self.nimm("STR"), "Endpunkt des Foerderers")
if was == "motorstation":
vf["subs"].append(collections.OrderedDict(
[("sub", "motorstation")]))
break
if was == "kettenende":
sub = collections.OrderedDict([("sub", "kettenende")])
sub["es"] = _ja(self.nimm("STR"), "ES-Element setzen")
self.linie(sub)
sub["hoehe"] = self.real()
self.opt_winkel_idx(sub)
vf["subs"].append(sub)
ziel_ende = True
es_gewuenscht = sub["es"]
break
wahl = _um(IM_VF, self.nimm("STR"), "Naechstes in der VF-Einheit")
sub = collections.OrderedDict([("sub", wahl)])
if wahl == "horizontal":
self.linie(sub)
sub["sep_vor"] = _ja(self.nimm("STR"), "Separator vor")
sub["sep_nach"] = _ja(self.nimm("STR"), "Separator nach")
elif wahl == "vario-kurve":
marke = self.nimm("STEP")
if marke != "Vario-Kurve":
raise ValueError("Vario-Kurve-Marker erwartet, %r "
"gefunden" % marke)
sub["winkel"] = self.ganz()
sub["seite"] = _um(SEITE, self.nimm("STR"),
"Vario-Kurve-Seite")
sub["variante"] = _um(VARIANTE, self.nimm("STR"),
"Vario-Kurve-Variante")
else: # auf-ab
self.linie(sub)
sub["hoehe"] = self.real()
self.opt_winkel_idx(sub)
vf["subs"].append(sub)
# --- Abschluss (vfl-vf-einheit-abschluss) ---
# Ohne ES-Wunsch am Zielpunkt endet die Kette dort direkt; bei
# auto_ende (Glied "Linie") oder erreichtem Zielpunkt folgt das
# ES-Glied ohne Frage. Sonst wird gefragt.
if ziel_ende and not es_gewuenscht:
vf["ende"] = "zielpunkt-ohne-es"
elif auto_ende or ziel_ende:
vf["ende"] = "automatisch"
else:
vf["ende"] = _um(ENDE2, self.nimm("STR"), "Ist das Kettenende")
if vf["ende"] != "ja-mit-es":
vf["separator"] = _ja(self.nimm("STR"),
"Separator an dieser Stelle")
return vf
def real_feld(self, name, wert, frage):
self.ziel[name] = wert
def int_feld(self, name, wert, frage):
self.ziel[name] = wert
def code_feld(self, name, code, wert, frage):
self.ziel[name] = wert
def roh_feld(self, name, wert, frage):
self.ziel[name] = wert
def ja_nein(self, name, code, wert, frage):
self.ziel[name] = wert
def implizit(self, name, wert):
self.ziel[name] = wert
# wahl()/glied_gewaehlt()/marke() schlagen sich in der STRUKTUR nieder
# (Sub-Knoten, Glied-Name) - die Spec speichert sie nicht noch einmal als
# Feld. Die Erbversionen aus Sink tun darum genau das Richtige: nichts.
def spec_aus_journal(tok):
return SpecLeser(tok).lies()
def spec_aus_journal(tok, grammatik=GRAMMATIK_AUFZEICHNUNG):
return lies_journal(tok, SpecSink(), grammatik)
# ============================================================
@@ -431,8 +252,9 @@ def spec_aus_journal(tok):
class SpecSchreiber(object):
def __init__(self, spec):
def __init__(self, spec, grammatik=GRAMMATIK_AUFZEICHNUNG):
self.spec = spec
self.grammatik = grammatik
self.tok = []
self.erster_dl = True
self.wo = "Spec"
@@ -497,10 +319,10 @@ class SpecSchreiber(object):
self.raus("PT", ",".join(_zahl(v) for v in punkt))
self.zahl("REAL", self.feld(start, "hoehe"))
gesetzt = bool(self.feld(start, "as"))
self.raus("STR", _ja_code(gesetzt))
self.raus("STR", code_aus_ja(gesetzt))
if gesetzt:
self.raus("STR", str(self.feld(start, "as_winkel")))
self.raus("STR", _code(SEITE, self.feld(start, "as_seite"),
self.raus("STR", code_aus_wert(SEITE, self.feld(start, "as_seite"),
"as_seite"))
def sektion(self, sek, erster):
@@ -508,7 +330,7 @@ class SpecSchreiber(object):
if label == "ES":
self.es_glied(sek)
return
tabelle = MENU_START if erster else MENU_WEITER
tabelle = self.grammatik.menu_start if erster else MENU_WEITER
if label not in tabelle:
raise ValueError("%s: Glied %r ist an dieser Stelle nicht "
"waehlbar (erlaubt: %s)"
@@ -517,7 +339,8 @@ class SpecSchreiber(object):
self.raus("STR", tabelle[label])
if label == "GF-Bogen":
self.raus("INT", str(int(self.feld(sek, "winkel"))))
self.raus("STR", _code(SEITE, self.feld(sek, "seite"), "seite"))
self.raus("STR", code_aus_wert(SEITE, self.feld(sek, "seite"),
"seite"))
elif label == "Linie-GF":
# Vorwahl "laenge_modus" (endpunkt/hoehe/steigend, Default
# "endpunkt" - deckt alte Specs ohne dieses Feld ab). "steigend"
@@ -540,12 +363,13 @@ class SpecSchreiber(object):
# (vf-linienzug-modus), kein Fehler in diesem Schreiber. Siehe
# doc/vf_linienzug_modus1_anleitung.md / Commit-Historie.
modus = self.feld(sek, "laenge_modus", pflicht=False) or "endpunkt"
self.raus("STR", _code(GF_LAENGE_MODUS, modus, "laenge_modus"))
self.raus("STR", code_aus_wert(GF_LAENGE_MODUS, modus,
"laenge_modus"))
if modus == "steigend":
smodus = (self.feld(sek, "steigend_modus", pflicht=False)
or "endpunkt")
self.raus("STR", _code(GF_LAENGE_MODUS, smodus,
"steigend_modus"))
self.raus("STR", code_aus_wert(GF_LAENGE_MODUS, smodus,
"steigend_modus"))
if smodus == "endpunkt":
self.linie(sek)
elif self.erster_dl:
@@ -556,23 +380,24 @@ class SpecSchreiber(object):
elif modus == "hoehe":
if self.erster_dl:
self.richtung(sek)
self.raus("STR", _ja_code(self.feld(sek, "sep_vor",
pflicht=False)))
self.raus("STR", _ja_code(self.feld(sek, "sep_nach",
pflicht=False)))
self.raus("STR", code_aus_ja(self.feld(sek, "sep_vor",
pflicht=False)))
self.raus("STR", code_aus_ja(self.feld(sek, "sep_nach",
pflicht=False)))
self.zahl("REAL", self.feld(sek, "hoehe"))
self.zahl("REAL", self.feld(sek, "winkel"))
else: # "endpunkt"
self.linie(sek)
self.raus("STR", _ja_code(self.feld(sek, "sep_vor",
pflicht=False)))
self.raus("STR", _ja_code(self.feld(sek, "sep_nach",
pflicht=False)))
self.raus("STR", code_aus_ja(self.feld(sek, "sep_vor",
pflicht=False)))
self.raus("STR", code_aus_ja(self.feld(sek, "sep_nach",
pflicht=False)))
art = self.feld(sek, "gefaelle")
self.raus("STR", _code(GEFAELLE, art, "gefaelle"))
self.raus("STR", code_aus_wert(GEFAELLE, art, "gefaelle"))
self.zahl("REAL", self.feld(sek, "winkel" if art == "winkel"
else "hoehe"))
self.raus("STR", _code(ENDE3, self.feld(sek, "ende"), "ende"))
self.raus("STR", code_aus_wert(ENDE3, self.feld(sek, "ende"),
"ende"))
elif label == "Linie-VF":
self.linie(sek)
self.zahl("REAL", self.feld(sek, "hoehe"))
@@ -580,6 +405,10 @@ class SpecSchreiber(object):
self.vf_einheit(sek, erst_hor=False)
elif label == "Horizontal-VF":
self.linie(sek)
# Optional: Ziel-Hoehe des GF1-Einlaufs - nur wenn der Eingang
# komplett neu gebaut wird (siehe SpecLeser.opt_eingang_hoehe).
if "eingang_hoehe" in sek:
self.zahl("REAL", sek["eingang_hoehe"])
self.vf_einheit(sek, erst_hor=True)
elif label == "Linie":
self.linie(sek)
@@ -593,15 +422,20 @@ class SpecSchreiber(object):
def es_glied(self, sek):
self.raus("STEP", "ES")
self.raus("STR", str(self.feld(sek, "winkel")))
self.raus("STR", _code(SEITE, self.feld(sek, "seite"), "seite"))
self.raus("STR", code_aus_wert(SEITE, self.feld(sek, "seite"),
"seite"))
def vf_einheit(self, sek, erst_hor):
vf = self.feld(sek, "vf")
self.raus("STR", _code(VERTEILUNG, self.feld(vf, "gf_verteilung"),
"gf_verteilung"))
# GF-Verteilung: bei horizontalem Erstkoerper stellt der heutige
# Ablauf die Frage nicht mehr (fest "2") - siehe Grammatik.
if not erst_hor or self.grammatik.horizontal_vf_fragt_gf_verteilung:
self.raus("STR", code_aus_wert(VERTEILUNG,
self.feld(vf, "gf_verteilung"),
"gf_verteilung"))
if erst_hor:
self.raus("STR", _ja_code(self.feld(vf, "sep_vor")))
self.raus("STR", _ja_code(self.feld(vf, "sep_nach")))
self.raus("STR", code_aus_ja(self.feld(vf, "sep_vor")))
self.raus("STR", code_aus_ja(self.feld(vf, "sep_nach")))
subs = vf.get("subs") or []
if not subs:
raise ValueError("%s: VF-Einheit ohne subs - sie muss mit "
@@ -614,10 +448,21 @@ class SpecSchreiber(object):
"folgen aber noch %d weitere"
% (self.wo, wahl, len(subs) - i))
if wahl == "motorstation":
self.raus("STR", _code(ENDPUNKT, "motorstation", "sub"))
self.raus("STR", code_aus_wert(ENDPUNKT, "motorstation",
"sub"))
elif wahl == "kettenende":
self.raus("STR", _code(ENDPUNKT, "kettenende", "sub"))
self.raus("STR", _ja_code(self.feld(sub, "es")))
self.raus("STR", code_aus_wert(ENDPUNKT, "kettenende", "sub"))
gesetzt = bool(self.feld(sub, "es"))
self.raus("STR", code_aus_ja(gesetzt))
if gesetzt:
# Winkel+Seite des ES-Elements stehen HIER im Journal
# (vfl-frage-es-seite, inkl. eigenem Marker) - der
# ES-Fussabdruck muss vor der Zerlegung feststehen.
self.raus("STEP", "ES")
self.raus("STR", str(self.feld(sub, "es_winkel")))
self.raus("STR", code_aus_wert(SEITE,
self.feld(sub, "es_seite"),
"es_seite"))
self.linie(sub)
self.zahl("REAL", self.feld(sub, "hoehe"))
self.winkel_idx(sub)
@@ -626,18 +471,19 @@ class SpecSchreiber(object):
raise ValueError("%s: VF-Einheit endet mit sub %r - der "
"letzte sub muss motorstation oder "
"kettenende sein" % (self.wo, wahl))
self.raus("STR", _code(ENDPUNKT, "weiter", "sub"))
self.raus("STR", _code(IM_VF, wahl, "sub"))
self.raus("STR", code_aus_wert(ENDPUNKT, "weiter", "sub"))
self.raus("STR", code_aus_wert(IM_VF, wahl, "sub"))
if wahl == "horizontal":
self.linie(sub)
self.raus("STR", _ja_code(self.feld(sub, "sep_vor")))
self.raus("STR", _ja_code(self.feld(sub, "sep_nach")))
self.raus("STR", code_aus_ja(self.feld(sub, "sep_vor")))
self.raus("STR", code_aus_ja(self.feld(sub, "sep_nach")))
elif wahl == "vario-kurve":
self.raus("STEP", "Vario-Kurve")
self.raus("INT", str(int(self.feld(sub, "winkel"))))
self.raus("STR", _code(SEITE, self.feld(sub, "seite"),
"seite"))
self.raus("STR", _code(VARIANTE,
self.raus("STR", code_aus_wert(SEITE,
self.feld(sub, "seite"),
"seite"))
self.raus("STR", code_aus_wert(VARIANTE,
self.feld(sub, "variante"),
"variante"))
else: # auf-ab
@@ -647,13 +493,13 @@ class SpecSchreiber(object):
ende = self.feld(vf, "ende")
if ende in ("zielpunkt-ohne-es", "automatisch"):
return # keine Frage
self.raus("STR", _code(ENDE2, ende, "vf.ende"))
self.raus("STR", code_aus_wert(ENDE2, ende, "vf.ende"))
if ende != "ja-mit-es":
self.raus("STR", _ja_code(self.feld(vf, "separator")))
self.raus("STR", code_aus_ja(self.feld(vf, "separator")))
def journal_aus_spec(spec):
return SpecSchreiber(spec).schreib()
def journal_aus_spec(spec, grammatik=GRAMMATIK_AUFZEICHNUNG):
return SpecSchreiber(spec, grammatik).schreib()
# ============================================================
@@ -704,6 +550,9 @@ def flache_objekte(spec_id, spec, kopf_extra=None):
continue
s[k] = 1 if v is True else (0 if v is False else v)
objekte.append(s)
# Freitext (beschreibung/quelle/...) darf kein " enthalten - der
# LISP-Leser kennt keine Escapes (siehe pruefe_flachen_freitext).
pruefe_flachen_freitext(objekte, "Spec %s" % spec_id)
return objekte
@@ -831,7 +680,12 @@ def main():
# Kette soll in beiden Testlaeufen (Spec-Pfad und Aufzeichnungs-
# Pfad) denselben Namen tragen, sonst laesst sich das Ergebnis
# nicht paaren.
alle.extend(flache_objekte(tid, spec, extra))
try:
alle.extend(flache_objekte(tid, spec, extra))
except ValueError as e:
print("%s %-20s FEHLER: %s" % (PREFIX, tid, e))
fehler += 1
continue
print("%s %-20s %2d Sektionen, %3d Tokens - Rundlauf ok"
% (PREFIX, tid, len(spec["sektionen"]), len(tok)))
@@ -848,6 +702,18 @@ def main():
", ".join(str(o.get("id", "?")) for o in fremde)))
alle.extend(fremde)
# Letzte Kontrolle vor dem Schreiben - sie deckt auch die von Hand
# ergaenzten Objekte ab, die nicht durch flache_objekte gelaufen sind.
# Gueltiges JSON reicht hier NICHT (siehe pruefe_flachen_freitext): der
# LISP-Leser schneidet einen Stringwert am ersten rohen " ab und verliert
# damit den Rest der Datei.
try:
pruefe_flachen_freitext(alle, args.ziel)
except ValueError as e:
print("%s FEHLER: %s" % (PREFIX, e))
print("%s nichts geschrieben" % PREFIX)
return 1
text = json.dumps(alle, indent=2, ensure_ascii=False)
with open(args.ziel, "w", encoding="utf-8", newline="\r\n") as fh:
fh.write(arrays_einzeilig(text))