diff options
Diffstat (limited to 'src/main/java/eu')
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()); } /** |
