diff options
| author | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2026-07-15 20:54:27 +0200 |
|---|---|---|
| committer | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2026-07-15 20:54:27 +0200 |
| commit | ebd5a7bcfb13c1a74a5ad65de772e171d98ad156 (patch) | |
| tree | d43c01a878323bd14999ca5f6167ecf0cc5bae82 /src/test/java | |
| parent | 73c5dbae7a487db2b9ea29cd050eac94644974bb (diff) | |
Support range structure replacement, struct renumbering, and multi-paragraph blocks
Adds bisStelle to StrukturErsetzung for coordinated target ranges, recognizes
deletion/renumbering of whole structural units (paragraphs and Gliederung
entries), and handles insertion/replacement of multi-paragraph blocks split on
§-headings. Updates FASSUNGEN.txt accordingly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Change-Id: I637cc5effbe32c094e62c070b63c470149a77c85
Diffstat (limited to 'src/test/java')
3 files changed, 345 insertions, 11 deletions
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java index 44903e7..483f06a 100644 --- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java +++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java @@ -110,11 +110,11 @@ class EndToEndTest { assertThat(parseErgebnis.artikel()).contains("1"); assertThat(parseErgebnis.befehle().size()).isGreaterThanOrEqualTo(100); - // Der Großteil der Befehle wird erkannt; nur einige Spezialfälle (Mehrquell-Ersetzungen, - // §§-Bereiche, Chapeau-Angaben) bleiben unbekannt. + // Im BGBl-Format werden inzwischen alle Befehle typisiert (Bereichs-/Struktur-Ersetzungen, + // §-Blöcke, Chapeau-Lokatoren, strukturelle Streichungen usw.). var unbekannt = parseErgebnis.befehle().stream().filter(b -> b instanceof UnbekannterBefehl).count(); - assertThat(unbekannt).isLessThanOrEqualTo(8); + assertThat(unbekannt).isZero(); // Das XML ist bereits konsolidiert; entscheidend ist, dass die Anwendung sauber terminiert // und jeden Befehl protokolliert. 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 230bfca..bbee916 100644 --- a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java +++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java @@ -441,14 +441,47 @@ class BefehlErkennerTest { } @Test - void faelltBeiBisBereichenAufUnbekanntZurueck() { - // „bis“-Bereiche über Struktureinheiten sind hier (Phase 1) noch nicht unterstützt. - assertThat( - erkenne( - "Die Absätze 2 bis 4 werden durch die folgenden Absätze 2 bis 6 ersetzt:" - + " „(2) Text.“", - Stelle.LEER)) - .isEmpty(); + void bereichsErsetzungAbsaetzeWirdStrukturErsetzung() { + // „bis“-Bereich über Absätze: das erste und letzte Ziel spannen den zu ersetzenden Bereich auf. + var befehl = + erkenne( + "Die Absätze 2 bis 4 werden durch die folgenden Absätze 2 bis 6 ersetzt:" + + " „(2) Text.“", + new Stelle(List.of(new Stelle.Paragraph("5")))); + assertThat(befehl).containsInstanceOf(Aenderungsbefehl.StrukturErsetzung.class); + var e = (Aenderungsbefehl.StrukturErsetzung) befehl.orElseThrow(); + assertThat(e.ebene()).isEqualTo(Ebene.ABSATZ); + assertThat(e.stelle().anzeigeText()).isEqualTo("§ 5 Absatz 2"); + assertThat(e.bisStelle().anzeigeText()).isEqualTo("§ 5 Absatz 4"); + } + + @Test + void koordinierteAbsatzErsetzungWirdBereich() { + // IfSG: „Die Absätze 8 und 9 werden durch die folgenden Absätze 8 bis 10 ersetzt: „…““ + var befehl = + erkenne( + "Die Absätze 8 und 9 werden durch die folgenden Absätze 8 bis 10 ersetzt:" + + " „(8) Erstes. (9) Zweites. (10) Drittes.“", + new Stelle(List.of(new Stelle.Paragraph("14")))); + assertThat(befehl).containsInstanceOf(Aenderungsbefehl.StrukturErsetzung.class); + var e = (Aenderungsbefehl.StrukturErsetzung) befehl.orElseThrow(); + assertThat(e.stelle().anzeigeText()).isEqualTo("§ 14 Absatz 8"); + assertThat(e.bisStelle().anzeigeText()).isEqualTo("§ 14 Absatz 9"); + } + + @Test + void mehrSatzNeufassungWirdStrukturErsetzung() { + // IfSG: „Die bisherigen Sätze 4 und 5 werden wie folgt gefasst: „…““ + var befehl = + erkenne( + "Die bisherigen Sätze 4 und 5 werden wie folgt gefasst: „Erster neuer Satz." + + " Zweiter neuer Satz.“", + new Stelle(List.of(new Stelle.Paragraph("14"), new Stelle.AbsatzNr("2")))); + assertThat(befehl).containsInstanceOf(Aenderungsbefehl.StrukturErsetzung.class); + var e = (Aenderungsbefehl.StrukturErsetzung) befehl.orElseThrow(); + assertThat(e.ebene()).isEqualTo(Ebene.SATZ); + assertThat(e.stelle().anzeigeText()).isEqualTo("§ 14 Absatz 2 Satz 4"); + assertThat(e.bisStelle().anzeigeText()).isEqualTo("§ 14 Absatz 2 Satz 5"); } @Test @@ -556,6 +589,162 @@ class BefehlErkennerTest { } @Test + void strukturStreichungGanzerEinheit() { + // „§ 9 wird gestrichen.“ — Streichung einer ganzen Einheit ist semantisch eine Aufhebung. + var befehl = erkenne("§ 9 wird gestrichen.", Stelle.LEER); + assertThat(befehl).get().isInstanceOf(Aufhebung.class); + assertThat(befehl.orElseThrow().stelle().anzeigeText()).isEqualTo("§ 9"); + } + + @Test + void strukturStreichungMitKontext() { + var befehl = + erkenne("Absatz 3 wird gestrichen.", new Stelle(List.of(new Stelle.Paragraph("102")))); + assertThat(befehl).get().isInstanceOf(Aufhebung.class); + assertThat(befehl.orElseThrow().stelle().anzeigeText()).isEqualTo("§ 102 Absatz 3"); + } + + @Test + void strukturStreichungBereichWirdSammelbefehl() { + var teile = + ((Sammelbefehl) erkenne("Die §§ 34 bis 39 werden gestrichen.", Stelle.LEER).orElseThrow()) + .teilbefehle(); + assertThat(teile).hasSize(6).allMatch(t -> t instanceof Aufhebung); + assertThat(teile).extracting(t -> t.stelle().anzeigeText()) + .containsExactly("§ 34", "§ 35", "§ 36", "§ 37", "§ 38", "§ 39"); + } + + @Test + void strukturStreichungGliederung() { + var befehl = erkenne("Der bisherige Teil 3 wird gestrichen.", Stelle.LEER); + assertThat(befehl).get().isInstanceOf(Aufhebung.class); + assertThat(befehl.orElseThrow().stelle().betrifftGliederung()).isTrue(); + } + + @Test + void chapeauLokatorBeziehtSichAufKontext() { + // „In der Angabe vor Nummer 1 …“ / „Im Satzteil vor Nummer 1 …“ tragen keine eigene Stelle; + // die Operation bezieht sich auf die Kontextstelle. + var kontext = new Stelle(List.of(new Stelle.Paragraph("48"), new Stelle.AbsatzNr("1"))); + var befehl = + erkenne( + "In der Angabe vor Nummer 1 wird die Angabe „2025“ durch die Angabe „2030“ ersetzt.", + kontext); + assertThat(befehl).get().isInstanceOf(Ersetzung.class); + var e = (Ersetzung) befehl.orElseThrow(); + assertThat(e.stelle().anzeigeText()).isEqualTo("§ 48 Absatz 1"); + assertThat(e.alt()).isEqualTo("2025"); + assertThat(e.neu()).isEqualTo("2030"); + } + + @Test + void bereichsUmnummerierungOhneBisherigen() { + var teile = + ((Sammelbefehl) + erkenne( + "Die Absätze 4 bis 7 werden zu den Absätzen 3 bis 6.", + new Stelle(List.of(new Stelle.Paragraph("108")))) + .orElseThrow()) + .teilbefehle(); + assertThat(teile).hasSize(4).allMatch(t -> t instanceof Umnummerierung); + assertThat(teile).extracting(t -> t.stelle().anzeigeText()) + .containsExactly("§ 108 Absatz 7", "§ 108 Absatz 6", "§ 108 Absatz 5", "§ 108 Absatz 4"); + } + + @Test + void strukturErsetzungMitEnumeratorPraefix() { + // Entwurfs-/Drucksachenform: das Aufzählungslabel steht außerhalb des Zitats. + var befehl = + erkenne( + "Nummer 3 wird durch die folgende Nummer 3 ersetzt: 3. „ die Maßgaben der §§ 42 bis 45" + + " entsprechend eingehalten werden.“", + new Stelle(List.of(new Stelle.Paragraph("10"), new Stelle.AbsatzNr("2")))); + assertThat(befehl).containsInstanceOf(Aenderungsbefehl.StrukturErsetzung.class); + var e = (Aenderungsbefehl.StrukturErsetzung) befehl.orElseThrow(); + assertThat(e.ebene()).isEqualTo(Ebene.NUMMER); + assertThat(e.stelle().anzeigeText()).isEqualTo("§ 10 Absatz 2 Nummer 3"); + // Das Label „3.“ wird dem Ersatztext wieder vorangestellt. + assertThat(e.text()).startsWith("3. die Maßgaben"); + } + + @Test + void paragraphUmnummerierung() { + var befehl = erkenne("§ 9a wird zu § 9.", Stelle.LEER); + assertThat(befehl).get().isInstanceOf(Umnummerierung.class); + var u = (Umnummerierung) befehl.orElseThrow(); + assertThat(u.stelle().anzeigeText()).isEqualTo("§ 9a"); + assertThat(u.neu().anzeigeText()).isEqualTo("§ 9"); + } + + @Test + void koordinierteParagraphUmnummerierung() { + var teile = + ((Sammelbefehl) erkenne("Die §§ 46 und 47 werden zu den §§ 34 und 35.", Stelle.LEER).orElseThrow()) + .teilbefehle(); + assertThat(teile).hasSize(2).allMatch(t -> t instanceof Umnummerierung); + assertThat(teile).extracting(t -> t.stelle().anzeigeText()).containsExactly("§ 46", "§ 47"); + assertThat(teile).extracting(t -> ((Umnummerierung) t).neu().anzeigeText()) + .containsExactly("§ 34", "§ 35"); + } + + @Test + void gliederungsUmnummerierung() { + var befehl = erkenne("Der bisherige Abschnitt 2 wird zu Abschnitt 3.", Stelle.LEER); + assertThat(befehl).get().isInstanceOf(Umnummerierung.class); + assertThat(befehl.orElseThrow().stelle().betrifftGliederung()).isTrue(); + } + + @Test + void paragraphenBlockEinfuegung() { + var befehl = + erkenne( + "Nach § 60a werden die folgenden §§ 60b und 60c eingefügt: „§ 60b Prüfung (1) Text." + + " § 60c Optimierung (1) Mehr.“", + Stelle.LEER); + assertThat(befehl).containsInstanceOf(StrukturEinfuegung.class); + var e = (StrukturEinfuegung) befehl.orElseThrow(); + assertThat(e.ebene()).isEqualTo(Ebene.PARAGRAPH); + assertThat(e.bezeichnung()).isNull(); + assertThat(e.stelle().anzeigeText()).isEqualTo("§ 60a"); + } + + @Test + void paragraphBlockErsetzung() { + var befehl = + erkenne( + "Die §§ 42 bis 45 werden durch die folgenden §§ 42 bis 45 ersetzt: „§ 42 Grundsatz" + + " (1) Text.“", + Stelle.LEER); + assertThat(befehl).containsInstanceOf(Aenderungsbefehl.StrukturErsetzung.class); + var e = (Aenderungsbefehl.StrukturErsetzung) befehl.orElseThrow(); + assertThat(e.ebene()).isEqualTo(Ebene.PARAGRAPH); + assertThat(e.stelle().anzeigeText()).isEqualTo("§ 42"); + assertThat(e.bisStelle().anzeigeText()).isEqualTo("§ 45"); + } + + @Test + void ueberschriftErsetzungMitStelle() { + var befehl = + erkenne( + "In Anlage 7 wird die Überschrift durch die folgende Überschrift ersetzt: „Anlage 7" + + " (zu § 36) Höchstwerte“.", + Stelle.LEER); + assertThat(befehl).get().isInstanceOf(Neufassung.class); + assertThat(befehl.orElseThrow().stelle().betrifftGliederung()).isTrue(); + } + + @Test + void inhaltsuebersichtErsetzung() { + var befehl = + erkenne( + "Die Inhaltsübersicht wird durch die folgende Inhaltsübersicht ersetzt: „Inhaltsübersicht" + + " § 1 Zweck § 2 Begriffe“.", + Stelle.LEER); + assertThat(befehl).get().isInstanceOf(Neufassung.class); + assertThat(befehl.orElseThrow().stelle().betrifftInhaltsuebersicht()).isTrue(); + } + + @Test void mehrfachErsetzungWirdSammelbefehl() { // Mehrere Ersetzungspaare unter einem gemeinsamen „ersetzt“. var befehl = diff --git a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java index 8a1b1e0..9b9c18d 100644 --- a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java +++ b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java @@ -20,6 +20,7 @@ import eu.mulk.aendggner.aenderung.Stelle; import eu.mulk.aendggner.anwendung.BefehlAnwender.Status; import eu.mulk.aendggner.gesetz.Absatz; import eu.mulk.aendggner.gesetz.Gesetz; +import eu.mulk.aendggner.gesetz.Gliederung; import eu.mulk.aendggner.gesetz.Norm; import java.util.List; import org.junit.jupiter.api.Test; @@ -349,6 +350,44 @@ class BefehlAnwenderTest { } @Test + void ersetztAbsatzBereichDurchBlock() { + // „Die Absätze 1 und 2 werden durch die folgenden Absätze 1 bis 3 ersetzt: „…““ + var befehl = + new StrukturErsetzung( + stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("1")), + stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("2")), + Ebene.ABSATZ, + "(1) Neu eins. (2) Neu zwei. (3) Neu drei.", + PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl)); + + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT); + var norm = ergebnis.neu().norm("§ 1").orElseThrow(); + assertThat(norm.absaetze()).hasSize(3); + assertThat(norm.absaetze()).extracting(Absatz::nummer).containsExactly("1", "2", "3"); + assertThat(norm.absaetze().get(0).text()).isEqualTo("Neu eins."); + } + + @Test + void ersetztSatzBereichDurchBlock() { + // „Die Sätze 1 und 2 werden wie folgt gefasst: „…““ in § 2 (3 Sätze). + var befehl = + new StrukturErsetzung( + stelle(new Stelle.Paragraph("2"), new Stelle.SatzNr("1")), + stelle(new Stelle.Paragraph("2"), new Stelle.SatzNr("2")), + Ebene.SATZ, + "Neuer Satz eins. Neuer Satz zwei.", + PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl)); + + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT); + assertThat(absatzText(ergebnis.neu(), "§ 2", 0)) + .isEqualTo("Neuer Satz eins. Neuer Satz zwei. Sie endet mit einem Bericht."); + } + + @Test void fuegtSatzNachSatzEin() { var befehl = new StrukturEinfuegung( @@ -422,6 +461,112 @@ class BefehlAnwenderTest { .isEqualTo("Zweck dieses Gesetzes ist die Prüfung."); } + @Test + void nummeriertParagraphUm() { + var befehl = + new Umnummerierung(stelle(new Stelle.Paragraph("3")), stelle(new Stelle.Paragraph("4")), PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl)); + + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT); + var enbezListe = ergebnis.neu().normen().stream().map(Norm::enbez).toList(); + assertThat(enbezListe).containsExactly("§ 1", "§ 2", "§ 4"); + assertThat(ergebnis.neu().norm("§ 4").orElseThrow().titel()).isEqualTo("Schlussvorschriften"); + } + + @Test + void paragraphUmnummerierungUeberschreibtWeggefalleneZielnorm() { + // § 2 aufheben, dann § 3 → § 2: die weggefallene Zielnorm wird ersetzt. + var befehle = + List.<eu.mulk.aendggner.aenderung.Aenderungsbefehl>of( + new Aufhebung(stelle(new Stelle.Paragraph("2")), PROV), + new Umnummerierung( + stelle(new Stelle.Paragraph("3")), stelle(new Stelle.Paragraph("2")), PROV)); + + var ergebnis = BefehlAnwender.anwenden(gesetz(), befehle); + + assertThat(ergebnis.protokoll()).allMatch(a -> a.status() == Status.ANGEWANDT); + var enbezListe = ergebnis.neu().normen().stream().map(Norm::enbez).toList(); + assertThat(enbezListe).containsExactly("§ 1", "§ 2"); + assertThat(ergebnis.neu().norm("§ 2").orElseThrow().titel()).isEqualTo("Schlussvorschriften"); + } + + @Test + void meldetKonfliktBeiParagraphUmnummerierung() { + var befehl = + new Umnummerierung(stelle(new Stelle.Paragraph("3")), stelle(new Stelle.Paragraph("1")), PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl)); + + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.MANUELL_PRUEFEN); + assertThat(ergebnis.protokoll().get(0).begruendung()).contains("existiert bereits"); + } + + @Test + void fuegtParagraphenBlockEin() { + // „Nach § 1 werden die folgenden §§ 1a und 1b eingefügt: „…““ (bezeichnung == null). + var befehl = + new StrukturEinfuegung( + stelle(new Stelle.Paragraph("1")), + false, + Ebene.PARAGRAPH, + null, + "§ 1a Erstes Neu (1) Inhalt eins. § 1b Zweites Neu (1) Inhalt zwei.", + PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl)); + + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT); + var enbezListe = ergebnis.neu().normen().stream().map(Norm::enbez).toList(); + assertThat(enbezListe).containsExactly("§ 1", "§ 1a", "§ 1b", "§ 2", "§ 3"); + assertThat(ergebnis.neu().norm("§ 1a").orElseThrow().titel()).isEqualTo("Erstes Neu"); + assertThat(ergebnis.neu().norm("§ 1b").orElseThrow().titel()).isEqualTo("Zweites Neu"); + } + + @Test + void ersetztParagraphBlock() { + // „§ 2 wird durch die folgenden §§ 2 und 2a ersetzt: „…““ — § 2 wird durch zwei §§ ersetzt. + // Der Querverweis „§ 1 Absatz 1“ im Text darf NICHT als Grenze zerteilt werden. + var befehl = + new StrukturErsetzung( + stelle(new Stelle.Paragraph("2")), + null, + Ebene.PARAGRAPH, + "§ 2 Begriffe (1) Erprobung nach § 1 Absatz 1 ist die Prüfung. § 2a Weiteres (1) Mehr.", + PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl)); + + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT); + var enbezListe = ergebnis.neu().normen().stream().map(Norm::enbez).toList(); + assertThat(enbezListe).containsExactly("§ 1", "§ 2", "§ 2a", "§ 3"); + assertThat(ergebnis.neu().norm("§ 2").orElseThrow().absaetze().get(0).text()) + .isEqualTo("Erprobung nach § 1 Absatz 1 ist die Prüfung."); + assertThat(ergebnis.neu().norm("§ 2a").orElseThrow().titel()).isEqualTo("Weiteres"); + } + + @Test + void nummeriertGliederungUm() { + var gesetz = + new Gesetz( + "TestG", + "Gesetz", + "Test", + List.of(new Norm("§ 1", "Zweck", null, List.of(new Absatz("1", "Text.")), false)), + List.of(new Gliederung("010020", "Abschnitt 2", "Früherkennung"))); + var befehl = + new Umnummerierung( + stelle(new Stelle.Gliederungseinheit("Abschnitt", "2")), + stelle(new Stelle.Gliederungseinheit("Abschnitt", "3")), + PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz, List.of(befehl)); + + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT); + assertThat(ergebnis.neu().gliederungen().get(0).bezeichnung()).isEqualTo("Abschnitt 3"); + assertThat(ergebnis.neu().gliederungen().get(0).titel()).isEqualTo("Früherkennung"); + } + private static Gesetz gesetz() { return new Gesetz( "TestG", |
