aboutsummaryrefslogtreecommitdiff
path: root/src/test/java/eu
diff options
context:
space:
mode:
Diffstat (limited to 'src/test/java/eu')
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java93
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParserTest.java128
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java117
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java27
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java30
-rw-r--r--src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java150
-rw-r--r--src/test/java/eu/mulk/aendggner/anwendung/SatzTeilerTest.java24
-rw-r--r--src/test/java/eu/mulk/aendggner/gesetz/bayern/BayRechtTextParserTest.java120
8 files changed, 689 insertions, 0 deletions
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index 0b7820d..2e19090 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -217,4 +217,97 @@ class EndToEndTest {
assertThat(neueNorm.titel()).isEqualTo("Außerkrafttreten");
assertThat(neueNorm.gesamtText()).contains("9. Dezember 2026 außer Kraft");
}
+
+ /**
+ * Bayerisches Landesrecht (Akzeptanzfall der fünften Welle): altes BayJG (Klartext, aus der
+ * archivierten Fassung von gesetze-bayern.de abgeleitet) + GVBl-Heft 6/2026. §§ 1 und 2 des
+ * Änderungsgesetzes betreffen das BayJG; alle Befehle werden erkannt und angewandt.
+ */
+ @Test
+ void bayJgGvblAcceptance() throws Exception {
+ var alt = SAMPLEDATA.resolve("BayJG/BayJG-alt.txt");
+ var pdf = SAMPLEDATA.resolve("BayJG/gvbl-2026-06.pdf");
+ 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);
+ assertThat(gesetz.jurabk()).isEqualTo("BayJG");
+ assertThat(gesetz.langue()).isEqualTo("Bayerisches Jagdgesetz");
+ assertThat(gesetz.norm("Art. 1").orElseThrow().absaetze().get(0).text())
+ .startsWith("¹Die freilebende Tierwelt");
+
+ // 2. GVBl-Heft extrahieren (Superskript-Erhalt; Kolumnentitel entfernt).
+ var text =
+ TextBereiniger.bereinige(
+ new PatchTextExtraktor(eu.mulk.aendggner.aenderung.parse.SuperskriptModus.BEHALTEN)
+ .extrahiere(pdf));
+ assertThat(text).doesNotContain("Gesetz- und Verordnungsblatt Nr. 6/2026 ");
+ assertThat(text).contains("für die kurzfristige Vermietung");
+
+ // 3. Parsen: §§ 1 und 2 betreffen das BayJG (nicht aber die AVBayJG-§§ 4 und 5,
+ // deren Titel das BayJG nur als Genitiv-Attribut nennt); alles wird erkannt.
+ var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null);
+ assertThat(parseErgebnis.artikel()).containsExactly("1", "2");
+ assertThat(parseErgebnis.befehle()).hasSize(154);
+ assertThat(parseErgebnis.befehle()).noneMatch(b -> b instanceof UnbekannterBefehl);
+
+ // 4. Anwenden auf die alte Fassung. Von 154 Befehlen bleiben genau fünf als „manuell prüfen“
+ // stehen — allesamt Folgeänderungen innerhalb zweier mehrschrittiger Umnummerierungs-
+ // sequenzen, die ÄndGgner bewusst nicht automatisch auflöst (statt fehlerhaft zu raten):
+ // * Art. 29a (§ 1 Nr. 23): ein vorangestellter Absatz und die Bereichs-Umnummerierung
+ // „Die bisherigen Abs. 1 bis 3 werden die Abs. 2 bis 4“ verschieben die Absatzzählung;
+ // die auf den neuen Abs. 5 zielenden Wort-/Satzbefehle finden ihren Alttext nicht mehr.
+ // * Art. 56 (§ 1 Nr. 48): eine umfangreiche Neunummerierung des Bußgeldkatalogs
+ // (Einfügen der Nrn. 5–7 und 13, Verschieben von Nr. 6→9, 11→12 …) verschiebt die
+ // Zielnummern der begleitenden Buchstaben-/Wortänderungen.
+ // Diese Residuen sind exakt gepinnt; sie landen mit Begründung im Abschnitt „Manuell prüfen“
+ // der Synopse und werden nie stillschweigend verworfen.
+ var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
+ assertThat(anwendung.protokoll()).hasSameSizeAs(parseErgebnis.befehle());
+ var manuellPfade =
+ anwendung.protokoll().stream()
+ .filter(a -> a.status() == BefehlAnwender.Status.MANUELL_PRUEFEN)
+ .map(a -> a.befehl().provenienz().gliederungsPfad())
+ .toList();
+ assertThat(manuellPfade)
+ .containsExactlyInAnyOrder(
+ "23. c) aa)", "23. c) cc)", "48. a) ee)", "48. a) gg)", "48. b) cc)");
+
+ // 5. Stichproben: § 2 (in Kraft 1.1.2027) hebt Art. 28 Abs. 1 Satz 4 auf und nummeriert
+ // Satz 5 um; § 1 Nr. 16 stellt Art. 22a die Abs. 1 bis 4 voran.
+ var art28 = anwendung.neu().norm("Art. 28").orElseThrow();
+ var abs1 = art28.absaetze().get(0);
+ assertThat(abs1.text()).doesNotContain("⁵");
+ var art22a = anwendung.neu().norm("Art. 22a").orElseThrow();
+ assertThat(art22a.absaetze().size()).isGreaterThanOrEqualTo(5);
+ assertThat(art22a.absaetze().get(0).nummer()).isEqualTo("1");
+
+ // 6. Synopse rendern.
+ var synopse = SynopseBuilder.baue(gesetz, anwendung, parseErgebnis.warnungen(), false);
+ var html = HtmlRenderer.rendere(synopse, "E2E-Test BayJG");
+ assertThat(html).contains("Art. 29");
+ }
+
+ /** Landtags-Gesetzentwurf (Ltg-Drs. 19/9707) auf derselben alten Fassung. */
+ @Test
+ void bayJgLandtagsEntwurf() throws Exception {
+ var alt = SAMPLEDATA.resolve("BayJG/BayJG-alt.txt");
+ 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 text =
+ TextBereiniger.bereinige(
+ new PatchTextExtraktor(eu.mulk.aendggner.aenderung.parse.SuperskriptModus.BEHALTEN)
+ .extrahiere(pdf));
+ var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null);
+
+ // Nur § 1 des Entwurfs ändert das BayJG; Vorblatt und Begründung erzeugen keine Befehle.
+ assertThat(parseErgebnis.artikel()).containsExactly("1");
+ assertThat(parseErgebnis.befehle()).hasSize(154);
+ assertThat(parseErgebnis.befehle()).noneMatch(b -> b instanceof UnbekannterBefehl);
+
+ var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
+ assertThat(anwendung.protokoll()).hasSameSizeAs(parseErgebnis.befehle());
+ }
}
diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParserTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParserTest.java
new file mode 100644
index 0000000..a8ec3aa
--- /dev/null
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParserTest.java
@@ -0,0 +1,128 @@
+package eu.mulk.aendggner.aenderung.parse;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import eu.mulk.aendggner.aenderung.Aenderungsbefehl;
+import eu.mulk.aendggner.gesetz.Gesetz;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class AenderungsgesetzParserTest {
+
+ private static final Gesetz BAYJG =
+ new Gesetz("BayJG", "Bayerisches Jagdgesetz", null, List.of(), List.of());
+
+ private static final Gesetz UWG =
+ new Gesetz(
+ "UWG", "Gesetz gegen den unlauteren Wettbewerb", "UWG-Langtitel", List.of(), List.of());
+
+ @Test
+ void teiltParagraphenGegliedertesAenderungsgesetz() {
+ // Bayerischer Stil: das Änderungsgesetz gliedert sich in §§; § 1 und § 2 treffen dasselbe
+ // Stammgesetz, § 3 ein anderes; Inkrafttretens-§ ohne Änderungsformel wird nicht gewählt.
+ var text =
+ """
+ § 1
+ Änderung des
+ Bayerischen Jagdgesetzes
+ Das Bayerische Jagdgesetz (BayJG) in der in der Bayerischen Rechtssammlung (BayRS 792-1-W) veröffentlichten bereinigten Fassung, das zuletzt durch § 5 des Gesetzes vom 23. Juli 2024 (GVBl. S. 247) geändert worden ist, wird wie folgt geändert:
+ 1. Art. 1 wird wie folgt geändert:
+ a) In Abs. 2 Satzteil vor Nr. 1 wird die Angabe „Bundesjagdgesetz“ durch die Angabe „BJagdG“ ersetzt.
+ b) Fußnote 1 wird aufgehoben.
+ § 2
+ Weitere Änderung des
+ Bayerischen Jagdgesetzes
+ Das Bayerische Jagdgesetz (BayJG) in der in der Bayerischen Rechtssammlung (BayRS 792-1-W) veröffentlichten bereinigten Fassung, das zuletzt durch § 1 dieses Gesetzes geändert worden ist, wird wie folgt geändert:
+ Art. 28 Abs. 1 wird wie folgt geändert:
+ 1. Satz 4 wird aufgehoben.
+ 2. Satz 5 wird Satz 4.
+ § 3
+ Änderung des
+ Bayerischen Umweltinformationsgesetzes
+ Art. 7 Abs. 2 des Bayerischen Umweltinformationsgesetzes (BayUIG) vom 8. Dezember 2006 (GVBl. S. 933, BayRS 2129-1-4-U), das zuletzt durch § 10 des Gesetzes vom 23. Dezember 2024 (GVBl. S. 605) geändert worden ist, wird wie folgt geändert:
+ 1. Der Wortlaut wird Satz 1.
+ § 4
+ Inkrafttreten
+ Dieses Gesetz tritt am 1. April 2026 in Kraft.
+ """;
+
+ var ergebnis = new AenderungsgesetzParser().parse(text, BAYJG, null);
+
+ assertThat(ergebnis.artikel()).containsExactly("1", "2");
+ // § 2: Vorspann-Rahmen „Art. 28 Abs. 1“ wird Kontext der Punkte.
+ var aufhebung =
+ ergebnis.befehle().stream()
+ .filter(b -> b instanceof Aenderungsbefehl.Aufhebung)
+ .map(Aenderungsbefehl::stelle)
+ .map(s -> s.anzeigeText())
+ .toList();
+ assertThat(aufhebung).contains("Art. 28 Absatz 1 Satz 4");
+ }
+
+ @Test
+ void eingebettetesRahmenzielSetztKontext() {
+ var bayUig =
+ new Gesetz(
+ "BayUIG", "Bayerisches Umweltinformationsgesetz", null, List.of(), List.of());
+ var text =
+ """
+ § 3
+ Änderung des
+ Bayerischen Umweltinformationsgesetzes
+ Art. 7 Abs. 2 des Bayerischen Umweltinformationsgesetzes (BayUIG) vom 8. Dezember 2006 (GVBl. S. 933, BayRS 2129-1-4-U), das zuletzt durch § 10 des Gesetzes vom 23. Dezember 2024 (GVBl. S. 605) geändert worden ist, wird wie folgt geändert:
+ 1. Der Wortlaut wird Satz 1.
+ 2. Folgender Satz 2 wird angefügt:
+ „²Die Voraussetzungen nach Satz 1 Nr. 2 sind insbesondere gegeben.“
+ """;
+
+ var ergebnis = new AenderungsgesetzParser().parse(text, bayUig, null);
+
+ assertThat(ergebnis.artikel()).containsExactly("3");
+ // Das eingebettete Ziel „Art. 7 Abs. 2“ ist Kontext aller Punkte — nicht etwa das „§ 10“
+ // aus der Zitierkette der Änderungshistorie.
+ assertThat(ergebnis.befehle())
+ .allSatisfy(
+ befehl ->
+ assertThat(befehl.stelle().anzeigeText()).startsWith("Art. 7 Absatz 2"));
+ }
+
+ @Test
+ void artikelUeberschriftenUnterdrueckenParagraphenModus() {
+ // Ein Bundes-Änderungsgesetz mit unzitiert abgedrucktem Ablösegesetz enthält freistehende
+ // „§ N“-Zeilen — sie dürfen nicht als äußere Gliederung gedeutet werden.
+ var text =
+ """
+ Artikel 1
+ Das Gesetz gegen den unlauteren Wettbewerb vom 3. Juli 2004, das zuletzt geändert worden ist, wird wie folgt geändert:
+ 1. In § 5 wird die Angabe „alt“ durch die Angabe „neu“ ersetzt.
+ Artikel 2
+ § 1
+ Dieses Gesetz tritt am 1. Januar 2027 in Kraft.
+ """;
+
+ var ergebnis = new AenderungsgesetzParser().parse(text, UWG, null);
+
+ assertThat(ergebnis.artikel()).containsExactly("1");
+ assertThat(ergebnis.befehle()).hasSize(1);
+ assertThat(ergebnis.befehle().get(0)).isInstanceOf(Aenderungsbefehl.Ersetzung.class);
+ }
+
+ @Test
+ void artikelFilterImParagraphenModusVerlangtAenderungsformel() {
+ var text =
+ """
+ § 1
+ Das Bayerische Jagdgesetz (BayJG), das zuletzt geändert worden ist, wird wie folgt geändert:
+ 1. In Art. 1 wird die Angabe „alt“ durch die Angabe „neu“ ersetzt.
+ § 2
+ Dieses Gesetz tritt am 1. April 2026 in Kraft.
+ """;
+
+ // § 2 ist der Inkrafttretens-§ ohne Änderungsformel: der Filter wählt ihn nicht.
+ var ergebnis = new AenderungsgesetzParser().parse(text, BAYJG, "2");
+ assertThat(ergebnis.artikel()).isEmpty();
+
+ var eins = new AenderungsgesetzParser().parse(text, BAYJG, "1");
+ assertThat(eins.artikel()).containsExactly("1");
+ }
+}
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 f8cfe82..8cf9eea 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
@@ -1026,4 +1026,121 @@ class BefehlErkennerTest {
assertThat(ersetzung.alt()).isEqualTo("einer Rechtsverordnung nach § 14,");
assertThat(ersetzung.neu()).isEqualTo("§ 14 Absatz 8,");
}
+
+ // --- Bayerisches Landesrecht ---------------------------------------------------------------
+
+ private static final Stelle ART_22 = new Stelle(List.of(new Stelle.Paragraph("22", "Art.")));
+
+ @Test
+ void erkenntBayerischeErsetzungMitAbkuerzungen() {
+ var befehl =
+ erkenne(
+ "In Abs. 2 Satz 1 Nr. 1 wird die Angabe „des Bundesbaugesetzes“ durch die Angabe"
+ + " „des Baugesetzbuchs (BauGB)“ ersetzt.",
+ new Stelle(List.of(new Stelle.Paragraph("6", "Art."))));
+
+ assertThat(befehl).containsInstanceOf(Ersetzung.class);
+ var ersetzung = (Ersetzung) befehl.orElseThrow();
+ assertThat(ersetzung.stelle().anzeigeText()).isEqualTo("Art. 6 Absatz 2 Satz 1 Nummer 1");
+ }
+
+ @Test
+ void erkenntFussnotenAufhebung() {
+ var einzeln = erkenne("Fußnote 1 wird aufgehoben.", ART_22);
+ assertThat(einzeln).containsInstanceOf(Aenderungsbefehl.FussnotenAufhebung.class);
+ assertThat(((Aenderungsbefehl.FussnotenAufhebung) einzeln.orElseThrow()).nummern())
+ .containsExactly("1");
+
+ var mehrere = erkenne("Die Fußnoten 9 und 10 werden aufgehoben.", ART_22);
+ assertThat(mehrere).containsInstanceOf(Aenderungsbefehl.FussnotenAufhebung.class);
+ assertThat(((Aenderungsbefehl.FussnotenAufhebung) mehrere.orElseThrow()).nummern())
+ .containsExactly("9", "10");
+ }
+
+ @Test
+ void erkenntSatznummerierungsStreichungImVerbund() {
+ // GVBl 2026 S. 113, Nr. 15 c) aa) — exakt die Verbundform des Belegdokuments.
+ var befehl =
+ erkenne(
+ "In Satz 1 wird die Satznummerierung „1“ gestrichen und die Angabe „Absätzen 1"
+ + " und 2“ wird durch die Angabe „Abs. 1 und 2“ ersetzt.",
+ ART_22);
+
+ assertThat(befehl).containsInstanceOf(Sammelbefehl.class);
+ var teile = ((Sammelbefehl) befehl.orElseThrow()).teilbefehle();
+ assertThat(teile.get(0)).isInstanceOf(Aenderungsbefehl.SatznummerierungStreichung.class);
+ var streichung = (Aenderungsbefehl.SatznummerierungStreichung) teile.get(0);
+ assertThat(streichung.nummer()).isEqualTo("1");
+ assertThat(streichung.stelle().anzeigeText()).isEqualTo("Art. 22 Satz 1");
+ assertThat(teile.get(1)).isInstanceOf(Ersetzung.class);
+
+ var einfach = erkenne("In Satz 1 wird die Satznummerierung „1“ gestrichen.", ART_22);
+ assertThat(einfach).containsInstanceOf(Aenderungsbefehl.SatznummerierungStreichung.class);
+ }
+
+ @Test
+ void erkenntBayerischeWortlautFormen() {
+ var zuSatz = erkenne("Der Wortlaut wird Satz 1.", ART_22);
+ assertThat(zuSatz).containsInstanceOf(Aenderungsbefehl.WortlautZuSatz.class);
+
+ var zuAbsatz = erkenne("Der bisherige Wortlaut wird Abs. 5.", ART_22);
+ assertThat(zuAbsatz).containsInstanceOf(WortlautZuAbsatz.class);
+ assertThat(((WortlautZuAbsatz) zuAbsatz.orElseThrow()).nummer()).isEqualTo("5");
+
+ var voranstellung =
+ erkenne(
+ "Dem Wortlaut werden die folgenden Abs. 1 bis 4 vorangestellt: „(1) Erster."
+ + " (2) Zweiter. (3) Dritter. (4) Vierter.“",
+ ART_22);
+ assertThat(voranstellung).containsInstanceOf(Aenderungsbefehl.WortlautVoranstellung.class);
+ assertThat(((Aenderungsbefehl.WortlautVoranstellung) voranstellung.orElseThrow()).text())
+ .contains("(3) Dritter.");
+ }
+
+ @Test
+ void erkenntWortlautZuAbsatzMitFolgeklausel() {
+ // „Der bisherige Wortlaut wird Abs. 5 und in Halbsatz 1 wird …“ — die lokative Folgeklausel
+ // bezieht sich auf den soeben nummerierten Absatz.
+ var befehl =
+ erkenne(
+ "Der bisherige Wortlaut wird Abs. 5 und in Halbsatz 1 wird die Angabe „Das"
+ + " Staatsministerium“ durch die Angabe „Die oberste Jagdbehörde“ ersetzt.",
+ ART_22);
+
+ assertThat(befehl).containsInstanceOf(Sammelbefehl.class);
+ var teile = ((Sammelbefehl) befehl.orElseThrow()).teilbefehle();
+ assertThat(teile.get(0)).isInstanceOf(WortlautZuAbsatz.class);
+ assertThat(teile.get(1)).isInstanceOf(Ersetzung.class);
+ assertThat(((Ersetzung) teile.get(1)).stelle().anzeigeText())
+ .isEqualTo("Art. 22 Absatz 5 Halbsatz 1");
+ }
+
+ @Test
+ void erkenntSatzUmnummerierungMitErsetzungsKlausel() {
+ // GVBl 2026 S. 113, Nr. 23 c) cc).
+ var befehl =
+ erkenne(
+ "Der bisherige Satz 2 wird Satz 3 und die Angabe „der Durchführung der Lehrgänge"
+ + " (Art. 28 Abs. 1 Satz 4),“ wird durch die Angabe „einer Durchführung von"
+ + " Lehrgängen für die Fallenjagd“ ersetzt.",
+ ART_22);
+
+ assertThat(befehl).containsInstanceOf(Sammelbefehl.class);
+ var teile = ((Sammelbefehl) befehl.orElseThrow()).teilbefehle();
+ assertThat(teile.get(0)).isInstanceOf(Umnummerierung.class);
+ assertThat(teile.get(1)).isInstanceOf(Ersetzung.class);
+ }
+
+ @Test
+ void erkenntBayerischeUmnummerierungsVerbuende() {
+ // „Abs. 3 wird Abs. 2 und wird wie folgt geändert:“ läuft über rahmenMitBefehl;
+ // „Satz 5 wird Satz 4.“ ist eine gewöhnliche Umnummerierung.
+ var satz = erkenne("Satz 5 wird Satz 4.", ART_22);
+ assertThat(satz).containsInstanceOf(Umnummerierung.class);
+ assertThat(((Umnummerierung) satz.orElseThrow()).neu().anzeigeText())
+ .isEqualTo("Art. 22 Satz 4");
+
+ var abs = erkenne("Abs. 3 wird Abs. 2.", ART_22);
+ assertThat(abs).containsInstanceOf(Umnummerierung.class);
+ }
}
diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java
index 9389534..f15b98f 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java
@@ -83,6 +83,33 @@ class StellenParserTest {
}
@Test
+ void parstBayerischeArtikelStelle() {
+ // Bayerisches Landesrecht: Normen heißen „Art. N“, Unterkomponenten sind abgekürzt.
+ var stelle = StellenParser.parse("Art. 6 Abs. 2 Satz 1 Nr. 2").orElseThrow();
+ assertThat(stelle.anzeigeText()).isEqualTo("Art. 6 Absatz 2 Satz 1 Nummer 2");
+ assertThat(stelle.paragraph().orElseThrow().enbez()).isEqualTo("Art. 6");
+ }
+
+ @Test
+ void parstBayerischenBereichMitAbkuerzung() {
+ // „In den Abs. 4 und 5“ und Bereiche wie „Art. 4 bis 6“ mit erhaltenem Sigel.
+ var stellen = StellenParser.parseMehrfach("Art. 4 Abs. 3, Art. 5 Abs. 2 und Art. 11 Abs. 6");
+ assertThat(stellen).extracting(Stelle::anzeigeText)
+ .containsExactly("Art. 4 Absatz 3", "Art. 5 Absatz 2", "Art. 11 Absatz 6");
+ var bereich = StellenParser.parseMehrfach("Abs. 1 bis 3");
+ assertThat(bereich).extracting(Stelle::anzeigeText)
+ .containsExactly("Absatz 1", "Absatz 2", "Absatz 3");
+ }
+
+ @Test
+ void ignoriertBayerischenChapeauZusatz() {
+ assertThat(StellenParser.parse("Abs. 2 Satzteil vor Nr. 1").orElseThrow().anzeigeText())
+ .isEqualTo("Absatz 2");
+ assertThat(StellenParser.parse("in dem Satzteil nach Nr. 3").isEmpty()).isTrue();
+ assertThat(StellenParser.istNurChapeau("in dem Satzteil nach Nr. 3")).isTrue();
+ }
+
+ @Test
void ignoriertChapeauZusatz() {
// „in der Angabe vor Nummer 1“ ist ein verfeinernder Zusatz ohne eigene Komponente.
assertThat(StellenParser.parse("Absatz 1 in der Angabe vor Nummer 1").orElseThrow().anzeigeText())
diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java
index 8a7cf12..755db46 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java
@@ -48,6 +48,36 @@ class TextBereinigerTest {
}
@Test
+ void entferntGvblKolumnentitel() {
+ // Gerade Seiten kleben die Seitenzahl ohne Zwischenraum an die Jahreszahl („…/202676“);
+ // eine am Seitenwechsel unterbrochene Silbentrennung heilt nach der Kopfentfernung.
+ var roh =
+ "Einheiten über Online-Plattformen für die kurz-\n"
+ + "Bayerisches Gesetz- und Verordnungsblatt Nr. 6/202676\n"
+ + "fristige Vermietung von Unterkünften anbietet, \n"
+ + "Bayerisches Gesetz- und Verordnungsblatt Nr. 6/2026 77\n"
+ + "Zweiter Satz. ";
+
+ assertThat(TextBereiniger.bereinige(roh))
+ .isEqualTo(
+ "Einheiten über Online-Plattformen für die kurzfristige Vermietung von Unterkünften"
+ + " anbietet,\nZweiter Satz.");
+ }
+
+ @Test
+ void entferntLandtagsKolumnentitel() {
+ var roh =
+ "Bayerischer Landtag \n"
+ + "19. Wahlperiode 28.01.2026 Drucksache 19/9707 \n"
+ + "Erster Satz. \n"
+ + "Drucksache 19/9707 Bayerischer Landtag 19. Wahlperiode Seite 2 \n"
+ + "Zweiter Satz. \n"
+ + "19. Wahlperiode Drucksache 19/9707 ";
+
+ assertThat(TextBereiniger.bereinige(roh)).isEqualTo("Erster Satz.\nZweiter Satz.");
+ }
+
+ @Test
void entferntKopfzeilenDesNeuenBgblFormats() {
var roh =
"Erster Satz. \n"
diff --git a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
index aa658dd..a23f444 100644
--- a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
+++ b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
@@ -984,4 +984,154 @@ class BefehlAnwenderTest {
private static String absatzText(Gesetz gesetz, String enbez, int index) {
return gesetz.norm(enbez).orElseThrow().absaetze().get(index).text();
}
+
+ // --- Bayerisches Landesrecht ---------------------------------------------------------------
+
+ /** Ein Gesetz nach bayerischem Muster: Art.-Normen, amtliche Satznummern, Fußnoten. */
+ private static Gesetz bayGesetz() {
+ return new Gesetz(
+ "BayTestG",
+ "Bayerisches Testgesetz",
+ null,
+ List.of(
+ new Norm(
+ "Art. 1",
+ "Zweck",
+ null,
+ List.of(
+ new Absatz(
+ "1",
+ "¹Die Erprobung ist Zweck dieses Gesetzes⁶). ²Sie erfolgt sorgfältig.\n"
+ + "⁶) [Amtl. Anm.:] BayRS 0-0-T"),
+ new Absatz("2", "¹Erster Satz. ²Zweiter Satz.")),
+ false),
+ new Norm(
+ "Art. 2",
+ "Begriffe",
+ null,
+ List.of(
+ new Absatz(
+ null,
+ "Erprobung ist die Prüfung der Tauglichkeit; sie endet mit einem"
+ + " Bericht.")),
+ false)),
+ List.of());
+ }
+
+ @Test
+ void wendetFussnotenAufhebungAn() {
+ var befehl =
+ new eu.mulk.aendggner.aenderung.Aenderungsbefehl.FussnotenAufhebung(
+ stelle(new Stelle.Paragraph("1", "Art.")), List.of("6"), PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(bayGesetz(), List.of(befehl));
+
+ assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT);
+ var text = absatzText(ergebnis.neu(), "Art. 1", 0);
+ assertThat(text).doesNotContain("⁶").doesNotContain("[Amtl. Anm.:]");
+ assertThat(text).startsWith("¹Die Erprobung ist Zweck dieses Gesetzes.");
+ }
+
+ @Test
+ void meldetFehlendeFussnoteAlsManuellPruefen() {
+ var befehl =
+ new eu.mulk.aendggner.aenderung.Aenderungsbefehl.FussnotenAufhebung(
+ stelle(new Stelle.Paragraph("1", "Art.")), List.of("7"), PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(bayGesetz(), List.of(befehl));
+
+ assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.MANUELL_PRUEFEN);
+ assertThat(ergebnis.protokoll().get(0).begruendung()).contains("Fußnote 7");
+ }
+
+ @Test
+ void wendetSatznummerierungsStreichungAn() {
+ var befehl =
+ new eu.mulk.aendggner.aenderung.Aenderungsbefehl.SatznummerierungStreichung(
+ stelle(
+ new Stelle.Paragraph("1", "Art."),
+ new Stelle.AbsatzNr("2"),
+ new Stelle.SatzNr("1")),
+ "1",
+ PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(bayGesetz(), List.of(befehl));
+
+ assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT);
+ assertThat(absatzText(ergebnis.neu(), "Art. 1", 1)).isEqualTo("Erster Satz. ²Zweiter Satz.");
+ }
+
+ @Test
+ void satzUmnummerierungSchreibtSuperskriptUm() {
+ // „Satz 2 wird Satz 3.“ — die amtliche Satznummer im Text wird umgeschrieben.
+ var befehl =
+ new Umnummerierung(
+ stelle(
+ new Stelle.Paragraph("1", "Art."),
+ new Stelle.AbsatzNr("2"),
+ new Stelle.SatzNr("2")),
+ stelle(
+ new Stelle.Paragraph("1", "Art."),
+ new Stelle.AbsatzNr("2"),
+ new Stelle.SatzNr("3")),
+ PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(bayGesetz(), List.of(befehl));
+
+ assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT);
+ assertThat(absatzText(ergebnis.neu(), "Art. 1", 1)).isEqualTo("¹Erster Satz. ³Zweiter Satz.");
+ }
+
+ @Test
+ void wendetWortlautZuSatzAn() {
+ var befehl =
+ new eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortlautZuSatz(
+ stelle(new Stelle.Paragraph("2", "Art.")), "1", PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(bayGesetz(), List.of(befehl));
+
+ assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT);
+ assertThat(absatzText(ergebnis.neu(), "Art. 2", 0)).startsWith("¹Erprobung ist die Prüfung");
+ }
+
+ @Test
+ void wendetWortlautVoranstellungUndNummerierungAn() {
+ // Bayerische Folge: erst „Dem Wortlaut werden die folgenden Abs. 1 und 2 vorangestellt“,
+ // dann „Der bisherige Wortlaut wird Abs. 3“ — nur der unnummerierte Absatz erhält die Nummer.
+ var voranstellung =
+ new eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortlautVoranstellung(
+ stelle(new Stelle.Paragraph("2", "Art.")), "(1) Erstens. (2) Zweitens.", PROV);
+ var nummerierung =
+ new eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortlautZuAbsatz(
+ stelle(new Stelle.Paragraph("2", "Art.")), "3", PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(bayGesetz(), List.of(voranstellung, nummerierung));
+
+ assertThat(ergebnis.protokoll())
+ .allSatisfy(eintrag -> assertThat(eintrag.status()).isEqualTo(Status.ANGEWANDT));
+ var norm = ergebnis.neu().norm("Art. 2").orElseThrow();
+ assertThat(norm.absaetze()).hasSize(3);
+ assertThat(norm.absaetze().get(0).nummer()).isEqualTo("1");
+ assertThat(norm.absaetze().get(1).nummer()).isEqualTo("2");
+ assertThat(norm.absaetze().get(2).nummer()).isEqualTo("3");
+ assertThat(norm.absaetze().get(2).text()).startsWith("Erprobung ist");
+ }
+
+ @Test
+ void wendetHalbsatzErsetzungAn() {
+ var befehl =
+ new Ersetzung(
+ stelle(new Stelle.Paragraph("2", "Art."), new Stelle.HalbsatzNr("2")),
+ "sie endet",
+ "sie schließt",
+ false,
+ false,
+ PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(bayGesetz(), List.of(befehl));
+
+ assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT);
+ assertThat(absatzText(ergebnis.neu(), "Art. 2", 0))
+ .isEqualTo("Erprobung ist die Prüfung der Tauglichkeit; sie schließt mit einem Bericht.");
+ }
}
diff --git a/src/test/java/eu/mulk/aendggner/anwendung/SatzTeilerTest.java b/src/test/java/eu/mulk/aendggner/anwendung/SatzTeilerTest.java
index 7df8f37..a97fc03 100644
--- a/src/test/java/eu/mulk/aendggner/anwendung/SatzTeilerTest.java
+++ b/src/test/java/eu/mulk/aendggner/anwendung/SatzTeilerTest.java
@@ -38,6 +38,30 @@ class SatzTeilerTest {
}
@Test
+ void teiltAnAmtlichenSatznummern() {
+ // Bayerisches Landesrecht: hochgestellte Satznummern sind exakte Satzgrenzen — auch dort,
+ // wo die Heuristik versagen würde (Abkürzung am Satzende).
+ var text = "¹Die Förderung erfolgt gem. Art. 26 und 27. ²Sie ist als Teil zu bewahren.";
+ assertThat(SatzTeiler.teileTexte(text))
+ .containsExactly(
+ "¹Die Förderung erfolgt gem. Art. 26 und 27.", "²Sie ist als Teil zu bewahren.");
+ assertThat(SatzTeiler.nummerVonSatz("²Sie ist als Teil zu bewahren.")).isEqualTo(2);
+ assertThat(SatzTeiler.nummerVonSatz("Ohne Nummer.")).isNull();
+ }
+
+ @Test
+ void fussnotenmarkerIstKeinSatzanfang() {
+ // „Enteignung⁶)“ mitten im Satz und die Fußnotenzeile „⁶) …“ eröffnen keinen neuen Satz.
+ var text =
+ "¹Es gilt das Gesetz über die entschädigungspflichtige Enteignung⁶) entsprechend."
+ + " ²Näheres regelt die Verordnung.\n⁶) [Amtl. Anm.:] BayRS 2141-1-I";
+ var saetze = SatzTeiler.teileTexte(text);
+ assertThat(saetze).hasSize(2);
+ assertThat(saetze.get(0)).contains("Enteignung⁶)");
+ assertThat(saetze.get(1)).startsWith("²Näheres").contains("⁶) [Amtl. Anm.:]");
+ }
+
+ @Test
void behandeltAufzaehlungenAlsTeilDesSatzes() {
var text =
"Die Erprobung umfasst\n"
diff --git a/src/test/java/eu/mulk/aendggner/gesetz/bayern/BayRechtTextParserTest.java b/src/test/java/eu/mulk/aendggner/gesetz/bayern/BayRechtTextParserTest.java
new file mode 100644
index 0000000..a5fc67a
--- /dev/null
+++ b/src/test/java/eu/mulk/aendggner/gesetz/bayern/BayRechtTextParserTest.java
@@ -0,0 +1,120 @@
+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");
+ }
+}