diff options
Diffstat (limited to 'src')
8 files changed, 167 insertions, 25 deletions
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java index fb578cc..80206f6 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java @@ -200,7 +200,19 @@ final class BefehlErkenner { private static final Pattern STRUKTUR_EINFUEGUNG = Pattern.compile( - "^(Nach|Vor) (.+?) (?:wird|werden) (?:der |die |das )?folgende[nrs]? (.+?) " + "^(Nach|Vor) (.+?) (?:wird|werden) (?:der |die |das )?folgende[nrs]? (?:neue[nrs]? )?(.+?) " + + "(?:ein|an)gefügt: " + + ENUM + + Z + + "\\.?$"); + + // „In Kapitel 4 wird nach § 12 der folgende neue § 13 angefügt: „…““ — gliederungsbezogene + // Einfügung. Die Gliederungsangabe nennt nur den Abschnitt, in dem die neue Einheit landet; + // maßgeblich für die Position ist der Anker („nach § 12“), der ohnehin eindeutig ist. + private static final Pattern STRUKTUR_EINFUEGUNG_IN_GLIEDERUNG = + Pattern.compile( + "^In (?:Buch|Teil|Kapitel|Abschnitt|Unterabschnitt|Titel) \\S+ (?:wird|werden) " + + "(?i:(nach|vor)) (.+?) (?:der |die |das )?folgende[nrs]? (?:neue[nrs]? )?(.+?) " + "(?:ein|an)gefügt: " + ENUM + Z @@ -897,8 +909,9 @@ final class BefehlErkenner { provenienz)); } - if ((m = STRUKTUR_EINFUEGUNG.matcher(text)).matches()) { - var vorher = m.group(1).equals("Vor"); + if ((m = STRUKTUR_EINFUEGUNG.matcher(text)).matches() + || (m = STRUKTUR_EINFUEGUNG_IN_GLIEDERUNG.matcher(text)).matches()) { + var vorher = m.group(1).equalsIgnoreCase("vor"); var stelle = StellenParser.parse(m.group(2)); if (stelle.isEmpty()) { return Optional.empty(); diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java index 7460b15..f1b75e1 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java @@ -190,6 +190,13 @@ public final class TextBereiniger { private static final Pattern INVERTIERTES_LISTEN_ZITAT = Pattern.compile("(?m)^(\\s*)(\\d+[a-z]?\\.|[a-z]{1,3}\\))[ \\t]+„[ \\t]+"); + // Sachnummern mit Leerzeichen: Niedersachsen zitiert eingeschobene Paragraphen als „§ 2 a“, der + // Rest der Rechtssprache als „§ 2a“. Auf die kanonische Form ziehen, damit Stellen- und + // Ebenenparser greifen. Ein folgendes „)“ oder „.“ schließt Aufzählungsmarker des + // Änderungsgesetzes aus („… nach § 8 c) In Absatz 2 …“). + private static final Pattern SACHNUMMER_MIT_LEERZEICHEN = + Pattern.compile("(§|Art\\.) (\\d+) ([a-z])(?![a-zäöüß).])"); + private TextBereiniger() {} public static String bereinige(String rohText) { @@ -206,7 +213,8 @@ public final class TextBereiniger { var verbunden = verbindeUmbrueche(zeilen); // Falsch-positive markerlose Zusammenzüge („durch“ + „die“ → „durchdie“) reparieren — die // Befehlsvokabeln sind nie Kompositum-Bestandteile. - return loeseArtikelFussnoten(trenneVerklebteZitatgrenzen(reflowUndStrippe(verbunden))); + var ergebnis = loeseArtikelFussnoten(trenneVerklebteZitatgrenzen(reflowUndStrippe(verbunden))); + return SACHNUMMER_MIT_LEERZEICHEN.matcher(ergebnis).replaceAll("$1 $2$3"); } /** diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java index b6bee1b..b2e855d 100644 --- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java +++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java @@ -2,6 +2,7 @@ package eu.mulk.aendggner.anwendung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Anfuegung; +import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Ebene; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Aufhebung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Ersetzung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.FussnotenAufhebung; @@ -26,6 +27,7 @@ import eu.mulk.aendggner.gesetz.Gliederung; import eu.mulk.aendggner.gesetz.Norm; import eu.mulk.aendggner.gesetz.Superskript; import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -71,16 +73,20 @@ public final class BefehlAnwender { var protokoll = new ArrayList<AngewandteAenderung>(); String neuerLangtitel = null; - for (var befehl : befehle) { + // Angewandt wird in Sachreihenfolge, protokolliert in der Reihenfolge des Änderungsgesetzes. + var ergebnisse = new AngewandteAenderung[befehle.size()]; + for (int index : anwendungsReihenfolge(befehle)) { + var befehl = befehle.get(index); // „Die Überschrift wird wie folgt gefasst / durch die folgende Überschrift ersetzt“ auf // oberster Ebene meint die Überschrift des Gesetzes selbst. if (befehl instanceof Neufassung n && istNurUeberschrift(n.stelle())) { neuerLangtitel = n.neuerText().replaceAll("\\s+", " ").strip(); - protokoll.add(angewandt(befehl, "(Gesetzesüberschrift)")); + ergebnisse[index] = angewandt(befehl, "(Gesetzesüberschrift)"); continue; } - protokoll.add(wendeAn(normen, gliederungen, befehl)); + ergebnisse[index] = wendeAn(normen, gliederungen, befehl); } + protokoll.addAll(Arrays.asList(ergebnisse)); var neu = alt.mitNormen(normen).mitGliederungen(gliederungen); if (neuerLangtitel != null) { @@ -89,6 +95,47 @@ public final class BefehlAnwender { return new AnwendungsErgebnis(neu, protokoll); } + /** + * Reihenfolge, in der die Befehle anzuwenden sind: grundsätzlich die des Änderungsgesetzes. + * + * <p>Eine Umnummerierung „Der bisherige § 13 wird § 14“ wird jedoch vor eine vorangehende + * Einfügung gezogen, die denselben Paragraphen neu besetzt („In Kapitel 4 wird nach § 12 der + * folgende neue § 13 angefügt“). „Bisherig“ bezeichnet den Stand vor der Änderung; die + * Umnummerierung geht der Neubesetzung sachlich also voraus. In der Textreihenfolge angewandt + * liefen beide Befehle dagegen auf zwei Paragraphen gleicher Bezeichnung hinaus. + */ + private static List<Integer> anwendungsReihenfolge(List<Aenderungsbefehl> befehle) { + var reihenfolge = new ArrayList<Integer>(befehle.size()); + for (int i = 0; i < befehle.size(); i++) { + reihenfolge.add(i); + } + for (int j = 0; j < befehle.size(); j++) { + if (!(befehle.get(j) instanceof Umnummerierung u) || !nurParagraph(u.stelle())) { + continue; + } + var quelle = u.stelle().paragraph().get().enbez(); + for (int i = 0; i < j; i++) { + if (quelle.equals(neuerParagraph(befehle.get(i)))) { + reihenfolge.remove(Integer.valueOf(j)); + reihenfolge.add(reihenfolge.indexOf(i), j); + break; + } + } + } + return reihenfolge; + } + + /** Bezeichnung des Paragraphen, den {@code befehl} neu anlegt — sonst {@code null}. */ + private static @Nullable String neuerParagraph(Aenderungsbefehl befehl) { + if (!(befehl instanceof StrukturEinfuegung s) + || s.ebene() != Ebene.PARAGRAPH + || s.bezeichnung() == null) { + return null; + } + var sigel = s.stelle().paragraph().map(Stelle.Paragraph::sigel).orElse("§"); + return sigel + " " + s.bezeichnung(); + } + private static boolean istNurUeberschrift(Stelle stelle) { return stelle.komponenten().size() == 1 && stelle.komponenten().get(0) instanceof Stelle.Ueberschrift; diff --git a/src/main/resources/sampledata/Landesrecht-Beispiele.adoc b/src/main/resources/sampledata/Landesrecht-Beispiele.adoc index 1d831ac..52ab028 100644 --- a/src/main/resources/sampledata/Landesrecht-Beispiele.adoc +++ b/src/main/resources/sampledata/Landesrecht-Beispiele.adoc @@ -88,9 +88,9 @@ folgt geändert"). der archivierten revosax-Volltextfassung abgeleitet) und End-to-End-Akzeptanztest (`EndToEndTest.saechsBeamtVgAcceptance`; fünf Befehle, kein Rest). * *Niedersachsen* ist umgesetzt: `Niedersachsen/NEFG-alt.txt` (seit 2022 unveränderter NEFG, aus - dem zweispaltigen Nds. GVBl 2022 Nr. 33) und `EndToEndTest.nefgAcceptance` (fünf von sieben - Befehlen angewandt; zwei Einfügeformen — Sachnummer mit Leerzeichen „§ 2 a“, kapitelbezogene - §-Block-Einfügung — noch nicht unterstützt, als Grenze dokumentiert). + dem zweispaltigen Nds. GVBl 2022 Nr. 33) und `EndToEndTest.nefgAcceptance` (alle sieben Befehle + angewandt, kein Rest — einschließlich der beiden niedersächsischen Einfügeformen: Sachnummer mit + Leerzeichen „§ 2 a“ und gliederungsbezogene Einfügung „In Kapitel 4 wird nach § 12 …“). * *Schleswig-Holstein* ist bis einschließlich der Befehlserkennung umgesetzt (`EndToEndTest.kommunalrechtAendGSchleswigHolstein`): Artikel 1 trifft die Gemeindeordnung, Artikel 2 die Kreisordnung, je ein Neufassungsbefehl, kein unbekannter Befehl. Ein voller diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java index b89a367..e7f7a00 100644 --- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java +++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java @@ -402,21 +402,18 @@ class EndToEndTest { assertThat(parseErgebnis.artikel()).containsExactly("1"); assertThat(parseErgebnis.befehle()).hasSize(7); - // Fünf der sieben Befehle werden angewandt. Zwei bleiben als „manuell prüfen“ stehen — zwei - // Einfügeformen, die ÄndGgner noch nicht beherrscht (bewusst dokumentierte Grenze, nicht - // stillschweigend verworfen): - // * Nr. 3 „Nach § 2 wird der folgende § 2 a eingefügt“ — Einfügung eines §-Blocks mit einer - // durch Leerzeichen getrennten Sachnummer („§ 2 a“). - // * Nr. 5 „In Kapitel 4 wird nach § 12 der folgende neue § 13 angefügt“ — kapitelbezogene - // §-Block-Einfügung. + // Alle sieben Befehle werden angewandt, darunter die beiden niedersächsischen Einfügeformen: + // * Nr. 3 „Nach § 2 wird der folgende § 2 a eingefügt“ — Sachnummer mit Leerzeichen. + // * Nr. 5 „In Kapitel 4 wird nach § 12 der folgende neue § 13 angefügt“ — gliederungs- + // bezogene Einfügung, deren Zielbezeichnung erst der nachfolgende Befehl Nr. 6 freimacht. var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle()); - assertThat(anwendung.anzahlAngewandt()).isEqualTo(5); - var manuellPfade = - anwendung.protokoll().stream() - .filter(a -> a.status() == BefehlAnwender.Status.MANUELL_PRUEFEN) - .map(a -> a.befehl().provenienz().gliederungsPfad()) - .toList(); - assertThat(manuellPfade).containsExactlyInAnyOrder("3.", "5."); + assertThat(anwendung.anzahlAngewandt()).isEqualTo(7); + assertThat(anwendung.protokoll()) + .noneMatch(a -> a.status() == BefehlAnwender.Status.MANUELL_PRUEFEN); + // Protokolliert wird trotz vorgezogener Umnummerierung in der Reihenfolge des Gesetzes. + assertThat(anwendung.protokoll()) + .extracting(a -> a.befehl().provenienz().gliederungsPfad()) + .containsExactly("1. a)", "1. b)", "2.", "3.", "4.", "5.", "6."); // Stichproben: „erhält folgende Fassung“ (§ 1 Abs. 1, § 2), Angaben-Ersetzung (§ 1 Abs. 5), // Wörter-Einfügung (§ 6 Abs. 1) und §-Umnummerierung (§ 13 → § 14). @@ -426,7 +423,18 @@ class EndToEndTest { assertThat(neu.norm("§ 1").orElseThrow().absaetze().get(4).text()).contains("§ 14 Abs. 3"); assertThat(neu.norm("§ 2").orElseThrow().titel()).isEqualTo("Registriernummer"); assertThat(neu.norm("§ 6").orElseThrow().gesamtText()).contains("8 bis 10"); - assertThat(neu.norm("§ 14")).isPresent(); + + // Die beiden neuen Paragraphen stehen an der richtigen Stelle, der bisherige § 13 ist § 14. + assertThat(neu.normen()).hasSize(15); + assertThat(neu.normen()) + .extracting(n -> n.enbez()) + .containsSubsequence("§ 2", "§ 2a", "§ 3") + .containsSubsequence("§ 12", "§ 13", "§ 14"); + assertThat(neu.norm("§ 2a").orElseThrow().titel()) + .isEqualTo("Anwendung bundesrechtlicher Vorschriften"); + assertThat(neu.norm("§ 13").orElseThrow().titel()) + .isEqualTo("Entbehrlichkeit von Vergabeverfahren im Unterschwellenbereich"); + assertThat(neu.norm("§ 14").orElseThrow().titel()).isEqualTo("Verordnungsermächtigungen"); var synopse = SynopseBuilder.baue(gesetz, anwendung, parseErgebnis.warnungen(), false); assertThat(HtmlRenderer.rendere(synopse, "E2E-Test NEFG")).contains("§ 1"); 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 d89c898..5213030 100644 --- a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java +++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java @@ -126,6 +126,27 @@ class BefehlErkennerTest { } @Test + void erkenntGliederungsbezogeneParagraphEinfuegung() { + // Echter Befehl aus Art. 1 Nr. 5 des Änderungsgesetzes zum NEFG (Nds. GVBl. 2026 Nr. 10). Die + // Kapitelangabe nennt nur den Abschnitt, in dem der neue Paragraph landet; maßgeblich für die + // Position ist der Anker „nach § 12“. + var befehl = + erkenne( + "In Kapitel 4 wird nach § 12 der folgende neue § 13 angefügt: „§ 13 Entbehrlichkeit" + + " von Vergabeverfahren im Unterschwellenbereich Das Gesetz ist nicht" + + " anzuwenden.“", + Stelle.LEER); + + assertThat(befehl).containsInstanceOf(StrukturEinfuegung.class); + var einfuegung = (StrukturEinfuegung) befehl.orElseThrow(); + assertThat(einfuegung.stelle().anzeigeText()).isEqualTo("§ 12"); + assertThat(einfuegung.vorher()).isFalse(); + assertThat(einfuegung.ebene()).isEqualTo(Ebene.PARAGRAPH); + assertThat(einfuegung.bezeichnung()).isEqualTo("13"); + assertThat(einfuegung.text()).startsWith("§ 13 Entbehrlichkeit"); + } + + @Test void erkenntSatzEinfuegung() { 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 f49e41b..4da5c79 100644 --- a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java +++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java @@ -224,6 +224,18 @@ class TextBereinigerTest { } @Test + void ziehtSachnummernMitLeerzeichenZusammen() { + // Niedersachsen zitiert eingeschobene Paragraphen als „§ 2 a“; kanonisch ist „§ 2a“. + assertThat(TextBereiniger.bereinige("Nach § 2 wird der folgende § 2 a eingefügt:")) + .isEqualTo("Nach § 2 wird der folgende § 2a eingefügt:"); + assertThat(TextBereiniger.bereinige("Art. 28 a Abs. 1")).isEqualTo("Art. 28a Abs. 1"); + // Aufzählungsmarker des Änderungsgesetzes bleiben unberührt, ebenso echte Folgewörter. + assertThat(TextBereiniger.bereinige("Nach § 8 c) In Absatz 2")) + .isEqualTo("Nach § 8 c) In Absatz 2"); + assertThat(TextBereiniger.bereinige("§ 5 des Gesetzes")).isEqualTo("§ 5 des Gesetzes"); + } + + @Test void entferntSeitenkopfDesGvoblSchleswigHolstein() { var roh = """ diff --git a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java index a23f444..a8f3daa 100644 --- a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java +++ b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java @@ -1134,4 +1134,37 @@ class BefehlAnwenderTest { assertThat(absatzText(ergebnis.neu(), "Art. 2", 0)) .isEqualTo("Erprobung ist die Prüfung der Tauglichkeit; sie schließt mit einem Bericht."); } + + @Test + void ziehtUmnummerierungVorDieNeubesetzendeEinfuegung() { + // „Nach § 2 wird der folgende neue § 3 eingefügt“ + „Der bisherige § 3 wird § 4“: In der + // Textreihenfolge angewandt kollidierten beide Befehle auf „§ 3“. Die Umnummerierung + // beschreibt den Stand vor der Änderung und geht der Neubesetzung sachlich voraus. + var einfuegung = + new StrukturEinfuegung( + stelle(new Stelle.Paragraph("2")), + false, + Ebene.PARAGRAPH, + "3", + "§ 3 Zwischennorm (1) Der neue Text.", + PROV); + var umnummerierung = + new Umnummerierung( + stelle(new Stelle.Paragraph("3")), + stelle(new Stelle.Paragraph("4")), + new Provenienz("1", "2.", "(Test)")); + + var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(einfuegung, umnummerierung)); + + assertThat(ergebnis.anzahlAngewandt()).isEqualTo(2); + // Protokolliert wird weiterhin in der Reihenfolge des Änderungsgesetzes. + assertThat(ergebnis.protokoll()) + .extracting(a -> a.befehl().provenienz().gliederungsPfad()) + .containsExactly("1.", "2."); + assertThat(ergebnis.neu().normen()) + .extracting(n -> n.enbez()) + .containsExactly("§ 1", "§ 2", "§ 3", "§ 4"); + assertThat(ergebnis.neu().norm("§ 3").orElseThrow().titel()).isEqualTo("Zwischennorm"); + assertThat(ergebnis.neu().norm("§ 4").orElseThrow().titel()).isEqualTo("Schlussvorschriften"); + } } |
