Generalize Bavarian loader to a shared Landesrecht loader
Prepare wave six (Landesrecht of the remaining Länder) by lifting the
Bavaria-specific stem-law loader to a shared one. Unlike Bavaria (Art./§
inversion), the other Länder structure their stem laws in §, so the loader must
handle both sigils.
- Move gesetz/bayern/{BayRechtLoader,BayRechtTextParser} to
gesetz/land/{LandesRechtLoader,LandesRechtTextParser}; the norm head now
matches "§ N" as well as "Art. N" and derives the sigil per norm from the
match. Cross-reference keywords in the norm head are excluded only as whole
words (so a title "Satzungen" no longer trips on "Satz"), and the juris
abbreviation may be multi-token ("(GO NRW)").
- Derive the superscript mode data-drivenly from the loaded stem law
(Superskript.traegtSatznummern) instead of from "is it gii-XML": Bavaria and
Lower Saxony keep their amtliche Satznummern, the Bund and Länder without
official sentence numbering drop them.
- Recognize the neufassung idiom "erhält/erhalten folgende Fassung" (Schleswig-
Holstein, Niedersachsen) via a NEUFASSUNG_VERB building block, additive to
"wird/werden wie folgt gefasst".
Federal and Bavarian behaviour is unchanged (219 tests green, pinned acceptance
numbers UWG 19/0, GEG 66/53, GEG-GModG 90/9, IfSG 42/24, BayJG 149/154 hold).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Change-Id: I55a70a7932bf657a2346ca70f3fa05e173bf80ad
diff --git a/FASSUNGEN.txt b/FASSUNGEN.txt
index a1361f4..52a4215 100644
--- a/FASSUNGEN.txt
+++ b/FASSUNGEN.txt
@@ -21,6 +21,59 @@
════════════════════════════════════════════════════════════════════════════════
+ Fassung vom 19. Juli 2026,
+ zuletzt geändert durch die am 19. Juli 2026 vorgenommenen Änderungen
+════════════════════════════════════════════════════════════════════════════════
+
+In einer sechsten Ertüchtigungswelle wird das Erzeugnis über das bayerische
+Landesrecht hinaus für das Landesrecht der übrigen Länder vorbereitet. Da diese
+ihre Stammgesetze — anders als Bayern — in Paragraphen gliedern, wird der
+bayerische Lader zu einem gemeinsamen Landesrecht-Lader verallgemeinert, der das
+Sigel (§ oder Art.) je Norm aus dem Text ableitet; der Superskript-Modus folgt
+nunmehr datengetrieben der Schreibweise des Stammgesetzes. Ferner wird die in
+mehreren Ländern übliche Neufassungsform „erhält folgende Fassung“ erkannt. Als
+Beispieldaten sind bereits Änderungsgesetze aus acht Ländern hinterlegt
+(sampledata: Berlin, Schleswig-Holstein, Hessen, Niedersachsen, Baden-Württem-
+berg, Nordrhein-Westfalen, Sachsen, Thüringen); ihre konsolidierten Stammfassungen
+und die dazugehörigen Akzeptanzfälle folgen in weiteren Fassungen dieser Welle.
+
+
+Artikel 1
+Verallgemeinerung des bayerischen Laders zu einem Landesrecht-Lader
+
+Die Klassen BayRechtLoader und BayRechtTextParser (Paket gesetz.bayern) werden zu
+LandesRechtLoader und LandesRechtTextParser (Paket gesetz.land) verallgemeinert.
+Der Normkopf erkennt statt allein „Art. N“ nunmehr „§ N“ wie „Art. N“ und leitet
+das Sigel je Norm aus dem Treffer ab (Norm.enbez), sodass die Gliederung des
+jeweiligen Landes ohne ein eigenes Land-Merkmal folgt. Das Querverweis-
+Schlüsselwort im Normkopf wird nur noch als ganzes Wort ausgeschlossen, damit ein
+Normtitel wie „Satzungen“ nicht am Verweis-Wort „Satz“ scheitert; die
+Juris-Abkürzung darf mehrteilig sein („(GO NRW)“). Das bayerische Verhalten
+bleibt unverändert.
+
+
+Artikel 2
+Datengetriebener Superskript-Modus aus der Stammfassung
+
+Ob ein Änderungsgesetz mit Erhalt der hochgestellten amtlichen Satznummern
+extrahiert wird, richtet sich nicht mehr danach, ob das Stammgesetz als gii-XML
+vorliegt, sondern danach, ob die geladene Stammfassung selbst amtliche Satznummern
+am Satzanfang trägt (Superskript.traegtSatznummern). Damit erhalten das bayerische
+Landesrecht und das niedersächsische Recht ihre Superskripte, das Bundesrecht und
+die Länder ohne amtliche Satzzählung verwerfen sie wie bisher.
+
+
+Artikel 3
+Erkennung der Neufassungsform „erhält folgende Fassung“
+
+Der Befehlserkenner erkennt die Neufassung nunmehr auch in der in Schleswig-
+Holstein und Niedersachsen üblichen Form „… erhält (erhalten) folgende Fassung:“,
+neben der bisherigen Form „… wird (werden) wie folgt gefasst:“. Die Erweiterung
+ist rein zusätzlich (Verb-Baustein NEUFASSUNG_VERB) und lässt die Muster des
+Bundes- und des bayerischen Rechts unberührt.
+
+
+════════════════════════════════════════════════════════════════════════════════
Fassung vom 17. Juli 2026,
zuletzt geändert durch die am 17. Juli 2026 vorgenommenen Änderungen
════════════════════════════════════════════════════════════════════════════════
diff --git a/src/main/java/eu/mulk/aendggner/AendGgner.java b/src/main/java/eu/mulk/aendggner/AendGgner.java
index e647b75..efa95a9 100644
--- a/src/main/java/eu/mulk/aendggner/AendGgner.java
+++ b/src/main/java/eu/mulk/aendggner/AendGgner.java
@@ -6,8 +6,9 @@
import eu.mulk.aendggner.aenderung.parse.TextBereiniger;
import eu.mulk.aendggner.anwendung.BefehlAnwender;
import eu.mulk.aendggner.gesetz.Gesetz;
-import eu.mulk.aendggner.gesetz.bayern.BayRechtLoader;
+import eu.mulk.aendggner.gesetz.Superskript;
import eu.mulk.aendggner.gesetz.gii.GiiXmlLoader;
+import eu.mulk.aendggner.gesetz.land.LandesRechtLoader;
import eu.mulk.aendggner.synopse.HtmlRenderer;
import eu.mulk.aendggner.synopse.SynopseBuilder;
import java.io.IOException;
@@ -125,7 +126,8 @@
}
if (extractOnly) {
- var extraktor = new PatchTextExtraktor(superskriptModus());
+ var gesetz = ladeStammgesetz();
+ var extraktor = new PatchTextExtraktor(superskriptModus(gesetz));
for (var file : patches) {
var text = extraktor.extrahiere(file);
System.out.println(raw ? text : TextBereiniger.bereinige(text));
@@ -135,7 +137,7 @@
if (dumpBefehle) {
var gesetz = ladeStammgesetz();
- var extraktor = new PatchTextExtraktor(superskriptModus());
+ var extraktor = new PatchTextExtraktor(superskriptModus(gesetz));
var parser = new AenderungsgesetzParser();
for (var file : patches) {
var text = TextBereiniger.bereinige(extraktor.extrahiere(file));
@@ -177,7 +179,7 @@
// Pipeline: Stammgesetz laden → Befehle parsen → anwenden → Synopse rendern.
var altesGesetz = ladeStammgesetz();
- var extraktor = new PatchTextExtraktor(superskriptModus());
+ var extraktor = new PatchTextExtraktor(superskriptModus(altesGesetz));
var parser = new AenderungsgesetzParser();
var gesetz = altesGesetz;
@@ -222,16 +224,27 @@
return gesamtErgebnis.anzahlAngewandt() == 0 && !protokoll.isEmpty() ? 2 : 0;
}
- /** Gii-XML → {@link GiiXmlLoader}; PDF/Klartext (bayerisches Landesrecht) → {@link
- * BayRechtLoader}. */
+ /** Gii-XML → {@link GiiXmlLoader}; PDF/Klartext (Landesrecht) → {@link LandesRechtLoader}. */
private Gesetz ladeStammgesetz() throws Exception {
- return istGiiXml() ? new GiiXmlLoader().load(baseFile) : new BayRechtLoader().load(baseFile);
+ return istGiiXml() ? new GiiXmlLoader().load(baseFile) : new LandesRechtLoader().load(baseFile);
}
- /** Bayerische Stammtexte tragen amtliche Satznummern — auch die Änderungsgesetze werden dann
- * mit Superskript-Erhalt extrahiert, damit Zitate und Stammtext dieselbe Schreibweise tragen. */
- private SuperskriptModus superskriptModus() throws IOException {
- return istGiiXml() ? SuperskriptModus.ENTFERNEN : SuperskriptModus.BEHALTEN;
+ /**
+ * Der Superskriptmodus folgt der Schreibweise des Stammgesetzes: Trägt es amtliche Satznummern
+ * (bayerisches Landesrecht, Niedersachsen u.a.), werden auch die Änderungsgesetze mit
+ * Superskript-Erhalt extrahiert, damit Zitate und Stammtext dieselbe Schreibweise tragen; sonst
+ * (Bundesrecht, Länder ohne amtliche Satzzählung) sind hochgestellte Ziffern bloße
+ * Fußnotenmarker und werden verworfen.
+ */
+ private SuperskriptModus superskriptModus(Gesetz gesetz) {
+ for (var norm : gesetz.normen()) {
+ for (var absatz : norm.absaetze()) {
+ if (Superskript.traegtSatznummern(absatz.text())) {
+ return SuperskriptModus.BEHALTEN;
+ }
+ }
+ }
+ return SuperskriptModus.ENTFERNEN;
}
private boolean istGiiXml() throws IOException {
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 edbe742..fb578cc 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
@@ -50,8 +50,14 @@
// Aufzählungslabel, das in Entwürfen/Drucksachen vor dem Zitat steht („… gefasst: 3. „…““).
private static final String ENUM = "((?:\\d+[a-z]?\\.|[a-z]{1,3}\\))\\s*)?";
+ // Verb der Neufassung. Neben „wird/werden wie folgt gefasst“ (Bund/Bayern) auch „erhält/erhalten
+ // folgende Fassung“ — die in mehreren Ländern (Schleswig-Holstein, Niedersachsen) übliche Form.
+ // Rein zusätzliche Alternation ohne eigene Fanggruppe, damit die Gruppennummern gleich bleiben.
+ private static final String NEUFASSUNG_VERB =
+ "(?:(?:wird|werden) wie folgt gefasst|(?:erhält|erhalten) folgende Fassung)";
+
private static final Pattern NEUFASSUNG =
- Pattern.compile("^(.+?) (?:wird|werden) wie folgt gefasst: " + ENUM + Z + "\\.?$");
+ Pattern.compile("^(.+?) " + NEUFASSUNG_VERB + ": " + ENUM + Z + "\\.?$");
// „§ 2 Absatz 2 wird durch die folgenden Absätze 2 und 3 ersetzt: „…““ (neues BGBl-Format);
// auch „Die Überschrift wird durch die folgende Überschrift ersetzt: „…““ (Entwürfe).
@@ -340,9 +346,7 @@
// der Zitatblock wird an „§ N“-Grenzen in Einzel-Neufassungen zerlegt.
private static final Pattern PARAGRAPH_BEREICH_NEUFASSUNG =
Pattern.compile(
- "^Die (?:§§|Artt?\\.) (\\d+[a-z]?) bis (\\d+[a-z]?) (?:wird|werden) wie folgt gefasst: "
- + Z
- + "\\.?$");
+ "^Die (?:§§|Artt?\\.) (\\d+[a-z]?) bis (\\d+[a-z]?) " + NEUFASSUNG_VERB + ": " + Z + "\\.?$");
// „Der Wortlaut wird Absatz 1.“, bayerisch auch „Der bisherige Wortlaut wird Abs. 5.“ und
// „Der Wortlaut wird Satz 1.“
diff --git a/src/main/java/eu/mulk/aendggner/gesetz/Superskript.java b/src/main/java/eu/mulk/aendggner/gesetz/Superskript.java
index 777fe35..733c1f7 100644
--- a/src/main/java/eu/mulk/aendggner/gesetz/Superskript.java
+++ b/src/main/java/eu/mulk/aendggner/gesetz/Superskript.java
@@ -46,6 +46,21 @@
}
/**
+ * Wahr, wenn {@code text} mindestens eine amtliche Satznummer am Satzanfang trägt. Dient dazu, die
+ * Schreibweise eines Stammgesetzes zu erkennen (Landesrecht mit amtlicher Satzzählung behält seine
+ * Superskripte, Bundesrecht ohne solche verwirft sie).
+ */
+ public static boolean traegtSatznummern(String text) {
+ var m = LAUF.matcher(text);
+ while (m.find()) {
+ if (istSatzanfang(text, m.start(), m.end())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
* Wahr, wenn der Superskript-Lauf {@code [start, ende)} in {@code text} eine amtliche Satznummer
* am Satzanfang ist — im Unterschied zum Fußnotenmarker, der einem Wort anhängt und auf den eine
* schließende Klammer folgt („Enteignung⁶)“).
diff --git a/src/main/java/eu/mulk/aendggner/gesetz/bayern/BayRechtLoader.java b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtLoader.java
similarity index 60%
rename from src/main/java/eu/mulk/aendggner/gesetz/bayern/BayRechtLoader.java
rename to src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtLoader.java
index 4150af4..6bd72ce 100644
--- a/src/main/java/eu/mulk/aendggner/gesetz/bayern/BayRechtLoader.java
+++ b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtLoader.java
@@ -1,4 +1,4 @@
-package eu.mulk.aendggner.gesetz.bayern;
+package eu.mulk.aendggner.gesetz.land;
import eu.mulk.aendggner.aenderung.parse.PatchTextExtraktor;
import eu.mulk.aendggner.aenderung.parse.SuperskriptModus;
@@ -12,19 +12,21 @@
import org.jboss.logging.Logger;
/**
- * Liest ein Stammgesetz des bayerischen Landesrechts aus der konsolidierten Fassung von
- * gesetze-bayern.de — als PDF oder als kanonischer Klartext.
+ * Liest ein Stammgesetz des Landesrechts aus der konsolidierten Fassung — als PDF oder als
+ * kanonischer Klartext. Deckt bayerisches Landesrecht (Gliederung in {@code Art.}, amtliche
+ * Satznummern) ebenso ab wie die {@code §}-gegliederten Gesetze der übrigen Länder; das Sigel
+ * folgt je Norm aus dem Text (siehe {@link LandesRechtTextParser}).
*
* <p>PDFs durchlaufen dieselbe Aufbereitung wie Änderungsgesetze (Fontgrößen-Filter mit
* Superskript-Erhalt, Textbereinigung); der bereinigte Lineartext ist zugleich das dokumentierte
- * Klartext-Format (siehe {@link BayRechtTextParser}): Wo die PDF-Extraktion versagt oder nur eine
- * andere Quelle verfügbar ist (etwa eine archivierte HTML-Fassung), kann der Text von Hand
- * aufbereitet und als {@code .txt} eingespeist werden. Amtliche Satznummern und Fußnotenmarker
- * stehen dabei als Unicode-Superskripte im Text (¹Die freilebende Tierwelt …, Enteignung⁶)).
+ * Klartext-Format: Wo die PDF-Extraktion versagt oder nur eine andere Quelle verfügbar ist (etwa
+ * eine archivierte HTML-Fassung), kann der Text von Hand aufbereitet und als {@code .txt}
+ * eingespeist werden. Amtliche Satznummern und Fußnotenmarker stehen dabei als Unicode-Superskripte
+ * im Text (¹Die freilebende Tierwelt …, Enteignung⁶)).
*/
-public final class BayRechtLoader {
+public final class LandesRechtLoader {
- private static final Logger log = Logger.getLogger(BayRechtLoader.class);
+ private static final Logger log = Logger.getLogger(LandesRechtLoader.class);
private final Tika tika = new Tika();
@@ -44,15 +46,15 @@
"Nicht unterstützter Dateityp %s für Stammgesetz %s (unterstützt: PDF, Klartext)"
.formatted(mimeType, datei));
};
- return BayRechtTextParser.parse(text);
+ return LandesRechtTextParser.parse(text);
}
/**
* Stellt einen vom Zeilen-Reflow an das Satzende der Vornorm geklebten Normkopf („… verlangen.
* Art. 17 Jagderlaubnis“) wieder auf eine eigene Zeile. Das doppelte Leerzeichen zwischen
- * Artikelnummer und Titel unterscheidet den Normkopf von gewöhnlichen Querverweisen.
+ * Norm-Nummer und Titel unterscheidet den Normkopf von gewöhnlichen Querverweisen.
*/
private static String nachSatzendeGetrennteNormkoepfe(String text) {
- return text.replaceAll("(?<=[.“?!])[ \\t]+(?=Art\\.\\s\\d+[a-z]?[ \\t]{2})", "\n");
+ return text.replaceAll("(?<=[.“?!])[ \\t]+(?=(?:§|Art\\.)\\s\\d+[a-z]?[ \\t]{2})", "\n");
}
}
diff --git a/src/main/java/eu/mulk/aendggner/gesetz/bayern/BayRechtTextParser.java b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java
similarity index 75%
rename from src/main/java/eu/mulk/aendggner/gesetz/bayern/BayRechtTextParser.java
rename to src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java
index 0744086..b1da42d 100644
--- a/src/main/java/eu/mulk/aendggner/gesetz/bayern/BayRechtTextParser.java
+++ b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java
@@ -1,4 +1,4 @@
-package eu.mulk.aendggner.gesetz.bayern;
+package eu.mulk.aendggner.gesetz.land;
import eu.mulk.aendggner.gesetz.Absatz;
import eu.mulk.aendggner.gesetz.Gesetz;
@@ -11,9 +11,12 @@
import org.jspecify.annotations.Nullable;
/**
- * Parst den kanonischen Lineartext eines bayerischen Stammgesetzes (gesetze-bayern.de) zu einem
- * {@link Gesetz}. Das Layout entspricht der {@code --extract-only}-Ausgabe der konsolidierten
- * PDF-Fassung (siehe {@link BayRechtLoader}):
+ * Parst den kanonischen Lineartext eines Landesrecht-Stammgesetzes zu einem {@link Gesetz}. Das
+ * Layout entspricht der {@code --extract-only}-Ausgabe der konsolidierten PDF-Fassung (siehe {@link
+ * LandesRechtLoader}). Die Norm-Gliederung folgt dem jeweiligen Land: der Bund und die meisten
+ * Länder zitieren in {@code §}, Bayern in {@code Art.} — das Sigel wird je Norm aus dem Normkopf
+ * abgeleitet und nach {@link Norm#enbez()} durchgereicht, ein zentrales „Land“-Merkmal ist nicht
+ * nötig.
*
* <pre>
* Bayerisches Jagdgesetz ← Langtitel
@@ -23,17 +26,20 @@
* BayRS 792-1-W
* Vollzitat nach RedR: … ← übersprungen (ggf. mehrzeilig)
* I. Abschnitt Grundsätze ← Gliederungs-Überschrift
- * 1. Allgemeine Vorschriften ← nummerierte Unter-Überschrift (vor einem Art.-Kopf)
- * Art. 1 Gesetzeszweck ← Normkopf; „Art. 60 (aufgehoben)“ → weggefallen
+ * 1. Allgemeine Vorschriften ← nummerierte Unter-Überschrift (vor einem Normkopf)
+ * Art. 1 Gesetzeszweck ← Normkopf (§/Art.); „Art. 60 (aufgehoben)“ → weggefallen
* (1) ¹Die freilebende Tierwelt … ← Absätze mit amtlichen Satznummern als Superskript
* ⁶) [Amtl. Anm.:] … ← Fußnotenzeile, verbleibt im Text des tragenden Absatzes
* </pre>
*/
-final class BayRechtTextParser {
+final class LandesRechtTextParser {
private static final Pattern ABSATZ_MARKER = Pattern.compile("^\\((\\d+[a-z]?)\\)\\s+");
- private static final Pattern JURABK_ZEILE = Pattern.compile("^\\((\\S+)\\)$");
+ // Juris-Abkürzung direkt hinter dem Langtitel. Bayern führt einteilige Kürzel („(BayJG)“), die
+ // übrigen Länder oft mehrteilige („(GO NRW)“); Nur die Zeile unmittelbar nach dem Titel wird
+ // geprüft, sodass spätere Fundstellen-Klammern („(BayRS V S. 595)“) nicht getroffen werden.
+ private static final Pattern JURABK_ZEILE = Pattern.compile("^\\(([^()]+)\\)$");
// Kopf der Druckfassung („BayJG: Bayerisches Jagdgesetz … (Art. 1–64)“), ggf. mit
// umbrochenem Rest („1–64)“) auf der Folgezeile.
@@ -45,13 +51,17 @@
private static final Pattern UNTER_GLIEDERUNG = Pattern.compile("^(\\d+[a-z]?)\\.\\s+(\\S.*)$");
- // Normkopf: „Art. N“ plus Titel auf derselben Zeile. Die Negativliste schließt
- // Querverweise am Zeilenanfang aus („Art. 4 Abs. 3 …“).
+ // Normkopf: „§ N“ bzw. „Art. N“ plus Titel auf derselben Zeile. Das Sigel steht in Gruppe 1, die
+ // Nummer in Gruppe 2, der Titel in Gruppe 3. Die Negativliste schließt Querverweise am
+ // Zeilenanfang aus („Art. 4 Abs. 3 …“, „§ 5 Absatz 2 …“).
private static final Pattern NORM_KOPF =
Pattern.compile(
- "^Art\\.\\s+(\\d+[a-z]?)\\s+"
- + "(?!Absatz|Abs\\.|Satz|Sätze|Nummer|Nr\\.|Buchstabe|Buchst\\."
- + "|und|bis|oder|sowie|des|der|dieses)"
+ "^(§|Art\\.)\\s+(\\d+[a-z]?)\\s+"
+ // Querverweis-Schlüsselwörter nur als ganzes Wort ausschließen: „§ 4 Satz 2“ ist ein
+ // Verweis, „§ 4 Satzungen“ dagegen ein Normtitel. Der Schutz „(?![a-zäöüß])“ verhindert,
+ // dass „Satz“ auch „Satzungen“, „Nummer“ auch „Nummerierung“ trifft.
+ + "(?!(?:Absatz|Abs\\.|Satz|Sätze|Nummer|Nr\\.|Buchstabe|Buchst\\."
+ + "|und|bis|oder|sowie|des|der|dieses)(?![a-zäöüß]))"
+ "((?:\\p{Lu}|\\().*)$");
private static final Pattern WEGGEFALLEN_TITEL =
@@ -60,7 +70,7 @@
private static final Pattern FUSSNOTE =
Pattern.compile("^([⁰¹²³⁴⁵⁶⁷⁸⁹]+|\\d{1,3})\\)\\s*(\\[Amtl\\. Anm\\.:\\].*)$");
- private BayRechtTextParser() {}
+ private LandesRechtTextParser() {}
static Gesetz parse(String text) {
var zeilen = text.lines().toList();
@@ -95,6 +105,7 @@
String elternKennzahl = null;
int gliederungsZaehler = 0;
+ String normSigel = null;
String normNummer = null;
String normTitel = null;
var normZeilen = new ArrayList<String>();
@@ -109,11 +120,11 @@
if (zeile == null
|| gliederung.matches()
- || (normKopf.matches() && istNeuerNormKopf(normKopf.group(1), letzteNormNummer))
+ || (normKopf.matches() && istNeuerNormKopf(normKopf.group(2), letzteNormNummer))
|| (unterGliederung.matches() && istUnterGliederung(unterGliederung, zeilen, i))) {
// Laufende Norm abschließen.
if (normNummer != null) {
- normen.add(baueNorm(normNummer, normTitel, aktuelleGliederung, normZeilen));
+ normen.add(baueNorm(normSigel, normNummer, normTitel, aktuelleGliederung, normZeilen));
}
normNummer = null;
normZeilen.clear();
@@ -131,9 +142,10 @@
gliederung.group(1) + ". " + gliederung.group(2),
titel.isEmpty() ? null : titel);
gliederungen.add(aktuelleGliederung);
- } else if (normKopf.matches() && istNeuerNormKopf(normKopf.group(1), letzteNormNummer)) {
- normNummer = normKopf.group(1);
- normTitel = normKopf.group(2).strip();
+ } else if (normKopf.matches() && istNeuerNormKopf(normKopf.group(2), letzteNormNummer)) {
+ normSigel = normKopf.group(1);
+ normNummer = normKopf.group(2);
+ normTitel = normKopf.group(3).strip();
letzteNormNummer = numerisch(normNummer);
} else {
var kennzahl =
@@ -152,8 +164,8 @@
if (normen.isEmpty()) {
throw new IllegalArgumentException(
- "Kein „Art. N“-Normkopf gefunden — ist das eine konsolidierte Fassung von"
- + " gesetze-bayern.de?");
+ "Kein „§ N“- oder „Art. N“-Normkopf gefunden — ist das eine konsolidierte Fassung im"
+ + " kanonischen Klartextformat?");
}
return new Gesetz(jurabk != null ? jurabk : langue, langue, null, normen, gliederungen);
}
@@ -211,8 +223,12 @@
}
private static Norm baueNorm(
- String nummer, @Nullable String titel, @Nullable Gliederung gliederung, List<String> zeilen) {
- var enbez = "Art. " + nummer;
+ String sigel,
+ String nummer,
+ @Nullable String titel,
+ @Nullable Gliederung gliederung,
+ List<String> zeilen) {
+ var enbez = sigel + " " + nummer;
boolean weggefallen = titel != null && WEGGEFALLEN_TITEL.matcher(titel).matches();
var absaetze = new ArrayList<Absatz>();
@@ -245,6 +261,7 @@
absaetze.add(new Absatz(absatzNummer, String.join("\n", absatzZeilen)));
}
- return new Norm(enbez, titel == null || titel.isEmpty() ? null : titel, gliederung, absaetze, weggefallen);
+ return new Norm(
+ enbez, titel == null || titel.isEmpty() ? null : titel, gliederung, absaetze, weggefallen);
}
}
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index 2e19090..0e90584 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -230,7 +230,7 @@
assumeTrue(Files.exists(alt) && Files.exists(pdf), "BayJG-Beispieldaten fehlen");
// 1. Stammgesetz laden (Art.-gegliedert, amtliche Satznummern als Superskripte).
- var gesetz = new eu.mulk.aendggner.gesetz.bayern.BayRechtLoader().load(alt);
+ var gesetz = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader().load(alt);
assertThat(gesetz.jurabk()).isEqualTo("BayJG");
assertThat(gesetz.langue()).isEqualTo("Bayerisches Jagdgesetz");
assertThat(gesetz.norm("Art. 1").orElseThrow().absaetze().get(0).text())
@@ -295,7 +295,7 @@
var pdf = SAMPLEDATA.resolve("BayJG/Ltg-Drs-19-9707_Gesetzentwurf.pdf");
assumeTrue(Files.exists(alt) && Files.exists(pdf), "BayJG-Beispieldaten fehlen");
- var gesetz = new eu.mulk.aendggner.gesetz.bayern.BayRechtLoader().load(alt);
+ var gesetz = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader().load(alt);
var text =
TextBereiniger.bereinige(
new PatchTextExtraktor(eu.mulk.aendggner.aenderung.parse.SuperskriptModus.BEHALTEN)
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 8cf9eea..d89c898 100644
--- a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java
@@ -91,6 +91,21 @@
}
@Test
+ void erkenntNeufassungMitErhaeltFolgendeFassung() {
+ // „… erhält folgende Fassung:“ ist die Neufassungsform in Schleswig-Holstein und Niedersachsen.
+ var befehl =
+ erkenne(
+ "§ 34a Absatz 1 erhält folgende Fassung: „(1) Durch Hauptsatzung kann bestimmt"
+ + " werden, dass Gemeindevertreterinnen und Gemeindevertreter teilnehmen.“",
+ Stelle.LEER);
+
+ assertThat(befehl).containsInstanceOf(Neufassung.class);
+ var neufassung = (Neufassung) befehl.orElseThrow();
+ assertThat(neufassung.stelle().anzeigeText()).isEqualTo("§ 34a Absatz 1");
+ assertThat(neufassung.neuerText()).startsWith("(1) Durch Hauptsatzung");
+ }
+
+ @Test
void erkenntParagraphEinfuegung() {
// Echter Befehl aus Art. 1 Nr. 17 des Dritten Bevölkerungsschutzgesetzes.
var befehl =
diff --git a/src/test/java/eu/mulk/aendggner/gesetz/bayern/BayRechtTextParserTest.java b/src/test/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParserTest.java
similarity index 62%
rename from src/test/java/eu/mulk/aendggner/gesetz/bayern/BayRechtTextParserTest.java
rename to src/test/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParserTest.java
index a5fc67a..3354255 100644
--- a/src/test/java/eu/mulk/aendggner/gesetz/bayern/BayRechtTextParserTest.java
+++ b/src/test/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParserTest.java
@@ -1,13 +1,14 @@
-package eu.mulk.aendggner.gesetz.bayern;
+package eu.mulk.aendggner.gesetz.land;
import static org.assertj.core.api.Assertions.assertThat;
import eu.mulk.aendggner.gesetz.Gliederung;
import org.junit.jupiter.api.Test;
-class BayRechtTextParserTest {
+class LandesRechtTextParserTest {
- private static final String BEISPIEL =
+ // Bayern: Gliederung in „Art.“, amtliche Satznummern als Superskript.
+ private static final String BAYERN =
"""
BayJG: Bayerisches Jagdgesetz (BayJG) Vom 13. Oktober 1978 (BayRS V S. 595) BayRS 792-1-W (Art.
1–64)
@@ -51,17 +52,38 @@
(1) Dieses Gesetz tritt am 1. Januar 1979 in Kraft.
""";
+ // Übriges Landesrecht: Gliederung in „§“ (wie der Bund), hier mit amtlichen Satznummern.
+ private static final String PARAGRAPHEN =
+ """
+ Gemeindeordnung für das Land Nordrhein-Westfalen
+ (GO NRW)
+ Vom 14. Juli 1994
+ I. Teil Grundlagen der Gemeindeverfassung
+ § 1 Wesen der Gemeinde
+
+ (1) ¹Die Gemeinden sind die Grundlage des demokratischen Staatsaufbaus. ²Sie fördern das Wohl der Einwohner.
+ (2) Die Gemeinden verwalten ihre Angelegenheiten selbst.
+ § 2 Aufgaben
+
+ Die Gemeinden erfüllen die Aufgaben der örtlichen Gemeinschaft.
+ § 3 (weggefallen)
+
+ § 4 Satzungen
+
+ (1) Die Gemeinden können ihre Angelegenheiten durch Satzung regeln.
+ """;
+
@Test
- void parstTitelblock() {
- var gesetz = BayRechtTextParser.parse(BEISPIEL);
+ void parstBayerischenTitelblock() {
+ var gesetz = LandesRechtTextParser.parse(BAYERN);
assertThat(gesetz.jurabk()).isEqualTo("BayJG");
assertThat(gesetz.langue()).isEqualTo("Bayerisches Jagdgesetz");
assertThat(gesetz.kurzue()).isNull();
}
@Test
- void parstNormenMitTitelnUndAbsaetzen() {
- var gesetz = BayRechtTextParser.parse(BEISPIEL);
+ void parstBayerischeNormenMitTitelnUndAbsaetzen() {
+ var gesetz = LandesRechtTextParser.parse(BAYERN);
assertThat(gesetz.normen())
.extracting(n -> n.enbez())
.containsExactly("Art. 1", "Art. 3", "Art. 8", "Art. 59", "Art. 60", "Art. 63", "Art. 64");
@@ -82,7 +104,7 @@
@Test
void querverweiseAmZeilenanfangEroeffnenKeineNorm() {
// „Art. 11 Abs. 6 …“ am Zeilenanfang in Art. 8 ist ein Querverweis, kein Normkopf.
- var gesetz = BayRechtTextParser.parse(BEISPIEL);
+ var gesetz = LandesRechtTextParser.parse(BAYERN);
var art8 = gesetz.norm("Art. 8").orElseThrow();
assertThat(art8.absaetze().get(0).text()).contains("Art. 11 Abs. 6 sind entsprechend");
assertThat(gesetz.norm("Art. 11")).isEmpty();
@@ -90,7 +112,7 @@
@Test
void parstGliederungenMitUnterUeberschriften() {
- var gesetz = BayRechtTextParser.parse(BEISPIEL);
+ var gesetz = LandesRechtTextParser.parse(BAYERN);
assertThat(gesetz.gliederungen())
.extracting(Gliederung::anzeigeText)
.containsExactly(
@@ -107,7 +129,7 @@
@Test
void erkenntWeggefalleneNormenUndFussnoten() {
- var gesetz = BayRechtTextParser.parse(BEISPIEL);
+ var gesetz = LandesRechtTextParser.parse(BAYERN);
assertThat(gesetz.norm("Art. 60").orElseThrow().weggefallen()).isTrue();
assertThat(gesetz.norm("Art. 63").orElseThrow().weggefallen()).isFalse();
@@ -117,4 +139,26 @@
.contains("Enteignung⁶)")
.contains("⁶) [Amtl. Anm.:] BayRS 2141-1-I");
}
+
+ @Test
+ void parstParagraphengegliedertesLandesrecht() {
+ // Die übrigen Länder zitieren in „§“; das Sigel folgt aus dem Normkopf, ein Land-Merkmal ist
+ // nicht nötig. Amtliche Satznummern werden ebenso wie bei Bayern erhalten.
+ var gesetz = LandesRechtTextParser.parse(PARAGRAPHEN);
+ assertThat(gesetz.jurabk()).isEqualTo("GO NRW");
+ assertThat(gesetz.langue()).isEqualTo("Gemeindeordnung für das Land Nordrhein-Westfalen");
+ assertThat(gesetz.normen())
+ .extracting(n -> n.enbez())
+ .containsExactly("§ 1", "§ 2", "§ 3", "§ 4");
+
+ var p1 = gesetz.norm("§ 1").orElseThrow();
+ assertThat(p1.titel()).isEqualTo("Wesen der Gemeinde");
+ assertThat(p1.absaetze()).hasSize(2);
+ assertThat(p1.absaetze().get(0).text()).startsWith("¹Die Gemeinden sind die Grundlage");
+
+ // Unnummerierter Einzelabsatz und weggefallene Norm wie im bayerischen Fall.
+ assertThat(gesetz.norm("§ 2").orElseThrow().absaetze().get(0).nummer()).isNull();
+ assertThat(gesetz.norm("§ 3").orElseThrow().weggefallen()).isTrue();
+ assertThat(gesetz.gliederungen()).extracting(Gliederung::anzeigeText).containsExactly("I. Teil — Grundlagen der Gemeindeverfassung");
+ }
}