From 83ba9f0db3462a4c2d5122ba836f646b055156ca Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Sat, 25 Jul 2026 10:35:45 +0200 Subject: Support the two Lower Saxony insertion forms; NEFG now applies fully MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The NEFG acceptance case left two commands as "check manually". Both are now supported, so Lower Saxony applies 7 of 7 with no remainder. * Lower Saxony cites inserted paragraphs with a space before the sub number ("§ 2 a"); the rest of German legal usage writes "§ 2a", which is what the Stelle and level parsers expect. TextBereiniger now pulls the form together, leaving enumeration markers of the amendment act alone ("… nach § 8 c) In Absatz 2 …"). * "In Kapitel 4 wird nach § 12 der folgende neue § 13 angefügt" — the division only names the section the new paragraph lands in; the anchor governs the position, as in the plain form. The optional "neue" is now accepted in both. Applying that insertion needed one more thing: the paragraph it creates is only vacated by the *following* command ("Der bisherige § 13 wird § 14"), so in document order the two collided and the insertion was rejected. "Bisherig" denotes the state before the amendment, so the renumbering logically precedes the reoccupation and is now pulled ahead. The log still lists commands in the order of the amendment act. 226 tests green; all other pinned figures unchanged. Co-Authored-By: Claude Opus 5 Change-Id: I0e2ea5b0389f8002157eeb8471800da51febe739 --- .../aendggner/aenderung/parse/BefehlErkenner.java | 19 ++++++-- .../aendggner/aenderung/parse/TextBereiniger.java | 10 +++- .../mulk/aendggner/anwendung/BefehlAnwender.java | 53 ++++++++++++++++++++-- .../sampledata/Landesrecht-Beispiele.adoc | 6 +-- 4 files changed, 78 insertions(+), 10 deletions(-) (limited to 'src/main') 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(); 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. + * + *

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 anwendungsReihenfolge(List befehle) { + var reihenfolge = new ArrayList(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 -- cgit v1.2.1