aboutsummaryrefslogtreecommitdiff
path: root/src/test
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-07-15 20:54:27 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-07-15 20:54:27 +0200
commitebd5a7bcfb13c1a74a5ad65de772e171d98ad156 (patch)
treed43c01a878323bd14999ca5f6167ecf0cc5bae82 /src/test
parent73c5dbae7a487db2b9ea29cd050eac94644974bb (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')
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java6
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java205
-rw-r--r--src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java145
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",