aboutsummaryrefslogtreecommitdiff
path: root/src/test
diff options
context:
space:
mode:
Diffstat (limited to 'src/test')
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java115
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java67
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java34
-rw-r--r--src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java56
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");
+ }
}