aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/eu
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-23 22:17:16 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-23 22:17:16 +0200
commite13b58a5e3b602b475c46e6d595749d375148836 (patch)
tree65ce18b25066866effe20be38c77b9bb5b92f96e /src/main/java/eu
parent2ada4600cbb8dbd0dc8ac2fa0537f6853ba62359 (diff)
Was nicht vorliegt, lässt sich nicht ändern
Baden-Württemberg war das letzte Land mit einem Änderungsdokument, aber ohne Belegfall. Es hat vier Funde des Satzbildes und einen der Anwendung gebracht. Der Seitenfuß des Gesetzblatts steht im Inhaltsstrom mitten im Befehlstext und trennt dort den Zieltext eines Befehls von seinem Verb — „… die Wörter ‚Telegramm, Fernschreiben,‘“, Fußzeile, „gestrichen.“; die Blattzählung steht mitunter für sich allein zwischen zwei Gliederungspunkten. Beides wird nun herausgeschnitten. Dazu zwei Idiome: Der Dativ einer Anfügung darf seinen Artikel verlieren („Absatz 2 wird folgender Satz angefügt“), und hinter einer Anlagenbezeichnung darf ein beschreibender Klammerzusatz stehen. Der Anwendungsfund betrifft den Halbsatz: Wird der zweite gestrichen, so nimmt er sein Semikolon mit, und der Satz behält seinen Schlusspunkt. „A; B.“ ohne B ist „A.“ und nicht „A;“. Bei der Gelegenheit fiel eine Ordnungsfrage auf, die allgemein ist: Die Glieder einer Fundstelle werden jetzt in der Reihenfolge aufgelöst, in der die Stelle sie nennt, und jedes verengt den Suchbereich des nächsten. „Absatz 2 Nummer 1 Satz 2“ meint den zweiten Satz der Nummer 1, „Satz 1 Nummer 3“ dagegen die dritte Nummer des ersten Satzes; wer stets zuerst den Satz oder stets zuerst die Nummer suchte, träfe in einem der beiden Fälle die falsche Einheit. Einunddreißig der siebenunddreißig Befehle werden angewandt, und einundneunzig der dreiundneunzig Normen gleichen danach der amtlichen Fassung. Die sechs liegengebliebenen ändern sämtlich die Muster der Anlagen — den Wahlschein, die Merkblätter, deren Fußnoten und Spiegelstriche. Sie sind nicht anwendbar, weil das Landesrechtsportal die Muster nicht als Text ausliefert: Die Anlagen 2 bis 14 bestehen dort nur aus Kopf und Fundstelle. Das ist eine Grenze der Quelle, nicht des Werkzeugs, und sie ist als solche benannt. Damit trägt jedes Land, von dem ein Änderungsdokument vorliegt, einen Belegfall bis zur Anwendung. Geprüft mit „mvnw verify“: 334 Prüfungen, kein Fehlschlag; keine gepinnte Zahl eines anderen Belegfalls hat sich geändert. REUSE 174/174. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Change-Id: If1709f0c037195a510febb7f8a21396ed0133d45
Diffstat (limited to 'src/main/java/eu')
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java7
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java11
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java18
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java22
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java80
5 files changed, 89 insertions, 49 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 3a342c5..122fb6b 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
@@ -292,9 +292,14 @@ final class BefehlErkenner {
// Auch mit Artefakt-Toleranz: verdoppeltes „wird“ und Leerzeichen vor dem Doppelpunkt
// („In § 51 Absatz 1 wird folgender Satz wird angefügt : „…““, BR-Drs).
+ //
+ // Der Artikel des Dativs darf fehlen: Baden-Württemberg schreibt „Absatz 2 wird folgender Satz
+ // angefügt:“ statt „Dem Absatz 2 …“. Verwechslungsfrei ist das, weil der Befehl das Angefügte
+ // eigens benennt — „Absatz 2 wird angefügt“ ohne diese Nennung bliebe die Anfügung des Absatzes
+ // selbst und wird von diesem Muster nicht getroffen.
private static final Pattern STRUKTUR_ANFUEGUNG_MIT_STELLE =
Pattern.compile(
- "^(?:Dem|Der|In) (.+?) (?:wird|werden) (?:der |die |das )?folgende[nrs]? (.+?)"
+ "^(?:Dem |Der |In |)(.+?) (?:wird|werden) (?:der |die |das )?folgende[nrs]? (.+?)"
+ "(?: wird| werden)? angefügt ?: "
+ ENUM
+ Z
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java
index c9cc8ff..8f9500c 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java
@@ -159,6 +159,17 @@ public final class StellenParser {
if (wert != null && NUMMER_WERT.matcher(wert).matches()) {
komponenten.add(new Stelle.Gliederungseinheit("Anlage", wert));
i++;
+ // „In Anlage 3b (Muster des Merkblatts zu den Stimmzetteln …) wird …“ — der
+ // Klammerzusatz beschreibt die Anlage und ist nicht selbst Änderungsziel; er wird
+ // übersprungen. Ohne das bräche die Stellenangabe hier ab und der ganze Befehl bliebe
+ // unerkannt.
+ var folgt = naechstesWort(woerter, i);
+ if (folgt != null && folgt.startsWith("(")) {
+ while (i + 1 < woerter.length && !woerter[i + 1].endsWith(")")) {
+ i++;
+ }
+ i++;
+ }
continue;
}
// „die Anlage zu § 2 Absatz 4 Satz 1“ — ein Gesetz mit einer einzigen Anlage benennt sie
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 7bd4a03..ffe3afa 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java
@@ -284,6 +284,22 @@ public final class TextBereiniger {
"[ \\t]*\\d{1,4}[ \\t]+Gesetz- und Verordnungsblatt für den Freistaat"
+ " Thüringen[ \\t]*");
+ // GBl. für Baden-Württemberg: Der Seitenfuß („Gesetzblatt für Baden-Württemberg, Jahrgang 2026,
+ // Nr. 26 vom 27. Februar 2026 Seite 2 von 7“) steht gleichfalls im Inhaltsstrom, und zwar mitten
+ // im Befehlstext — er trennt dort sogar den Zieltext eines Befehls von seinem Verb („… die
+ // Wörter ‚Telegramm, Fernschreiben,‘“ / Fußzeile / „gestrichen.“). Auch Seitenzahl und Blattzahl
+ // stehen mitunter getrennt vom übrigen Fuß.
+ private static final Pattern GBL_BW_FUSS =
+ Pattern.compile(
+ "[ \\t]*Gesetzblatt für Baden-Württemberg, Jahrgang \\d{4},[ \\t\\n]*"
+ + "Nr\\.\\s*\\d+ vom \\d{1,2}\\. \\p{L}+ \\d{4}"
+ + "(?:[ \\t\\n]*Seite \\d+ von \\d+)?[ \\t]*");
+
+ // Die Blattzählung des GBl. BW („Seite 2 von 7“) steht im Inhaltsstrom mitunter für sich allein,
+ // getrennt vom übrigen Seitenfuß, und zwar zwischen zwei Gliederungspunkten — sie hinge sonst dem
+ // vorangehenden Befehl an und machte ihn unkenntlich.
+ private static final Pattern GBL_BW_BLATTZAHL = Pattern.compile("^\\s*Seite \\d+ von \\d+\\s*$");
+
// 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
@@ -322,6 +338,7 @@ public final class TextBereiniger {
text = GVBL_BERLIN_KOPF.matcher(text).replaceAll("\n");
text = GVBL_TH_KOPF.matcher(text).replaceAll("\n");
text = GVBL_TH_FUSS.matcher(text).replaceAll("\n");
+ text = GBL_BW_FUSS.matcher(text).replaceAll("\n");
var zeilen = entferneKolumnentitel(zerlegeInZeilen(text));
var verbunden = verbindeUmbrueche(zeilen);
// Falsch-positive markerlose Zusammenzüge („durch“ + „die“ → „durchdie“) reparieren — die
@@ -511,6 +528,7 @@ public final class TextBereiniger {
|| GVOBL_SH_LAND.matcher(zeile).matches()
|| GVOBL_SH_HEFT.matcher(zeile).matches()
|| GVBL_HESSEN_FUSS.matcher(zeile).matches()
+ || GBL_BW_BLATTZAHL.matcher(zeile).matches()
|| GVBL_HESSEN_KOPF.matcher(zeile).matches()
|| ZUSAMMENSTELLUNG_SPALTENKOPF.matcher(zeile).matches()
|| FUNDSTELLEN_FUSSNOTE.matcher(zeile).matches();
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
index 486f77b..677b0ee 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
@@ -1655,10 +1655,24 @@ public final class BefehlAnwender {
+ " (weggefallen)"
+ text.substring(bereich.bis()));
}
- return TextErgebnis.ok(
- (text.substring(0, bereich.von()) + text.substring(bereich.bis()))
- .replaceAll(" +", " ")
- .strip());
+ var rumpf = text.substring(0, bereich.von());
+ var rest = text.substring(bereich.bis());
+ // Ein Halbsatz nimmt sein Trennzeichen mit. „A; B.“ ohne den zweiten Halbsatz ist „A.“
+ // und nicht „A;“ — das Semikolon trennte ja gerade ihn ab, und der Satz braucht seinen
+ // Schlusspunkt. So setzt es auch die amtliche Nachfassung (§ 24 Abs. 2 KomWO BW).
+ var danach = rest.stripLeading();
+ boolean satzEndeErreicht =
+ danach.isEmpty()
+ || Character.isUpperCase(danach.codePointAt(0))
+ || danach.startsWith("§");
+ if (stelle.komponenten().stream().anyMatch(k -> k instanceof Stelle.HalbsatzNr)
+ && satzEndeErreicht) {
+ var gestutzt = rumpf.stripTrailing();
+ if (gestutzt.endsWith(";") || gestutzt.endsWith(",")) {
+ rumpf = gestutzt.substring(0, gestutzt.length() - 1) + ".";
+ }
+ }
+ return TextErgebnis.ok((rumpf + rest).replaceAll(" +", " ").strip());
});
}
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java b/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java
index 0bb2d91..0d9d8d9 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java
@@ -178,62 +178,54 @@ final class StellenAufloeser {
*/
private static SatzTeiler.@Nullable SatzBereich loeseFeinKomponentenAuf(
Stelle stelle, String text) {
- // Nennt die Stelle einen Satz und darin eine Nummer („§ 5 Absatz 2 Satz 1 Nummer 3“), so ist
- // der Satz der Suchbereich der Nummer. Ohne diese Verengung suchte die Marke „3.“ im ganzen
- // Absatz und fand sie auch in der Aufzählung eines anderen Satzes — die Stelle galt dann als
- // mehrdeutig und der Befehl blieb liegen.
- var satzRahmen = satzRahmen(stelle, text);
- if (satzRahmen != null && satzRahmen.bis() == satzRahmen.von()) {
- return null;
- }
- SatzTeiler.SatzBereich bereich = satzRahmen;
- boolean zeilenKette = false;
+ // Die Glieder werden in der Reihenfolge aufgelöst, in der die Stelle sie nennt, und jedes
+ // verengt den Suchbereich des nächsten. Das ist keine Feinheit, sondern die Aussage der
+ // Zitierweise selbst: „Absatz 2 Nummer 1 Satz 2“ meint den zweiten Satz der Nummer 1,
+ // „Satz 1 Nummer 3“ dagegen die dritte Nummer des ersten Satzes. Wer stets zuerst den Satz
+ // oder stets zuerst die Nummer suchte, träfe in einem der beiden Fälle die falsche Einheit
+ // oder gar keine.
+ var bereich = new SatzTeiler.SatzBereich(0, text.length());
+ boolean verengt = false;
for (var komponente : stelle.komponenten()) {
- String labelRegex =
- switch (komponente) {
- case Stelle.NummerNr nummer -> Pattern.quote(nummer.nummer()) + "\\.";
- case Stelle.BuchstabeNr buchstabe -> Pattern.quote(buchstabe.kennung()) + "\\)";
- default -> null;
- };
- if (labelRegex == null) {
- continue;
+ switch (komponente) {
+ case Stelle.NummerNr nummer -> {
+ bereich = zeilenBlock(text, Pattern.quote(nummer.nummer()) + "\\.", bereich);
+ verengt = true;
+ }
+ case Stelle.BuchstabeNr buchstabe -> {
+ bereich = zeilenBlock(text, Pattern.quote(buchstabe.kennung()) + "\\)", bereich);
+ verengt = true;
+ }
+ case Stelle.SatzNr satz -> {
+ bereich = satzBereichIn(text, bereich, satz);
+ verengt = true;
+ }
+ default -> {}
}
- bereich =
- zeilenBlock(
- text,
- labelRegex,
- bereich != null ? bereich : new SatzTeiler.SatzBereich(0, text.length()));
if (bereich == null) {
return null;
}
- zeilenKette = true;
}
- if (zeilenKette) {
- return bereich;
- }
- if (satzRahmen != null) {
- return halbsatzBereich(text, stelle, satzRahmen);
- }
- // Halbsatz ohne Satzangabe („in Halbsatz 1“ im Rahmen eines Satzes bzw. Absatzes).
+ // Der Halbsatz teilt zuletzt, was die übrigen Glieder übriggelassen haben.
if (stelle.komponenten().stream().anyMatch(k -> k instanceof Stelle.HalbsatzNr)) {
- return halbsatzBereich(text, stelle, new SatzTeiler.SatzBereich(0, text.length()));
+ return halbsatzBereich(text, stelle, bereich);
}
- return null;
+ return verengt ? bereich : null;
}
/**
- * Der Bereich des von der Stelle benannten Satzes; {@code null}, wenn sie keinen nennt. Ein
- * leerer Bereich ({@code von == bis}) bedeutet: Der Satz ist benannt, aber nicht auffindbar.
+ * Der Bereich des benannten Satzes <em>innerhalb</em> des schon verengten Bereichs. Gezählt wird
+ * in diesem Ausschnitt, nicht im ganzen Absatz — sonst wäre „Nummer 1 Satz 2“ der zweite Satz des
+ * Absatzes statt der zweite Satz der Nummer 1.
*/
- private static SatzTeiler.@Nullable SatzBereich satzRahmen(Stelle stelle, String text) {
- for (var komponente : stelle.komponenten()) {
- if (komponente instanceof Stelle.SatzNr satz) {
- int nummer = Integer.parseInt(satz.nummer().replaceAll("[a-z]$", ""));
- var bereich = satzBereich(text, nummer, SatzTeiler.teile(text));
- return bereich == null ? new SatzTeiler.SatzBereich(0, 0) : bereich;
- }
- }
- return null;
+ private static SatzTeiler.@Nullable SatzBereich satzBereichIn(
+ String text, SatzTeiler.SatzBereich rahmen, Stelle.SatzNr satz) {
+ int nummer = Integer.parseInt(satz.nummer().replaceAll("[a-z]$", ""));
+ var ausschnitt = text.substring(rahmen.von(), rahmen.bis());
+ var innen = satzBereich(ausschnitt, nummer, SatzTeiler.teile(ausschnitt));
+ return innen == null
+ ? null
+ : new SatzTeiler.SatzBereich(rahmen.von() + innen.von(), rahmen.von() + innen.bis());
}
/**