aboutsummaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-07-25 10:35:45 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-07-25 10:35:45 +0200
commit83ba9f0db3462a4c2d5122ba836f646b055156ca (patch)
tree82b786836898972cf1291f2b8739098f762e3baf /src/main/java
parent35e90f4ab0f9b40d4752c9b8cba57c9452a8189f (diff)
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
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java19
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java10
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java53
3 files changed, 75 insertions, 7 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;