aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
-rw-r--r--src/main/resources/sampledata/Landesrecht-Beispiele.adoc6
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java38
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java21
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java12
-rw-r--r--src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java33
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");
+ }
}