aboutsummaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-24 20:47:09 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-24 20:47:09 +0200
commita88d5707a4a58b1d37db97b211770d3509e95ee5 (patch)
treead557ef63a87dfc33396818f7095d9e1ffd5a8b0 /src/main/java
parentb682c225ab39859b6488ad2c327c6a67bbaf5b50 (diff)
Drei benannte Reste, drei allgemeine Mängel
Sie standen als begründete Kommentare in den Prüfungen, jeder mit seinem Belegfall — und jeder war allgemein, nicht die Eigenheit seines Falls. Erstens räumt auch eine Aufhebung eine Bezeichnung. Die Schritt-Ordnung kannte bisher nur Umnummerierungen als Räumende. Im GEG macht ein Punkt der Kaskade des § 108 Absatz 1 aus den bisherigen Nummern 4 bis 6 die Nummern 8 bis 10, während ein anderer die bisherige Nummer 9 aufhebt; die Aufhebung suchte danach eine Nummer 9, die inzwischen eine andere Einheit war. Dass eine Aufhebung mitunter einen Platzhalter hinterlässt und dann nichts freigibt, steht erst am Bestand fest und schadet nicht: Vorrang erhält sie nur gegenüber Schritten, die dieselbe Bezeichnung neu vergeben, und eine zugleich weggefallene und neu vergebene Bezeichnung gibt es nicht. Zweitens ist ein Zwischentitel kein Teil des vorangehenden Absatzes. „Aus dem Bereich Verkehr:“ trägt keine Absatzbezeichnung und wurde deshalb angehängt — der Absatz endete dann nicht auf den Punkt, den „In Absatz 4a wird der Punkt am Ende durch ein Semikolon ersetzt“ meint. Er ist jetzt ein eigener, bezeichnungsloser Absatz, wie ihn der Vorspann einer Norm ohnehin bildet. Drittens erbt neu eingesetzter Wortlaut die amtliche Satzzählung. Führt das Stammgesetz sie, so trägt der aus dem Gesetzblatt zitierte Text sie gleichwohl nicht: Sie gehört zur Fassung, nicht zum Verkündungstext. Angefasst wird nur ein geänderter, einzeiliger, mehrsätziger Absatz ohne Zählung — die Mehrzeiligkeit hält Aufzählungen heraus. Geprüft: 348 Tests, REUSE 180/180. GEG 117/2 (war 116/3), dessen Beschlussempfehlung 70 (war 69), § 108 Absatz 1 mit lückenloser Nummernfolge 1 bis 32. Berlin ASOG 6/6 und alle 171 Normen gleich der amtlichen Nachfassung — die beiden benannten Abweichungen sind fort. Bayern, Niedersachsen und die übrigen Belegfälle unverändert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Change-Id: I47d3f9f150cb628ba5a298e95f2c647dac6ffa45
Diffstat (limited to 'src/main/java')
-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.