Support the two Lower Saxony insertion forms; NEFG now applies fully
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 <noreply@anthropic.com>
Change-Id: I0e2ea5b0389f8002157eeb8471800da51febe739
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 @@
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 @@
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 @@
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 @@
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 @@
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.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 @@
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 @@
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 @@
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