diff options
Diffstat (limited to 'src/test')
4 files changed, 271 insertions, 1 deletions
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java index fe0ab21..22f1662 100644 --- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java +++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java @@ -960,6 +960,121 @@ class EndToEndTest { } /** + * Thüringen: Vierte Verordnung zur Änderung der Thüringer Kindergartenfinanzierungsverordnung + * (GVBl. für den Freistaat Thüringen Nr. 2 vom 13.02.2026, S. 77). Neun der zehn Befehle des + * Artikels 1 werden erkannt. + * + * <p>Das Heft ist ein Sammelheft: Es trägt vier Verkündungen, von denen nur diese eine ein + * Änderungsdokument ist. Die übrigen — ein Zuständigkeitsbeschluss mit eigener Nummernfolge, eine + * Stammverordnung und eine Inkrafttretens-Bekanntmachung — bleiben folgenlos. + * + * <p>Der unterscheidende Satzbefund: Das Heft führt <em>kein einziges</em> {@code „}, sondern + * setzt gerade Anführungszeichen beidseitig. Würden sie wie im BGBl durchweg schließend gelesen, + * fände der Zitatextraktor kein Zitat und keinen einzigen Befehl. + * + * <p>Kein voller Akzeptanztest: {@code landesrecht.thueringen.de} ist dieselbe anmeldepflichtige + * juris-Anwendung wie die Portale Schleswig-Holsteins, Berlins, Baden-Württembergs und Hessens. + */ + @Test + void thuerKiGaFinVOAendVO() throws Exception { + var pdf = SAMPLEDATA.resolve("Thueringen/GVBl-TH-2026-02_KiGaFinanzVO-AendVO-ua.pdf"); + assumeTrue(Files.exists(pdf), "GVBl-Thüringen-Beispiel-PDF fehlt"); + + var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf)); + + // Gerade Anführungszeichen paarweise gelesen: Das Heft führt beide Sorten in gleicher Zahl. + assertThat(text).doesNotContain("\""); + assertThat(text.chars().filter(c -> c == '„').count()) + .isEqualTo(text.chars().filter(c -> c == '“').count()); + + // Laufender Kolumnentitel und Seitenfuß herausgeschnitten. Der Kolumnentitel stand mitten in + // einem getrennten Wort — nach dem Schnitt ist die Silbentrennung geheilt. + assertThat(text).doesNotContain("Tag der Ausgabe"); + assertThat(text).doesNotContain("Verordnungsblatt für den Freistaat"); + assertThat(text).contains("Frauenhäusern und Frauenschutzwohnungen"); + + var alt = SAMPLEDATA.resolve("Thueringen/ThuerKigaFinVO-alt.txt"); + var verordnung = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader().load(alt); + assertThat(verordnung.jurabk()).isEqualTo("ThürKigaFinVO"); + // Stand vor der Vierten ÄndVO: §§ 1 bis 12, darunter die aufgehobenen Platzhalter § 7 und + // § 10 und der 2023 eingefügte § 7a. + assertThat(verordnung.normen()).hasSize(13); + assertThat(verordnung.norm("§ 7").orElseThrow().weggefallen()).isTrue(); + assertThat(verordnung.norm("§ 10").orElseThrow().weggefallen()).isTrue(); + var ergebnis = new AenderungsgesetzParser().parse(text, verordnung, null); + // Artikel 2 regelt nur das Inkrafttreten. + assertThat(ergebnis.artikel()).containsExactly("1"); + assertThat(ergebnis.befehle()).hasSize(10); + assertThat(ergebnis.befehle()).noneMatch(b -> b instanceof UnbekannterBefehl); + + // „Die §§ 1 bis 3 erhalten folgende Fassung:“ — ein Zitat, drei Normen. + var bereichsNeufassung = (Aenderungsbefehl.Sammelbefehl) befehlZu(ergebnis, "1."); + assertThat(bereichsNeufassung.teilbefehle()) + .extracting(b -> b.stelle().anzeigeText()) + .containsExactly("§ 1", "§ 2", "§ 3"); + + // „die Verweisung „X“ durch die Verweisung „Y“ ersetzt“ — dasselbe Muster wie „die Angabe“. + var verweisung = (Aenderungsbefehl.Ersetzung) befehlZu(ergebnis, "2. b)"); + assertThat(verweisung.stelle().anzeigeText()).isEqualTo("§ 4 Satz 2"); + assertThat(verweisung.alt()).isEqualTo("§ 27 Abs. 1, 3 und 5"); + assertThat(verweisung.neu()).isEqualTo("§ 27 Abs. 1"); + + // „Der bisherige § 8 wird § 7 und Absatz 1 wird wie folgt geändert:“ — der Rahmen verengt sich + // auf eine Untereinheit der umnummerierten Norm; die Unterpunkte meinen den NEUEN § 7. + var rahmenUmnummerierung = befehlZu(ergebnis, "6."); + assertThat(rahmenUmnummerierung).isInstanceOf(Aenderungsbefehl.Umnummerierung.class); + assertThat(rahmenUmnummerierung.stelle().anzeigeText()).isEqualTo("§ 8"); + assertThat(befehlZu(ergebnis, "6. a)").stelle().anzeigeText()).isEqualTo("§ 7 Absatz 1 Satz 1"); + assertThat(befehlZu(ergebnis, "6. b)").stelle().anzeigeText()).isEqualTo("§ 7 Absatz 1 Satz 2"); + + // „Die bisherigen §§ 9 bis 12 werden die §§ 8 bis 10.“ nennt vier Ausgangs-, aber drei + // Zielbezeichnungen — kein Widerspruch, sondern eine Lücke: § 10 ist aufgehoben und zählt nicht + // mit. Welche Einheiten der Bereich trägt, weiß erst das Gesetz; der Befehl bleibt deshalb bis + // zur Anwendung ungeteilt. + var bereich = befehlZu(ergebnis, "7."); + assertThat(bereich).isInstanceOf(Aenderungsbefehl.BereichsUmnummerierung.class); + + // Anwendung auf die konsolidierte Fassung vom Stand der Dritten ÄndVO (siehe SOURCES). + var anwendung = BefehlAnwender.anwenden(verordnung, ergebnis.befehle()); + assertThat(anwendung.anzahlAngewandt()).isEqualTo(10); + assertThat(anwendung.protokoll()) + .noneMatch(a -> a.status() == BefehlAnwender.Status.MANUELL_PRUEFEN); + + var neu = anwendung.neu(); + // Die Verordnung zählt danach lückenlos §§ 1 bis 10: § 6 ist aufgehoben, § 7a wird § 6, § 8 + // wird § 7, und aus §§ 9, 11 und 12 werden §§ 8, 9 und 10 — die aufgehobenen Platzhalter § 7 + // und § 10 weichen den neuen Bezeichnungen. + assertThat(neu.normen()) + .extracting(n -> n.enbez()) + .containsExactly("§ 1", "§ 2", "§ 3", "§ 4", "§ 5", "§ 6", "§ 7", "§ 8", "§ 9", "§ 10"); + assertThat(neu.norm("§ 6").orElseThrow().titel()) + .isEqualTo("Zuschuss für die praxisintegrierte Ausbildung"); + assertThat(neu.norm("§ 10").orElseThrow().titel()).isEqualTo("Inkrafttreten, Außerkrafttreten"); + + // Der umnummerierte § 7 (bisher § 8) trägt die beiden Verweisungs-Ersetzungen aus Punkt 6. + var paragraph7 = neu.norm("§ 7").orElseThrow().gesamtText(); + assertThat(paragraph7) + .contains("Meldungen nach § 27 Abs. 1 ThürKigaG") + .contains("Meldungen nach § 30 Abs. 4 und 6 ThürKigaG") + .doesNotContain("§ 27 Abs. 1 und 3 ThürKigaG") + .doesNotContain("§ 27 Abs. 5 sowie"); + // Die neu gefassten §§ 1 bis 3 stammen aus einem einzigen Zitat. + assertThat(neu.norm("§ 1").orElseThrow().gesamtText()) + .contains("Die Zahlung der Landeszuschüsse nach § 25 Satz 1 Nr. 2 und 3 ThürKigaG"); + // Die Überschrift des zitierten § 3 läuft über zwei Zeilen; sie gehört ganz in den Titel … + assertThat(neu.norm("§ 3").orElseThrow().titel()) + .isEqualTo("Zahlungen an den örtlichen Träger der öffentlichen Jugendhilfe"); + // … und die des § 1, die im Satz am Kopf klebt, gehört nicht zusätzlich in den Normtext. + assertThat(neu.norm("§ 1").orElseThrow().gesamtText()) + .doesNotContain("Zahlungen an die Wohnsitzgemeinde"); + // Die Silbentrennung des Spaltenumbruchs ist geheilt: „Thür-“ + „KigaG“ ist ein Wort, obwohl + // die Folgezeile großgeschrieben beginnt — der Wortbestand des Heftes bezeugt es. + assertThat(neu.norm("§ 5").orElseThrow().gesamtText()) + .contains("nach den §§ 25 und 26 ThürKigaG") + .doesNotContain("Thür-KigaG"); + } + + /** * Änderungsantrag der GRÜNEN (Ltg-Drs. 19/10365) zum Landtags-Gesetzentwurf 19/9707: Er ändert * nicht das Stammgesetz, sondern die Drucksache — genau den Befehl § 3 Nr. 22, der die Artenliste * des neuen § 18 AVBayJG zitiert. 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 cbf790b..02538ce 100644 --- a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java +++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java @@ -661,7 +661,7 @@ class BefehlErkennerTest { var teile = ((Sammelbefehl) erkenne( - "Die §§ 52 bis 56 werden wie folgt gefasst: „§ 52 (weggefallen) § 53" + "Die §§ 52 bis 54 werden wie folgt gefasst: „§ 52 (weggefallen) § 53" + " (weggefallen) § 54 (weggefallen)“.", Stelle.LEER) .orElseThrow()) @@ -672,6 +672,71 @@ class BefehlErkennerTest { .containsExactly("§ 52", "§ 53", "§ 54"); } + /** + * Der neue Wortlaut einer Bereichs-Neufassung zitiert reihenweise andere Vorschriften. + * Geschnitten wird allein an den Bezeichnungen des angekündigten Bereichs — an den Querverweisen + * zu schneiden erfände Normen, die es nicht gibt. + */ + @Test + void paragraphBereichNeufassungSchneidetNichtAnQuerverweisen() { + var teile = + ((Sammelbefehl) + erkenne( + "Die §§ 1 bis 2 erhalten folgende Fassung: „§ 1 Zahlungen\nDie Zahlung" + + " nach § 25 Satz 1 Nr. 2 erfolgt nach § 30 Abs. 2.\n§ 2" + + " Auszahlung\nDie Zahlung nach § 26 Abs. 1 erfolgt jährlich.“", + Stelle.LEER) + .orElseThrow()) + .teilbefehle(); + assertThat(teile) + .hasSize(2) + .extracting(t -> t.stelle().anzeigeText()) + .containsExactly("§ 1", "§ 2"); + } + + /** + * Nennt der Bereich mehr Paragraphen, als das Zitat trägt, so sagen Ankündigung und Wortlaut + * Verschiedenes; geraten wird nicht, der Befehl bleibt manuell zu prüfen. + */ + @Test + void paragraphBereichNeufassungMitFehlendemNormkopfBleibtUnerkannt() { + assertThat( + erkenne( + "Die §§ 52 bis 56 werden wie folgt gefasst: „§ 52 (weggefallen) § 53" + + " (weggefallen) § 54 (weggefallen)“.", + Stelle.LEER)) + .isEmpty(); + } + + /** + * „Die bisherigen §§ 9 bis 12 werden die §§ 8 bis 10.“ — ungleich viele Bezeichnungen auf beiden + * Seiten. Bei Bereichen ist das kein Widerspruch, sondern eine Lücke; die Zuordnung bleibt bis + * zur Anwendung offen. + */ + @Test + void ungleicherParagraphenBereichBleibtUngeteilt() { + var befehl = erkenne("Die bisherigen §§ 9 bis 12 werden die §§ 8 bis 10.", Stelle.LEER); + assertThat(befehl).get().isInstanceOf(Aenderungsbefehl.BereichsUmnummerierung.class); + var bereich = (Aenderungsbefehl.BereichsUmnummerierung) befehl.orElseThrow(); + assertThat(bereich.stelle().anzeigeText()).isEqualTo("§ 9"); + assertThat(bereich.bis().anzeigeText()).isEqualTo("§ 12"); + assertThat(bereich.neu().anzeigeText()).isEqualTo("§ 8"); + assertThat(bereich.neuBis().anzeigeText()).isEqualTo("§ 10"); + } + + /** Geht die Zählung auf, bleibt es bei der paarweisen Zuordnung. */ + @Test + void gleicherParagraphenBereichWirdSofortGeteilt() { + var teile = + ((Sammelbefehl) + erkenne("Die §§ 46 und 47 werden zu den §§ 34 und 35.", Stelle.LEER).orElseThrow()) + .teilbefehle(); + assertThat(teile) + .hasSize(2) + .extracting(t -> t.stelle().anzeigeText()) + .containsExactly("§ 46", "§ 47"); + } + @Test void wortlautWirdAbsatz() { var befehl = diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java index 4d78639..334fac3 100644 --- a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java +++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java @@ -315,4 +315,38 @@ class TextBereinigerTest { assertThat(TextBereiniger.bereinige("§ 19„\nAußerkrafttreten")) .isEqualTo("„§ 19\nAußerkrafttreten"); } + + /** + * Manche Gesetzblätter setzen gerade Anführungszeichen beidseitig (GVBl. für den Freistaat + * Thüringen). Führt ein Text kein einziges {@code „}, werden sie paarweise gelesen — sonst fände + * der Zitatextraktor dort kein Zitat. + */ + @Test + void geradeAnfuehrungszeichenWerdenPaarweiseGelesen() { + var bereinigt = + TextBereiniger.bereinige( + "In Satz 2 wird die Verweisung \"§ 27 Abs. 1, 3 und 5\" durch die Verweisung" + + " \"§ 27 Abs. 1\" ersetzt."); + assertThat(bereinigt) + .isEqualTo( + "In Satz 2 wird die Verweisung „§ 27 Abs. 1, 3 und 5“ durch die Verweisung" + + " „§ 27 Abs. 1“ ersetzt."); + } + + /** + * Führt der Text dagegen das deutsche öffnende Zeichen, so ist ein gerades ein Satz- oder + * Extraktionsfehler und schließend zu lesen — so steht es in BGBl und Drucksachen. + */ + @Test + void geradesZeichenNebenDeutschemBleibtSchliessend() { + var bereinigt = TextBereiniger.bereinige("Das Wort „alt\" wird durch das Wort „neu“ ersetzt."); + assertThat(bereinigt).isEqualTo("Das Wort „alt“ wird durch das Wort „neu“ ersetzt."); + } + + /** Bei ungerader Anzahl lässt sich nicht paaren; dann bleibt es bei der schließenden Lesart. */ + @Test + void ungeradeAnzahlGeraderZeichenWirdNichtGepaart() { + var bereinigt = TextBereiniger.bereinige("Die Angabe \"X\" wird durch \"Y gestrichen."); + assertThat(bereinigt).doesNotContain("„").contains("“"); + } } diff --git a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java index 212a037..094de7e 100644 --- a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java +++ b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java @@ -4,6 +4,7 @@ import static org.assertj.core.api.Assertions.assertThat; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Anfuegung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Aufhebung; +import eu.mulk.aendggner.aenderung.Aenderungsbefehl.BereichsUmnummerierung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Ebene; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Ersetzung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.GliederungsUeberschriften; @@ -1574,4 +1575,59 @@ class BefehlAnwenderTest { + " 3. die schriftliche Bestätigung.")), false))); } + + /** + * „Die bisherigen §§ 2 bis 4 werden die §§ 2 und 3.“ — der Bereich nennt drei Bezeichnungen, das + * Gesetz trägt darin aber nur zwei Einheiten: § 3 ist aufgehoben und zählt nicht mit. Erst am + * Gesetz steht die Zuordnung fest. + */ + @Test + void bereichsUmnummerierungUeberspringtAufgehobeneEinheiten() { + var gesetz = + new Gesetz( + "TestG", + "Gesetz zur Erprobung", + "Testgesetz", + List.of( + new Norm("§ 1", "Zweck", null, List.of(new Absatz(null, "Erprobung.")), false), + new Norm( + "§ 2", "Mittel", null, List.of(new Absatz(null, "Mittel sind frei.")), false), + new Norm("§ 3", "(aufgehoben)", null, List.of(), true), + new Norm( + "§ 4", "Schluss", null, List.of(new Absatz(null, "Tritt in Kraft.")), false)), + List.of()); + + var befehl = + new BereichsUmnummerierung( + stelle(new Stelle.Paragraph("2")), + stelle(new Stelle.Paragraph("4")), + stelle(new Stelle.Paragraph("2")), + stelle(new Stelle.Paragraph("3")), + PROV); + var ergebnis = BefehlAnwender.anwenden(gesetz, List.of(befehl)); + + assertThat(ergebnis.anzahlManuell()).isZero(); + assertThat(ergebnis.neu().normen()) + .extracting(Norm::enbez) + .containsExactly("§ 1", "§ 2", "§ 3"); + assertThat(ergebnis.neu().norm("§ 3").orElseThrow().titel()).isEqualTo("Schluss"); + } + + /** + * Trägt der Bereich mehr Einheiten, als der Befehl neue Bezeichnungen nennt, so ist er nicht + * auflösbar — dann wird nicht geraten, sondern gemeldet. + */ + @Test + void unaufloesbarerBereichBleibtManuell() { + var befehl = + new BereichsUmnummerierung( + stelle(new Stelle.Paragraph("1")), + stelle(new Stelle.Paragraph("3")), + stelle(new Stelle.Paragraph("1")), + stelle(new Stelle.Paragraph("1")), + PROV); + var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl)); + assertThat(ergebnis.anzahlManuell()).isEqualTo(1); + assertThat(ergebnis.protokoll().get(0).begruendung()).contains("nicht so viele Einheiten"); + } } |
