diff options
| author | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2026-08-23 22:17:16 +0200 |
|---|---|---|
| committer | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2026-08-23 22:17:16 +0200 |
| commit | e13b58a5e3b602b475c46e6d595749d375148836 (patch) | |
| tree | 65ce18b25066866effe20be38c77b9bb5b92f96e /src/main/java | |
| parent | 2ada4600cbb8dbd0dc8ac2fa0537f6853ba62359 (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')
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()); } /** |
