aboutsummaryrefslogtreecommitdiff
path: root/src/test/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/test/java')
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java146
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java121
-rw-r--r--src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java126
-rw-r--r--src/test/java/eu/mulk/aendggner/anwendung/SatzTeilerTest.java44
-rw-r--r--src/test/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParserTest.java61
5 files changed, 489 insertions, 9 deletions
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index c4ba894..959e664 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -257,15 +257,16 @@ class EndToEndTest {
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.
+ // 4. Anwenden auf die alte Fassung. Von 154 Befehlen bleiben genau drei als „manuell prüfen“
+ // stehen — Folgeänderungen innerhalb einer mehrschrittigen Umnummerierungssequenz, die
+ // ÄndGgner bewusst nicht automatisch auflöst (statt fehlerhaft zu raten):
// * 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.
+ // Die Sequenz in Art. 29a (§ 1 Nr. 23) löst sich dagegen seit der Kaskaden-Ordnung des
+ // BefehlAnwenders auf: „Der bisherige Abs. 4 wird Abs. 5“ läuft vor der Bereichs-
+ // Umnummerierung „Die bisherigen Abs. 1 bis 3 werden die Abs. 2 bis 4“, sodass die auf den
+ // neuen Abs. 5 zielenden Wort- und Satzbefehle ihren Alttext finden.
// 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());
@@ -275,9 +276,14 @@ class EndToEndTest {
.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)");
+ assertThat(manuellPfade).containsExactlyInAnyOrder("48. a) ee)", "48. a) gg)", "48. b) cc)");
+
+ // Art. 29a Abs. 5 trägt nach der Kaskade die neue Behördenbezeichnung und den eingefügten Satz.
+ var art29a = anwendung.neu().norm("Art. 29a").orElseThrow();
+ assertThat(art29a.absaetze()).hasSize(5);
+ assertThat(art29a.absaetze().get(4).text())
+ .startsWith("¹Die oberste Jagdbehörde wird ermächtigt")
+ .contains("²Die oberste Jagdbehörde kann zudem durch Rechtsverordnung");
// 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.
@@ -561,6 +567,128 @@ class EndToEndTest {
}
/**
+ * Nordrhein-Westfalen, Massentest: Artikel 1 desselben Heftes ändert das WDR-Gesetz — 101 Befehle
+ * an 31 Normen, darunter die Inhaltsübersicht, mehrere Umnummerierungs-Kaskaden über ganze
+ * Absatz- und Nummernfolgen und ein Sammelrahmen („Es werden ersetzt:“). Genau ein Befehl bleibt
+ * manuell; das Ergebnis wurde gegen die amtliche Fassung vom 1. April 2026 (recht.nrw.de)
+ * abgeglichen.
+ */
+ @Test
+ void wdrGesetzAcceptance() throws Exception {
+ var alt = SAMPLEDATA.resolve("NRW/WDR-Gesetz-alt.txt");
+ var pdf = SAMPLEDATA.resolve("NRW/GV-NRW-2026-S202_22-Rundfunkaenderungsgesetz.pdf");
+ assumeTrue(Files.exists(alt) && Files.exists(pdf), "WDR-Gesetz-Beispieldaten fehlen");
+
+ var gesetz = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader().load(alt);
+ assertThat(gesetz.jurabk()).isEqualTo("WDR-Gesetz");
+ // Die Inhaltsübersicht ist eine eigene Norm — nur so greifen die Angabe-Befehle des Artikels 1.
+ assertThat(gesetz.norm("Inhaltsübersicht")).isPresent();
+
+ var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf));
+ var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null);
+ assertThat(parseErgebnis.artikel()).containsExactly("1");
+ assertThat(parseErgebnis.befehle()).noneMatch(b -> b instanceof UnbekannterBefehl);
+
+ var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
+ var manuellPfade =
+ anwendung.protokoll().stream()
+ .filter(a -> a.status() == BefehlAnwender.Status.MANUELL_PRUEFEN)
+ .map(a -> a.befehl().provenienz().gliederungsPfad())
+ .toList();
+ // Einziges Residuum: „In Satz 2 Nummer 1 wird … der Punkt am Ende des Satzes durch ein
+ // Semikolon ersetzt …“ — die adressierte Nummer 1 trägt zwei Sätze und endet selbst auf ein
+ // Komma; welchen Punkt der Befehl meint, ist dem Wortlaut nicht sicher zu entnehmen.
+ assertThat(manuellPfade).containsExactly("13. e) aa)");
+ assertThat(anwendung.anzahlAngewandt()).isEqualTo(parseErgebnis.befehle().size() - 1);
+
+ var neu = anwendung.neu();
+ // 1. Umnummerierungs-Kaskade in § 3: Absatz 2 wird durch zwei Absätze ersetzt, die bisherigen
+ // Absätze 3 bis 11 rücken auf, ein neuer Absatz 13 kommt hinzu — 15 Absätze in der Reihe der
+ // amtlichen Nachfassung.
+ var paragraph3 = neu.norm("§ 3").orElseThrow();
+ assertThat(paragraph3.absaetze().stream().map(a -> a.nummer()).toList())
+ .containsExactly(
+ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15");
+ assertThat(paragraph3.absaetze().get(2).text())
+ .startsWith("Der WDR veranstaltet ein landesweites Fernsehprogramm");
+ assertThat(paragraph3.absaetze().get(12).text())
+ .startsWith("Der WDR strebt Partnerschaften");
+
+ // 2. Nummern-Kaskade in § 15 Absatz 3: Nummer 22 entfällt, die folgenden rücken auf — die
+ // Aufzählungsmarken im Text sind mitgezogen.
+ var absatz3 = neu.norm("§ 15").orElseThrow().absaetze().get(2).text();
+ assertThat(absatz3)
+ .contains("22. den Landesbehindertenrat NRW e.V.")
+ .contains("25. die Allianz Deutscher Produzentinnen und Produzenten")
+ .doesNotContain("22. den Sozialverband VdK");
+
+ // 3. Sammelrahmen „Es werden ersetzt:“ — die Unterpunkte tragen die Fundstelle, nicht das Verb.
+ assertThat(neu.norm("§ 17").orElseThrow().gesamtText())
+ .contains("schriftlich oder elektronisch");
+ assertThat(neu.norm("§ 40").orElseThrow().gesamtText())
+ .contains("schriftliche oder elektronische");
+
+ // 4. Die Angabe-Befehle auf die Inhaltsübersicht sind angewandt.
+ assertThat(neu.norm("Inhaltsübersicht").orElseThrow().gesamtText())
+ .contains("§ 11 | (weggefallen)")
+ .contains("§ 57a | Übergangsregelung zu Amtszeiten, Entsendung und Hörfunkprogrammen");
+
+ // 5. „Dem Wortlaut des Absatzes 3 werden die folgenden Sätze vorangestellt“ trifft den Absatz,
+ // nicht die Norm.
+ assertThat(neu.norm("§ 24").orElseThrow().absaetze().get(0).text())
+ .startsWith("Die Intendantin oder der Intendant wird auf sechs Jahre gewählt");
+ assertThat(neu.norm("§ 24").orElseThrow().absaetze().get(2).text())
+ .startsWith("Die inhaltlichen Anforderungen an das Amt");
+ }
+
+ /**
+ * Nordrhein-Westfalen: Artikel 2 desselben Heftes ändert das Landesmediengesetz — 18 Befehle,
+ * *alle* angewandt. Das Ergebnis stimmt mit der amtlichen Fassung vom 1. April 2026 überein; nur
+ * § 93 Absatz 3 Nummer 11 weicht ab, weil die amtliche Nachfassung dort über den Befehlswortlaut
+ * hinaus auch die Wörter „Film (“ gestrichen hat.
+ */
+ @Test
+ void lmgNrwAcceptance() throws Exception {
+ var alt = SAMPLEDATA.resolve("NRW/LMG-NRW-alt.txt");
+ var pdf = SAMPLEDATA.resolve("NRW/GV-NRW-2026-S202_22-Rundfunkaenderungsgesetz.pdf");
+ assumeTrue(Files.exists(alt) && Files.exists(pdf), "LMG-NRW-Beispieldaten fehlen");
+
+ var gesetz = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader().load(alt);
+ assertThat(gesetz.jurabk()).isEqualTo("LMG NRW");
+ // Die Inhaltsübersicht führt auch die Gliederungs-Überschriften („Abschnitt 1 …“); sie zerfällt
+ // daran nicht, sondern bleibt eine Norm bis zum ersten Normkopf.
+ var uebersicht = gesetz.norm("Inhaltsübersicht").orElseThrow();
+ assertThat(uebersicht.gesamtText())
+ .contains("Abschnitt 1 Allgemeine Vorschriften")
+ .contains("§ 127 | Übergangsregelung zur Neukonstituierung der Medienkommission");
+
+ var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf));
+ var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null);
+ assertThat(parseErgebnis.artikel()).containsExactly("2");
+ assertThat(parseErgebnis.befehle()).hasSize(18);
+ assertThat(parseErgebnis.befehle()).noneMatch(b -> b instanceof UnbekannterBefehl);
+
+ var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
+ assertThat(anwendung.anzahlManuell()).isZero();
+
+ var neu = anwendung.neu();
+ // § 49 Absatz 2 Satz 1: Die Fundstellen-Abkürzung „BGBl.“ darf den Satz nicht teilen, sonst
+ // fände der zweite Teil des Befehls seinen Alttext nicht.
+ assertThat(neu.norm("§ 49").orElseThrow().absaetze().get(1).text())
+ .contains("der der Verordnung (EU) 2016/679")
+ .contains("Telekommunikation-Digitale-Dienste-Datenschutz-Gesetzes vom 23. Juni 2021")
+ .doesNotContain("Telekommunikation-Telemedien-Datenschutz-Gesetzes");
+ // § 127 ist neu gefasst — samt Überschrift in der Inhaltsübersicht.
+ assertThat(neu.norm("§ 127").orElseThrow().titel())
+ .isEqualTo("Übergangsregelung zu Amtszeiten und Entsendung");
+ assertThat(neu.norm("Inhaltsübersicht").orElseThrow().gesamtText())
+ .contains("§ 127 | Übergangsregelung zu Amtszeiten und Entsendung");
+ // § 88 Absatz 6 Satz 4 ist aufgehoben.
+ assertThat(neu.norm("§ 88").orElseThrow().absaetze().get(5).text())
+ .doesNotContain("Absatz 5 Satz 2 gilt entsprechend");
+ }
+
+ /**
* Berlin: Das GVBl. ist zweispaltig gesetzt (Wolters-Kluwer-Satz). Der Test hält das Ergebnis des
* Spaltenspikes fest — die Spaltenreihenfolge des Inhaltsstroms ist bereits die Lesereihenfolge,
* eine koordinatenbasierte Spaltenerkennung ist dafür nicht nötig.
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 95d15e1..8fcf3e5 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
@@ -26,6 +26,8 @@ class BefehlErkennerTest {
private static final Provenienz PROV = new Provenienz("1", "1.", "(Test)");
+ private static final Stelle PARAGRAPH_5 = new Stelle(List.of(new Stelle.Paragraph("5")));
+
/** Erkennt einen Befehl aus echtem Gesetzestext (mit „…“-Zitaten). */
private static Optional<Aenderungsbefehl> erkenne(String befehlsSatz, Stelle kontext) {
var zitate = ZitatExtraktor.extrahiere(befehlsSatz);
@@ -1204,4 +1206,123 @@ class BefehlErkennerTest {
var abs = erkenne("Abs. 3 wird Abs. 2.", ART_22);
assertThat(abs).containsInstanceOf(Umnummerierung.class);
}
+
+ // --- Idiome des nordrhein-westfälischen Gesetz- und Verordnungsblatts --------------------
+
+ /** „wird wie folgt neu gefasst“ und das im amtlichen Satz fehlende „wird“. */
+ @Test
+ void erkenntNeufassungMitNeuUndOhneVerb() {
+ assertThat(erkenne("§ 10 Absatz 2 wird wie folgt neu gefasst: „(2) Neuer Text.“", Stelle.LEER))
+ .containsInstanceOf(Neufassung.class);
+ assertThat(erkenne("Nummer 9 wie folgt gefasst: „9. Neuer Text,“.", Stelle.LEER))
+ .containsInstanceOf(Neufassung.class);
+ }
+
+ /** Geteilte Fundstelle: der Rahmen vor dem Verb, die adressierte Einheit dahinter. */
+ @Test
+ void erkenntNeufassungMitGeteilterStelle() {
+ var befehl = erkenne("In Absatz 2 wird Satz 1 wie folgt gefasst: „Neuer Satz.“", PARAGRAPH_5);
+
+ assertThat(befehl).containsInstanceOf(Neufassung.class);
+ assertThat(befehl.orElseThrow().stelle().anzeigeText()).isEqualTo("§ 5 Absatz 2 Satz 1");
+ }
+
+ /** Struktur-Einfügung mit vorangestelltem Fundstellen-Rahmen und Artikel vor dem Anker. */
+ @Test
+ void erkenntEinfuegungMitVorangestellterStelle() {
+ var befehl =
+ erkenne(
+ "In Absatz 1 wird nach dem Satz 1 folgender Satz eingefügt: „Neuer Satz.“",
+ PARAGRAPH_5);
+
+ assertThat(befehl).containsInstanceOf(StrukturEinfuegung.class);
+ assertThat(befehl.orElseThrow().stelle().anzeigeText()).isEqualTo("§ 5 Absatz 1 Satz 1");
+ assertThat(((StrukturEinfuegung) befehl.orElseThrow()).vorher()).isFalse();
+ }
+
+ /** Das Satzzeichen ist selbst das Subjekt; die Fundstelle liefert der Kontextrahmen. */
+ @Test
+ void erkenntSatzzeichenErsetzungOhneStelle() {
+ var befehl =
+ erkenne("Der Punkt am Ende wird durch die Angabe „(Gesellschaftsdialog).“ ersetzt.",
+ PARAGRAPH_5);
+
+ assertThat(befehl).containsInstanceOf(Ersetzung.class);
+ var ersetzung = (Ersetzung) befehl.orElseThrow();
+ assertThat(ersetzung.alt()).isEqualTo(".");
+ assertThat(ersetzung.neu()).isEqualTo("(Gesellschaftsdialog).");
+ assertThat(ersetzung.stelle().anzeigeText()).isEqualTo("§ 5");
+ }
+
+ /** Rahmen und Folgebefehl stehen in einem Punkt statt in Punkt und Unterpunkt. */
+ @Test
+ void erkenntRahmenMitFolgebefehlInEinemSatz() {
+ var befehl =
+ erkenne(
+ "Die bisherige Nummer 26 wird zu Nummer 25 und wird wie folgt geändert: Die Angabe"
+ + " „alt“ wird durch die Angabe „neu“ ersetzt.",
+ PARAGRAPH_5);
+
+ assertThat(befehl).containsInstanceOf(Sammelbefehl.class);
+ var teile = ((Sammelbefehl) befehl.orElseThrow()).teilbefehle();
+ assertThat(teile.get(0)).isInstanceOf(Umnummerierung.class);
+ assertThat(teile.get(1).stelle().anzeigeText()).isEqualTo("§ 5 Nummer 25");
+ }
+
+ /** Verb-Rahmen „Es werden ersetzt:“ — der Unterpunkt trägt nur die Fundstelle. */
+ @Test
+ void vervollstaendigtVerbRahmenPunkt() {
+ assertThat(BefehlErkenner.verbRahmen("Es werden ersetzt:")).contains("ersetzt");
+ var zitate =
+ ZitatExtraktor.extrahiere(
+ "in § 35 Absatz 3 sowie § 44 Absatz 1 die Angabe „schriftlichen“ jeweils durch die"
+ + " Angabe „schriftlichen oder elektronischen“,");
+ var satz =
+ BefehlErkenner.vervollstaendigeVerbRahmenPunkt(
+ zitate.text().replaceAll("\\s+", " ").strip(), "ersetzt");
+ assertThat(satz).isPresent();
+ var befehl = BefehlErkenner.erkenne(satz.orElseThrow(), Stelle.LEER, zitate, PROV);
+ assertThat(befehl).containsInstanceOf(Sammelbefehl.class);
+ var teile = ((Sammelbefehl) befehl.orElseThrow()).teilbefehle();
+ assertThat(teile.stream().map(t -> t.stelle().anzeigeText()))
+ .containsExactly("§ 35 Absatz 3", "§ 44 Absatz 1");
+ }
+
+ /** Nach einer Absatz-Umnummerierung erbt die lokative Folgeklausel die neue Bezeichnung. */
+ @Test
+ void erbtNeueAbsatzbezeichnungInFolgeklausel() {
+ var befehl =
+ erkenne(
+ "Der bisherige Absatz 8 wird Absatz 9 und in Satz 3 wird die Angabe „32a“ durch die"
+ + " Angabe „30b“ ersetzt.",
+ PARAGRAPH_5);
+
+ assertThat(befehl).containsInstanceOf(Sammelbefehl.class);
+ var teile = ((Sammelbefehl) befehl.orElseThrow()).teilbefehle();
+ assertThat(teile.get(1).stelle().anzeigeText()).isEqualTo("§ 5 Absatz 9 Satz 3");
+ }
+
+ /** „Dem Wortlaut des Absatzes 3 …“ nennt die Fundstelle selbst. */
+ @Test
+ void erkenntWortlautVoranstellungMitAbsatzangabe() {
+ var befehl =
+ erkenne(
+ "Dem Wortlaut des Absatzes 3 werden die folgenden Sätze vorangestellt: „Erster Satz.“",
+ PARAGRAPH_5);
+
+ assertThat(befehl).containsInstanceOf(Aenderungsbefehl.WortlautVoranstellung.class);
+ assertThat(befehl.orElseThrow().stelle().anzeigeText()).isEqualTo("§ 5 Absatz 3");
+ }
+
+ /** Ein überzähliges schließendes Anführungszeichen am Satzende ist Beiwerk. */
+ @Test
+ void toleriertUeberzaehligesSchlussAnfuehrungszeichen() {
+ var zitate =
+ ZitatExtraktor.extrahiere(
+ "In Absatz 7 Satz 3 wird die Angabe „schriftlich“ durch die Angabe „schriftlich oder"
+ + " elektronisch“ ersetzt.“");
+ var text = zitate.text().replaceAll("\\s+", " ").strip();
+ assertThat(BefehlErkenner.erkenne(text, Stelle.LEER, zitate, PROV))
+ .containsInstanceOf(Ersetzung.class);
+ }
}
diff --git a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
index 2706ac8..e9ecbcd 100644
--- a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
+++ b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
@@ -1066,6 +1066,132 @@ class BefehlAnwenderTest {
assertThat(ergebnis.neu().langue()).isEqualTo("Gesetz zur gründlichen Erprobung");
}
+ /**
+ * Aufsteigende Umnummerierungs-Kaskade: Beide Befehle meinen die ursprüngliche Zählung. In
+ * Dokumentreihenfolge angewandt entstünde ein zweiter Absatz 2; der Anwender zieht deshalb den
+ * räumenden Befehl vor.
+ */
+ @Test
+ void loestAufsteigendeUmnummerierungsKaskadeAuf() {
+ var eins =
+ new Umnummerierung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("1")),
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("2")),
+ PROV);
+ var zwei =
+ new Umnummerierung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("2")),
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("3")),
+ PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(eins, zwei));
+
+ assertThat(ergebnis.anzahlManuell()).isZero();
+ assertThat(ergebnis.neu().norm("§ 1").orElseThrow().absaetze().stream().map(Absatz::nummer))
+ .containsExactly("2", "3");
+ // Das Protokoll bleibt in Dokumentreihenfolge, unabhängig von der Anwendungsreihenfolge.
+ assertThat(ergebnis.protokoll().get(0).befehl()).isSameAs(eins);
+ }
+
+ /** Eine Umnummerierung läuft vor der Einfügung, die ihre bisherige Bezeichnung neu vergibt. */
+ @Test
+ void raeumtBezeichnungVorEinfuegung() {
+ var einfuegung =
+ new StrukturEinfuegung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("1")),
+ false,
+ Ebene.ABSATZ,
+ "2",
+ "(2) Neuer Absatz.",
+ PROV);
+ var umnummerierung =
+ new Umnummerierung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("2")),
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("3")),
+ PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(einfuegung, umnummerierung));
+
+ assertThat(ergebnis.anzahlManuell()).isZero();
+ assertThat(ergebnis.neu().norm("§ 1").orElseThrow().absaetze().stream().map(Absatz::nummer))
+ .containsExactly("1", "2", "3");
+ assertThat(absatzText(ergebnis.neu(), "§ 1", 1)).isEqualTo("Neuer Absatz.");
+ }
+
+ /**
+ * Anders als ein Absatz trägt eine Aufzählungsnummer ihre Bezeichnung als Marke im Text; die
+ * Umnummerierung muss sie dort austauschen.
+ */
+ @Test
+ void nummeriertAufzaehlungsNummerImTextUm() {
+ var befehl =
+ new Umnummerierung(
+ stelle(
+ new Stelle.Paragraph("1"), new Stelle.AbsatzNr("2"), new Stelle.NummerNr("2")),
+ stelle(
+ new Stelle.Paragraph("1"), new Stelle.AbsatzNr("2"), new Stelle.NummerNr("4")),
+ PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl));
+
+ assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT);
+ assertThat(absatzText(ergebnis.neu(), "§ 1", 1))
+ .contains(" 4. die Anwendung von Befehlen und")
+ .doesNotContain(" 2. die Anwendung");
+ }
+
+ /** Der weggefallene Platzhalter mit der Zielbezeichnung weicht der Umnummerierung. */
+ @Test
+ void ueberschreibtWeggefalleneNummer() {
+ var mitLuecke =
+ new Gesetz(
+ "TestG",
+ "Gesetz zur Erprobung",
+ "Testgesetz",
+ List.of(
+ new Norm(
+ "§ 1",
+ "Zweck",
+ null,
+ List.of(
+ new Absatz(
+ "1",
+ "Die Erprobung umfasst\n"
+ + " 1. (weggefallen)\n"
+ + " 2. die Anwendung von Befehlen.")),
+ false)),
+ List.of());
+ var befehl =
+ new Umnummerierung(
+ stelle(
+ new Stelle.Paragraph("1"), new Stelle.AbsatzNr("1"), new Stelle.NummerNr("2")),
+ stelle(
+ new Stelle.Paragraph("1"), new Stelle.AbsatzNr("1"), new Stelle.NummerNr("1")),
+ PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(mitLuecke, List.of(befehl));
+
+ assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT);
+ assertThat(absatzText(ergebnis.neu(), "§ 1", 0))
+ .isEqualTo("Die Erprobung umfasst\n 1. die Anwendung von Befehlen.");
+ }
+
+ /** Beginnt der Einschub mit einem Satzzeichen, entfällt das Leerzeichen hinter dem Anker. */
+ @Test
+ void fuegtSatzzeichenOhneVorangehendesLeerzeichenEin() {
+ var befehl =
+ new WoerterEinfuegung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("1")),
+ new WortAnker.NachWoertern("die Erprobung"),
+ ", auch in Teilen,",
+ PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl));
+
+ assertThat(absatzText(ergebnis.neu(), "§ 1", 0))
+ .isEqualTo("Zweck dieses Gesetzes ist die Erprobung, auch in Teilen,.");
+ }
+
private static String absatzText(Gesetz gesetz, String enbez, int index) {
return gesetz.norm(enbez).orElseThrow().absaetze().get(index).text();
}
diff --git a/src/test/java/eu/mulk/aendggner/anwendung/SatzTeilerTest.java b/src/test/java/eu/mulk/aendggner/anwendung/SatzTeilerTest.java
index a97fc03..c0da1f3 100644
--- a/src/test/java/eu/mulk/aendggner/anwendung/SatzTeilerTest.java
+++ b/src/test/java/eu/mulk/aendggner/anwendung/SatzTeilerTest.java
@@ -74,4 +74,48 @@ class SatzTeilerTest {
assertThat(saetze.get(0)).contains("2. die Anwendung und");
assertThat(saetze.get(1)).isEqualTo("Weitere Einzelheiten regelt die Verordnung.");
}
+
+ @Test
+ void aufzaehlungsmarkeAmZeilenanfangBeendetKeinenSatz() {
+ // Anders als im Test darüber beginnen die Glieder großgeschrieben — erst die Marke am
+ // Zeilenanfang unterscheidet sie von einer Zahl am Satzende.
+ var text =
+ "Dem Rundfunkrat obliegen insbesondere folgende Aufgaben\n"
+ + "1. Erlaß von Satzungen,\n"
+ + "2. Wahl der Intendantin oder des Intendanten.\n"
+ + "Vor Beschlüssen ist Gelegenheit zur Stellungnahme zu geben.";
+ var saetze = SatzTeiler.teileTexte(text);
+
+ assertThat(saetze).hasSize(2);
+ assertThat(saetze.get(0)).contains("2. Wahl der Intendantin");
+ assertThat(saetze.get(1)).startsWith("Vor Beschlüssen");
+ }
+
+ @Test
+ void zahlAmSatzendeBeendetDenSatz() {
+ var saetze = SatzTeiler.teileTexte("Die Frist beträgt 30. Der Lauf beginnt am Folgetag.");
+
+ assertThat(saetze).containsExactly("Die Frist beträgt 30.", "Der Lauf beginnt am Folgetag.");
+ }
+
+ @Test
+ void fundstellenAbkuerzungTeiltDenSatzNicht() {
+ var saetze =
+ SatzTeiler.teileTexte(
+ "Es gilt das Gesetz vom 23. Juni 2021 (BGBl. I S. 1982) in der geltenden Fassung."
+ + " Näheres regelt die Satzung.");
+
+ assertThat(saetze).hasSize(2);
+ assertThat(saetze.get(0)).endsWith("in der geltenden Fassung.");
+ }
+
+ @Test
+ void paragraphenzeichenEroeffnetEinenSatz() {
+ var saetze =
+ SatzTeiler.teileTexte(
+ "Die Angebote können zusammengefasst werden. § 27 Absatz 2 bleibt unberührt.");
+
+ assertThat(saetze).containsExactly(
+ "Die Angebote können zusammengefasst werden.", "§ 27 Absatz 2 bleibt unberührt.");
+ }
}
diff --git a/src/test/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParserTest.java b/src/test/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParserTest.java
index b170c5f..77249f6 100644
--- a/src/test/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParserTest.java
+++ b/src/test/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParserTest.java
@@ -193,4 +193,65 @@ class LandesRechtTextParserTest {
assertThat(mitGedankenstrich.jurabk()).isEqualTo("GO");
assertThat(mitGedankenstrich.kurzue()).isEqualTo("Gemeindeordnung");
}
+
+ /**
+ * Die Inhaltsübersicht ist eine eigene Norm — nur unter diesem Namen finden sie die
+ * Angabe-Befehle. Sie führt auch die Gliederungs-Überschriften des Gesetzes mit; daran darf sie
+ * nicht zerfallen.
+ */
+ @Test
+ void liestInhaltsuebersichtAlsNorm() {
+ var gesetz =
+ LandesRechtTextParser.parse(
+ """
+ Landesmediengesetz Nordrhein-Westfalen
+ (LMG NRW)
+ Vom 2. Juli 2002
+ Inhaltsübersicht
+ Abschnitt 1 Allgemeine Vorschriften
+ § 1 | Geltungsbereich
+ Abschnitt 2 Zulassung
+ § 4 | Grundsätze
+ Abschnitt 1 Allgemeine Vorschriften
+ § 1 Geltungsbereich
+ (1) Dieses Gesetz gilt für die Veranstaltung von Rundfunk.
+ """);
+
+ var uebersicht = gesetz.norm("Inhaltsübersicht").orElseThrow();
+ assertThat(uebersicht.gesamtText())
+ .contains("Abschnitt 1 Allgemeine Vorschriften")
+ .contains("§ 1 | Geltungsbereich")
+ .contains("Abschnitt 2 Zulassung")
+ .contains("§ 4 | Grundsätze");
+ // Nur die Gliederung des Textteils zählt, nicht die zitierte in der Übersicht.
+ assertThat(gesetz.gliederungen()).hasSize(1);
+ assertThat(gesetz.norm("§ 1").orElseThrow().titel()).isEqualTo("Geltungsbereich");
+ }
+
+ /** Römische Gliederung ohne Schlüsselwort — sie ist nur an ihrer Stellung erkennbar. */
+ @Test
+ void erkenntRoemischeGliederungOhneSchluesselwort() {
+ var gesetz =
+ LandesRechtTextParser.parse(
+ """
+ Gesetz über den Westdeutschen Rundfunk Köln
+ (WDR-Gesetz)
+ Vom 25. April 1998
+ I. Rechtsform und Aufgaben
+ § 1 Name, Rechtsform
+ (1) Der WDR ist eine Anstalt des öffentlichen Rechts.
+ II. Organisation
+ 1. Der Rundfunkrat
+ § 15 Zusammensetzung
+ (1) Der Rundfunkrat besteht aus Mitgliedern.
+ """);
+
+ assertThat(gesetz.gliederungen())
+ .extracting(Gliederung::bezeichnung, Gliederung::titel)
+ .containsExactly(
+ org.assertj.core.groups.Tuple.tuple("I.", "Rechtsform und Aufgaben"),
+ org.assertj.core.groups.Tuple.tuple("II.", "Organisation"),
+ org.assertj.core.groups.Tuple.tuple("1.", "Der Rundfunkrat"));
+ assertThat(gesetz.norm("§ 15").orElseThrow().gliederung().titel()).isEqualTo("Der Rundfunkrat");
+ }
}