From 3f81addcfbeae7aabcdd823340397f66e394f02f Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Fri, 24 Jul 2026 05:34:32 +0200 Subject: =?UTF-8?q?Add=20Saxony=20S=C3=A4chsBeamtVG=20acceptance=20case=20?= =?UTF-8?q?(first=20non-Bavarian=20Landesrecht)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete the first §-structured Landesrecht acceptance case for wave six: the Saxon Civil Servants' Pension Act (SächsBeamtVG) in its 1 Feb 2025 version plus the amending act of 13 April 2026 (SächsGVBl. S. 134). All five commands are recognised and applied without residue — the amount replacements in § 47 and the re-enactment of § 80(1) sentence 2. - LandesRechtTextParser: recognise arabic, keyword-first structural headings ("Abschnitt 1", "Unterabschnitt 2", "Teil 3") via GLIEDERUNG_ARABISCH, in addition to Bavaria's roman "I. Abschnitt" form. Bavaria behaviour unchanged. - TextBereiniger: strip the footer of revosax full-text output (REVOSAX_FUSS, "https://…revosax… Fassung vom … Seite N von M"). - Add the consolidated pre-amendment stem as canonical plaintext (sampledata/Sachsen/SaechsBeamtVG-alt.txt, derived from the archived revosax full text) and EndToEndTest.saechsBeamtVgAcceptance (5 applied, 0 manual). 220 tests green; federal and Bavarian pinned numbers unchanged. Co-Authored-By: Claude Opus 4.8 Change-Id: I8e878bc9506f24028cf643a873de000f3d066d64 --- .../aendggner/aenderung/parse/TextBereiniger.java | 9 ++++++++- .../aendggner/gesetz/land/LandesRechtTextParser.java | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) (limited to 'src/main/java/eu') diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java index 4016256..d79df60 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java @@ -117,6 +117,12 @@ public final class TextBereiniger { "^\\s*\\d+\\. Wahlperiode\\s+(?:\\d{2}\\.\\d{2}\\.\\d{4}\\s+)?Drucksache \\d+/\\d+\\s*$"); private static final Pattern LANDTAG_MARKE = Pattern.compile("^\\s*Bayerischer Landtag\\s*$"); + // Fußzeile der revosax-Volltextausgabe (Sachsen), z.B. „https://www.revosax.sachsen.de + // Fassung vom 13.04.2026 Seite 1 von 1“ — Herausgeber-URL, Fassungsdatum und Seitenangabe. + private static final Pattern REVOSAX_FUSS = + Pattern.compile( + "^\\s*https?://\\S+\\s+Fassung vom \\d{1,2}\\.\\d{1,2}\\.\\d{4}\\s+Seite \\d+ von \\d+\\s*$"); + /** Konjunktionen, die typischerweise auf einen Suspensivstrich folgen („Wirk- und …“). */ private static final Pattern KONJUNKTION = Pattern.compile("^(und|oder|sowie|bzw\\.|beziehungsweise)\\b.*"); @@ -285,7 +291,8 @@ public final class TextBereiniger { || GVBL_KOPF.matcher(zeile).matches() || LANDTAG_SEITENKOPF.matcher(zeile).matches() || LANDTAG_TITELKOPF.matcher(zeile).matches() - || LANDTAG_MARKE.matcher(zeile).matches(); + || LANDTAG_MARKE.matcher(zeile).matches() + || REVOSAX_FUSS.matcher(zeile).matches(); } /** diff --git a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java index b1da42d..23260de 100644 --- a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java +++ b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java @@ -49,6 +49,13 @@ final class LandesRechtTextParser { private static final Pattern GLIEDERUNG = Pattern.compile("^([IVXLCDM]+)\\.\\s+(Abschnitt|Teil|Kapitel)\\b\\s*(.*)$"); + // Gliederungs-Überschrift in keyword-erster, arabischer Form („Abschnitt 1“, „Unterabschnitt 2“, + // „Teil 3“). Die meisten Länder (z.B. Sachsen) gliedern so; Bayern dagegen römisch mit + // nachgestelltem Schlüsselwort („I. Abschnitt“, Muster GLIEDERUNG). Der Titel steht — nach der + // kanonischen Aufbereitung — mit doppeltem Leerzeichen auf derselben Zeile. + private static final Pattern GLIEDERUNG_ARABISCH = + Pattern.compile("^(Buch|Teil|Kapitel|Abschnitt|Unterabschnitt|Titel) (\\d+[a-z]?)\\s+(\\S.*)$"); + private static final Pattern UNTER_GLIEDERUNG = Pattern.compile("^(\\d+[a-z]?)\\.\\s+(\\S.*)$"); // Normkopf: „§ N“ bzw. „Art. N“ plus Titel auf derselben Zeile. Das Sigel steht in Gruppe 1, die @@ -95,6 +102,7 @@ final class LandesRechtTextParser { // Rest des Titelblocks (Datum, Fundstellen, Vollzitat) bis zur ersten Struktur überspringen. while (i < zeilen.size() && !GLIEDERUNG.matcher(zeilen.get(i).strip()).matches() + && !GLIEDERUNG_ARABISCH.matcher(zeilen.get(i).strip()).matches() && !NORM_KOPF.matcher(zeilen.get(i).strip()).matches()) { i++; } @@ -115,11 +123,13 @@ final class LandesRechtTextParser { var zeile = i < zeilen.size() ? zeilen.get(i).strip() : null; var gliederung = zeile != null ? GLIEDERUNG.matcher(zeile) : null; + var arabisch = zeile != null ? GLIEDERUNG_ARABISCH.matcher(zeile) : null; var unterGliederung = zeile != null ? UNTER_GLIEDERUNG.matcher(zeile) : null; var normKopf = zeile != null ? NORM_KOPF.matcher(zeile) : null; if (zeile == null || gliederung.matches() + || arabisch.matches() || (normKopf.matches() && istNeuerNormKopf(normKopf.group(2), letzteNormNummer)) || (unterGliederung.matches() && istUnterGliederung(unterGliederung, zeilen, i))) { // Laufende Norm abschließen. @@ -142,6 +152,16 @@ final class LandesRechtTextParser { gliederung.group(1) + ". " + gliederung.group(2), titel.isEmpty() ? null : titel); gliederungen.add(aktuelleGliederung); + } else if (arabisch.matches()) { + gliederungsZaehler++; + elternKennzahl = String.format("%03d", gliederungsZaehler); + var titel = arabisch.group(3).strip(); + aktuelleGliederung = + new Gliederung( + elternKennzahl, + arabisch.group(1) + " " + arabisch.group(2), + titel.isEmpty() ? null : titel); + gliederungen.add(aktuelleGliederung); } else if (normKopf.matches() && istNeuerNormKopf(normKopf.group(2), letzteNormNummer)) { normSigel = normKopf.group(1); normNummer = normKopf.group(2); -- cgit v1.2.1