aboutsummaryrefslogtreecommitdiff
path: root/src/test/java/eu
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-07-13 21:33:20 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-07-13 21:33:20 +0200
commitb05e44a627b3bbdf6f8b2e920b963709fd348f5a (patch)
treeaf77a6cf2fb584de346e6a9387aa10fca9b14767 /src/test/java/eu
parent91a57498aa4acf266d0ab255aa06c18527846eec (diff)
Support multi-target "jeweils" commands and range renumbering
Add a Sammelbefehl command that applies one operation to several "und"/"sowie"/comma-coordinated Stellen sharing a common prefix, and resolve "Die bisherigen Absätze X bis Y werden zu den Absätzen X' bis Y'" into descending single renumberings. Parsing gains StellenParser.parseMehrfach; the applier folds sub-commands into one log entry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Change-Id: Iaa2c7161066d40d0605c00d3b6ca9a08696070e7
Diffstat (limited to 'src/test/java/eu')
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java16
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java77
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java55
-rw-r--r--src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java53
4 files changed, 200 insertions, 1 deletions
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index 5254ff2..bd17d79 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -140,13 +140,27 @@ class EndToEndTest {
assertThat(parseErgebnis.befehle().size()).isBetween(15, 40);
var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
- assertThat(anwendung.anzahlAngewandt()).isGreaterThan(10);
+ // Inkl. der Mehrfachziel-„jeweils“-Befehle (§ 3, § 20, § 30) werden hier ~22 Befehle
+ // angewandt; nur die zwei Sonderfälle (positionaler Lokator, Verbundbefehl) bleiben manuell.
+ assertThat(anwendung.anzahlAngewandt()).isGreaterThanOrEqualTo(20);
+ assertThat(anwendung.anzahlManuell()).isLessThanOrEqualTo(3);
var synopse = SynopseBuilder.baue(gesetz, anwendung, parseErgebnis.warnungen(), false);
var html = HtmlRenderer.rendere(synopse, "E2E-Test");
assertThat(html).contains("<del>").contains("<ins>");
// Stichprobe aus Artikel 1 Nummer 1: „Alters“ → „Lebensalters“ in § 1.
assertThat(html).contains("<ins>Lebensalters</ins>");
+ // Mehrfachziel-Einfügung (§ 30 Absatz 2 Satz 1 und Absatz 3): „Bildung,“ vor „Familie“.
+ assertThat(html).contains("<ins>Bildung,");
+ // Mehrfachziel-Streichung (§ 3 Absatz 1 Satz 2 und Absatz 4): gezielt entfernt — der
+ // untargetierte Absatz 5 behält die Angabe.
+ var norm3 = anwendung.neu().norm("§ 3").orElseThrow();
+ var absatz1 =
+ norm3.absaetze().stream().filter(a -> "1".equals(a.nummer())).findFirst().orElseThrow();
+ assertThat(absatz1.text()).doesNotContain("in Bezug auf § 2 Abs. 1 Nr. 1 bis 4");
+ var absatz5 =
+ norm3.absaetze().stream().filter(a -> "5".equals(a.nummer())).findFirst().orElseThrow();
+ assertThat(absatz5.text()).contains("in Bezug auf § 2 Abs. 1 Nr. 1 bis 4");
}
/**
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 f0282c5..05055e0 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
@@ -8,6 +8,7 @@ import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Aufhebung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Ebene;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Ersetzung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Neufassung;
+import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Sammelbefehl;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Streichung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturEinfuegung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Umnummerierung;
@@ -347,6 +348,82 @@ class BefehlErkennerTest {
}
@Test
+ void erkenntMehrfachzielStreichung() {
+ var befehl =
+ erkenne(
+ "In § 3 Absatz 1 Satz 2 und Absatz 4 wird jeweils die Angabe „in Bezug auf § 2 Abs."
+ + " 1 Nr. 1 bis 4“ gestrichen.",
+ Stelle.LEER);
+
+ assertThat(befehl).containsInstanceOf(Sammelbefehl.class);
+ var teile = ((Sammelbefehl) befehl.orElseThrow()).teilbefehle();
+ assertThat(teile).hasSize(2).allMatch(t -> t instanceof Streichung);
+ assertThat(teile).extracting(t -> t.stelle().anzeigeText())
+ .containsExactly("§ 3 Absatz 1 Satz 2", "§ 3 Absatz 4");
+ assertThat(((Streichung) teile.get(0)).woerter()).isEqualTo("in Bezug auf § 2 Abs. 1 Nr. 1 bis 4");
+ }
+
+ @Test
+ void erkenntMehrfachzielErsetzung() {
+ var befehl =
+ erkenne(
+ "In § 20 Absatz 1 Satz 1 und Absatz 2 Satz 2 wird jeweils die Angabe „Alters“ durch"
+ + " die Angabe „Lebensalters“ ersetzt.",
+ Stelle.LEER);
+
+ assertThat(befehl).containsInstanceOf(Sammelbefehl.class);
+ var teile = ((Sammelbefehl) befehl.orElseThrow()).teilbefehle();
+ assertThat(teile).hasSize(2).allMatch(t -> t instanceof Ersetzung);
+ assertThat(teile).extracting(t -> t.stelle().anzeigeText())
+ .containsExactly("§ 20 Absatz 1 Satz 1", "§ 20 Absatz 2 Satz 2");
+ assertThat(((Ersetzung) teile.get(0)).neu()).isEqualTo("Lebensalters");
+ }
+
+ @Test
+ void erkenntMehrfachzielEinfuegung() {
+ var befehl =
+ erkenne(
+ "In § 30 Absatz 2 Satz 1 und Absatz 3 wird jeweils vor der Angabe „Familie“ die"
+ + " Angabe „Bildung,“ eingefügt.",
+ Stelle.LEER);
+
+ assertThat(befehl).containsInstanceOf(Sammelbefehl.class);
+ var teile = ((Sammelbefehl) befehl.orElseThrow()).teilbefehle();
+ assertThat(teile).hasSize(2).allMatch(t -> t instanceof WoerterEinfuegung);
+ assertThat(teile).extracting(t -> t.stelle().anzeigeText())
+ .containsExactly("§ 30 Absatz 2 Satz 1", "§ 30 Absatz 3");
+ }
+
+ @Test
+ void erkenntBereichsUmnummerierungAbsteigend() {
+ var befehl =
+ erkenne(
+ "Die bisherigen Absätze 2 bis 4 werden zu den Absätzen 3 bis 5.",
+ new Stelle(List.of(new Stelle.Paragraph("5"))));
+
+ assertThat(befehl).containsInstanceOf(Sammelbefehl.class);
+ var teile = ((Sammelbefehl) befehl.orElseThrow()).teilbefehle();
+ assertThat(teile).hasSize(3).allMatch(t -> t instanceof Umnummerierung);
+ // Absteigend, damit die Anwendung keine Labels kollidieren lässt: 4→5, 3→4, 2→3.
+ assertThat(teile).extracting(t -> t.stelle().anzeigeText())
+ .containsExactly("§ 5 Absatz 4", "§ 5 Absatz 3", "§ 5 Absatz 2");
+ assertThat(teile).extracting(t -> ((Umnummerierung) t).neu().anzeigeText())
+ .containsExactly("§ 5 Absatz 5", "§ 5 Absatz 4", "§ 5 Absatz 3");
+ }
+
+ @Test
+ void verbundZweierBefehleBleibtUnbekannt() {
+ // „… wird zu Absatz 2 und nach Satz 2 werden … eingefügt“ verbindet zwei verschiedene
+ // Befehle per „und“ — bewusst nicht unterstützt.
+ assertThat(
+ erkenne(
+ "Der bisherige Absatz 1 wird zu Absatz 2 und nach Satz 2 werden die folgenden"
+ + " Sätze eingefügt: „Ein Satz.“",
+ Stelle.LEER))
+ .isEmpty();
+ }
+
+ @Test
void faelltBeiBereichsbefehlenAufUnbekanntZurueck() {
// Bereichs- und Mehrfachbefehle sind in v1 bewusst nicht unterstützt.
assertThat(
diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java
new file mode 100644
index 0000000..4c2c7ef
--- /dev/null
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/StellenParserTest.java
@@ -0,0 +1,55 @@
+package eu.mulk.aendggner.aenderung.parse;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import eu.mulk.aendggner.aenderung.Stelle;
+import org.junit.jupiter.api.Test;
+
+class StellenParserTest {
+
+ @Test
+ void parstEinfacheStelle() {
+ assertThat(StellenParser.parse("§ 5a Absatz 2 Satz 1").orElseThrow().anzeigeText())
+ .isEqualTo("§ 5a Absatz 2 Satz 1");
+ }
+
+ @Test
+ void lehntKoordinationInParseAb() {
+ // parse (einfach) bleibt bewusst streng: Koordination fällt durch.
+ assertThat(StellenParser.parse("§ 3 Absatz 1 und Absatz 4")).isEmpty();
+ }
+
+ @Test
+ void parstMehrfachMitGemeinsamemPraefix() {
+ // Zweites Segment erbt „§ 3“, ersetzt aber ab „Absatz“.
+ var stellen = StellenParser.parseMehrfach("§ 3 Absatz 1 Satz 2 und Absatz 4");
+ assertThat(stellen).extracting(Stelle::anzeigeText)
+ .containsExactly("§ 3 Absatz 1 Satz 2", "§ 3 Absatz 4");
+ }
+
+ @Test
+ void parstMehrfachMitSatzTiefe() {
+ var stellen = StellenParser.parseMehrfach("§ 20 Absatz 1 Satz 1 und Absatz 2 Satz 2");
+ assertThat(stellen).extracting(Stelle::anzeigeText)
+ .containsExactly("§ 20 Absatz 1 Satz 1", "§ 20 Absatz 2 Satz 2");
+ }
+
+ @Test
+ void parstMehrfachMitKomma() {
+ var stellen = StellenParser.parseMehrfach("§ 1, § 2 und § 3");
+ assertThat(stellen).extracting(Stelle::anzeigeText)
+ .containsExactly("§ 1", "§ 2", "§ 3");
+ }
+
+ @Test
+ void einfacheStelleBleibtEinElementig() {
+ assertThat(StellenParser.parseMehrfach("§ 5 Absatz 2"))
+ .extracting(Stelle::anzeigeText)
+ .containsExactly("§ 5 Absatz 2");
+ }
+
+ @Test
+ void unparsbaresSegmentLiefertLeereListe() {
+ assertThat(StellenParser.parseMehrfach("§ 5 Absatz 2 und Kauderwelsch")).isEmpty();
+ }
+}
diff --git a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
index beae519..a854632 100644
--- a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
+++ b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
@@ -7,6 +7,7 @@ import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Aufhebung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Ebene;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Ersetzung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Neufassung;
+import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Sammelbefehl;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Streichung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturEinfuegung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturErsetzung;
@@ -369,6 +370,58 @@ class BefehlAnwenderTest {
// --- Helfer --------------------------------------------------------------------------------
+ @Test
+ void wendetSammelbefehlAlsEinenEintragAn() {
+ var teil1 =
+ new Ersetzung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("1")),
+ "Erprobung",
+ "Prüfung",
+ false,
+ false,
+ PROV);
+ var teil2 =
+ new Ersetzung(
+ stelle(new Stelle.Paragraph("2")), "Erprobung", "Prüfung", false, false, PROV);
+ var sammel = new Sammelbefehl(List.of(teil1, teil2));
+
+ var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(sammel));
+
+ // Ein Befehl → genau ein Protokolleintrag, aber beide Teiledits wirken.
+ assertThat(ergebnis.protokoll()).hasSize(1);
+ assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT);
+ assertThat(ergebnis.protokoll().get(0).betroffeneEnbez()).containsExactlyInAnyOrder("§ 1", "§ 2");
+ assertThat(absatzText(ergebnis.neu(), "§ 1", 0))
+ .isEqualTo("Zweck dieses Gesetzes ist die Prüfung.");
+ assertThat(absatzText(ergebnis.neu(), "§ 2", 0)).startsWith("Prüfung ist die Prüfung");
+ }
+
+ @Test
+ void sammelbefehlMitFehlschlagendemTeilWirdManuell() {
+ var teil1 =
+ new Ersetzung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("1")),
+ "Erprobung",
+ "Prüfung",
+ false,
+ false,
+ PROV);
+ var teil2 =
+ new Ersetzung(
+ stelle(new Stelle.Paragraph("2")), "gibt es nicht", "egal", false, false, PROV);
+ var sammel = new Sammelbefehl(List.of(teil1, teil2));
+
+ var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(sammel));
+
+ assertThat(ergebnis.protokoll()).hasSize(1);
+ var eintrag = ergebnis.protokoll().get(0);
+ assertThat(eintrag.status()).isEqualTo(Status.MANUELL_PRUEFEN);
+ assertThat(eintrag.begruendung()).contains("Teil 2");
+ // Der gelungene Teil bleibt trotzdem wirksam.
+ assertThat(absatzText(ergebnis.neu(), "§ 1", 0))
+ .isEqualTo("Zweck dieses Gesetzes ist die Prüfung.");
+ }
+
private static Gesetz gesetz() {
return new Gesetz(
"TestG",