aboutsummaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java56
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtAnwender.java22
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/SatzTeiler.java42
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java34
4 files changed, 138 insertions, 16 deletions
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
index 13a235a..145334d 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
@@ -792,7 +792,7 @@ public final class BefehlAnwender {
if (brauchtFuge(befehl.alt(), befehl.neu())) {
return TextErgebnis.ok(ersetzeMitFuge(text, befehl.alt(), befehl.neu()));
}
- return TextErgebnis.ok(text.replace(befehl.alt(), befehl.neu()));
+ return TextErgebnis.ok(ersetzeWortweise(text, befehl.alt(), befehl.neu()));
}));
}
@@ -811,6 +811,17 @@ public final class BefehlAnwender {
&& (Character.isLetter(neu.codePointAt(0)) || neu.charAt(0) == '(');
}
+ /** Ersetzt jedes Vorkommen, das als Wort steht (siehe {@link #findeVorkommen}). */
+ private static String ersetzeWortweise(String text, String alt, String neu) {
+ var sb = new StringBuilder();
+ int von = 0;
+ for (int idx = findeVorkommen(text, alt, 0); idx >= 0; idx = findeVorkommen(text, alt, von)) {
+ sb.append(text, von, idx).append(neu);
+ von = idx + alt.length();
+ }
+ return sb.append(text, von, text.length()).toString();
+ }
+
private static String ersetzeMitFuge(String text, String alt, String neu) {
var sb = new StringBuilder();
int von = 0;
@@ -840,7 +851,7 @@ public final class BefehlAnwender {
return TextErgebnis.fehler(
"„" + befehl.woerter() + "“ kommt " + anzahl + "-mal vor.");
}
- int naht = text.indexOf(befehl.woerter());
+ int naht = findeVorkommen(text, befehl.woerter(), 0);
return TextErgebnis.ok(
heileNaht(
text.substring(0, naht) + text.substring(naht + befehl.woerter().length()),
@@ -2104,7 +2115,7 @@ public final class BefehlAnwender {
if (anzahl > 1) {
return new Fundpruefung(-1, "„" + woerter + "“ kommt " + anzahl + "-mal vor; mehrdeutig.");
}
- return new Fundpruefung(text.indexOf(woerter), null);
+ return new Fundpruefung(findeVorkommen(text, woerter, 0), null);
}
private static int zaehleVorkommen(String text, String suchtext) {
@@ -2113,13 +2124,50 @@ public final class BefehlAnwender {
}
int anzahl = 0;
int index = 0;
- while ((index = text.indexOf(suchtext, index)) >= 0) {
+ while ((index = findeVorkommen(text, suchtext, index)) >= 0) {
anzahl++;
index += suchtext.length();
}
return anzahl;
}
+ /**
+ * Das nächste Vorkommen des Suchtextes ab {@code von} — als <em>Wort</em>, nicht als beliebige
+ * Zeichenfolge. Ein Befehl, der „das Wort ‚schwerwiegende‘“ nennt, meint dieses Wort und nicht
+ * die ersten dreizehn Buchstaben von „schwerwiegender“; ohne diese Grenze ersetzte die
+ * Doppelanweisung des § 36 IfSG ihr zweites Wort mit, um es danach nicht mehr zu finden. Die
+ * Grenze wird nur dort verlangt, wo der Suchtext selbst mit einem Buchstaben oder einer Ziffer
+ * anfängt bzw. endet: Ein Satzzeichen, eine Klammer oder ein Bindestrich am Rand bringt seine
+ * Grenze schon mit.
+ */
+ private static int findeVorkommen(String text, String suchtext, int von) {
+ if (suchtext.isEmpty()) {
+ return -1;
+ }
+ for (int index = text.indexOf(suchtext, von);
+ index >= 0;
+ index = text.indexOf(suchtext, index + 1)) {
+ if (stehtAlsWort(text, index, suchtext)) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ private static boolean stehtAlsWort(String text, int index, String suchtext) {
+ int ende = index + suchtext.length();
+ if (istWortzeichen(suchtext.charAt(0)) && index > 0 && istWortzeichen(text.charAt(index - 1))) {
+ return false;
+ }
+ return !istWortzeichen(suchtext.charAt(suchtext.length() - 1))
+ || ende >= text.length()
+ || !istWortzeichen(text.charAt(ende));
+ }
+
+ private static boolean istWortzeichen(char c) {
+ return Character.isLetterOrDigit(c);
+ }
+
// Eine §-Überschrift beginnt mit „§ N“, gefolgt von einem großgeschriebenen Titelwort — im
// Gegensatz zu Querverweisen wie „§ 71 Absatz 1“ oder „§§ 42 bis 45“. Die Negativliste schließt
// die Untergliederungs- und Verbindungswörter aus, sodass an solchen Stellen nicht getrennt wird.
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtAnwender.java
index e1cf16f..5ffe0b7 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtAnwender.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtAnwender.java
@@ -73,11 +73,25 @@ final class InhaltsuebersichtAnwender {
};
}
- /** Die adressierte Angabe: die §-/Gliederungs-Komponenten hinter der Inhaltsübersichts-Marke. */
+ /**
+ * Die adressierte Angabe: die §-/Gliederungs-Komponenten hinter der Inhaltsübersichts-Marke.
+ *
+ * <p>Nennt der Rahmen dieselbe Einheit wie der Befehl selbst — „In der Inhaltsübersicht wird …
+ * Teil 2 wie folgt geändert: … Die Angabe zur Überschrift von Teil 2 Abschnitt 4 wird gestrichen“
+ * —, so steht sie zweimal hintereinander in der Stelle. Gemeint ist sie einmal: Das zweite Glied
+ * suchte sich sonst innerhalb seiner selbst und bliebe unauffindbar.
+ */
private static List<Stelle.Komponente> zielKette(Stelle stelle) {
- return stelle.komponenten().stream()
- .filter(k -> k instanceof Stelle.Paragraph || k instanceof Stelle.Gliederungseinheit)
- .toList();
+ var kette = new ArrayList<Stelle.Komponente>();
+ for (var k : stelle.komponenten()) {
+ if (!(k instanceof Stelle.Paragraph || k instanceof Stelle.Gliederungseinheit)) {
+ continue;
+ }
+ if (kette.isEmpty() || !kette.get(kette.size() - 1).equals(k)) {
+ kette.add(k);
+ }
+ }
+ return List.copyOf(kette);
}
/**
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/SatzTeiler.java b/src/main/java/eu/mulk/aendggner/anwendung/SatzTeiler.java
index be0a1f2..0b39962 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/SatzTeiler.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/SatzTeiler.java
@@ -79,6 +79,30 @@ public final class SatzTeiler {
"Dezember");
/**
+ * Gliederungswörter, vor denen eine Zahl mit Punkt eine Ordnungszahl ist und kein Satzende: „…
+ * nach dem 5. Abschnitt des Elften Buches …“. Sie stehen groß und rissen den Satz sonst genau
+ * dort auseinander, wo er auf eine andere Vorschrift verweist.
+ */
+ private static final Set<String> GLIEDERUNGSWOERTER =
+ Set.of(
+ "Abschnitt",
+ "Abschnitts",
+ "Abschnitte",
+ "Abschnitten",
+ "Teil",
+ "Teils",
+ "Teile",
+ "Teilen",
+ "Kapitel",
+ "Kapitels",
+ "Buch",
+ "Buches",
+ "Titel",
+ "Titels",
+ "Unterabschnitt",
+ "Unterabschnitts");
+
+ /**
* Kandidat für ein Satzende: Punkt (ggf. gefolgt von schließendem Zitat oder Klammer), dann
* Leerraum, dann Großbuchstabe, Zitat, Klammer oder Paragraphenzeichen — Rechtssätze beginnen
* häufig mit einem Verweis („… zusammengefasst werden. § 27 Absatz 2 … bleibt unberührt.“).
@@ -99,6 +123,7 @@ public final class SatzTeiler {
int punkt = matcher.start();
if (istAbkuerzung(text, punkt)
|| istDatum(text, punkt, matcher.end())
+ || istOrdinalzahl(text, punkt, matcher.end())
|| istAufzaehlungsMarke(text, punkt)) {
continue;
}
@@ -181,11 +206,25 @@ public final class SatzTeiler {
return MONATE.contains(naechstesWort);
}
+ /** „nach dem 5. Abschnitt“ — Ziffern vor dem Punkt, ein Gliederungswort dahinter. */
+ private static boolean istOrdinalzahl(String text, int punktPosition, int naechstesWortPosition) {
+ if (!wortVor(text, punktPosition).matches("\\d{1,2}")) {
+ return false;
+ }
+ var rest = text.substring(naechstesWortPosition);
+ return GLIEDERUNGSWOERTER.contains(rest.split("[\\s,.;]", 2)[0]);
+ }
+
/**
* Punkt einer Aufzählungsmarke am Zeilenanfang („… folgende Aufgaben ⏎ 1. Erlaß von Satzungen
* …“). Er beendet keinen Satz: die Glieder einer Aufzählung gehören zum tragenden Satz. Die
* Beschränkung auf den Zeilenanfang unterscheidet die Marke von einer Zahl am Satzende („…
* beträgt 30. Die Frist …“).
+ *
+ * <p>Die Einrückung, die die Marke trägt, gehört zum Zeilenanfang: Der Klartext- wie der
+ * XML-Zweig rücken die Glieder ein („⏎␣␣1.␣Personen, die …“), und ohne diese Nachsicht zählte
+ * jedes Glied als eigener Satz — § 20 Abs. 12 IfSG käme so auf neun Sätze statt der amtlichen
+ * fünf, und ein Befehl auf „Satz 5“ träfe den falschen.
*/
private static boolean istAufzaehlungsMarke(String text, int punktPosition) {
var wort = wortVor(text, punktPosition);
@@ -193,6 +232,9 @@ public final class SatzTeiler {
return false;
}
int anfang = punktPosition - wort.length();
+ while (anfang > 0 && (text.charAt(anfang - 1) == ' ' || text.charAt(anfang - 1) == '\t')) {
+ anfang--;
+ }
return anfang == 0 || text.charAt(anfang - 1) == '\n';
}
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java b/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java
index bcbc752..b8d22d6 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java
@@ -152,7 +152,15 @@ final class StellenAufloeser {
*/
private static SatzTeiler.@Nullable SatzBereich loeseFeinKomponentenAuf(
Stelle stelle, String text) {
- SatzTeiler.SatzBereich bereich = null;
+ // 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;
for (var komponente : stelle.komponenten()) {
String labelRegex =
@@ -177,13 +185,8 @@ final class StellenAufloeser {
if (zeilenKette) {
return bereich;
}
- for (var komponente : stelle.komponenten()) {
- if (komponente instanceof Stelle.SatzNr satz) {
- int nummer = Integer.parseInt(satz.nummer().replaceAll("[a-z]$", ""));
- var saetze = SatzTeiler.teile(text);
- var satzBereich = satzBereich(text, nummer, saetze);
- return satzBereich == null ? null : halbsatzBereich(text, stelle, satzBereich);
- }
+ if (satzRahmen != null) {
+ return halbsatzBereich(text, stelle, satzRahmen);
}
// Halbsatz ohne Satzangabe („in Halbsatz 1“ im Rahmen eines Satzes bzw. Absatzes).
if (stelle.komponenten().stream().anyMatch(k -> k instanceof Stelle.HalbsatzNr)) {
@@ -193,6 +196,21 @@ final class StellenAufloeser {
}
/**
+ * 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.
+ */
+ 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;
+ }
+
+ /**
* Der Bereich des Satzes mit der gegebenen Nummer. Amtlich nummerierte Sätze (bayerisches
* Landesrecht) werden nach Satznummer statt Position aufgelöst — nach Streichungen kann die
* Zählung von der Position abweichen. Trägt mindestens ein Satz eine Nummer, entscheidet allein