aboutsummaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java36
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/Satznummerierung.java90
-rw-r--r--src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java43
3 files changed, 166 insertions, 3 deletions
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
index cccba63..15fe2ed 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
@@ -111,7 +111,13 @@ public final class BefehlAnwender {
}
protokoll.addAll(verdichte(befehle, schritte, ergebnisse));
- var neu = alt.mitNormen(normen).mitGliederungen(gliederungen);
+ // Führt das Gesetz amtliche Satznummern, so erbt sie auch der neu eingesetzte Wortlaut: Das
+ // Gesetzblatt zitiert ohne Zählung, die Fassung führt sie.
+ var fortgeschrieben =
+ Satznummerierung.fuehrtSatznummern(alt)
+ ? Satznummerierung.schreibeFort(alt, normen)
+ : normen;
+ var neu = alt.mitNormen(fortgeschrieben).mitGliederungen(gliederungen);
if (neuerLangtitel != null) {
neu = neu.mitLangue(neuerLangtitel);
}
@@ -359,15 +365,41 @@ public final class BefehlAnwender {
befehl, Status.MANUELL_PRUEFEN, String.join(" ", fehler), betroffene, ersterGrund);
}
- /** Bezeichnungen, die ein Befehl freigibt — die Ausgangsstellen seiner Umnummerierungen. */
+ /**
+ * Bezeichnungen, die ein Befehl freigibt — die Ausgangsstellen seiner Umnummerierungen und die
+ * Stellen seiner Aufhebungen.
+ *
+ * <p>Dass auch eine Aufhebung räumt, ist nicht selbstverständlich, denn sie hinterlässt vielfach
+ * einen Platzhalter („9. (weggefallen)“) und gibt die Bezeichnung dann gerade nicht frei. Ob das
+ * geschieht, steht erst am Bestand fest ({@code haeltPlatz}), also nach der Ordnung. Vorgezogen
+ * wird die Aufhebung gleichwohl: Sie rückt nur gegenüber Schritten vor, die dieselbe Bezeichnung
+ * neu vergeben — und wo eine Bezeichnung zugleich weggefallen und neu vergeben sein soll,
+ * widerspräche das Gesetz sich selbst. So ist es im GEG (§ 108 Absatz 1): Die Kaskade macht die
+ * bisherigen Nummern 4 bis 6 zu 8 bis 10, während die bisherige Nummer 9 aufgehoben wird. Läuft
+ * die Aufhebung nach der Umnummerierung, sucht sie eine Nummer 9, die inzwischen eine andere
+ * Einheit ist.
+ */
private static Set<String> geraeumteBezeichnungen(Aenderungsbefehl befehl) {
var raeumt = new LinkedHashSet<String>();
for (var u : umnummerierungen(befehl)) {
raeumt.add(u.stelle().anzeigeText());
}
+ for (var a : aufhebungen(befehl)) {
+ raeumt.add(a.stelle().anzeigeText());
+ }
return raeumt;
}
+ /** Die Aufhebungen eines Befehls — auch die in einem Verbund. */
+ private static List<Aufhebung> aufhebungen(Aenderungsbefehl befehl) {
+ return switch (befehl) {
+ case Aufhebung a -> List.of(a);
+ case Sammelbefehl s ->
+ s.teilbefehle().stream().flatMap(t -> aufhebungen(t).stream()).toList();
+ default -> List.of();
+ };
+ }
+
/** Bezeichnungen, die ein Befehl neu vergibt — Umnummerierungsziele und eingefügte Einheiten. */
private static Set<String> belegteBezeichnungen(Aenderungsbefehl befehl) {
var belegt = new LinkedHashSet<String>();
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/Satznummerierung.java b/src/main/java/eu/mulk/aendggner/anwendung/Satznummerierung.java
new file mode 100644
index 0000000..5165546
--- /dev/null
+++ b/src/main/java/eu/mulk/aendggner/anwendung/Satznummerierung.java
@@ -0,0 +1,90 @@
+// SPDX-FileCopyrightText: 2020 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+// SPDX-License-Identifier: AGPL-3.0-or-later
+package eu.mulk.aendggner.anwendung;
+
+import eu.mulk.aendggner.gesetz.Absatz;
+import eu.mulk.aendggner.gesetz.Gesetz;
+import eu.mulk.aendggner.gesetz.Norm;
+import eu.mulk.aendggner.gesetz.Superskript;
+import java.util.ArrayList;
+import java.util.List;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Amtliche Satznummern für neu gesetzten Text.
+ *
+ * <p>Ein Teil der Länder zählt die Sätze eines Absatzes amtlich mit („(2) ¹Gegen einen … ²In diesem
+ * Fall …“). Das Gesetzblatt, aus dem der neue Wortlaut zitiert wird, setzt diese Zählung nicht —
+ * sie gehört zur Fassung, nicht zum Verkündungstext. Wer den Zitatwortlaut unverändert einsetzt,
+ * erhält deshalb einen Absatz, der als einziger seines Gesetzes ohne Satzzählung dasteht; die
+ * amtliche Nachfassung führt sie (Berlin, ASOG § 67 Absatz 2).
+ *
+ * <p>Angefasst wird nur, was angefasst werden muss: ein Absatz, der sich geändert hat, keine
+ * Zählung trägt, aus mehr als einem Satz besteht und auf einer Zeile steht. Die Mehrzeiligkeit
+ * schließt Aufzählungen aus — dort ist der Chapeau ein Satz und jedes Glied ein eigener Block, und
+ * eine fortlaufende Satzzählung wäre falsch.
+ */
+final class Satznummerierung {
+
+ private Satznummerierung() {}
+
+ /** Wahr, wenn das Gesetz die Sätze seiner Absätze amtlich zählt. */
+ static boolean fuehrtSatznummern(Gesetz gesetz) {
+ for (var norm : gesetz.normen()) {
+ for (var absatz : norm.absaetze()) {
+ if (Superskript.traegtSatznummern(absatz.text())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /** Schreibt die Satzzählung in den geänderten Absätzen fort. */
+ static List<Norm> schreibeFort(Gesetz alt, List<Norm> normen) {
+ var ergebnis = new ArrayList<Norm>(normen.size());
+ for (var norm : normen) {
+ var alteNorm = alt.norm(norm.enbez()).orElse(null);
+ var absaetze = new ArrayList<Absatz>(norm.absaetze().size());
+ boolean geaendert = false;
+ for (var absatz : norm.absaetze()) {
+ var neuerText = fortgeschrieben(absatz, alteNorm);
+ geaendert |= !neuerText.equals(absatz.text());
+ absaetze.add(neuerText.equals(absatz.text()) ? absatz : absatz.mitText(neuerText));
+ }
+ ergebnis.add(geaendert ? norm.mitAbsaetzen(absaetze) : norm);
+ }
+ return ergebnis;
+ }
+
+ private static String fortgeschrieben(Absatz absatz, @Nullable Norm alteNorm) {
+ var text = absatz.text();
+ if (text.contains("\n") || Superskript.traegtSatznummern(text)) {
+ return text;
+ }
+ if (alteNorm != null && trugDenselbenText(alteNorm, absatz)) {
+ return text;
+ }
+ var saetze = SatzTeiler.teileTexte(text);
+ if (saetze.size() < 2) {
+ return text;
+ }
+ var sb = new StringBuilder();
+ for (int i = 0; i < saetze.size(); i++) {
+ if (i > 0) {
+ sb.append(' ');
+ }
+ sb.append(Superskript.zuSuperskript(String.valueOf(i + 1))).append(saetze.get(i));
+ }
+ return sb.toString();
+ }
+
+ private static boolean trugDenselbenText(Norm alteNorm, Absatz absatz) {
+ for (var alterAbsatz : alteNorm.absaetze()) {
+ if (java.util.Objects.equals(alterAbsatz.nummer(), absatz.nummer())) {
+ return alterAbsatz.text().equals(absatz.text());
+ }
+ }
+ return false;
+ }
+}
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 ae1e861..d1d4002 100644
--- a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java
+++ b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java
@@ -411,6 +411,34 @@ final class LandesRechtTextParser {
return false;
}
+ /**
+ * Ein Zwischentitel gliedert einen Katalog, ohne selbst eine Vorschrift zu sein: „Aus dem Bereich
+ * Verkehr:“. Er steht für sich auf einer Zeile und endet auf einem Doppelpunkt.
+ */
+ private static boolean istZwischentitel(String zeile) {
+ return zeile.endsWith(":")
+ && !ABSATZ_MARKER.matcher(zeile).find()
+ && !AUFZAEHLUNGS_MARKE.matcher(zeile).find();
+ }
+
+ /**
+ * Wahr, wenn auf die Zeile {@code index} ein bezeichneter Absatz folgt. Nur dann ist ein
+ * Doppelpunkt am Zeilenende ein Zwischentitel und kein Chapeau, der seine Aufzählung einleitet.
+ */
+ private static boolean folgtAbsatz(List<String> zeilen, int index) {
+ for (int i = index + 1; i < zeilen.size(); i++) {
+ var naechste = zeilen.get(i).strip();
+ if (naechste.isEmpty()) {
+ continue;
+ }
+ return ABSATZ_MARKER.matcher(naechste).find();
+ }
+ return false;
+ }
+
+ private static final Pattern AUFZAEHLUNGS_MARKE =
+ Pattern.compile("^(?:\\d+[a-z]?\\.|[a-z]{1,2}\\))\\s");
+
private static Norm baueNorm(
String enbez, @Nullable String titel, @Nullable Gliederung gliederung, List<String> zeilen) {
boolean weggefallen = titel != null && WEGGEFALLEN_TITEL.matcher(titel).matches();
@@ -418,11 +446,24 @@ final class LandesRechtTextParser {
var absaetze = new ArrayList<Absatz>();
String absatzNummer = null;
var absatzZeilen = new ArrayList<String>();
- for (var zeile : zeilen) {
+ for (int i = 0; i < zeilen.size(); i++) {
+ var zeile = zeilen.get(i);
var gestutzt = zeile.strip();
if (gestutzt.isEmpty()) {
continue;
}
+ if (istZwischentitel(gestutzt) && !absatzZeilen.isEmpty() && folgtAbsatz(zeilen, i)) {
+ // Ein Zwischentitel trägt keine Absatzbezeichnung und gehört doch nicht zum
+ // vorangehenden Absatz: Dessen Text endete sonst nicht dort, wo er endet — und ein
+ // Befehl wie „In Absatz 4a wird der Punkt am Ende durch ein Semikolon ersetzt“ fände
+ // seinen Punkt nicht. Er wird deshalb ein eigener, bezeichnungsloser Absatz, wie ihn
+ // der Vorspann einer Norm ohnehin bildet.
+ absaetze.add(new Absatz(absatzNummer, String.join("\n", absatzZeilen)));
+ absatzZeilen.clear();
+ absatzNummer = null;
+ absaetze.add(new Absatz(null, gestutzt));
+ continue;
+ }
var fussnote = FUSSNOTE.matcher(gestutzt);
if (fussnote.matches()) {
// Fußnoten verbleiben superskript-normalisiert als eigene Zeile im tragenden Absatz.