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.java14
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java84
-rw-r--r--src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java41
3 files changed, 121 insertions, 18 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 60ff9bb..3a342c5 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
@@ -1437,7 +1437,11 @@ final class BefehlErkenner {
}
if ((m = AUFHEBUNG.matcher(text)).matches()) {
- return ausStellen(m.group(1), s -> new Aufhebung(kontext.plus(s), provenienz));
+ // Absteigend: Ob eine aufgehobene Einheit einen nummerierten Platzhalter hinterlässt, hängt
+ // davon ab, ob ihr noch eine Einheit folgt (siehe BefehlAnwender#haeltPlatz). Von hinten
+ // aufgehoben, sieht jede Einheit den endgültigen Bestand — von vorn sähe sie die
+ // Geschwister, die derselbe Befehl gleich mit beseitigt.
+ return ausStellen(m.group(1), true, s -> new Aufhebung(kontext.plus(s), provenienz));
}
if ((m = UEBERSCHRIFT_STREICHUNG.matcher(text)).matches()) {
@@ -1954,6 +1958,11 @@ final class BefehlErkenner {
*/
private static Optional<Aenderungsbefehl> ausStellen(
String phrase, Function<Stelle, Aenderungsbefehl> bauer) {
+ return ausStellen(phrase, false, bauer);
+ }
+
+ private static Optional<Aenderungsbefehl> ausStellen(
+ String phrase, boolean absteigend, Function<Stelle, Aenderungsbefehl> bauer) {
var stellen = StellenParser.parseMehrfach(phrase);
if (stellen.isEmpty()) {
// „Im Satzteil vor Nummer 1 …“, „In der Angabe vor Nummer 1 …“ — reiner Chapeau-Qualifier
@@ -1966,7 +1975,8 @@ final class BefehlErkenner {
if (stellen.size() == 1) {
return Optional.of(bauer.apply(stellen.get(0)));
}
- return Optional.of(new Sammelbefehl(stellen.stream().map(bauer).toList()));
+ var geordnet = absteigend ? stellen.reversed() : stellen;
+ return Optional.of(new Sammelbefehl(geordnet.stream().map(bauer).toList()));
}
/**
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
index 20dde3e..13a235a 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
@@ -769,14 +769,10 @@ public final class BefehlAnwender {
return TextErgebnis.fehler("Der Text endet nicht mit „" + befehl.alt() + "“.");
}
var rumpf = gestutzt.substring(0, gestutzt.length() - befehl.alt().length());
- // Tritt an die Stelle des Satzzeichens ein Klammerzusatz („Der Punkt am Ende wird
- // durch die Angabe „(Gesellschaftsdialog).“ ersetzt“), gehört davor ein Leerzeichen
- // —
- // so setzt es auch die amtliche Nachfassung.
var fuge =
- befehl.neu().startsWith("(")
+ brauchtFuge(befehl.alt(), befehl.neu())
&& !rumpf.isEmpty()
- && Character.isLetterOrDigit(rumpf.charAt(rumpf.length() - 1))
+ && !Character.isWhitespace(rumpf.charAt(rumpf.length() - 1))
? " "
: "";
return TextErgebnis.ok(rumpf + fuge + befehl.neu());
@@ -793,10 +789,42 @@ public final class BefehlAnwender {
+ anzahl
+ "-mal vor (ohne „jeweils“ mehrdeutig).");
}
+ if (brauchtFuge(befehl.alt(), befehl.neu())) {
+ return TextErgebnis.ok(ersetzeMitFuge(text, befehl.alt(), befehl.neu()));
+ }
return TextErgebnis.ok(text.replace(befehl.alt(), befehl.neu()));
}));
}
+ private static final Pattern NUR_SATZZEICHEN = Pattern.compile("\\p{Punct}+");
+
+ /**
+ * Tritt an die Stelle eines Satzzeichens ein Wort oder ein Klammerzusatz, so gehört ein
+ * Leerzeichen davor. „Die bisherige Nr. 7 wird Nr. 5 und das Komma wird durch das Wort „und“
+ * ersetzt“ führt sonst auf „…Fachkräfte)und“ statt auf „…Fachkräfte) und“; so setzt es auch die
+ * amtliche Nachfassung. Maßgeblich ist, dass das Ersetzte <em>nur</em> aus Satzzeichen besteht —
+ * eine Ersetzung ganzer Wörter bringt ihren Zwischenraum schon mit.
+ */
+ private static boolean brauchtFuge(String alt, String neu) {
+ return NUR_SATZZEICHEN.matcher(alt).matches()
+ && !neu.isEmpty()
+ && (Character.isLetter(neu.codePointAt(0)) || neu.charAt(0) == '(');
+ }
+
+ private static String ersetzeMitFuge(String text, String alt, String neu) {
+ var sb = new StringBuilder();
+ int von = 0;
+ for (int idx = text.indexOf(alt); idx >= 0; idx = text.indexOf(alt, von)) {
+ sb.append(text, von, idx);
+ if (idx > 0 && !Character.isWhitespace(text.charAt(idx - 1))) {
+ sb.append(' ');
+ }
+ sb.append(neu);
+ von = idx + alt.length();
+ }
+ return sb.append(text, von, text.length()).toString();
+ }
+
private static AngewandteAenderung wendeStreichungAn(List<Norm> normen, Streichung befehl) {
return bearbeiteText(
normen,
@@ -1338,10 +1366,14 @@ public final class BefehlAnwender {
var absaetze = new ArrayList<>(norm.absaetze());
for (int i = 0; i < absaetze.size(); i++) {
if (nummer.equals(absaetze.get(i).nummer())) {
- // Als weggefallen markieren (Nummer behalten) — konsistent mit der Aufhebung über einen
- // Absatz-Lokator; eine etwaige Folge-Umnummerierung „Abs. N+1 wird Abs. N“ räumt den
- // weggefallenen Absatz dann weg.
- absaetze.set(i, new Absatz(nummer, "(weggefallen)"));
+ // Gestrichen wird die Bezeichnung, nicht der Absatz: „Die Absatzbezeichnung „(1)“ wird
+ // gestrichen“ nimmt dem Wortlaut seine Nummer und lässt ihn im Übrigen unberührt. Das
+ // unterscheidet diesen Befehl von der Aufhebung über einen Absatz-Lokator („Abs. 1 wird
+ // aufgehoben“), die den Wortlaut beseitigt und einen nummerierten Platzhalter
+ // zurücklässt. Beides zusammen — Streichung der Bezeichnung des einen, Aufhebung des
+ // anderen Absatzes — führt eine zweigliedrige Vorschrift auf einen unnummerierten
+ // Wortlaut zurück (so § 13 der hessischen Verkehrsrechts-Zuständigkeitsverordnung).
+ absaetze.set(i, new Absatz(null, absaetze.get(i).text()));
normen.set(aufloesung.normIndex(), norm.mitAbsaetzen(absaetze));
return angewandt(befehl, norm.enbez());
}
@@ -1372,7 +1404,15 @@ public final class BefehlAnwender {
var norm = normen.get(fundstelle.normIndex());
var absaetze = new ArrayList<>(norm.absaetze());
var alter = absaetze.get(fundstelle.absatzIndex());
- absaetze.set(fundstelle.absatzIndex(), new Absatz(alter.nummer(), "(weggefallen)"));
+ // Ein Platzhalter braucht eine Nummer, um einen Platz zu halten. Führt die Norm nach der
+ // Aufhebung keine Absatzzählung mehr — weil ihr erster Absatz unnummeriert ist — und steht
+ // der aufgehobene Absatz am Ende, so ist kein Platz zu halten: der Absatz entfällt ganz.
+ boolean ohneZaehlung = !absaetze.isEmpty() && absaetze.get(0).nummer() == null;
+ if (ohneZaehlung && fundstelle.absatzIndex() == absaetze.size() - 1) {
+ absaetze.remove((int) fundstelle.absatzIndex());
+ } else {
+ absaetze.set(fundstelle.absatzIndex(), new Absatz(alter.nummer(), "(weggefallen)"));
+ }
normen.set(fundstelle.normIndex(), norm.mitAbsaetzen(absaetze));
return angewandt(befehl, norm.enbez());
}
@@ -1383,7 +1423,7 @@ public final class BefehlAnwender {
befehl,
(text, bereich) -> {
var label = labelVon(stelle);
- if (label != null) {
+ if (label != null && haeltPlatz(text, bereich.bis(), label)) {
var einrueckung = einrueckungVon(text, bereich.von());
return TextErgebnis.ok(
text.substring(0, bereich.von())
@@ -2024,6 +2064,26 @@ public final class BefehlAnwender {
return null;
}
+ /**
+ * Hält die aufgehobene Aufzählungseinheit einen Platz? Ein Platzhalter „9. (weggefallen)“ ist nur
+ * dort sinnvoll, wo ihm eine weitere Einheit derselben Art folgt, deren Bezeichnung er unberührt
+ * lassen soll. Steht die aufgehobene Einheit am Ende der Aufzählung — auch dann, wenn ihr nur
+ * weitere Platzhalter folgen —, so ist kein Platz zu halten und sie entfällt ganz. So endet § 14
+ * der hessischen Verkehrsrechts-Zuständigkeitsverordnung nach Aufhebung der Nrn. 9 bis 11 mit der
+ * Nr. 6, wie es auch die amtliche Nachfassung tut.
+ */
+ private static boolean haeltPlatz(String text, int ab, String label) {
+ var art =
+ label.endsWith(")")
+ ? Pattern.compile("^\\s*[a-zA-Z]{1,2}\\)\\s+(.*)$")
+ : Pattern.compile("^\\s*\\d+[a-z]?\\.\\s+(.*)$");
+ return text.substring(ab)
+ .lines()
+ .map(art::matcher)
+ .filter(java.util.regex.Matcher::matches)
+ .anyMatch(m -> !m.group(1).strip().equals("(weggefallen)"));
+ }
+
private static String einrueckungVon(String text, int position) {
int i = position;
var sb = new StringBuilder();
diff --git a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java
index 14f8da0..559160b 100644
--- a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java
+++ b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java
@@ -62,6 +62,16 @@ final class LandesRechtTextParser {
Pattern.compile(
"^(Buch|Teil|Kapitel|Abschnitt|Unterabschnitt|Titel) (\\d+[a-z]?)\\s+(\\S.*)$");
+ // Gliederungs-Überschrift mit ausgeschriebenem Ordinale und nachgestelltem Schlüsselwort
+ // („Erster Teil“, „Achtundzwanzigster Teil“). So gliedern Hessen und das ältere Bundesrecht.
+ // Der Titel steht — wie bei GLIEDERUNG_ARABISCH — auf derselben Zeile; damit ein Satz, der
+ // zufällig so beginnt („Erster Teil der Verordnung ist …“), nicht als Überschrift gilt, muss er
+ // dieselbe Probe bestehen wie ein Normtitel: großgeschriebener Anfang, kein Schlusspunkt.
+ private static final Pattern GLIEDERUNG_ORDINALWORT =
+ Pattern.compile(
+ "^(\\p{Lu}\\p{Ll}*(?:ter|ster)) (Buch|Teil|Kapitel|Abschnitt|Unterabschnitt|Titel)"
+ + "(?:\\s+((?:\\p{Lu}|\\().*[^.]))?\\s*$");
+
// Römische Gliederung ohne Schlüsselwort („I. Rechtsform und Aufgaben“, NRW). Sie ist nur an der
// Stellung erkennbar, deshalb gilt dieselbe Absicherung wie für UNTER_GLIEDERUNG: kurzer,
// großgeschriebener, satzzeichenfreier Titel und ein Normkopf oder eine weitere Überschrift als
@@ -83,7 +93,12 @@ final class LandesRechtTextParser {
// Zeilenanfang aus („Art. 4 Abs. 3 …“, „§ 5 Absatz 2 …“).
private static final Pattern NORM_KOPF =
Pattern.compile(
- "^(§|Art\\.)\\s+(\\d+[a-z]?)\\s+"
+ "^(§|Art\\.)\\s+(\\d+[a-z]?)"
+ // Die Überschrift darf fehlen: Verordnungen führen ihre Paragraphen vielfach ohne
+ // Titel (so die hessische Verkehrsrechts-Zuständigkeitsverordnung). Eine Zeile, die
+ // aus nichts als der Bezeichnung besteht, ist dann der Normkopf; ein Querverweis
+ // steht nie allein auf einer Zeile, und die Monotonieprobe sichert zusätzlich ab.
+ + "(?:\\s+"
// Querverweis-Schlüsselwörter nur als ganzes Wort ausschließen: „§ 4 Satz 2“ ist ein
// Verweis, „§ 4 Satzungen“ dagegen ein Normtitel. Der Schutz „(?![a-zäöüß])“
// verhindert,
@@ -94,7 +109,7 @@ final class LandesRechtTextParser {
// ganzen Satz. So wird ein am Zeilenanfang stehender Querverweis-Satz („§ 7
// GAPInVeKoSG
// findet entsprechend Anwendung.“) nicht fälschlich als Normkopf „§ 7“ gelesen.
- + "((?:\\p{Lu}|\\().*[^.])\\s*$");
+ + "((?:\\p{Lu}|\\().*[^.]))?\\s*$");
private static final Pattern WEGGEFALLEN_TITEL =
Pattern.compile("^\\((?:aufgehoben|weggefallen)\\)$");
@@ -155,6 +170,7 @@ final class LandesRechtTextParser {
var gliederung = zeile != null ? GLIEDERUNG.matcher(zeile) : null;
var arabisch = zeile != null ? GLIEDERUNG_ARABISCH.matcher(zeile) : null;
+ var ordinalwort = zeile != null ? GLIEDERUNG_ORDINALWORT.matcher(zeile) : null;
var roemisch = zeile != null ? GLIEDERUNG_ROEMISCH.matcher(zeile) : null;
var unterGliederung = zeile != null ? UNTER_GLIEDERUNG.matcher(zeile) : null;
var normKopf = zeile != null ? NORM_KOPF.matcher(zeile) : null;
@@ -167,6 +183,7 @@ final class LandesRechtTextParser {
zeile != null
&& (gliederung.matches()
|| arabisch.matches()
+ || ordinalwort.matches()
|| (roemisch.matches() && istUnterGliederung(roemisch, zeilen, i))
|| (unterGliederung.matches() && istUnterGliederung(unterGliederung, zeilen, i)))
&& (!INHALTSUEBERSICHT.equals(normEnbez) || folgtNormkopf(zeilen, i));
@@ -208,9 +225,19 @@ final class LandesRechtTextParser {
arabisch.group(1) + " " + arabisch.group(2),
titel.isEmpty() ? null : titel);
gliederungen.add(aktuelleGliederung);
+ } else if (ordinalwort.matches()) {
+ gliederungsZaehler++;
+ elternKennzahl = String.format("%03d", gliederungsZaehler);
+ var titel = ordinalwort.group(3) != null ? ordinalwort.group(3).strip() : "";
+ aktuelleGliederung =
+ new Gliederung(
+ elternKennzahl,
+ ordinalwort.group(1) + " " + ordinalwort.group(2),
+ titel.isEmpty() ? null : titel);
+ gliederungen.add(aktuelleGliederung);
} else if (normKopf.matches() && istNeuerNormKopf(normKopf.group(2), letzteNormNummer)) {
normEnbez = normKopf.group(1) + " " + normKopf.group(2);
- normTitel = normKopf.group(3).strip();
+ normTitel = normKopf.group(3) != null ? normKopf.group(3).strip() : null;
letzteNormNummer = numerisch(normKopf.group(2));
} else if (roemisch.matches()) {
gliederungsZaehler++;
@@ -248,6 +275,7 @@ final class LandesRechtTextParser {
return INHALTSUEBERSICHT.equals(zeile)
|| GLIEDERUNG.matcher(zeile).matches()
|| GLIEDERUNG_ARABISCH.matcher(zeile).matches()
+ || GLIEDERUNG_ORDINALWORT.matcher(zeile).matches()
|| NORM_KOPF.matcher(zeile).matches()
|| (roemisch.matches() && istUnterGliederung(roemisch, zeilen, i));
}
@@ -304,7 +332,12 @@ final class LandesRechtTextParser {
var titel = unterGliederung.group(2);
if (titel.length() > 80
|| !Character.isUpperCase(titel.codePointAt(0))
- || titel.matches(".*[.,;:]$")) {
+ || titel.matches(".*[.,;:]$")
+ // Eine Überschrift endet nicht auf einer Konjunktion — ein Aufzählungsglied schon, und
+ // zwar gerade dann, wenn ihm das nächste folgt und die Stellungsprobe deshalb anschlägt
+ // („2. Aufsicht über technische Prüfstellen nach § 13 Abs. 1 Satz 1 und“, § 17 der
+ // hessischen Verkehrsrechts-Zuständigkeitsverordnung).
+ || titel.matches("(?s).*\\b(und|oder|sowie)$")) {
return false;
}
for (int j = index + 1; j < zeilen.size(); j++) {