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.java48
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java25
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java19
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/ZitatExtraktorTest.java62
-rw-r--r--src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java85
5 files changed, 227 insertions, 12 deletions
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index b3bea08..c4ba894 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -4,10 +4,13 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl;
+import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturEinfuegung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.UnbekannterBefehl;
+import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortAnker;
import eu.mulk.aendggner.aenderung.parse.AenderungsgesetzParser;
import eu.mulk.aendggner.aenderung.parse.PatchTextExtraktor;
import eu.mulk.aendggner.aenderung.parse.TextBereiniger;
+import eu.mulk.aendggner.aenderung.parse.ZitatExtraktor;
import eu.mulk.aendggner.anwendung.BefehlAnwender;
import eu.mulk.aendggner.gesetz.Gesetz;
import eu.mulk.aendggner.gesetz.gii.GiiXmlLoader;
@@ -517,10 +520,12 @@ class EndToEndTest {
assertThat(parseErgebnis.befehle()).hasSize(4);
assertThat(parseErgebnis.befehle()).noneMatch(b -> b instanceof UnbekannterBefehl);
- // Im amtlichen Satz fehlt bei Artikel 2 Nr. 11 das schließende Anführungszeichen. Ohne die
- // Artikel-Grenze im ZitatExtraktor verschlänge dieses Zitat die Artikel 3 bis 5.
+ // Im amtlichen Satz fehlt bei Artikel 2 Nr. 11 das schließende Anführungszeichen. Ohne eine
+ // Strukturgrenze im ZitatExtraktor verschlänge dieses Zitat die Artikel 3 bis 5. Die Grenze
+ // greift seit der Aufzählungs-Grenze schon am nächsten Punkt desselben Artikels („12.“) und
+ // nicht mehr erst an der Überschrift „Artikel 3“ — Artikel 2 Nr. 12 bleibt damit erhalten.
assertThat(parseErgebnis.warnungen())
- .anyMatch(w -> w.startsWith("Zitat vor einer Artikel-Überschrift nicht geschlossen"));
+ .anyMatch(w -> w.startsWith("Zitat vor dem Aufzählungspunkt „12.“ nicht geschlossen"));
var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
assertThat(anwendung.anzahlAngewandt()).isEqualTo(4);
@@ -562,8 +567,10 @@ class EndToEndTest {
*
* <p>Kein voller Akzeptanztest: gesetze.berlin.de ist wie das schleswig-holsteinische Portal eine
* anmeldepflichtige juris-Anwendung, die Stammfassungen sind daraus nicht zu beschaffen. Artikel 1
- * ändert zudem eine *Anlage* mit eigener Nummern-Gliederung — ein Änderungsziel, das ÄndGgner
- * nicht modelliert (dieselbe Grenze, an der Baden-Württemberg zurückgestellt wurde).
+ * ändert zudem eine *Anlage*; anlagenbezogene Befehle wendet ÄndGgner an (siehe GEG), doch der
+ * {@link eu.mulk.aendggner.gesetz.land.LandesRechtTextParser} kennt nur „§“- und
+ * „Art.“-Normköpfe — eine handgepflegte Stammfassung könnte diese Anlage nicht tragen (dieselbe
+ * Grenze, an der Baden-Württemberg zurückgestellt wurde).
*/
@Test
void asogLafAendGBerlin() throws Exception {
@@ -612,12 +619,29 @@ class EndToEndTest {
"§ 67 Absatz 2",
"Anlage Nummer 6 Absatz 2",
"Anlage Nummer 23 Absatz 4a",
- "Anlage Nummer 23");
- // Bekannte Grenze: „Vor den Wörtern „…“ wird folgender Absatz 5 eingefügt“ — eine
- // Struktureinfügung, deren Position ein Wortanker statt einer Stelle bestimmt.
- assertThat(artikel1.befehle().stream().filter(b -> b instanceof UnbekannterBefehl).toList())
- .singleElement()
- .extracting(b -> b.provenienz().gliederungsPfad())
- .isEqualTo("2. b) bb)");
+ "Anlage Nummer 23",
+ "Anlage Nummer 23 Absatz 9",
+ "Anlage Nummer 31");
+ assertThat(artikel1.befehle()).noneMatch(b -> b instanceof UnbekannterBefehl);
+
+ // Nr. 2 b) bb) ist die Struktureinfügung mit Wortanker: „Vor den Wörtern „Aus dem Bereich
+ // Verkehr:“ wird folgender Absatz 5 eingefügt“ — die Position bestimmt der Wortanker, das Ziel
+ // erbt der Befehl aus dem Rahmen („Nummer 23“ der Anlage).
+ var bb =
+ artikel1.befehle().stream()
+ .filter(b -> b.provenienz().gliederungsPfad().equals("2. b) bb)"))
+ .findFirst()
+ .orElseThrow();
+ assertThat(bb).isInstanceOf(StrukturEinfuegung.class);
+ var einfuegung = (StrukturEinfuegung) bb;
+ assertThat(einfuegung.vorher()).isTrue();
+ assertThat(einfuegung.bezeichnung()).isEqualTo("5");
+ assertThat(einfuegung.anker())
+ .isEqualTo(new WortAnker.VorWoertern("Aus dem Bereich Verkehr:"));
+
+ // Bei diesem Punkt fehlt im amtlichen Satz das schließende Anführungszeichen. Ohne die Grenze
+ // am nächsten Aufzählungspunkt verschlänge das offene Zitat die Punkte cc) und c).
+ assertThat(ZitatExtraktor.extrahiere(text).warnungen())
+ .anyMatch(w -> w.startsWith("Zitat vor dem Aufzählungspunkt „cc)“ nicht geschlossen"));
}
}
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 5213030..95d15e1 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
@@ -162,6 +162,31 @@ class BefehlErkennerTest {
}
@Test
+ void erkenntStrukturEinfuegungMitWortanker() {
+ // Echter Befehl aus Artikel 1 Nr. 2 b) bb) des ASOG-/LAF-Änderungsgesetzes (GVBl. für Berlin
+ // 17/2026): Die Position der neuen Einheit bestimmt ein Wortanker, das Ziel erbt der Befehl aus
+ // dem Rahmen („Nummer 23“ der Anlage).
+ var kontext =
+ new Stelle(
+ List.of(new Stelle.Gliederungseinheit("Anlage", ""), new Stelle.NummerNr("23")));
+ var befehl =
+ erkenne(
+ "Vor den Wörtern „Aus dem Bereich Verkehr:“ wird folgender Absatz 5 eingefügt:"
+ + " „(5) die Identifizierung unerlaubt eingereister Ausländerinnen und Ausländer.“",
+ kontext);
+
+ assertThat(befehl).containsInstanceOf(StrukturEinfuegung.class);
+ var einfuegung = (StrukturEinfuegung) befehl.orElseThrow();
+ assertThat(einfuegung.stelle().anzeigeText()).isEqualTo("Anlage Nummer 23");
+ assertThat(einfuegung.vorher()).isTrue();
+ assertThat(einfuegung.ebene()).isEqualTo(Ebene.ABSATZ);
+ assertThat(einfuegung.bezeichnung()).isEqualTo("5");
+ assertThat(einfuegung.anker())
+ .isEqualTo(new WortAnker.VorWoertern("Aus dem Bereich Verkehr:"));
+ assertThat(einfuegung.text()).startsWith("(5) die Identifizierung");
+ }
+
+ @Test
void erkenntWoerterEinfuegung() {
var befehl =
erkenne(
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 5b7e850..284c506 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java
@@ -20,6 +20,25 @@ class TextBereinigerTest {
}
@Test
+ void setztZerlegteUmlauteKanonischZusammen() {
+ // Das GV.-NRW.-Heft 7/2026 kodiert einen Teil seiner Umlaute zerlegt; für die Befehlsmuster wäre
+ // „angefügt“ dann ein anderes Wort und dutzende Befehle könnten nie matchen.
+ // Der Umlaut steht hier bewusst zerlegt (u + U+0308), wie im Heft.
+ var roh = "Dem § 128 wird folgender Absatz 3 angefu\u0308gt:";
+
+ assertThat(TextBereiniger.bereinige(roh))
+ .isEqualTo("Dem § 128 wird folgender Absatz 3 angefügt:");
+ }
+
+ @Test
+ void erhaeltAmtlicheSatznummernBeiDerNormalisierung() {
+ // NFC, nicht NFKC: Letzteres plättete die amtlichen Satznummern zu gewöhnlichen Ziffern und
+ // nähme SatzTeiler und Superskript ihre Grundlage.
+ assertThat(TextBereiniger.bereinige("¹Erster Satz. ²Zweiter Satz."))
+ .isEqualTo("¹Erster Satz. ²Zweiter Satz.");
+ }
+
+ @Test
void ziehtSilbentrennungZusammen() {
assertThat(TextBereiniger.bereinige("die Bundes-\nregierung")).isEqualTo("die Bundesregierung");
}
diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/ZitatExtraktorTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/ZitatExtraktorTest.java
index 618d078..5b39857 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/ZitatExtraktorTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/ZitatExtraktorTest.java
@@ -94,6 +94,68 @@ class ZitatExtraktorTest {
}
@Test
+ void schliesstOffenesZitatVorDemNaechstenAufzaehlungspunkt() {
+ // GVBl. für Berlin 17/2026 Artikel 1 Nr. 2 b) bb): Am Ende des neu gefassten Absatzes fehlt das
+ // schließende Anführungszeichen, das Zitat verschlänge sonst die Punkte cc) und c).
+ var ergebnis =
+ ZitatExtraktor.extrahiere(
+ "Artikel 1\n"
+ + "bb) Vor den Wörtern „Aus dem Bereich Verkehr:“ wird folgender Absatz 5"
+ + " eingefügt:\n"
+ + "„(5) Die Sicherheitskontrolle.\n"
+ + "cc) Die bisherigen Absätze 5 bis 9 werden die Absätze 6 bis 10.\n"
+ + "c) Nummer 31 wird wie folgt gefasst:\n"
+ + "„Nummer 31\nNeuer Text.“\n");
+
+ // Zitat 0 ist der Wortanker, Zitat 1 der unvollständig zitierte neue Absatz, Zitat 2 die
+ // Neufassung aus Punkt c) — dieser Punkt bleibt also erhalten.
+ assertThat(ergebnis.zitate()).hasSize(3);
+ assertThat(ergebnis.zitat(1)).isEqualTo("(5) Die Sicherheitskontrolle.");
+ assertThat(ergebnis.zitat(2)).isEqualTo("Nummer 31\nNeuer Text.");
+ assertThat(ergebnis.text())
+ .contains("cc) Die bisherigen Absätze 5 bis 9 werden die Absätze 6 bis 10.")
+ .contains("c) Nummer 31 wird wie folgt gefasst:");
+ assertThat(ergebnis.warnungen()).hasSize(1);
+ assertThat(ergebnis.warnungen().get(0))
+ .startsWith("Zitat vor dem Aufzählungspunkt „cc)“ nicht geschlossen");
+ }
+
+ @Test
+ void raetNichtImAusbalanciertenAbschnitt() {
+ // Ein zitiertes Änderungsgesetz trägt selbst Befehlssprache in seinen Aufzählungspunkten (so in
+ // den bayerischen GVBl-Heften). Solange die Anführungszeichen des Artikels aufgehen, wird
+ // deshalb nicht geraten — das Zitat läuft über die Punkte hinweg.
+ var ergebnis =
+ ZitatExtraktor.extrahiere(
+ "Artikel 1\n"
+ + "2. § 5 wird wie folgt gefasst:\n"
+ + "„Die Verordnung wird wie folgt geändert:\n"
+ + "1. § 2 wird wie folgt geändert:\n"
+ + "2. § 3 wird aufgehoben.“\n");
+
+ assertThat(ergebnis.zitate()).hasSize(1);
+ assertThat(ergebnis.zitat(0)).contains("1. § 2 wird wie folgt geändert:");
+ assertThat(ergebnis.warnungen()).isEmpty();
+ }
+
+ @Test
+ void schliesstNichtAnTieferemAufzaehlungspunkt() {
+ // Der Marker der Folgezeile muss auf derselben oder einer flacheren Ebene liegen als der Punkt,
+ // auf dem das Zitat aufging: eine zitierte Untergliederung setzt das Zitat nicht ab.
+ var ergebnis =
+ ZitatExtraktor.extrahiere(
+ "Artikel 1\n"
+ + "2. § 5 wird wie folgt gefasst:\n"
+ + "„(1) Es gilt:\n"
+ + "aa) § 7 wird aufgehoben.\n");
+
+ assertThat(ergebnis.zitate()).hasSize(1);
+ assertThat(ergebnis.zitat(0)).contains("aa) § 7 wird aufgehoben.");
+ assertThat(ergebnis.warnungen()).hasSize(1);
+ assertThat(ergebnis.warnungen().get(0)).contains("offen");
+ }
+
+ @Test
void stelltZitateWiederHer() {
var original = "Das Wort „alt“ wird durch das Wort „neu“ ersetzt.";
var ergebnis = ZitatExtraktor.extrahiere(original);
diff --git a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
index a8f3daa..2706ac8 100644
--- a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
+++ b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
@@ -219,6 +219,91 @@ class BefehlAnwenderTest {
}
@Test
+ void fuegtEinheitVorWortankerEin() {
+ // „Vor den Wörtern „…“ wird folgende Nummer 1a eingefügt: „…““ — die Position bestimmt der
+ // Wortanker, nicht die Struktur (GVBl. für Berlin 17/2026, Artikel 1 Nr. 2 b) bb)).
+ var befehl =
+ new StrukturEinfuegung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("2")),
+ true,
+ Ebene.NUMMER,
+ "1a",
+ "1a. die Prüfung von Zitaten,",
+ new WortAnker.VorWoertern("die Anwendung von Befehlen"),
+ PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl));
+
+ assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT);
+ assertThat(absatzText(ergebnis.neu(), "§ 1", 1))
+ .isEqualTo(
+ "Die Erprobung umfasst\n"
+ + " 1. das Einlesen von Gesetzen,\n"
+ + " 1a. die Prüfung von Zitaten,\n"
+ + " 2. die Anwendung von Befehlen und\n"
+ + " 3. die Ausgabe von Synopsen.");
+ }
+
+ @Test
+ void fuegtEinheitNachWortankerEin() {
+ var befehl =
+ new StrukturEinfuegung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("2")),
+ false,
+ Ebene.NUMMER,
+ "1a",
+ "1a. die Prüfung von Zitaten,",
+ new WortAnker.NachWoertern("das Einlesen von Gesetzen"),
+ 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(" 1. das Einlesen von Gesetzen,\n 1a. die Prüfung von Zitaten,\n 2. die"
+ + " Anwendung von Befehlen und");
+ }
+
+ @Test
+ void meldetFehlendenWortankerEinerEinfuegung() {
+ var befehl =
+ new StrukturEinfuegung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("2")),
+ true,
+ Ebene.NUMMER,
+ "1a",
+ "1a. die Prüfung von Zitaten,",
+ new WortAnker.VorWoertern("gibt es nicht"),
+ PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl));
+
+ var eintrag = ergebnis.protokoll().get(0);
+ assertThat(eintrag.status()).isEqualTo(Status.MANUELL_PRUEFEN);
+ assertThat(eintrag.begruendung()).contains("kommt im Zieltext nicht vor");
+ }
+
+ @Test
+ void meldetMehrdeutigenWortankerEinerEinfuegung() {
+ // „Prüfung“ steht zweimal im Wortlaut des § 2 — die Position wäre nicht bestimmt.
+ var befehl =
+ new StrukturEinfuegung(
+ stelle(new Stelle.Paragraph("2")),
+ true,
+ Ebene.SATZ,
+ null,
+ "Die Prüfung ist zu dokumentieren.",
+ new WortAnker.VorWoertern("Prüfung"),
+ PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl));
+
+ var eintrag = ergebnis.protokoll().get(0);
+ assertThat(eintrag.status()).isEqualTo(Status.MANUELL_PRUEFEN);
+ assertThat(eintrag.begruendung()).contains("mehrdeutig");
+ }
+
+ @Test
void fuegtWoerterNachAnkerEin() {
var befehl =
new WoerterEinfuegung(