From c2b57decf3d57d4808dd37723c562b8e001cc342 Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Sun, 19 Jul 2026 10:15:00 +0200 Subject: Generalize Bavarian loader to a shared Landesrecht loader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prepare wave six (Landesrecht of the remaining Länder) by lifting the Bavaria-specific stem-law loader to a shared one. Unlike Bavaria (Art./§ inversion), the other Länder structure their stem laws in §, so the loader must handle both sigils. - Move gesetz/bayern/{BayRechtLoader,BayRechtTextParser} to gesetz/land/{LandesRechtLoader,LandesRechtTextParser}; the norm head now matches "§ N" as well as "Art. N" and derives the sigil per norm from the match. Cross-reference keywords in the norm head are excluded only as whole words (so a title "Satzungen" no longer trips on "Satz"), and the juris abbreviation may be multi-token ("(GO NRW)"). - Derive the superscript mode data-drivenly from the loaded stem law (Superskript.traegtSatznummern) instead of from "is it gii-XML": Bavaria and Lower Saxony keep their amtliche Satznummern, the Bund and Länder without official sentence numbering drop them. - Recognize the neufassung idiom "erhält/erhalten folgende Fassung" (Schleswig- Holstein, Niedersachsen) via a NEUFASSUNG_VERB building block, additive to "wird/werden wie folgt gefasst". Federal and Bavarian behaviour is unchanged (219 tests green, pinned acceptance numbers UWG 19/0, GEG 66/53, GEG-GModG 90/9, IfSG 42/24, BayJG 149/154 hold). Co-Authored-By: Claude Opus 4.8 Change-Id: I55a70a7932bf657a2346ca70f3fa05e173bf80ad --- src/test/java/eu/mulk/aendggner/EndToEndTest.java | 4 +- .../aenderung/parse/BefehlErkennerTest.java | 15 ++ .../gesetz/bayern/BayRechtTextParserTest.java | 120 --------------- .../gesetz/land/LandesRechtTextParserTest.java | 164 +++++++++++++++++++++ 4 files changed, 181 insertions(+), 122 deletions(-) delete mode 100644 src/test/java/eu/mulk/aendggner/gesetz/bayern/BayRechtTextParserTest.java create mode 100644 src/test/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParserTest.java (limited to 'src/test/java') diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java index 2e19090..0e90584 100644 --- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java +++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java @@ -230,7 +230,7 @@ class EndToEndTest { assumeTrue(Files.exists(alt) && Files.exists(pdf), "BayJG-Beispieldaten fehlen"); // 1. Stammgesetz laden (Art.-gegliedert, amtliche Satznummern als Superskripte). - var gesetz = new eu.mulk.aendggner.gesetz.bayern.BayRechtLoader().load(alt); + var gesetz = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader().load(alt); assertThat(gesetz.jurabk()).isEqualTo("BayJG"); assertThat(gesetz.langue()).isEqualTo("Bayerisches Jagdgesetz"); assertThat(gesetz.norm("Art. 1").orElseThrow().absaetze().get(0).text()) @@ -295,7 +295,7 @@ class EndToEndTest { var pdf = SAMPLEDATA.resolve("BayJG/Ltg-Drs-19-9707_Gesetzentwurf.pdf"); assumeTrue(Files.exists(alt) && Files.exists(pdf), "BayJG-Beispieldaten fehlen"); - var gesetz = new eu.mulk.aendggner.gesetz.bayern.BayRechtLoader().load(alt); + var gesetz = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader().load(alt); var text = TextBereiniger.bereinige( new PatchTextExtraktor(eu.mulk.aendggner.aenderung.parse.SuperskriptModus.BEHALTEN) diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java index 8cf9eea..d89c898 100644 --- a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java +++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java @@ -90,6 +90,21 @@ class BefehlErkennerTest { assertThat(neufassung.neuerText()).startsWith("(1) Der Deutsche Bundestag"); } + @Test + void erkenntNeufassungMitErhaeltFolgendeFassung() { + // „… erhält folgende Fassung:“ ist die Neufassungsform in Schleswig-Holstein und Niedersachsen. + var befehl = + erkenne( + "§ 34a Absatz 1 erhält folgende Fassung: „(1) Durch Hauptsatzung kann bestimmt" + + " werden, dass Gemeindevertreterinnen und Gemeindevertreter teilnehmen.“", + Stelle.LEER); + + assertThat(befehl).containsInstanceOf(Neufassung.class); + var neufassung = (Neufassung) befehl.orElseThrow(); + assertThat(neufassung.stelle().anzeigeText()).isEqualTo("§ 34a Absatz 1"); + assertThat(neufassung.neuerText()).startsWith("(1) Durch Hauptsatzung"); + } + @Test void erkenntParagraphEinfuegung() { // Echter Befehl aus Art. 1 Nr. 17 des Dritten Bevölkerungsschutzgesetzes. diff --git a/src/test/java/eu/mulk/aendggner/gesetz/bayern/BayRechtTextParserTest.java b/src/test/java/eu/mulk/aendggner/gesetz/bayern/BayRechtTextParserTest.java deleted file mode 100644 index a5fc67a..0000000 --- a/src/test/java/eu/mulk/aendggner/gesetz/bayern/BayRechtTextParserTest.java +++ /dev/null @@ -1,120 +0,0 @@ -package eu.mulk.aendggner.gesetz.bayern; - -import static org.assertj.core.api.Assertions.assertThat; - -import eu.mulk.aendggner.gesetz.Gliederung; -import org.junit.jupiter.api.Test; - -class BayRechtTextParserTest { - - private static final String BEISPIEL = - """ - BayJG: Bayerisches Jagdgesetz (BayJG) Vom 13. Oktober 1978 (BayRS V S. 595) BayRS 792-1-W (Art. - 1–64) - Bayerisches Jagdgesetz - (BayJG) - Vom 13. Oktober 1978 - (BayRS V S. 595) - BayRS 792-1-W - Vollzitat nach RedR: Bayerisches Jagdgesetz (BayJG) in der in der Bayerischen Rechtssammlung (BayRS - 792-1-L) veröffentlichten bereinigten Fassung - I. Abschnitt Grundsätze - Art. 1 Gesetzeszweck - - (1) ¹Die freilebende Tierwelt ist wesentlicher Bestandteil der heimischen Natur. ²Sie ist zu bewahren. - (2) Dieses Gesetz soll dazu dienen: - 1. einen artenreichen Wildbestand zu erhalten, - - 2. die Lebensgrundlagen des Wildes zu sichern. - - II. Abschnitt Jagdreviere, Hegegemeinschaften - 1. Allgemeine Vorschriften - Art. 3 Feststellung der Jagdreviere - - Bestand, Umfang und Grenzen eines Jagdreviers werden durch die - Jagdbehörde festgestellt. - 2. Jagdreviere - Art. 8 Eigenjagdreviere - - (1) ¹Die Mindestgröße beträgt 81,755 ha. ²Die Art. 4 Abs. 3, Art. 5 Abs. 2 und - Art. 11 Abs. 6 sind entsprechend anzuwenden. - Art. 59 Enteignende Maßnahmen - - (1) Es ist nach den Vorschriften des Gesetzes über die entschädigungspflichtige Enteignung⁶) Entschädigung zu leisten. - 6) [Amtl. Anm.:] BayRS 2141-1-I - Art. 60 (aufgehoben) - - Art. 63 (Änderungsbestimmung) - - Art. 64 Inkrafttreten; Aufhebung von Vorschriften - - (1) Dieses Gesetz tritt am 1. Januar 1979 in Kraft. - """; - - @Test - void parstTitelblock() { - var gesetz = BayRechtTextParser.parse(BEISPIEL); - assertThat(gesetz.jurabk()).isEqualTo("BayJG"); - assertThat(gesetz.langue()).isEqualTo("Bayerisches Jagdgesetz"); - assertThat(gesetz.kurzue()).isNull(); - } - - @Test - void parstNormenMitTitelnUndAbsaetzen() { - var gesetz = BayRechtTextParser.parse(BEISPIEL); - assertThat(gesetz.normen()) - .extracting(n -> n.enbez()) - .containsExactly("Art. 1", "Art. 3", "Art. 8", "Art. 59", "Art. 60", "Art. 63", "Art. 64"); - - var art1 = gesetz.norm("Art. 1").orElseThrow(); - assertThat(art1.titel()).isEqualTo("Gesetzeszweck"); - assertThat(art1.absaetze()).hasSize(2); - assertThat(art1.absaetze().get(0).nummer()).isEqualTo("1"); - assertThat(art1.absaetze().get(0).text()).startsWith("¹Die freilebende Tierwelt"); - assertThat(art1.absaetze().get(1).text()).contains("1. einen artenreichen"); - - // Unnummerierter Einzelabsatz. - var art3 = gesetz.norm("Art. 3").orElseThrow(); - assertThat(art3.absaetze()).hasSize(1); - assertThat(art3.absaetze().get(0).nummer()).isNull(); - } - - @Test - void querverweiseAmZeilenanfangEroeffnenKeineNorm() { - // „Art. 11 Abs. 6 …“ am Zeilenanfang in Art. 8 ist ein Querverweis, kein Normkopf. - var gesetz = BayRechtTextParser.parse(BEISPIEL); - var art8 = gesetz.norm("Art. 8").orElseThrow(); - assertThat(art8.absaetze().get(0).text()).contains("Art. 11 Abs. 6 sind entsprechend"); - assertThat(gesetz.norm("Art. 11")).isEmpty(); - } - - @Test - void parstGliederungenMitUnterUeberschriften() { - var gesetz = BayRechtTextParser.parse(BEISPIEL); - assertThat(gesetz.gliederungen()) - .extracting(Gliederung::anzeigeText) - .containsExactly( - "I. Abschnitt — Grundsätze", - "II. Abschnitt — Jagdreviere, Hegegemeinschaften", - "1. — Allgemeine Vorschriften", - "2. — Jagdreviere"); - // Kennzahlen sind eindeutig und hängen an der Eltern-Gliederung. - assertThat(gesetz.gliederungen()) - .extracting(Gliederung::kennzahl) - .containsExactly("001", "002", "002.1", "002.2"); - assertThat(gesetz.norm("Art. 8").orElseThrow().gliederung().kennzahl()).isEqualTo("002.2"); - } - - @Test - void erkenntWeggefalleneNormenUndFussnoten() { - var gesetz = BayRechtTextParser.parse(BEISPIEL); - assertThat(gesetz.norm("Art. 60").orElseThrow().weggefallen()).isTrue(); - assertThat(gesetz.norm("Art. 63").orElseThrow().weggefallen()).isFalse(); - - // Fußnotenzeile bleibt superskript-normalisiert im tragenden Absatz. - var art59 = gesetz.norm("Art. 59").orElseThrow(); - assertThat(art59.absaetze().get(0).text()) - .contains("Enteignung⁶)") - .contains("⁶) [Amtl. Anm.:] BayRS 2141-1-I"); - } -} diff --git a/src/test/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParserTest.java b/src/test/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParserTest.java new file mode 100644 index 0000000..3354255 --- /dev/null +++ b/src/test/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParserTest.java @@ -0,0 +1,164 @@ +package eu.mulk.aendggner.gesetz.land; + +import static org.assertj.core.api.Assertions.assertThat; + +import eu.mulk.aendggner.gesetz.Gliederung; +import org.junit.jupiter.api.Test; + +class LandesRechtTextParserTest { + + // Bayern: Gliederung in „Art.“, amtliche Satznummern als Superskript. + private static final String BAYERN = + """ + BayJG: Bayerisches Jagdgesetz (BayJG) Vom 13. Oktober 1978 (BayRS V S. 595) BayRS 792-1-W (Art. + 1–64) + Bayerisches Jagdgesetz + (BayJG) + Vom 13. Oktober 1978 + (BayRS V S. 595) + BayRS 792-1-W + Vollzitat nach RedR: Bayerisches Jagdgesetz (BayJG) in der in der Bayerischen Rechtssammlung (BayRS + 792-1-L) veröffentlichten bereinigten Fassung + I. Abschnitt Grundsätze + Art. 1 Gesetzeszweck + + (1) ¹Die freilebende Tierwelt ist wesentlicher Bestandteil der heimischen Natur. ²Sie ist zu bewahren. + (2) Dieses Gesetz soll dazu dienen: + 1. einen artenreichen Wildbestand zu erhalten, + + 2. die Lebensgrundlagen des Wildes zu sichern. + + II. Abschnitt Jagdreviere, Hegegemeinschaften + 1. Allgemeine Vorschriften + Art. 3 Feststellung der Jagdreviere + + Bestand, Umfang und Grenzen eines Jagdreviers werden durch die + Jagdbehörde festgestellt. + 2. Jagdreviere + Art. 8 Eigenjagdreviere + + (1) ¹Die Mindestgröße beträgt 81,755 ha. ²Die Art. 4 Abs. 3, Art. 5 Abs. 2 und + Art. 11 Abs. 6 sind entsprechend anzuwenden. + Art. 59 Enteignende Maßnahmen + + (1) Es ist nach den Vorschriften des Gesetzes über die entschädigungspflichtige Enteignung⁶) Entschädigung zu leisten. + 6) [Amtl. Anm.:] BayRS 2141-1-I + Art. 60 (aufgehoben) + + Art. 63 (Änderungsbestimmung) + + Art. 64 Inkrafttreten; Aufhebung von Vorschriften + + (1) Dieses Gesetz tritt am 1. Januar 1979 in Kraft. + """; + + // Übriges Landesrecht: Gliederung in „§“ (wie der Bund), hier mit amtlichen Satznummern. + private static final String PARAGRAPHEN = + """ + Gemeindeordnung für das Land Nordrhein-Westfalen + (GO NRW) + Vom 14. Juli 1994 + I. Teil Grundlagen der Gemeindeverfassung + § 1 Wesen der Gemeinde + + (1) ¹Die Gemeinden sind die Grundlage des demokratischen Staatsaufbaus. ²Sie fördern das Wohl der Einwohner. + (2) Die Gemeinden verwalten ihre Angelegenheiten selbst. + § 2 Aufgaben + + Die Gemeinden erfüllen die Aufgaben der örtlichen Gemeinschaft. + § 3 (weggefallen) + + § 4 Satzungen + + (1) Die Gemeinden können ihre Angelegenheiten durch Satzung regeln. + """; + + @Test + void parstBayerischenTitelblock() { + var gesetz = LandesRechtTextParser.parse(BAYERN); + assertThat(gesetz.jurabk()).isEqualTo("BayJG"); + assertThat(gesetz.langue()).isEqualTo("Bayerisches Jagdgesetz"); + assertThat(gesetz.kurzue()).isNull(); + } + + @Test + void parstBayerischeNormenMitTitelnUndAbsaetzen() { + var gesetz = LandesRechtTextParser.parse(BAYERN); + assertThat(gesetz.normen()) + .extracting(n -> n.enbez()) + .containsExactly("Art. 1", "Art. 3", "Art. 8", "Art. 59", "Art. 60", "Art. 63", "Art. 64"); + + var art1 = gesetz.norm("Art. 1").orElseThrow(); + assertThat(art1.titel()).isEqualTo("Gesetzeszweck"); + assertThat(art1.absaetze()).hasSize(2); + assertThat(art1.absaetze().get(0).nummer()).isEqualTo("1"); + assertThat(art1.absaetze().get(0).text()).startsWith("¹Die freilebende Tierwelt"); + assertThat(art1.absaetze().get(1).text()).contains("1. einen artenreichen"); + + // Unnummerierter Einzelabsatz. + var art3 = gesetz.norm("Art. 3").orElseThrow(); + assertThat(art3.absaetze()).hasSize(1); + assertThat(art3.absaetze().get(0).nummer()).isNull(); + } + + @Test + void querverweiseAmZeilenanfangEroeffnenKeineNorm() { + // „Art. 11 Abs. 6 …“ am Zeilenanfang in Art. 8 ist ein Querverweis, kein Normkopf. + var gesetz = LandesRechtTextParser.parse(BAYERN); + var art8 = gesetz.norm("Art. 8").orElseThrow(); + assertThat(art8.absaetze().get(0).text()).contains("Art. 11 Abs. 6 sind entsprechend"); + assertThat(gesetz.norm("Art. 11")).isEmpty(); + } + + @Test + void parstGliederungenMitUnterUeberschriften() { + var gesetz = LandesRechtTextParser.parse(BAYERN); + assertThat(gesetz.gliederungen()) + .extracting(Gliederung::anzeigeText) + .containsExactly( + "I. Abschnitt — Grundsätze", + "II. Abschnitt — Jagdreviere, Hegegemeinschaften", + "1. — Allgemeine Vorschriften", + "2. — Jagdreviere"); + // Kennzahlen sind eindeutig und hängen an der Eltern-Gliederung. + assertThat(gesetz.gliederungen()) + .extracting(Gliederung::kennzahl) + .containsExactly("001", "002", "002.1", "002.2"); + assertThat(gesetz.norm("Art. 8").orElseThrow().gliederung().kennzahl()).isEqualTo("002.2"); + } + + @Test + void erkenntWeggefalleneNormenUndFussnoten() { + var gesetz = LandesRechtTextParser.parse(BAYERN); + assertThat(gesetz.norm("Art. 60").orElseThrow().weggefallen()).isTrue(); + assertThat(gesetz.norm("Art. 63").orElseThrow().weggefallen()).isFalse(); + + // Fußnotenzeile bleibt superskript-normalisiert im tragenden Absatz. + var art59 = gesetz.norm("Art. 59").orElseThrow(); + assertThat(art59.absaetze().get(0).text()) + .contains("Enteignung⁶)") + .contains("⁶) [Amtl. Anm.:] BayRS 2141-1-I"); + } + + @Test + void parstParagraphengegliedertesLandesrecht() { + // Die übrigen Länder zitieren in „§“; das Sigel folgt aus dem Normkopf, ein Land-Merkmal ist + // nicht nötig. Amtliche Satznummern werden ebenso wie bei Bayern erhalten. + var gesetz = LandesRechtTextParser.parse(PARAGRAPHEN); + assertThat(gesetz.jurabk()).isEqualTo("GO NRW"); + assertThat(gesetz.langue()).isEqualTo("Gemeindeordnung für das Land Nordrhein-Westfalen"); + assertThat(gesetz.normen()) + .extracting(n -> n.enbez()) + .containsExactly("§ 1", "§ 2", "§ 3", "§ 4"); + + var p1 = gesetz.norm("§ 1").orElseThrow(); + assertThat(p1.titel()).isEqualTo("Wesen der Gemeinde"); + assertThat(p1.absaetze()).hasSize(2); + assertThat(p1.absaetze().get(0).text()).startsWith("¹Die Gemeinden sind die Grundlage"); + + // Unnummerierter Einzelabsatz und weggefallene Norm wie im bayerischen Fall. + assertThat(gesetz.norm("§ 2").orElseThrow().absaetze().get(0).nummer()).isNull(); + assertThat(gesetz.norm("§ 3").orElseThrow().weggefallen()).isTrue(); + assertThat(gesetz.gliederungen()).extracting(Gliederung::anzeigeText).containsExactly("I. Teil — Grundlagen der Gemeindeverfassung"); + } +} -- cgit v1.2.1