diff options
Diffstat (limited to 'src/test')
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"); + } } |
