diff options
Diffstat (limited to 'src/main/java/eu')
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. |
