aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/eu
diff options
context:
space:
mode:
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());
}
/**