| // 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.aenderung.Aenderungsbefehl; |
| import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Anfuegung; |
| import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Aufhebung; |
| import eu.mulk.aendggner.aenderung.Aenderungsbefehl.BereichsUmnummerierung; |
| import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Ebene; |
| import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Ersetzung; |
| import eu.mulk.aendggner.aenderung.Aenderungsbefehl.FussnotenAufhebung; |
| import eu.mulk.aendggner.aenderung.Aenderungsbefehl.GliederungsUeberschriften; |
| import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Neufassung; |
| import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Sammelbefehl; |
| import eu.mulk.aendggner.aenderung.Aenderungsbefehl.SatznummerierungStreichung; |
| import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Streichung; |
| import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturEinfuegung; |
| import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturErsetzung; |
| import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Umnummerierung; |
| import eu.mulk.aendggner.aenderung.Aenderungsbefehl.UnbekannterBefehl; |
| import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WoerterEinfuegung; |
| import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortAnker; |
| import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortlautVoranstellung; |
| import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortlautZuAbsatz; |
| import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortlautZuSatz; |
| import eu.mulk.aendggner.aenderung.Stelle; |
| import eu.mulk.aendggner.gesetz.Absatz; |
| import eu.mulk.aendggner.gesetz.Gesetz; |
| import eu.mulk.aendggner.gesetz.Gliederung; |
| import eu.mulk.aendggner.gesetz.Norm; |
| import eu.mulk.aendggner.gesetz.Superskript; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.regex.Pattern; |
| import org.jspecify.annotations.Nullable; |
| |
| /** |
| * Wendet Änderungsbefehle nacheinander auf ein Stammgesetz an (die Reihenfolge im Änderungsgesetz |
| * ist maßgeblich). Jeder Befehl erhält einen Protokolleintrag; was nicht sicher angewandt werden |
| * kann, wird mit Begründung als „manuell prüfen“ markiert — niemals stillschweigend verworfen. |
| */ |
| public final class BefehlAnwender { |
| |
| public enum Status { |
| ANGEWANDT, |
| MANUELL_PRUEFEN, |
| /** Der Befehl gehört zu einem Teil des Änderungsgesetzes, der am Stichtag noch nicht galt. */ |
| NICHT_IN_KRAFT |
| } |
| |
| /** |
| * Ein Protokolleintrag: was mit dem Befehl geschehen ist. Der ausformulierte Grund bleibt |
| * maßgeblich; {@code grund} ordnet ihn nur einer Art zu und ist bei angewandten Befehlen leer. |
| */ |
| public record AngewandteAenderung( |
| Aenderungsbefehl befehl, |
| Status status, |
| String begruendung, |
| Set<String> betroffeneEnbez, |
| @Nullable Grund grund) {} |
| |
| public record AnwendungsErgebnis(Gesetz neu, List<AngewandteAenderung> protokoll) { |
| |
| public long anzahlAngewandt() { |
| return protokoll.stream().filter(a -> a.status() == Status.ANGEWANDT).count(); |
| } |
| |
| public long anzahlManuell() { |
| return protokoll.stream().filter(a -> a.status() == Status.MANUELL_PRUEFEN).count(); |
| } |
| } |
| |
| // Absatzmarker in Zitaten: „(1) “ am Zeilenanfang oder nach Leerraum (PDF-Umbrüche verwischen |
| // die Zeilenstruktur). |
| private static final Pattern ABSATZ_MARKER = |
| Pattern.compile("(?m)(?:^|(?<=\\s))\\((\\d+[a-z]?)\\)\\s+"); |
| |
| private BefehlAnwender() {} |
| |
| public static AnwendungsErgebnis anwenden(Gesetz alt, List<Aenderungsbefehl> befehle) { |
| var normen = new ArrayList<>(alt.normen()); |
| var gliederungen = new ArrayList<>(alt.gliederungen()); |
| var protokoll = new ArrayList<AngewandteAenderung>(); |
| String neuerLangtitel = null; |
| |
| // Bereichsweise Umnummerierungen nennen Bezeichnungen, meinen aber Einheiten — erst am Gesetz |
| // steht fest, welche. Sie werden deshalb zuerst entfaltet; alles Weitere sieht nur noch |
| // gewöhnliche Umnummerierungen. |
| befehle = entfalteBereiche(befehle, normen); |
| |
| // Angewandt wird schrittweise in Sachreihenfolge, protokolliert befehlsweise in der |
| // Reihenfolge des Änderungsgesetzes. |
| var schritte = schritte(befehle); |
| var ergebnisse = new AngewandteAenderung[schritte.size()]; |
| for (int index : anwendungsReihenfolge(schritte)) { |
| var schritt = schritte.get(index); |
| // „Die Überschrift wird wie folgt gefasst / durch die folgende Überschrift ersetzt“ auf |
| // oberster Ebene meint die Überschrift des Gesetzes selbst — innerhalb eines Verbunds |
| // dagegen nie, dort steht sie für die Überschrift der Einheit, die der Verbund betrifft. |
| if (schritt.ganzerBefehl() |
| && schritt.teil() instanceof Neufassung n |
| && istNurUeberschrift(n.stelle())) { |
| neuerLangtitel = n.neuerText().replaceAll("\\s+", " ").strip(); |
| ergebnisse[index] = angewandt(schritt.teil(), "(Gesetzesüberschrift)"); |
| continue; |
| } |
| ergebnisse[index] = wendeAn(normen, gliederungen, schritt.teil()); |
| } |
| protokoll.addAll(verdichte(befehle, schritte, ergebnisse)); |
| |
| // 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); |
| } |
| return new AnwendungsErgebnis(neu, protokoll); |
| } |
| |
| /** |
| * Ein einzeln anzuwendender Schritt: der Teilbefehl selbst und der Index des Befehls, in dessen |
| * Protokolleintrag sein Ergebnis eingeht. Ein Verbund ({@link Sammelbefehl}) zerfällt in je einen |
| * Schritt pro Teilbefehl; jeder andere Befehl ist sein eigener einziger Schritt ({@code |
| * ganzerBefehl}). |
| */ |
| private record Schritt(int befehlIndex, Aenderungsbefehl teil, boolean ganzerBefehl) {} |
| |
| /** Faltet die Befehlsliste zur Schrittliste auf, in Dokumentreihenfolge. */ |
| private static List<Schritt> schritte(List<Aenderungsbefehl> befehle) { |
| var schritte = new ArrayList<Schritt>(befehle.size()); |
| for (int i = 0; i < befehle.size(); i++) { |
| var befehl = befehle.get(i); |
| if (befehl instanceof Sammelbefehl s) { |
| falte(s, i, schritte); |
| } else { |
| schritte.add(new Schritt(i, befehl, true)); |
| } |
| } |
| return schritte; |
| } |
| |
| private static void falte(Sammelbefehl befehl, int befehlIndex, List<Schritt> ziel) { |
| for (var teil : befehl.teilbefehle()) { |
| if (teil instanceof Sammelbefehl geschachtelt) { |
| falte(geschachtelt, befehlIndex, ziel); |
| } else { |
| ziel.add(new Schritt(befehlIndex, teil, false)); |
| } |
| } |
| } |
| |
| /** |
| * Anwendungsreihenfolge der Schritte. Sie folgt dem Dokument, mit einer Ausnahme: |
| * Umnummerierungen beziehen sich stets auf die ursprüngliche Zählung, nicht auf den Stand nach |
| * den vorangegangenen Punkten. Wer eine Bezeichnung räumt, muss daher vor den kommen, der sie neu |
| * besetzt — sonst trüge das Gesetz vorübergehend zwei gleich bezeichnete Einheiten und die |
| * Fundstelle wäre mehrdeutig. Aus dieser einen Regel folgt beides: die absteigende Reihenfolge |
| * einer aufsteigenden Kaskade („Abs. 3 wird 4“, „Abs. 4 wird 5“, …) und der Vorrang einer |
| * Umnummerierung vor einer Einfügung, die deren Bezeichnung neu vergibt. |
| * |
| * <p>Verschoben wird stets nur nach vorn, und wer vorrückt, nimmt mit, was ihm selbst vorausgehen |
| * muss: Vor jedem Schritt laufen erst diejenigen, die ihm eine Bezeichnung räumen, und vor diesen |
| * wiederum die ihren. Ohne diese Mitnahme zerrisse eine Kette wie „Nr. 15 wird Nr. 16“, „Nrn. 13 |
| * und 14 werden die Nrn. 14 und 15“, „nach Nr. 12 wird Nr. 13 eingefügt“: die Einfügung zöge nur |
| * das letzte Glied vor sich, und dieses beträfe eine Bezeichnung, die noch besetzt ist. Ein |
| * Schritt ohne solche Voraussetzung bleibt dagegen an seinem Platz — daran hängt, dass eine |
| * Begleitänderung ihre Dokumentstelle behält. |
| * |
| * <p>Geordnet werden <em>Schritte</em> und nicht Befehle, weil ein Verbund aus Umnummerierung und |
| * Begleitänderung zwei gegenläufige zeitliche Ansprüche in einer Einheit trägt: Die |
| * Umnummerierung gehört nach vorn, ihre Begleitänderung aber an ihre Dokumentstelle, denn sie |
| * setzt die vorangegangenen Punkte als vollzogen voraus. „Die bisherige Nr. 11 wird Nr. 12 und |
| * die Angabe „schriftliche“ wird gestrichen“ (BayJG Art. 56 Abs. 1) trifft nach vorn gezogen noch |
| * zwei Fundstellen und wäre mehrdeutig; an seinem Platz belassen genau eine, weil der vorherige |
| * Punkt die andere längst ersetzt hat. |
| */ |
| /** |
| * Entfaltet jede {@link BereichsUmnummerierung} zu einzelnen {@link Umnummerierung}en, indem sie |
| * die im Ausgangsbereich <em>vorhandenen</em> Normen der Reihe nach den neuen Bezeichnungen |
| * zuordnet. Aufgehobene Platzhalter zählen nicht mit: Sie tragen keinen Inhalt, und zwei |
| * Einheiten gleicher Bezeichnung kann es nicht geben — dieselbe Regel, nach der ein Platzhalter |
| * einer Einfügung weicht. Geht die Zählung nicht auf, bleibt der Befehl ungeteilt und wird als |
| * manuell zu prüfen gemeldet. |
| */ |
| private static List<Aenderungsbefehl> entfalteBereiche( |
| List<Aenderungsbefehl> befehle, List<Norm> normen) { |
| var entfaltet = new ArrayList<Aenderungsbefehl>(befehle.size()); |
| for (var befehl : befehle) { |
| if (befehl instanceof BereichsUmnummerierung bereich) { |
| var teile = entfalte(bereich, normen); |
| entfaltet.add(teile != null ? teile : befehl); |
| } else { |
| entfaltet.add(befehl); |
| } |
| } |
| return entfaltet; |
| } |
| |
| private static @Nullable Aenderungsbefehl entfalte( |
| BereichsUmnummerierung bereich, List<Norm> normen) { |
| var von = paragraphNummer(bereich.stelle()); |
| var bis = paragraphNummer(bereich.bis()); |
| var neuVon = paragraphNummer(bereich.neu()); |
| var neuBis = paragraphNummer(bereich.neuBis()); |
| if (von == null || bis == null || neuVon == null || neuBis == null) { |
| return null; |
| } |
| var sigel = ((Stelle.Paragraph) bereich.stelle().komponenten().get(0)).sigel(); |
| var vorhanden = new ArrayList<String>(); |
| for (var norm : normen) { |
| var enbez = norm.enbez(); |
| if (enbez == null || !enbez.startsWith(sigel + " ") || norm.weggefallen()) { |
| continue; |
| } |
| var nummer = enbez.substring(sigel.length() + 1).strip(); |
| if (istImBereich(nummer, von, bis)) { |
| vorhanden.add(nummer); |
| } |
| } |
| int anzahl = neuBis - neuVon + 1; |
| if (vorhanden.size() != anzahl) { |
| return null; |
| } |
| var teile = new ArrayList<Aenderungsbefehl>(anzahl); |
| for (int i = 0; i < anzahl; i++) { |
| teile.add( |
| new Umnummerierung( |
| new Stelle(List.of(new Stelle.Paragraph(vorhanden.get(i), sigel))), |
| new Stelle(List.of(new Stelle.Paragraph(String.valueOf(neuVon + i), sigel))), |
| bereich.provenienz())); |
| } |
| return teile.size() == 1 ? teile.get(0) : new Sammelbefehl(teile); |
| } |
| |
| /** Die numerische Bezeichnung eines Paragraphen-/Artikelziels, sonst {@code null}. */ |
| private static @Nullable Integer paragraphNummer(Stelle stelle) { |
| if (stelle.komponenten().size() != 1 |
| || !(stelle.komponenten().get(0) instanceof Stelle.Paragraph p)) { |
| return null; |
| } |
| var ziffern = p.nummer().replaceAll("[^0-9]", ""); |
| return ziffern.isEmpty() ? null : Integer.valueOf(ziffern); |
| } |
| |
| /** Liegt die Bezeichnung („9“, „9a“) im Bereich der beiden Zahlen? */ |
| private static boolean istImBereich(String nummer, int von, int bis) { |
| var ziffern = nummer.replaceAll("[^0-9]", ""); |
| if (ziffern.isEmpty()) { |
| return false; |
| } |
| int n = Integer.parseInt(ziffern); |
| return n >= von && n <= bis; |
| } |
| |
| private static List<Integer> anwendungsReihenfolge(List<Schritt> schritte) { |
| int anzahl = schritte.size(); |
| var raeumt = new ArrayList<Set<String>>(anzahl); |
| var belegt = new ArrayList<Set<String>>(anzahl); |
| for (var schritt : schritte) { |
| raeumt.add(geraeumteBezeichnungen(schritt.teil())); |
| belegt.add(belegteBezeichnungen(schritt.teil())); |
| } |
| var reihenfolge = new ArrayList<Integer>(anzahl); |
| // 0 = offen, 1 = in Arbeit (Zyklus-Bremse), 2 = eingereiht. |
| var stand = new byte[anzahl]; |
| for (int i = 0; i < anzahl; i++) { |
| reiheEin(i, stand, raeumt, belegt, reihenfolge); |
| } |
| return reihenfolge; |
| } |
| |
| /** |
| * Reiht den Schritt ein, nachdem alle Schritte eingereiht sind, die ihm eine seiner Bezeichnungen |
| * räumen. Eine ringförmige Abhängigkeit — zwei Schritte, die einander räumen — bricht an der |
| * Marke „in Arbeit“ ab; die beteiligten Schritte behalten dann ihre Dokumentreihenfolge. |
| */ |
| private static void reiheEin( |
| int schritt, |
| byte[] stand, |
| List<Set<String>> raeumt, |
| List<Set<String>> belegt, |
| List<Integer> reihenfolge) { |
| if (stand[schritt] != 0) { |
| return; |
| } |
| stand[schritt] = 1; |
| var eigene = belegt.get(schritt); |
| if (!eigene.isEmpty()) { |
| for (int vorgaenger = 0; vorgaenger < stand.length; vorgaenger++) { |
| if (vorgaenger != schritt && raeumt.get(vorgaenger).stream().anyMatch(eigene::contains)) { |
| reiheEin(vorgaenger, stand, raeumt, belegt, reihenfolge); |
| } |
| } |
| } |
| stand[schritt] = 2; |
| reihenfolge.add(schritt); |
| } |
| |
| /** |
| * Verdichtet die Schrittergebnisse zu je einem Protokolleintrag pro Befehl. Ein Verbund gilt nur |
| * dann als angewandt, wenn jeder seiner Teile griff; sonst nennt die Begründung die gescheiterten |
| * Teile in ihrer Reihenfolge im Verbund. |
| */ |
| private static List<AngewandteAenderung> verdichte( |
| List<Aenderungsbefehl> befehle, List<Schritt> schritte, AngewandteAenderung[] ergebnisse) { |
| var protokoll = new ArrayList<AngewandteAenderung>(befehle.size()); |
| int schritt = 0; |
| for (int i = 0; i < befehle.size(); i++) { |
| int von = schritt; |
| while (schritt < schritte.size() && schritte.get(schritt).befehlIndex() == i) { |
| schritt++; |
| } |
| if (von == schritt) { |
| // Ein Verbund ohne Teilbefehle — nichts anzuwenden, aber auch nichts zu verschweigen. |
| protokoll.add(manuell(befehle.get(i), Grund.NICHT_ERKANNT, "Verbund ohne Teilbefehle.")); |
| continue; |
| } |
| if (schritte.get(von).ganzerBefehl()) { |
| protokoll.add(ergebnisse[von]); |
| continue; |
| } |
| protokoll.add( |
| verbund(befehle.get(i), Arrays.asList(ergebnisse).subList(von, schritt), schritte, von)); |
| } |
| return protokoll; |
| } |
| |
| private static AngewandteAenderung verbund( |
| Aenderungsbefehl befehl, |
| List<AngewandteAenderung> teilErgebnisse, |
| List<Schritt> schritte, |
| int von) { |
| var betroffene = new LinkedHashSet<String>(); |
| var fehler = new ArrayList<String>(); |
| Grund ersterGrund = null; |
| for (int k = 0; k < teilErgebnisse.size(); k++) { |
| var ergebnis = teilErgebnisse.get(k); |
| betroffene.addAll(ergebnis.betroffeneEnbez()); |
| if (ergebnis.status() != Status.ANGEWANDT) { |
| if (ersterGrund == null) { |
| ersterGrund = ergebnis.grund(); |
| } |
| fehler.add( |
| "Teil " |
| + (k + 1) |
| + " (" |
| + schritte.get(von + k).teil().stelle().anzeigeText() |
| + "): " |
| + ergebnis.begruendung()); |
| } |
| } |
| if (fehler.isEmpty()) { |
| return new AngewandteAenderung(befehl, Status.ANGEWANDT, "", betroffene, null); |
| } |
| // Ein Verbund kann aus mehreren Gründen liegenbleiben; maßgeblich für die Bündelung ist der |
| // erste, der aufgetreten ist. Die Wortlaute stehen ohnehin alle nebeneinander. |
| return new AngewandteAenderung( |
| befehl, Status.MANUELL_PRUEFEN, String.join(" ", fehler), betroffene, ersterGrund); |
| } |
| |
| /** |
| * 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>(); |
| for (var u : umnummerierungen(befehl)) { |
| belegt.add(u.neu().anzeigeText()); |
| } |
| for (var e : einfuegungen(befehl)) { |
| var bezeichnungen = neueBezeichnungen(e); |
| // Nennt der Befehl keine Einzelbezeichnung („die folgenden Nrn. 5 bis 7“), stehen sie als |
| // Aufzählungsmarken im eingefügten Block. |
| if (bezeichnungen.isEmpty()) { |
| bezeichnungen = markenBezeichnungen(e.stelle(), e.ebene(), e.text()); |
| } |
| for (var bezeichnung : bezeichnungen) { |
| // Trägt die neue Einheit die Bezeichnung ihres eigenen Ankers („Dem Abs. 1 wird folgender |
| // Abs. 1 vorangestellt“), so setzt der Befehl die alte Zählung voraus: er muss vor der |
| // Umnummerierung laufen, die sie auflöst, nicht danach. |
| if (!bezeichnung.equals(e.stelle().anzeigeText())) { |
| belegt.add(bezeichnung); |
| } |
| } |
| } |
| // „Absatz 2 wird durch die folgenden Absätze 2 und 3 ersetzt: „(2) … (3) …““ — welche |
| // Bezeichnungen der Ersatzblock vergibt, steht ebenfalls in seinen Marken. |
| for (var e : ersetzungen(befehl)) { |
| belegt.addAll(markenBezeichnungen(e.stelle(), e.ebene(), e.text())); |
| } |
| return belegt; |
| } |
| |
| // Aufzählungsmarken in Zitatblöcken, je Ebene. |
| private static final Pattern NUMMER_MARKER = Pattern.compile("(?m)^[ \\t]*(\\d+[a-z]?)\\.[ \\t]"); |
| private static final Pattern BUCHSTABE_MARKER = |
| Pattern.compile("(?m)^[ \\t]*([a-z]{1,3})\\)[ \\t]"); |
| |
| /** Die Bezeichnungen, die die Marken eines Zitatblocks vergeben, relativ zur Ankerstelle. */ |
| private static List<String> markenBezeichnungen(Stelle stelle, Ebene ebene, String text) { |
| var muster = |
| switch (ebene) { |
| case ABSATZ -> ABSATZ_MARKER; |
| case NUMMER -> NUMMER_MARKER; |
| case BUCHSTABE -> BUCHSTABE_MARKER; |
| default -> null; |
| }; |
| if (muster == null) { |
| return List.of(); |
| } |
| var bezeichnungen = new ArrayList<String>(); |
| var marker = muster.matcher(text); |
| while (marker.find()) { |
| bezeichnungen.add(mitMarke(stelle, ebene, marker.group(1))); |
| } |
| return bezeichnungen; |
| } |
| |
| /** Die Stelle mit ausgetauschter feinster Komponente („§ 3 Absatz 2“ → „§ 3 Absatz 3“). */ |
| private static String mitMarke(Stelle stelle, Ebene ebene, String bezeichnung) { |
| Stelle.Komponente komponente = |
| ebene == Ebene.BUCHSTABE |
| ? new Stelle.BuchstabeNr(bezeichnung) |
| : ebene == Ebene.NUMMER |
| ? new Stelle.NummerNr(bezeichnung) |
| : new Stelle.AbsatzNr(bezeichnung); |
| var komponenten = new ArrayList<>(stelle.komponenten()); |
| komponenten.removeIf(k -> rang(k) >= rang(komponente)); |
| komponenten.add(komponente); |
| return new Stelle(komponenten).anzeigeText(); |
| } |
| |
| private static final Pattern BEZEICHNUNGS_BEREICH = |
| Pattern.compile("(\\d+)(?:[a-z])? bis (\\d+)(?:[a-z])?"); |
| |
| /** |
| * Die vollen Bezeichnungen der eingefügten Einheiten: die Ankerstelle, deren feinste Komponente |
| * durch die neue Bezeichnung ersetzt ist („Nach Art. 29a Abs. 5 Satz 1 … folgender Satz 2“ → |
| * „Art. 29a Abs. 5 Satz 2“). Ein Block („die folgenden Nrn. 5 bis 7“) belegt alle Bezeichnungen |
| * des Bereichs. |
| */ |
| private static List<String> neueBezeichnungen(StrukturEinfuegung s) { |
| if (s.bezeichnung() == null) { |
| return List.of(); |
| } |
| var bereich = BEZEICHNUNGS_BEREICH.matcher(s.bezeichnung()); |
| var nummern = new ArrayList<String>(); |
| if (bereich.matches()) { |
| int von = Integer.parseInt(bereich.group(1)); |
| int bis = Integer.parseInt(bereich.group(2)); |
| for (int n = von; n <= bis && n - von < 100; n++) { |
| nummern.add(String.valueOf(n)); |
| } |
| } else { |
| nummern.add(s.bezeichnung()); |
| } |
| return nummern.stream().map(n -> volleBezeichnung(s, n)).toList(); |
| } |
| |
| private static String volleBezeichnung(StrukturEinfuegung s, String bezeichnung) { |
| var komponente = |
| switch (s.ebene()) { |
| case PARAGRAPH -> |
| new Stelle.Paragraph( |
| bezeichnung, s.stelle().paragraph().map(Stelle.Paragraph::sigel).orElse("§")); |
| case ABSATZ -> new Stelle.AbsatzNr(bezeichnung); |
| case SATZ -> new Stelle.SatzNr(bezeichnung); |
| case NUMMER -> new Stelle.NummerNr(bezeichnung); |
| case BUCHSTABE -> new Stelle.BuchstabeNr(bezeichnung); |
| }; |
| var komponenten = new ArrayList<>(s.stelle().komponenten()); |
| // Die letzte Komponente weicht, wenn sie gleich fein oder feiner ist als die neue („In § 5 |
| // Absatz 2 wird folgender Absatz 3 eingefügt“ meint § 5 Absatz 3, nicht § 5 Absatz 2 Absatz 3). |
| // Die Nummer einer Anlage weicht nicht: Sie gehört zur Bezeichnung der Norm und nicht zu ihrem |
| // Inneren, und ohne sie träfe der neue Absatz eine andere Norm als der Befehl. |
| boolean anlagenNummer = |
| s.stelle().anlagenEnbez().isPresent() |
| && !komponenten.isEmpty() |
| && s.stelle() |
| .nummer() |
| .filter(komponenten.get(komponenten.size() - 1)::equals) |
| .isPresent(); |
| if (!komponenten.isEmpty() |
| && !anlagenNummer |
| && rang(komponenten.get(komponenten.size() - 1)) >= rang(komponente)) { |
| komponenten.remove(komponenten.size() - 1); |
| } |
| komponenten.add(komponente); |
| return new Stelle(komponenten).anzeigeText(); |
| } |
| |
| private static int rang(Stelle.Komponente komponente) { |
| return switch (komponente) { |
| case Stelle.Paragraph p -> 1; |
| case Stelle.AbsatzNr a -> 2; |
| case Stelle.SatzNr s -> 3; |
| case Stelle.NummerNr n -> 4; |
| case Stelle.BuchstabeNr b -> 5; |
| default -> 0; |
| }; |
| } |
| |
| /** Die Umnummerierungen eines Befehls — auch die in einem Verbund ({@link Sammelbefehl}). */ |
| private static List<Umnummerierung> umnummerierungen(Aenderungsbefehl befehl) { |
| return switch (befehl) { |
| case Umnummerierung u -> List.of(u); |
| case Sammelbefehl s -> |
| s.teilbefehle().stream().flatMap(t -> umnummerierungen(t).stream()).toList(); |
| default -> List.of(); |
| }; |
| } |
| |
| /** Die Struktur-Einfügungen eines Befehls — auch die in einem Verbund. */ |
| private static List<StrukturEinfuegung> einfuegungen(Aenderungsbefehl befehl) { |
| return switch (befehl) { |
| case StrukturEinfuegung e -> List.of(e); |
| case Sammelbefehl s -> |
| s.teilbefehle().stream().flatMap(t -> einfuegungen(t).stream()).toList(); |
| default -> List.of(); |
| }; |
| } |
| |
| /** Die Struktur-Ersetzungen eines Befehls — auch die in einem Verbund. */ |
| private static List<StrukturErsetzung> ersetzungen(Aenderungsbefehl befehl) { |
| return switch (befehl) { |
| case StrukturErsetzung e -> List.of(e); |
| case Sammelbefehl s -> |
| s.teilbefehle().stream().flatMap(t -> ersetzungen(t).stream()).toList(); |
| default -> List.of(); |
| }; |
| } |
| |
| private static boolean istNurUeberschrift(Stelle stelle) { |
| return stelle.komponenten().size() == 1 |
| && stelle.komponenten().get(0) instanceof Stelle.Ueberschrift; |
| } |
| |
| private static AngewandteAenderung wendeAn( |
| List<Norm> normen, List<Gliederung> gliederungen, Aenderungsbefehl befehl) { |
| if (befehl instanceof UnbekannterBefehl) { |
| return manuell(befehl, Grund.NICHT_ERKANNT, "Befehl nicht erkannt."); |
| } |
| // Sammelbefehle vor den Spezialweichen dispatchen (jeder Teil wird einzeln geroutet). |
| if (befehl instanceof Sammelbefehl s) { |
| return wendeSammelAn(normen, gliederungen, s); |
| } |
| try { |
| if (befehl.stelle().betrifftInhaltsuebersicht()) { |
| var speziell = InhaltsuebersichtAnwender.wendeAn(normen, befehl); |
| if (speziell != null) { |
| return speziell; |
| } |
| // Wortweise Operationen laufen durch die normalen Zweige — die Inhaltsübersicht ist eine |
| // gewöhnliche Norm, deren enbez der StellenAufloeser bereits auflöst. |
| } |
| // Änderungen an Gliederungs-Überschriften (Teil/Abschnitt/…) wirken auf den Gliederungsbaum. |
| // Anhänge/Anlagen sind dagegen eigene Normen und laufen durch die normalen Zweige. |
| else if (befehl.stelle().betrifftEchteGliederung()) { |
| return switch (befehl) { |
| case Neufassung n -> wendeGliederungNeufassungAn(gliederungen, n); |
| case Aufhebung a -> wendeGliederungStreichungAn(gliederungen, a); |
| case Umnummerierung u -> wendeGliederungUmnummerierungAn(gliederungen, u); |
| default -> |
| manuell( |
| befehl, |
| Grund.NICHT_UNTERSTUETZT, |
| "Strukturänderung wird nicht automatisch angewandt."); |
| }; |
| } |
| return switch (befehl) { |
| case Ersetzung e -> wendeErsetzungAn(normen, e); |
| case Streichung s -> wendeStreichungAn(normen, s); |
| case WoerterEinfuegung w -> wendeWoerterEinfuegungAn(normen, w); |
| case Neufassung n -> wendeNeufassungAn(normen, n); |
| case StrukturErsetzung s -> wendeStrukturErsetzungAn(normen, s); |
| case StrukturEinfuegung s -> wendeStrukturEinfuegungAn(normen, s); |
| case Anfuegung a -> wendeAnfuegungAn(normen, a); |
| case Aufhebung a -> wendeAufhebungAn(normen, a); |
| case Umnummerierung u -> wendeUmnummerierungAn(normen, u); |
| // Bereiche werden vor der Anwendung entfaltet (siehe #entfalteBereiche); hierher gelangt |
| // nur, was sich am Gesetz nicht auflösen ließ. |
| case BereichsUmnummerierung b -> |
| manuell( |
| befehl, |
| Grund.BEREICH_UNGUELTIG, |
| "Der Bereich " |
| + b.stelle().anzeigeText() |
| + " bis " |
| + b.bis().anzeigeText() |
| + " trägt nicht so viele Einheiten, wie der Befehl neue Bezeichnungen nennt."); |
| case WortlautZuAbsatz w -> wendeWortlautZuAbsatzAn(normen, w); |
| case WortlautZuSatz w -> wendeWortlautZuSatzAn(normen, w); |
| case WortlautVoranstellung w -> wendeWortlautVoranstellungAn(normen, w); |
| case FussnotenAufhebung f -> wendeFussnotenAufhebungAn(normen, f); |
| case SatznummerierungStreichung s -> wendeSatznummerierungStreichungAn(normen, s); |
| case GliederungsUeberschriften g -> |
| wendeGliederungsUeberschriftenAn(normen, gliederungen, g); |
| case Sammelbefehl s -> wendeSammelAn(normen, gliederungen, s); |
| case UnbekannterBefehl u -> manuell(befehl, Grund.NICHT_ERKANNT, "Befehl nicht erkannt."); |
| }; |
| } catch (RuntimeException e) { |
| return manuell(befehl, Grund.FEHLGESCHLAGEN, "Anwendung fehlgeschlagen: " + e); |
| } |
| } |
| |
| // --- Gliederungs-Überschriften ------------------------------------------------------------- |
| |
| private static AngewandteAenderung wendeGliederungNeufassungAn( |
| List<Gliederung> gliederungen, Neufassung befehl) { |
| int idx = findeGliederung(gliederungen, befehl.stelle().gliederungsPfad()); |
| if (idx < 0) { |
| return manuell(befehl, Grund.STELLE_NICHT_AUFLOESBAR, "Gliederungseinheit nicht gefunden."); |
| } |
| var alt = gliederungen.get(idx); |
| var titel = befehl.neuerText().replaceAll("\\s+", " ").strip(); |
| // Führende Eigenbezeichnung („Abschnitt 2 …“ oder „2. Abschnitt …“) aus dem Zitat entfernen. |
| var label = Pattern.compile("^(\\d+[a-z]?\\.\\s+\\S+|\\S+\\s+\\d+[a-z]?)\\s+").matcher(titel); |
| if (label.find() |
| && kanonischeBezeichnung(label.group(1)).equals(kanonischeBezeichnung(alt.bezeichnung()))) { |
| titel = titel.substring(label.end()).strip(); |
| } else if (titel.startsWith(alt.bezeichnung())) { |
| titel = titel.substring(alt.bezeichnung().length()).strip(); |
| } |
| gliederungen.set(idx, alt.mitTitel(titel.isEmpty() ? null : titel)); |
| return angewandt(befehl, alt.bezeichnung()); |
| } |
| |
| private static AngewandteAenderung wendeGliederungStreichungAn( |
| List<Gliederung> gliederungen, Aufhebung befehl) { |
| int idx = findeGliederung(gliederungen, befehl.stelle().gliederungsPfad()); |
| if (idx < 0) { |
| return manuell(befehl, Grund.STELLE_NICHT_AUFLOESBAR, "Gliederungseinheit nicht gefunden."); |
| } |
| var alt = gliederungen.get(idx); |
| gliederungen.set(idx, alt.mitTitel("(weggefallen)")); |
| return angewandt(befehl, alt.bezeichnung()); |
| } |
| |
| /** |
| * „Der bisherige Abschnitt 2 wird zu Abschnitt 3.“ — Bezeichnung der Gliederungseinheit umsetzen. |
| */ |
| private static AngewandteAenderung wendeGliederungUmnummerierungAn( |
| List<Gliederung> gliederungen, Umnummerierung befehl) { |
| int idx = findeGliederung(gliederungen, befehl.stelle().gliederungsPfad()); |
| if (idx < 0) { |
| return manuell( |
| befehl, |
| Grund.STELLE_NICHT_AUFLOESBAR, |
| "Gliederungseinheit nicht gefunden: " + befehl.stelle().anzeigeText()); |
| } |
| var neuPfad = befehl.neu().gliederungsPfad(); |
| if (neuPfad.isEmpty()) { |
| return manuell(befehl, Grund.NICHT_ERKANNT, "Neue Gliederungsbezeichnung fehlt."); |
| } |
| var neueBezeichnung = neuPfad.get(neuPfad.size() - 1).bezeichnung(); |
| var alt = gliederungen.get(idx); |
| gliederungen.set(idx, alt.mitBezeichnung(neueBezeichnung)); |
| return angewandt(befehl, neueBezeichnung); |
| } |
| |
| /** |
| * „Nach § 33 werden die folgenden Überschriften zu Teil 3 und zu Teil 3 Abschnitt 1 eingefügt“ |
| * bzw. „Die bisherigen Überschriften zu X werden durch die folgende Überschrift zu Y ersetzt“: |
| * neue Gliederungen entstehen im Gliederungsbaum, und die Normen des betroffenen Blocks werden |
| * der (innersten) neuen Einheit zugeordnet. |
| */ |
| private static AngewandteAenderung wendeGliederungsUeberschriftenAn( |
| List<Norm> normen, List<Gliederung> gliederungen, GliederungsUeberschriften befehl) { |
| // Titel der neuen Einheiten aus dem Zitat ziehen: das Zitat reiht „<Bezeichnung> <Titel>“ |
| // in Befehlreihenfolge aneinander. |
| var flach = befehl.text().replaceAll("\\s+", " ").strip(); |
| var starts = new int[befehl.neue().size()]; |
| int suchAb = 0; |
| for (int i = 0; i < befehl.neue().size(); i++) { |
| var bezeichnung = befehl.neue().get(i).bezeichnung(); |
| starts[i] = flach.indexOf(bezeichnung, suchAb); |
| if (starts[i] < 0) { |
| return manuell( |
| befehl, |
| Grund.ZITAT_UNBRAUCHBAR, |
| "Die Überschrift zu „" + bezeichnung + "“ fehlt im Zitat."); |
| } |
| suchAb = starts[i] + bezeichnung.length(); |
| } |
| var neueGliederungen = new ArrayList<Gliederung>(); |
| for (int i = 0; i < befehl.neue().size(); i++) { |
| var bezeichnung = befehl.neue().get(i).bezeichnung(); |
| int titelVon = starts[i] + bezeichnung.length(); |
| int titelBis = i + 1 < starts.length ? starts[i + 1] : flach.length(); |
| var titel = flach.substring(titelVon, titelBis).strip(); |
| neueGliederungen.add(new Gliederung(bezeichnung, titel.isEmpty() ? null : titel)); |
| } |
| var ziel = neueGliederungen.get(neueGliederungen.size() - 1); |
| |
| if (!befehl.ersetzte().isEmpty()) { |
| // Ersetzungsform: die bisherigen Einheiten weichen den neuen. |
| var indizes = new ArrayList<Integer>(); |
| for (var pfad : befehl.ersetzte()) { |
| int idx = findeGliederung(gliederungen, List.copyOf(pfad)); |
| if (idx < 0) { |
| return manuell( |
| befehl, |
| Grund.STELLE_NICHT_AUFLOESBAR, |
| "Gliederungseinheit nicht gefunden: " + pfad.get(pfad.size() - 1).bezeichnung()); |
| } |
| indizes.add(idx); |
| } |
| var alte = |
| indizes.stream().map(gliederungen::get).collect(java.util.stream.Collectors.toSet()); |
| int einfuegePos = java.util.Collections.min(indizes); |
| indizes.sort(java.util.Comparator.reverseOrder()); |
| for (int idx : indizes) { |
| gliederungen.remove(idx); |
| } |
| gliederungen.addAll(einfuegePos, neueGliederungen); |
| for (int k = 0; k < normen.size(); k++) { |
| var g = normen.get(k).gliederung(); |
| if (g != null && alte.contains(g)) { |
| normen.set(k, normen.get(k).mitGliederung(ziel)); |
| } |
| } |
| return angewandt(befehl, neueGliederungen.stream().map(Gliederung::bezeichnung).toList()); |
| } |
| |
| // Einfügeform: hinter dem Anker-§. |
| var aufloesung = loeseNormAuf(normen, befehl.stelle()); |
| if (aufloesung.fehler() != null) { |
| return manuell(befehl, aufloesung.grund(), aufloesung.fehler()); |
| } |
| var anker = normen.get(aufloesung.normIndex()); |
| int gliederungsPos = |
| anker.gliederung() != null |
| ? gliederungen.indexOf(anker.gliederung()) + 1 |
| : gliederungen.size(); |
| if (gliederungsPos == 0) { |
| gliederungsPos = gliederungen.size(); |
| } |
| gliederungen.addAll(gliederungsPos, neueGliederungen); |
| int normPos = aufloesung.normIndex() + 1; |
| if (normPos < normen.size()) { |
| // Der zusammenhängende Block mit unveränderter bisheriger Gliederung wird umgehängt; |
| // spätere Überschriften-Befehle ordnen ihre Abschnitte ihrerseits neu zu. |
| var bisherige = normen.get(normPos).gliederung(); |
| for (int k = normPos; |
| k < normen.size() && java.util.Objects.equals(normen.get(k).gliederung(), bisherige); |
| k++) { |
| normen.set(k, normen.get(k).mitGliederung(ziel)); |
| } |
| } |
| return angewandt(befehl, neueGliederungen.stream().map(Gliederung::bezeichnung).toList()); |
| } |
| |
| /** |
| * Findet die Gliederungseinheit zum Pfad („Teil 3 Abschnitt 2“): jede Ebene wird per Bezeichnung |
| * innerhalb des Kennzahl-Präfixes der übergeordneten Ebene aufgelöst. |
| */ |
| private static int findeGliederung( |
| List<Gliederung> gliederungen, List<Stelle.Gliederungseinheit> pfad) { |
| if (pfad.isEmpty()) { |
| return -1; |
| } |
| String praefix = ""; |
| int gefunden = -1; |
| for (var einheit : pfad) { |
| gefunden = -1; |
| for (int i = 0; i < gliederungen.size(); i++) { |
| var g = gliederungen.get(i); |
| if (kanonischeBezeichnung(g.bezeichnung()) |
| .equals(kanonischeBezeichnung(einheit.bezeichnung())) |
| && (g.kennzahl() == null || g.kennzahl().startsWith(praefix))) { |
| gefunden = i; |
| break; |
| } |
| } |
| if (gefunden < 0) { |
| return -1; |
| } |
| var kennzahl = gliederungen.get(gefunden).kennzahl(); |
| if (kennzahl != null) { |
| praefix = kennzahl; |
| } |
| } |
| return gefunden; |
| } |
| |
| /** Normalisiert Gliederungsbezeichnungen: „2. Abschnitt“ und „Abschnitt 2“ sind dieselbe. */ |
| static String kanonischeBezeichnung(String bezeichnung) { |
| return bezeichnung.strip().replaceFirst("^(\\d+[a-z]?)\\.\\s+(\\S+)$", "$2 $1"); |
| } |
| |
| // --- Wortweise Textoperationen ------------------------------------------------------------- |
| |
| private static AngewandteAenderung wendeErsetzungAn(List<Norm> normen, Ersetzung befehl) { |
| return bearbeiteText( |
| normen, |
| befehl, |
| ohneFussnoten( |
| text -> { |
| if (befehl.amEnde()) { |
| var gestutzt = text.stripTrailing(); |
| if (!gestutzt.endsWith(befehl.alt())) { |
| return TextErgebnis.fehler( |
| "Der Text endet nicht mit „" + befehl.alt() + "“.", Grund.ZIELTEXT_FEHLT); |
| } |
| var rumpf = gestutzt.substring(0, gestutzt.length() - befehl.alt().length()); |
| var fuge = |
| brauchtFuge(befehl.alt(), befehl.neu()) |
| && !rumpf.isEmpty() |
| && !Character.isWhitespace(rumpf.charAt(rumpf.length() - 1)) |
| ? " " |
| : ""; |
| return TextErgebnis.ok(rumpf + fuge + befehl.neu()); |
| } |
| int anzahl = zaehleVorkommen(text, befehl.alt()); |
| if (anzahl == 0) { |
| return TextErgebnis.fehler( |
| "„" + befehl.alt() + "“ kommt im Zieltext nicht vor.", Grund.ZIELTEXT_FEHLT); |
| } |
| if (anzahl > 1 && !befehl.jeweils()) { |
| return TextErgebnis.fehler( |
| "„" |
| + befehl.alt() |
| + "“ kommt " |
| + anzahl |
| + "-mal vor (ohne „jeweils“ mehrdeutig).", |
| Grund.MEHRDEUTIG); |
| } |
| if (brauchtFuge(befehl.alt(), befehl.neu())) { |
| return TextErgebnis.ok(ersetzeMitFuge(text, befehl.alt(), befehl.neu())); |
| } |
| return TextErgebnis.ok(ersetzeWortweise(text, 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) == '('); |
| } |
| |
| /** 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; |
| 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, |
| befehl, |
| ohneFussnoten( |
| text -> { |
| int anzahl = zaehleVorkommen(text, befehl.woerter()); |
| if (anzahl == 0) { |
| return TextErgebnis.fehler( |
| "„" + befehl.woerter() + "“ kommt im Zieltext nicht vor.", |
| Grund.ZIELTEXT_FEHLT); |
| } |
| if (anzahl > 1) { |
| return TextErgebnis.fehler( |
| "„" + befehl.woerter() + "“ kommt " + anzahl + "-mal vor.", Grund.MEHRDEUTIG); |
| } |
| int naht = findeVorkommen(text, befehl.woerter(), 0); |
| return TextErgebnis.ok( |
| heileNaht( |
| text.substring(0, naht) + text.substring(naht + befehl.woerter().length()), |
| naht)); |
| })); |
| } |
| |
| private static AngewandteAenderung wendeWoerterEinfuegungAn( |
| List<Norm> normen, WoerterEinfuegung befehl) { |
| return bearbeiteText( |
| normen, |
| befehl, |
| ohneFussnoten( |
| text -> |
| switch (befehl.anker()) { |
| case WortAnker.NachWoertern nach -> { |
| var pruefung = eindeutigeFundstelle(text, nach.woerter()); |
| if (pruefung.fehler() != null) { |
| yield TextErgebnis.fehler(pruefung.fehler(), pruefung.grund()); |
| } |
| int ende = pruefung.index() + nach.woerter().length(); |
| yield TextErgebnis.ok( |
| text.substring(0, ende) |
| + fuge(befehl.woerter()) |
| + befehl.woerter() |
| + text.substring(ende)); |
| } |
| case WortAnker.VorWoertern vor -> { |
| var pruefung = eindeutigeFundstelle(text, vor.woerter()); |
| if (pruefung.fehler() != null) { |
| yield TextErgebnis.fehler(pruefung.fehler(), pruefung.grund()); |
| } |
| yield TextErgebnis.ok( |
| text.substring(0, pruefung.index()) |
| + befehl.woerter() |
| + " " |
| + text.substring(pruefung.index())); |
| } |
| case WortAnker.VorKommaAmEnde ignoriert -> { |
| var gestutzt = text.stripTrailing(); |
| if (!gestutzt.endsWith(",")) { |
| yield TextErgebnis.fehler( |
| "Der Zieltext endet nicht mit einem Komma.", Grund.ZIELTEXT_FEHLT); |
| } |
| yield TextErgebnis.ok( |
| gestutzt.substring(0, gestutzt.length() - 1) |
| + " " |
| + befehl.woerter() |
| + ","); |
| } |
| case WortAnker.AmEnde ignoriert -> { |
| var gestutzt = text.stripTrailing(); |
| if (gestutzt.endsWith(".") |
| || gestutzt.endsWith(",") |
| || gestutzt.endsWith(";")) { |
| var satzzeichen = gestutzt.charAt(gestutzt.length() - 1); |
| yield TextErgebnis.ok( |
| gestutzt.substring(0, gestutzt.length() - 1) |
| + " " |
| + befehl.woerter() |
| + satzzeichen); |
| } |
| yield TextErgebnis.ok(gestutzt + " " + befehl.woerter()); |
| } |
| })); |
| } |
| |
| // --- Strukturoperationen ------------------------------------------------------------------- |
| |
| private static AngewandteAenderung wendeNeufassungAn(List<Norm> normen, Neufassung befehl) { |
| var stelle = befehl.stelle(); |
| |
| if (stelle.betrifftUeberschrift()) { |
| var aufloesung = loeseNormAuf(normen, stelle); |
| if (aufloesung.fehler() != null) { |
| return manuell(befehl, aufloesung.grund(), aufloesung.fehler()); |
| } |
| var norm = normen.get(aufloesung.normIndex()); |
| var titel = befehl.neuerText().replaceFirst("^(?:§|Art\\.)\\s*\\S+\\s+", "").strip(); |
| normen.set(aufloesung.normIndex(), norm.mitTitel(titel)); |
| return angewandt(befehl, norm.enbez()); |
| } |
| |
| if (nurNorm(normen, stelle)) { |
| var aufloesung = loeseNormAuf(normen, stelle); |
| if (aufloesung.fehler() != null) { |
| return manuell(befehl, aufloesung.grund(), aufloesung.fehler()); |
| } |
| var alteNorm = normen.get(aufloesung.normIndex()); |
| var neueNorm = parseNorm(befehl.neuerText(), alteNorm.enbez(), alteNorm); |
| normen.set(aufloesung.normIndex(), neueNorm); |
| return angewandt(befehl, alteNorm.enbez()); |
| } |
| |
| if (stelle.absatz().isPresent() && feinsteIstAbsatz(stelle)) { |
| var ergebnis = StellenAufloeser.aufloese(gesetzAus(normen), stelle); |
| if (ergebnis instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) { |
| return manuell(befehl, nicht.grund(), nicht.begruendung()); |
| } |
| var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle(); |
| var norm = normen.get(fundstelle.normIndex()); |
| var absaetze = new ArrayList<>(norm.absaetze()); |
| var alterAbsatz = absaetze.get(fundstelle.absatzIndex()); |
| var neueAbsaetze = parseAbsaetze(befehl.neuerText()); |
| if (neueAbsaetze.size() == 1) { |
| var neuer = neueAbsaetze.get(0); |
| absaetze.set( |
| fundstelle.absatzIndex(), |
| new Absatz( |
| neuer.nummer() != null ? neuer.nummer() : alterAbsatz.nummer(), neuer.text())); |
| } else { |
| absaetze.remove((int) fundstelle.absatzIndex()); |
| absaetze.addAll(fundstelle.absatzIndex(), neueAbsaetze); |
| } |
| normen.set(fundstelle.normIndex(), norm.mitAbsaetzen(absaetze)); |
| return angewandt(befehl, norm.enbez()); |
| } |
| |
| // Neufassung einer Nummer / eines Buchstabens: Der neue Wortlaut tritt an die Stelle der |
| // alten Einheit — und zwar auf deren Einrückung. Ohne sie stünde die neugefasste Zeile in |
| // Spalte 0, und der Zeilenblock ihrer Marke reichte fortan bis ans Absatzende, weil er so |
| // weit reicht, wie tiefer eingerückt wird (StellenAufloeser.zeilenBlock): ein späterer |
| // Befehl „Nach Nr. 4 werden die folgenden Nrn. 5 bis 7 eingefügt“ träte dann hinter die |
| // letzte Nummer des Absatzes statt hinter die Nr. 4. |
| if (feinsteIstAufzaehlung(befehl.stelle())) { |
| return bearbeiteBereich( |
| normen, |
| befehl, |
| (text, bereich) -> |
| TextErgebnis.ok( |
| text.substring(0, bereich.von()) |
| + rueckeZitatEin( |
| normalisiereZitatText(befehl.neuerText()), |
| einrueckungVon(text, bereich.von())) |
| + text.substring(bereich.bis()))); |
| } |
| |
| // Neufassung eines Satzes / Halbsatzes: Bereich ersetzen. Eine Einrückung gibt es hier |
| // nicht — der Bereich beginnt mitten in der Zeile. |
| return bearbeiteText( |
| normen, befehl, text -> TextErgebnis.ok(normalisiereZitatText(befehl.neuerText()))); |
| } |
| |
| /** Ein Ziel (Absatz, Satz, Nummer, Buchstabe) wird durch einen Block ersetzt (ggf. 1 → N). */ |
| private static AngewandteAenderung wendeStrukturErsetzungAn( |
| List<Norm> normen, StrukturErsetzung befehl) { |
| return switch (befehl.ebene()) { |
| case ABSATZ -> { |
| var stelle = befehl.stelle(); |
| if (stelle.absatz().isEmpty()) { |
| yield manuell( |
| befehl, |
| Grund.STELLE_NICHT_AUFLOESBAR, |
| "Ersetzungsziel nennt keinen Absatz: " + stelle.anzeigeText()); |
| } |
| var ergebnis = StellenAufloeser.aufloese(gesetzAus(normen), stelle); |
| if (ergebnis instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) { |
| yield manuell(befehl, nicht.grund(), nicht.begruendung()); |
| } |
| var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle(); |
| var norm = normen.get(fundstelle.normIndex()); |
| int vonIndex = fundstelle.absatzIndex(); |
| int bisIndex = vonIndex; |
| // Bereich („Die Absätze 8 und 9 werden … ersetzt“): das letzte Ziel bestimmt das Ende. |
| if (befehl.bisStelle() != null) { |
| var e2 = StellenAufloeser.aufloese(gesetzAus(normen), befehl.bisStelle()); |
| if (e2 instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) { |
| yield manuell(befehl, nicht.grund(), nicht.begruendung()); |
| } |
| var f2 = ((StellenAufloeser.Ergebnis.Gefunden) e2).fundstelle(); |
| if (f2.normIndex() != fundstelle.normIndex() || f2.absatzIndex() == null) { |
| yield manuell( |
| befehl, |
| Grund.BEREICH_UNGUELTIG, |
| "Ersetzungsbereich liegt nicht in einer einzigen Norm."); |
| } |
| bisIndex = f2.absatzIndex(); |
| } |
| if (bisIndex < vonIndex) { |
| yield manuell( |
| befehl, Grund.BEREICH_UNGUELTIG, "Ersetzungsbereich ist leer oder absteigend."); |
| } |
| var absaetze = new ArrayList<>(norm.absaetze()); |
| for (int k = bisIndex; k >= vonIndex; k--) { |
| absaetze.remove(k); |
| } |
| absaetze.addAll(vonIndex, parseAbsaetze(befehl.text())); |
| normen.set(fundstelle.normIndex(), norm.mitAbsaetzen(absaetze)); |
| yield angewandt(befehl, norm.enbez()); |
| } |
| case SATZ -> { |
| if (befehl.bisStelle() == null) { |
| yield bearbeiteBereich( |
| normen, |
| befehl, |
| (text, bereich) -> |
| TextErgebnis.ok( |
| text.substring(0, bereich.von()) |
| + befehl.text().strip().replaceAll("\\s+", " ") |
| + text.substring(bereich.bis()))); |
| } |
| yield wendeSatzBereichsErsetzungAn(normen, befehl); |
| } |
| case NUMMER, BUCHSTABE -> { |
| if (befehl.bisStelle() != null) { |
| yield wendeZeilenBereichsErsetzungAn(normen, befehl); |
| } |
| yield bearbeiteBereich( |
| normen, |
| befehl, |
| (text, bereich) -> { |
| var einrueckung = einrueckungVon(text, bereich.von()); |
| var ersatz = rueckeZitatEin(normalisiereZitatText(befehl.text()), einrueckung); |
| return TextErgebnis.ok( |
| text.substring(0, bereich.von()) + ersatz + text.substring(bereich.bis())); |
| }); |
| } |
| case PARAGRAPH -> { |
| // „§ 71 wird durch die folgenden §§ 71 bis 71p ersetzt: „…““ — der adressierte §-Bereich |
| // wird entfernt und durch die Paragraphen des Blocks ersetzt. |
| var aufloesung = loeseNormAuf(normen, befehl.stelle()); |
| if (aufloesung.fehler() != null) { |
| yield manuell(befehl, aufloesung.grund(), aufloesung.fehler()); |
| } |
| int vonIndex = aufloesung.normIndex(); |
| int bisIndex = vonIndex; |
| if (befehl.bisStelle() != null) { |
| var a2 = loeseNormAuf(normen, befehl.bisStelle()); |
| if (a2.fehler() != null) { |
| yield manuell(befehl, a2.grund(), a2.fehler()); |
| } |
| bisIndex = a2.normIndex(); |
| } |
| if (bisIndex < vonIndex) { |
| yield manuell( |
| befehl, Grund.BEREICH_UNGUELTIG, "Ersetzungsbereich ist leer oder absteigend."); |
| } |
| var neue = parseNormenBlock(befehl.text(), normen.get(vonIndex).gliederung()); |
| if (neue.isEmpty()) { |
| yield manuell( |
| befehl, Grund.ZITAT_UNBRAUCHBAR, "Im Ersetzungsblock wurde kein Paragraph erkannt."); |
| } |
| for (int k = bisIndex; k >= vonIndex; k--) { |
| normen.remove(k); |
| } |
| normen.addAll(vonIndex, neue); |
| yield angewandt(befehl, neue.stream().map(Norm::enbez).toList()); |
| } |
| }; |
| } |
| |
| /** |
| * Ersetzt einen zusammenhängenden Satz-Bereich („Die Sätze 4 und 5 werden … gefasst“) durch einen |
| * Block: vom Anfang des ersten bis zum Ende des letzten adressierten Satzes (beide müssen im |
| * selben Absatz derselben Norm liegen). |
| */ |
| private static AngewandteAenderung wendeSatzBereichsErsetzungAn( |
| List<Norm> normen, StrukturErsetzung befehl) { |
| var e1 = StellenAufloeser.aufloese(gesetzAus(normen), befehl.stelle()); |
| if (e1 instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) { |
| return manuell(befehl, nicht.grund(), nicht.begruendung()); |
| } |
| var e2 = StellenAufloeser.aufloese(gesetzAus(normen), befehl.bisStelle()); |
| if (e2 instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) { |
| return manuell(befehl, nicht.grund(), nicht.begruendung()); |
| } |
| var f1 = ((StellenAufloeser.Ergebnis.Gefunden) e1).fundstelle(); |
| var f2 = ((StellenAufloeser.Ergebnis.Gefunden) e2).fundstelle(); |
| if (f1.normIndex() != f2.normIndex() |
| || f1.absatzIndex() == null |
| || !f1.absatzIndex().equals(f2.absatzIndex()) |
| || f1.bereich() == null |
| || f2.bereich() == null) { |
| return manuell( |
| befehl, Grund.BEREICH_UNGUELTIG, "Satz-Bereich liegt nicht in einem einzigen Absatz."); |
| } |
| int von = f1.bereich().von(); |
| int bis = f2.bereich().bis(); |
| if (bis < von) { |
| return manuell(befehl, Grund.BEREICH_UNGUELTIG, "Satz-Bereich ist leer oder absteigend."); |
| } |
| var norm = normen.get(f1.normIndex()); |
| var absaetze = new ArrayList<>(norm.absaetze()); |
| var absatz = absaetze.get(f1.absatzIndex()); |
| var text = absatz.text(); |
| var neu = |
| text.substring(0, von) |
| + befehl.text().strip().replaceAll("\\s+", " ") |
| + text.substring(bis); |
| absaetze.set(f1.absatzIndex(), absatz.mitText(neu)); |
| normen.set(f1.normIndex(), norm.mitAbsaetzen(absaetze)); |
| return angewandt(befehl, norm.enbez()); |
| } |
| |
| /** |
| * Ersetzt einen zusammenhängenden Nummern-/Buchstaben-Bereich („Nummer 3 bis 6 wird durch die |
| * folgenden Nummern 3 und 4 ersetzt“) durch den zitierten Block: vom Zeilenanfang der ersten bis |
| * zum Zeilenende der letzten Einheit (beide im selben Absatz derselben Norm). |
| */ |
| private static AngewandteAenderung wendeZeilenBereichsErsetzungAn( |
| List<Norm> normen, StrukturErsetzung befehl) { |
| var e1 = StellenAufloeser.aufloese(gesetzAus(normen), befehl.stelle()); |
| if (e1 instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) { |
| return manuell(befehl, nicht.grund(), nicht.begruendung()); |
| } |
| var e2 = StellenAufloeser.aufloese(gesetzAus(normen), befehl.bisStelle()); |
| if (e2 instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) { |
| return manuell(befehl, nicht.grund(), nicht.begruendung()); |
| } |
| var f1 = ((StellenAufloeser.Ergebnis.Gefunden) e1).fundstelle(); |
| var f2 = ((StellenAufloeser.Ergebnis.Gefunden) e2).fundstelle(); |
| if (f1.normIndex() != f2.normIndex() |
| || f1.absatzIndex() == null |
| || !f1.absatzIndex().equals(f2.absatzIndex()) |
| || f1.bereich() == null |
| || f2.bereich() == null) { |
| return manuell( |
| befehl, |
| Grund.BEREICH_UNGUELTIG, |
| "Ersetzungsbereich liegt nicht in einem einzigen Absatz."); |
| } |
| int von = f1.bereich().von(); |
| int bis = f2.bereich().bis(); |
| if (bis < von) { |
| return manuell( |
| befehl, Grund.BEREICH_UNGUELTIG, "Ersetzungsbereich ist leer oder absteigend."); |
| } |
| var norm = normen.get(f1.normIndex()); |
| var absaetze = new ArrayList<>(norm.absaetze()); |
| var absatz = absaetze.get(f1.absatzIndex()); |
| var text = absatz.text(); |
| var einrueckung = einrueckungVon(text, von); |
| var ersatz = rueckeZitatEin(normalisiereZitatText(befehl.text()), einrueckung); |
| absaetze.set( |
| f1.absatzIndex(), absatz.mitText(text.substring(0, von) + ersatz + text.substring(bis))); |
| normen.set(f1.normIndex(), norm.mitAbsaetzen(absaetze)); |
| return angewandt(befehl, norm.enbez()); |
| } |
| |
| private static AngewandteAenderung wendeStrukturEinfuegungAn( |
| List<Norm> normen, StrukturEinfuegung befehl) { |
| var wortAnker = befehl.anker(); |
| if (wortAnker != null) { |
| return wendeWortankerEinfuegungAn(normen, befehl, wortAnker); |
| } |
| return switch (befehl.ebene()) { |
| case PARAGRAPH -> { |
| var aufloesung = loeseNormAuf(normen, befehl.stelle()); |
| if (aufloesung.fehler() != null) { |
| yield manuell(befehl, aufloesung.grund(), aufloesung.fehler()); |
| } |
| var anker = normen.get(aufloesung.normIndex()); |
| int position = aufloesung.normIndex() + (befehl.vorher() ? 0 : 1); |
| |
| // „Nach § 60a werden die folgenden §§ 60b und 60c eingefügt: „…““ — Block mehrerer §§. |
| if (befehl.bezeichnung() == null) { |
| var neue = parseNormenBlock(befehl.text(), anker.gliederung()); |
| if (neue.isEmpty()) { |
| yield manuell( |
| befehl, Grund.ZITAT_UNBRAUCHBAR, "Im Einfügeblock wurde kein Paragraph erkannt."); |
| } |
| for (var n : neue) { |
| if (StellenAufloeser.normIndex(gesetzAus(normen), n.enbez()) >= 0) { |
| yield manuell( |
| befehl, |
| Grund.BESTAND_WIDERSPRICHT, |
| n.enbez() + " existiert bereits im Stammgesetz."); |
| } |
| } |
| normen.addAll(position, neue); |
| yield angewandt(befehl, neue.stream().map(Norm::enbez).toList()); |
| } |
| |
| var sigelNeu = befehl.stelle().paragraph().map(Stelle.Paragraph::sigel).orElse("§"); |
| var enbezNeu = sigelNeu + " " + befehl.bezeichnung(); |
| if (StellenAufloeser.normIndex(gesetzAus(normen), enbezNeu) >= 0) { |
| yield manuell( |
| befehl, Grund.BESTAND_WIDERSPRICHT, enbezNeu + " existiert bereits im Stammgesetz."); |
| } |
| var neueNorm = |
| parseNorm( |
| befehl.text(), |
| enbezNeu, |
| new Norm(enbezNeu, null, anker.gliederung(), List.of(), false)); |
| normen.add(position, neueNorm); |
| yield angewandt(befehl, enbezNeu); |
| } |
| case ABSATZ -> { |
| var stelle = befehl.stelle(); |
| if (stelle.absatz().isEmpty()) { |
| yield manuell( |
| befehl, |
| Grund.STELLE_NICHT_AUFLOESBAR, |
| "Einfügeanker nennt keinen Absatz: " + stelle.anzeigeText()); |
| } |
| var ergebnis = StellenAufloeser.aufloese(gesetzAus(normen), stelle); |
| if (ergebnis instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) { |
| yield manuell(befehl, nicht.grund(), nicht.begruendung()); |
| } |
| var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle(); |
| var norm = normen.get(fundstelle.normIndex()); |
| var absaetze = new ArrayList<>(norm.absaetze()); |
| int position = fundstelle.absatzIndex() + (befehl.vorher() ? 0 : 1); |
| absaetze.addAll(position, parseAbsaetze(befehl.text())); |
| normen.set(fundstelle.normIndex(), norm.mitAbsaetzen(absaetze)); |
| yield angewandt(befehl, norm.enbez()); |
| } |
| case SATZ -> |
| bearbeiteBereich( |
| normen, |
| befehl, |
| (text, bereich) -> { |
| var einschub = " " + befehl.text().strip(); |
| int position = befehl.vorher() ? bereich.von() : bereich.bis(); |
| return TextErgebnis.ok( |
| befehl.vorher() |
| ? text.substring(0, position) |
| + befehl.text().strip() |
| + " " |
| + text.substring(position) |
| : text.substring(0, position) + einschub + text.substring(position)); |
| }); |
| case NUMMER, BUCHSTABE -> |
| bearbeiteBereich( |
| normen, |
| befehl, |
| (text, bereich) -> { |
| var einrueckung = einrueckungVon(text, bereich.von()); |
| int position = befehl.vorher() ? bereich.von() : bereich.bis(); |
| // Der Einfügeblock darf mehrere Einheiten enthalten („die Nummern 4a bis 4c“); |
| // jede Aufzählungszeile des Zitats bleibt eine eigene Zeile. |
| var block = rueckeZitatEin(normalisiereZitatText(befehl.text()), einrueckung); |
| var neuerText = |
| befehl.vorher() |
| ? text.substring(0, position) + block + "\n" + text.substring(position) |
| : text.substring(0, position) + "\n" + block + text.substring(position); |
| // Vergibt der Block eine Bezeichnung, die ein leerer Platzhalter der Altfassung |
| // noch hält („7. (aufgehoben)“), so weicht dieser: Zwei Einheiten gleicher |
| // Bezeichnung kann es nicht geben, und Inhalt geht dabei nicht verloren. Es ist |
| // dieselbe Regel, die schon bei der Umnummerierung auf eine weggefallene |
| // Bezeichnung gilt, und so zeigt es die amtliche Nachfassung. |
| for (var marke : platzhalterMarken(block, befehl.ebene())) { |
| neuerText = entferneWeggefallenenPlatzhalter(neuerText, marke); |
| } |
| return TextErgebnis.ok(neuerText); |
| }); |
| }; |
| } |
| |
| /** |
| * „Vor den Wörtern „Aus dem Bereich Verkehr:“ wird folgender Absatz 5 eingefügt: „…““ — die |
| * Position der neuen Einheit bestimmt hier ein Wortanker, nicht die Struktur. Der Einfügeblock |
| * tritt deshalb als eigene Zeile vor bzw. hinter die Zeile des Ankers; welche strukturelle Ebene |
| * der Befehl nennt, ist dabei ohne Belang (in einer Anlage stehen Absätze und Nummern als Zeilen |
| * eines Textes). |
| */ |
| /** |
| * „Vor den Wörtern ‚Aus dem Bereich Verkehr:‘ wird folgender Absatz 5 eingefügt“ — ein |
| * <em>Absatz</em>, der an einem Wortlaut ausgerichtet ist. Er wird ein Absatz und keine Zeile im |
| * Nachbarabsatz: Der tragende Absatz wird an der Marke geteilt, und was hinter ihr stand, setzt |
| * den neuen Absatz fort — so steht es hernach auch in der amtlichen Nachfassung, denn ein |
| * Zwischentitel ohne eigene Absatzbezeichnung schließt sich stets dem vorangehenden an. |
| * |
| * <p>Steht die Marke am Anfang des Absatzes, so wird nicht geteilt: Der neue Absatz tritt dann |
| * schlicht vor ihn. |
| */ |
| private static AngewandteAenderung wendeAbsatzWortankerEinfuegungAn( |
| List<Norm> normen, StrukturEinfuegung befehl, String woerter) { |
| var aufloesung = loeseNormAuf(normen, befehl.stelle()); |
| if (aufloesung.fehler() != null) { |
| return manuell(befehl, aufloesung.grund(), aufloesung.fehler()); |
| } |
| var norm = normen.get(aufloesung.normIndex()); |
| var absaetze = new ArrayList<>(norm.absaetze()); |
| int treffer = -1; |
| for (int i = 0; i < absaetze.size(); i++) { |
| int anzahl = zaehleVorkommen(absaetze.get(i).text(), woerter); |
| if (anzahl > 1) { |
| return manuell( |
| befehl, Grund.MEHRDEUTIG, "„" + woerter + "“ kommt mehrfach vor; mehrdeutig."); |
| } |
| if (anzahl == 1) { |
| if (treffer >= 0) { |
| return manuell( |
| befehl, |
| Grund.MEHRDEUTIG, |
| "„" + woerter + "“ kommt in mehreren Absätzen vor; mehrdeutig."); |
| } |
| treffer = i; |
| } |
| } |
| if (treffer < 0) { |
| return manuell( |
| befehl, Grund.ZIELTEXT_FEHLT, "„" + woerter + "“ kommt im Zieltext nicht vor."); |
| } |
| var alt = absaetze.get(treffer); |
| int marke = findeVorkommen(alt.text(), woerter, 0); |
| int schnitt = |
| befehl.vorher() |
| ? alt.text().lastIndexOf('\n', marke) + 1 |
| : naechsterZeilenAnfang(alt.text(), marke); |
| var vorn = alt.text().substring(0, schnitt).stripTrailing(); |
| var hinten = alt.text().substring(schnitt).strip(); |
| |
| var neue = parseAbsaetze(befehl.text()); |
| if (neue.isEmpty()) { |
| return manuell(befehl, Grund.ZITAT_UNBRAUCHBAR, "Im Zitat wurde kein Absatz erkannt."); |
| } |
| var erster = neue.get(0); |
| var nummer = erster.nummer() != null ? erster.nummer() : befehl.bezeichnung(); |
| var text = hinten.isEmpty() ? erster.text() : erster.text() + "\n" + hinten; |
| |
| if (vorn.isEmpty()) { |
| // Die Marke eröffnet den Absatz: nichts zu teilen, der neue tritt davor. |
| absaetze.add(treffer, new Absatz(nummer, erster.text())); |
| } else { |
| absaetze.set(treffer, alt.mitText(vorn)); |
| absaetze.add(treffer + 1, new Absatz(nummer, text)); |
| } |
| for (int k = 1; k < neue.size(); k++) { |
| absaetze.add(treffer + 1 + k, neue.get(k)); |
| } |
| normen.set(aufloesung.normIndex(), norm.mitAbsaetzen(absaetze)); |
| return angewandt(befehl, norm.enbez()); |
| } |
| |
| private static int naechsterZeilenAnfang(String text, int position) { |
| int umbruch = text.indexOf('\n', position); |
| return umbruch < 0 ? text.length() : umbruch + 1; |
| } |
| |
| private static AngewandteAenderung wendeWortankerEinfuegungAn( |
| List<Norm> normen, StrukturEinfuegung befehl, WortAnker anker) { |
| // Ein ganzer Absatz wird ein Absatz, keine Zeile in einem anderen. |
| if (befehl.ebene() == Ebene.ABSATZ) { |
| var woerter = |
| switch (anker) { |
| case WortAnker.NachWoertern nach -> nach.woerter(); |
| case WortAnker.VorWoertern vor -> vor.woerter(); |
| default -> null; |
| }; |
| if (woerter != null) { |
| return wendeAbsatzWortankerEinfuegungAn(normen, befehl, woerter); |
| } |
| } |
| return bearbeiteText( |
| normen, |
| befehl, |
| ohneFussnoten( |
| text -> { |
| var woerter = |
| switch (anker) { |
| case WortAnker.NachWoertern nach -> nach.woerter(); |
| case WortAnker.VorWoertern vor -> vor.woerter(); |
| // „am Ende“ und „vor dem Komma am Ende“ sind keine Einfügeanker für ganze |
| // Einheiten: Wo eine Einheit ans Ende tritt, sagt das Gesetzblatt „angefügt“, |
| // und dafür gibt es die Anfügung. Die Grenze ist bewusst gezogen und nicht |
| // etwa eine offene Lücke — im gesamten Prüfbestand (elf Änderungsdokumente aus |
| // Bund und sieben Ländern) kommt die Verbindung kein einziges Mal vor. |
| case WortAnker.AmEnde ignoriert -> null; |
| case WortAnker.VorKommaAmEnde ignoriert -> null; |
| }; |
| if (woerter == null) { |
| return TextErgebnis.fehler( |
| "Eine ganze Einheit wird nicht „am Ende“ eingefügt, sondern angefügt;" |
| + " so bezeichnet es auch das Gesetzblatt.", |
| Grund.NICHT_UNTERSTUETZT); |
| } |
| var pruefung = eindeutigeFundstelle(text, woerter); |
| if (pruefung.fehler() != null) { |
| return TextErgebnis.fehler(pruefung.fehler(), pruefung.grund()); |
| } |
| int zeilenAnfang = text.lastIndexOf('\n', pruefung.index()) + 1; |
| int zeilenEnde = text.indexOf('\n', pruefung.index()); |
| if (zeilenEnde < 0) { |
| zeilenEnde = text.length(); |
| } |
| var block = |
| rueckeZitatEin( |
| normalisiereZitatText(befehl.text()), einrueckungVon(text, zeilenAnfang)); |
| return TextErgebnis.ok( |
| befehl.vorher() |
| ? text.substring(0, zeilenAnfang) |
| + block |
| + "\n" |
| + text.substring(zeilenAnfang) |
| : text.substring(0, zeilenEnde) + "\n" + block + text.substring(zeilenEnde)); |
| })); |
| } |
| |
| private static AngewandteAenderung wendeAnfuegungAn(List<Norm> normen, Anfuegung befehl) { |
| return switch (befehl.ebene()) { |
| case ABSATZ -> { |
| var aufloesung = loeseNormAuf(normen, befehl.stelle()); |
| if (aufloesung.fehler() != null) { |
| yield manuell(befehl, aufloesung.grund(), aufloesung.fehler()); |
| } |
| var norm = normen.get(aufloesung.normIndex()); |
| var absaetze = new ArrayList<>(norm.absaetze()); |
| absaetze.addAll(parseAbsaetze(befehl.text())); |
| normen.set(aufloesung.normIndex(), norm.mitAbsaetzen(absaetze)); |
| yield angewandt(befehl, norm.enbez()); |
| } |
| case SATZ -> |
| bearbeiteText( |
| normen, |
| befehl, |
| text -> TextErgebnis.ok(text.stripTrailing() + " " + befehl.text().strip())); |
| case NUMMER, BUCHSTABE -> |
| bearbeiteText( |
| normen, |
| befehl, |
| text -> { |
| // Auch Blöcke mehrerer Einheiten („Die folgenden Nummern 9 bis 11 werden |
| // angefügt“): jede Aufzählungszeile des Zitats bleibt eine eigene Zeile. |
| var block = |
| normalisiereZitatText(befehl.text()) |
| .lines() |
| .map(zeile -> " " + zeile.strip()) |
| .reduce((a, b) -> a + "\n" + b) |
| .orElse(""); |
| return TextErgebnis.ok(text.stripTrailing() + "\n" + block); |
| }); |
| case PARAGRAPH -> haengeParagraphenAn(normen, befehl); |
| }; |
| } |
| |
| /** |
| * „Dem Gesetz werden die folgenden §§ … angefügt“ — ohne Anker. Die verankerte Form („Nach § 114 |
| * wird folgender § 115 angefügt“) ist eine Struktureinfügung und läuft dort; hier geht es um den |
| * Fall, dass der Befehl nur das Ende bezeichnet. |
| * |
| * <p>Angehängt wird ans Ende des Gesetzes, bei benannter Gliederungseinheit ans Ende ihres |
| * Blocks. Trägt das Zitat keinen Normkopf, bleibt der Befehl liegen — ein Wortlaut ohne |
| * Bezeichnung ließe sich nur raten. |
| */ |
| private static AngewandteAenderung haengeParagraphenAn(List<Norm> normen, Anfuegung befehl) { |
| var pfad = befehl.stelle().gliederungsPfad(); |
| var gliederung = pfad.isEmpty() ? null : gliederungVon(normen, pfad); |
| var neue = parseNormenBlock(befehl.text(), gliederung); |
| if (neue.isEmpty()) { |
| return manuell( |
| befehl, Grund.ZITAT_UNBRAUCHBAR, "Im angefügten Block wurde kein Paragraph erkannt."); |
| } |
| for (var n : neue) { |
| if (StellenAufloeser.normIndex(gesetzAus(normen), n.enbez()) >= 0) { |
| return manuell( |
| befehl, Grund.BESTAND_WIDERSPRICHT, n.enbez() + " existiert bereits im Stammgesetz."); |
| } |
| } |
| normen.addAll(endeDesBlocks(normen, gliederung), neue); |
| return angewandt(befehl, neue.stream().map(Norm::enbez).toList()); |
| } |
| |
| /** Die Gliederung, die der Pfad benennt; {@code null}, wenn keine Norm ihr angehört. */ |
| private static @Nullable Gliederung gliederungVon( |
| List<Norm> normen, List<Stelle.Gliederungseinheit> pfad) { |
| var bezeichnung = pfad.get(pfad.size() - 1).bezeichnung(); |
| for (var norm : normen) { |
| var g = norm.gliederung(); |
| if (g != null && g.bezeichnung().equals(bezeichnung)) { |
| return g; |
| } |
| } |
| return null; |
| } |
| |
| /** Hinter die letzte Norm der Gliederung — ohne Gliederung ans Ende des Gesetzes. */ |
| private static int endeDesBlocks(List<Norm> normen, @Nullable Gliederung gliederung) { |
| if (gliederung == null) { |
| return normen.size(); |
| } |
| int ende = normen.size(); |
| for (int i = 0; i < normen.size(); i++) { |
| if (gliederung.equals(normen.get(i).gliederung())) { |
| ende = i + 1; |
| } |
| } |
| return ende; |
| } |
| |
| private static AngewandteAenderung wendeAufhebungAn(List<Norm> normen, Aufhebung befehl) { |
| var stelle = befehl.stelle(); |
| |
| if (stelle.absatzbezeichnung().isPresent()) { |
| var aufloesung = loeseNormAuf(normen, stelle); |
| if (aufloesung.fehler() != null) { |
| return manuell(befehl, aufloesung.grund(), aufloesung.fehler()); |
| } |
| var norm = normen.get(aufloesung.normIndex()); |
| var nummer = stelle.absatzbezeichnung().get().nummer(); |
| var absaetze = new ArrayList<>(norm.absaetze()); |
| for (int i = 0; i < absaetze.size(); i++) { |
| if (nummer.equals(absaetze.get(i).nummer())) { |
| // 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()); |
| } |
| } |
| return manuell( |
| befehl, Grund.STELLE_NICHT_AUFLOESBAR, "Absatz (" + nummer + ") nicht gefunden."); |
| } |
| |
| if (nurNorm(normen, stelle)) { |
| var aufloesung = loeseNormAuf(normen, stelle); |
| if (aufloesung.fehler() != null) { |
| return manuell(befehl, aufloesung.grund(), aufloesung.fehler()); |
| } |
| var norm = normen.get(aufloesung.normIndex()); |
| if (norm.weggefallen()) { |
| // Idempotent: Die Aufhebung einer bereits weggefallenen Norm ist bereits vollzogen. |
| return angewandt(befehl, norm.enbez()); |
| } |
| normen.set(aufloesung.normIndex(), norm.alsWeggefallen()); |
| return angewandt(befehl, norm.enbez()); |
| } |
| |
| if (stelle.absatz().isPresent() && feinsteIstAbsatz(stelle)) { |
| var ergebnis = StellenAufloeser.aufloese(gesetzAus(normen), stelle); |
| if (ergebnis instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) { |
| return manuell(befehl, nicht.grund(), nicht.begruendung()); |
| } |
| var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle(); |
| var norm = normen.get(fundstelle.normIndex()); |
| var absaetze = new ArrayList<>(norm.absaetze()); |
| var alter = absaetze.get(fundstelle.absatzIndex()); |
| // 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()); |
| } |
| |
| // Satz / Nummer / Buchstabe aufheben: Bereich entfernen bzw. als weggefallen markieren. |
| return bearbeiteBereich( |
| normen, |
| befehl, |
| (text, bereich) -> { |
| var label = labelVon(stelle); |
| if (label != null && haeltPlatz(text, bereich.bis(), label)) { |
| var einrueckung = einrueckungVon(text, bereich.von()); |
| return TextErgebnis.ok( |
| text.substring(0, bereich.von()) |
| + einrueckung |
| + label |
| + " (weggefallen)" |
| + text.substring(bereich.bis())); |
| } |
| var rumpf = text.substring(0, bereich.von()); |
| var rest = text.substring(bereich.bis()); |
| // Ein Halbsatz nimmt sein Trennzeichen mit. „A; B.“ ohne den zweiten Halbsatz ist „A.“ |
| // und nicht „A;“ — das Semikolon trennte ja gerade ihn ab, und der Satz braucht seinen |
| // Schlusspunkt. So setzt es auch die amtliche Nachfassung (§ 24 Abs. 2 KomWO BW). |
| var danach = rest.stripLeading(); |
| boolean satzEndeErreicht = |
| danach.isEmpty() |
| || Character.isUpperCase(danach.codePointAt(0)) |
| || danach.startsWith("§"); |
| if (stelle.komponenten().stream().anyMatch(k -> k instanceof Stelle.HalbsatzNr) |
| && satzEndeErreicht) { |
| var gestutzt = rumpf.stripTrailing(); |
| if (gestutzt.endsWith(";") || gestutzt.endsWith(",")) { |
| rumpf = gestutzt.substring(0, gestutzt.length() - 1) + "."; |
| } |
| } |
| return TextErgebnis.ok((rumpf + rest).replaceAll(" +", " ").strip()); |
| }); |
| } |
| |
| private static AngewandteAenderung wendeUmnummerierungAn( |
| List<Norm> normen, Umnummerierung befehl) { |
| // „§ 9a wird zu § 9.“ — Umbenennung einer ganzen Norm. |
| if (nurParagraph(befehl.stelle()) && nurParagraph(befehl.neu())) { |
| var enbezAlt = befehl.stelle().paragraph().get().enbez(); |
| var enbezNeu = befehl.neu().paragraph().get().enbez(); |
| int idx = StellenAufloeser.normIndex(gesetzAus(normen), enbezAlt); |
| if (idx < 0) { |
| return manuell( |
| befehl, Grund.BESTAND_WIDERSPRICHT, enbezAlt + " existiert nicht im Gesetz."); |
| } |
| if (!enbezNeu.equals(enbezAlt)) { |
| int zielIdx = StellenAufloeser.normIndex(gesetzAus(normen), enbezNeu); |
| if (zielIdx >= 0 && !normen.get(zielIdx).weggefallen()) { |
| return manuell( |
| befehl, Grund.BESTAND_WIDERSPRICHT, enbezNeu + " existiert bereits im Gesetz."); |
| } |
| // Eine bereits weggefallene Zielnorm wird durch die Umnummerierung überschrieben. |
| if (zielIdx >= 0) { |
| normen.remove(zielIdx); |
| if (zielIdx < idx) { |
| idx--; |
| } |
| } |
| } |
| normen.set(idx, normen.get(idx).mitEnbez(enbezNeu)); |
| return angewandt(befehl, enbezNeu); |
| } |
| |
| var altAbsatz = befehl.stelle().absatz(); |
| var neuAbsatz = befehl.neu().absatz(); |
| // Nur wenn der Absatz selbst das Umnummerierungsziel ist — bei „Satz 5 wird Satz 4“ stammt |
| // eine etwaige Absatzangabe aus dem Kontextrahmen und der Satz-Zweig unten ist zuständig. |
| if (altAbsatz.isPresent() |
| && neuAbsatz.isPresent() |
| && feinsteIstAbsatz(befehl.stelle()) |
| && feinsteIstAbsatz(befehl.neu())) { |
| var ergebnis = StellenAufloeser.aufloese(gesetzAus(normen), befehl.stelle()); |
| if (ergebnis instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) { |
| return manuell(befehl, nicht.grund(), nicht.begruendung()); |
| } |
| var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle(); |
| var norm = normen.get(fundstelle.normIndex()); |
| var absaetze = new ArrayList<>(norm.absaetze()); |
| int quelleIdx = fundstelle.absatzIndex(); |
| var neueNummer = neuAbsatz.get().nummer(); |
| // Ein leerer Platzhalter-Absatz mit der Zielnummer (weggefallen/gegenstandslos) wird durch |
| // die |
| // Umnummerierung überschrieben (analog zur Norm-Umnummerierung auf eine weggefallene |
| // Zielnorm). |
| for (int i = absaetze.size() - 1; i >= 0; i--) { |
| if (i != quelleIdx |
| && neueNummer.equals(absaetze.get(i).nummer()) |
| && istLeererPlatzhalter(absaetze.get(i).text())) { |
| absaetze.remove(i); |
| if (i < quelleIdx) { |
| quelleIdx--; |
| } |
| } |
| } |
| absaetze.set(quelleIdx, new Absatz(neueNummer, absaetze.get(quelleIdx).text())); |
| normen.set(fundstelle.normIndex(), norm.mitAbsaetzen(absaetze)); |
| return angewandt(befehl, norm.enbez()); |
| } |
| // Nummer-/Buchstaben-Umnummerierung: anders als ein Absatz trägt eine Aufzählungseinheit ihre |
| // Bezeichnung als Marke im Absatztext („22. den Sozialverband …“); sie wird dort ausgetauscht. |
| // Ein weggefallener Platzhalter mit der Zielmarke weicht dabei — genau wie bei der |
| // Absatz-Umnummerierung, und wie es die amtliche Nachfassung zeigt. |
| var alteMarke = aufzaehlungsMarke(befehl.stelle()); |
| var neueMarke = aufzaehlungsMarke(befehl.neu()); |
| if (alteMarke != null && neueMarke != null && !alteMarke.equals(neueMarke)) { |
| return bearbeiteBereich( |
| normen, |
| befehl, |
| (text, bereich) -> { |
| var marke = |
| Pattern.compile("^([ \\t]*)" + Pattern.quote(alteMarke)) |
| .matcher(text.substring(bereich.von(), bereich.bis())); |
| if (!marke.find()) { |
| return TextErgebnis.fehler( |
| "„" |
| + alteMarke |
| + "“ steht nicht am Anfang von " |
| + befehl.stelle().anzeigeText() |
| + ".", |
| Grund.ZIELTEXT_FEHLT); |
| } |
| var umbenannt = |
| text.substring(0, bereich.von()) |
| + marke.group(1) |
| + neueMarke |
| + text.substring(bereich.von() + marke.end()); |
| return TextErgebnis.ok(entferneWeggefallenenPlatzhalter(umbenannt, neueMarke)); |
| }); |
| } |
| |
| // Satz-Umnummerierung: unnummerierte Sätze brauchen keine Textänderung; amtlich nummerierte |
| // (bayerisches Landesrecht, „Satz 5 wird Satz 4.“) erhalten die neue Satznummer im Text. |
| var altSatz = letzteKomponente(befehl.stelle(), Stelle.SatzNr.class); |
| var neuSatz = letzteKomponente(befehl.neu(), Stelle.SatzNr.class); |
| if (altSatz != null && neuSatz != null) { |
| var ergebnis = StellenAufloeser.aufloese(gesetzAus(normen), befehl.stelle()); |
| if (ergebnis instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) { |
| return manuell(befehl, nicht.grund(), nicht.begruendung()); |
| } |
| var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle(); |
| if (fundstelle.bereich() != null && fundstelle.absatzIndex() != null) { |
| var norm = normen.get(fundstelle.normIndex()); |
| var absaetze = new ArrayList<>(norm.absaetze()); |
| var absatz = absaetze.get(fundstelle.absatzIndex()); |
| var text = absatz.text(); |
| var marker = |
| Superskript.LAUF |
| .matcher(text) |
| .region(fundstelle.bereich().von(), fundstelle.bereich().bis()); |
| if (marker.lookingAt() && Superskript.istSatzanfang(text, marker.start(), marker.end())) { |
| var neuerText = |
| text.substring(0, marker.start()) |
| + Superskript.zuSuperskript(neuSatz.nummer()) |
| + text.substring(marker.end()); |
| absaetze.set(fundstelle.absatzIndex(), absatz.mitText(neuerText)); |
| normen.set(fundstelle.normIndex(), norm.mitAbsaetzen(absaetze)); |
| return angewandt(befehl, norm.enbez()); |
| } |
| } |
| } |
| return angewandt(befehl, "(keine Textänderung nötig)"); |
| } |
| |
| /** |
| * Die Fuge vor einem hinter einen Wortanker eingefügten Text: ein Leerzeichen, außer wenn der |
| * Einschub mit einem Satzzeichen beginnt („nach der Angabe „§ 39 Absatz 5“ die Angabe „, bei der |
| * Erstellung …““ ergibt „§ 39 Absatz 5, bei der …“, nicht „§ 39 Absatz 5 , bei der …“). |
| */ |
| private static String fuge(String einschub) { |
| return einschub.isEmpty() || ",;.:!?".indexOf(einschub.charAt(0)) < 0 ? " " : ""; |
| } |
| |
| /** Die Aufzählungsmarke der feinsten Komponente („Nummer 25“ → „25.“, „Buchstabe b“ → „b)“). */ |
| private static @Nullable String aufzaehlungsMarke(Stelle stelle) { |
| var komponenten = stelle.komponenten(); |
| if (komponenten.isEmpty()) { |
| return null; |
| } |
| return switch (komponenten.get(komponenten.size() - 1)) { |
| case Stelle.NummerNr n -> n.nummer() + "."; |
| case Stelle.BuchstabeNr b -> b.kennung() + ")"; |
| default -> null; |
| }; |
| } |
| |
| private static final Pattern LEERER_PLATZHALTER = |
| Pattern.compile("\\((?:weggefallen|gegenstandslos|aufgehoben)\\)"); |
| |
| /** |
| * Die Aufzählungsmarken, die ein eingefügter Block vergibt („5.“, „6.“, „7.“) — ohne solche, die |
| * er selbst als leeren Platzhalter führt, denn ein Platzhalter verdrängt keinen Platzhalter. |
| */ |
| private static List<String> platzhalterMarken(String block, Ebene ebene) { |
| var muster = |
| switch (ebene) { |
| case NUMMER -> NUMMER_MARKER; |
| case BUCHSTABE -> BUCHSTABE_MARKER; |
| default -> null; |
| }; |
| if (muster == null) { |
| return List.of(); |
| } |
| var marken = new ArrayList<String>(); |
| var marker = muster.matcher(block); |
| while (marker.find()) { |
| int zeilenEnde = block.indexOf('\n', marker.end()); |
| var rest = block.substring(marker.end(), zeilenEnde < 0 ? block.length() : zeilenEnde); |
| if (LEERER_PLATZHALTER.matcher(rest.strip()).matches()) { |
| continue; |
| } |
| marken.add(marker.group(1) + (ebene == Ebene.BUCHSTABE ? ")" : ".")); |
| } |
| return marken; |
| } |
| |
| /** |
| * Entfernt die weggefallene Aufzählungszeile mit der gegebenen Marke — die soeben umbenannte |
| * Zeile bleibt stehen, weil nur ein leerer Platzhalter getroffen wird. |
| */ |
| private static String entferneWeggefallenenPlatzhalter(String text, String marke) { |
| return text.replaceFirst( |
| "(?m)^[ \\t]*" |
| + Pattern.quote(marke) |
| + "[ \\t]+\\((?:weggefallen|gegenstandslos|aufgehoben)\\)\\n?", |
| ""); |
| } |
| |
| /** Ein leerer Platzhalter-Absatz ohne Inhalt („(weggefallen)“, „(gegenstandslos)“). */ |
| private static boolean istLeererPlatzhalter(String text) { |
| var t = text.strip(); |
| return t.equals("(weggefallen)") || t.equals("(gegenstandslos)"); |
| } |
| |
| private static <K extends Stelle.Komponente> @Nullable K letzteKomponente( |
| Stelle stelle, Class<K> art) { |
| K letzte = null; |
| for (var komponente : stelle.komponenten()) { |
| if (art.isInstance(komponente)) { |
| letzte = art.cast(komponente); |
| } |
| } |
| return letzte; |
| } |
| |
| private static AngewandteAenderung wendeWortlautZuAbsatzAn( |
| List<Norm> normen, WortlautZuAbsatz befehl) { |
| var aufloesung = loeseNormAuf(normen, befehl.stelle()); |
| if (aufloesung.fehler() != null) { |
| return manuell(befehl, aufloesung.grund(), aufloesung.fehler()); |
| } |
| var norm = normen.get(aufloesung.normIndex()); |
| // Steht genau ein unnummerierter Absatz zwischen nummerierten (bayerische Folge „Dem Wortlaut |
| // werden … Abs. 1 bis 4 vorangestellt“ → „Der bisherige Wortlaut wird Abs. 5“), erhält nur |
| // dieser die Nummer. Sind alle Absätze unnummeriert, wird der Gesamtwortlaut zu einem Absatz. |
| var unnummerierte = norm.absaetze().stream().filter(a -> a.nummer() == null).toList(); |
| if (unnummerierte.size() == 1 && norm.absaetze().size() > 1) { |
| var absaetze = new ArrayList<>(norm.absaetze()); |
| int idx = absaetze.indexOf(unnummerierte.get(0)); |
| absaetze.set(idx, new Absatz(befehl.nummer(), unnummerierte.get(0).text())); |
| normen.set(aufloesung.normIndex(), norm.mitAbsaetzen(absaetze)); |
| return angewandt(befehl, norm.enbez()); |
| } |
| var sb = new StringBuilder(); |
| for (var absatz : norm.absaetze()) { |
| if (sb.length() > 0) { |
| sb.append("\n\n"); |
| } |
| sb.append(absatz.text()); |
| } |
| normen.set( |
| aufloesung.normIndex(), |
| norm.mitAbsaetzen(List.of(new Absatz(befehl.nummer(), sb.toString())))); |
| return angewandt(befehl, norm.enbez()); |
| } |
| |
| /** „Der Wortlaut wird Satz 1.“ — der Zieltext erhält die amtliche Satznummer als Superskript. */ |
| private static AngewandteAenderung wendeWortlautZuSatzAn( |
| List<Norm> normen, WortlautZuSatz befehl) { |
| var ergebnis = StellenAufloeser.aufloese(gesetzAus(normen), befehl.stelle()); |
| if (ergebnis instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) { |
| return manuell(befehl, nicht.grund(), nicht.begruendung()); |
| } |
| var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle(); |
| var norm = normen.get(fundstelle.normIndex()); |
| Integer absatzIndex = fundstelle.absatzIndex(); |
| if (absatzIndex == null) { |
| if (norm.absaetze().size() != 1) { |
| return manuell( |
| befehl, |
| Grund.MEHRDEUTIG, |
| norm.enbez() + " hat " + norm.absaetze().size() + " Absätze; Ziel unklar."); |
| } |
| absatzIndex = 0; |
| } |
| var absaetze = new ArrayList<>(norm.absaetze()); |
| var absatz = absaetze.get(absatzIndex); |
| var marke = Superskript.zuSuperskript(befehl.nummer()); |
| if (!absatz.text().startsWith(marke)) { |
| absaetze.set(absatzIndex, absatz.mitText(marke + absatz.text())); |
| normen.set(fundstelle.normIndex(), norm.mitAbsaetzen(absaetze)); |
| } |
| return angewandt(befehl, norm.enbez()); |
| } |
| |
| /** „Dem Wortlaut werden die folgenden Abs. 1 bis 4 vorangestellt: „…““ */ |
| private static AngewandteAenderung wendeWortlautVoranstellungAn( |
| List<Norm> normen, WortlautVoranstellung befehl) { |
| var aufloesung = loeseNormAuf(normen, befehl.stelle()); |
| if (aufloesung.fehler() != null) { |
| return manuell(befehl, aufloesung.grund(), aufloesung.fehler()); |
| } |
| var norm = normen.get(aufloesung.normIndex()); |
| |
| // „Dem Wortlaut des Absatzes 3 werden die folgenden Sätze vorangestellt“ — nennt der Befehl |
| // einen Absatz, treten die neuen Sätze vor dessen Text, nicht vor die ganze Norm. |
| if (befehl.stelle().absatz().isPresent()) { |
| var ergebnis = StellenAufloeser.aufloese(gesetzAus(normen), befehl.stelle()); |
| if (ergebnis instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) { |
| return manuell(befehl, nicht.grund(), nicht.begruendung()); |
| } |
| var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle(); |
| var absaetze = new ArrayList<>(norm.absaetze()); |
| var absatz = absaetze.get(fundstelle.absatzIndex()); |
| absaetze.set( |
| fundstelle.absatzIndex(), |
| absatz.mitText( |
| normalisiereZitatText(befehl.text()).strip() + " " + absatz.text().stripLeading())); |
| normen.set(fundstelle.normIndex(), norm.mitAbsaetzen(absaetze)); |
| return angewandt(befehl, norm.enbez()); |
| } |
| |
| var neue = parseAbsaetze(befehl.text()); |
| if (neue.isEmpty()) { |
| return manuell(befehl, Grund.ZITAT_UNBRAUCHBAR, "Im Zitat wurde kein Absatz erkannt."); |
| } |
| var absaetze = new ArrayList<>(neue); |
| absaetze.addAll(norm.absaetze()); |
| normen.set(aufloesung.normIndex(), norm.mitAbsaetzen(absaetze)); |
| return angewandt(befehl, norm.enbez()); |
| } |
| |
| /** „Fußnote 1 wird aufgehoben.“ — entfernt Fußnotenzeile und Inline-Marker in der Kontextnorm. */ |
| private static AngewandteAenderung wendeFussnotenAufhebungAn( |
| List<Norm> normen, FussnotenAufhebung befehl) { |
| var aufloesung = loeseNormAuf(normen, befehl.stelle()); |
| if (aufloesung.fehler() != null) { |
| return manuell(befehl, aufloesung.grund(), aufloesung.fehler()); |
| } |
| var norm = normen.get(aufloesung.normIndex()); |
| var absaetze = new ArrayList<>(norm.absaetze()); |
| var fehlend = new ArrayList<String>(); |
| for (var nummer : befehl.nummern()) { |
| var marke = Superskript.zuSuperskript(nummer) + ")"; |
| var zeilenMuster = |
| Pattern.compile("(?m)^" + Pattern.quote(marke) + "\\s*\\[Amtl\\. Anm\\.:\\].*(?:\n|$)"); |
| boolean gefunden = false; |
| for (int i = 0; i < absaetze.size(); i++) { |
| var text = absaetze.get(i).text(); |
| var ohneZeile = zeilenMuster.matcher(text).replaceAll(""); |
| var ohneMarker = ohneZeile.replace(marke, ""); |
| if (!ohneMarker.equals(text)) { |
| absaetze.set(i, absaetze.get(i).mitText(ohneMarker.stripTrailing())); |
| gefunden = true; |
| } |
| } |
| if (!gefunden) { |
| fehlend.add(nummer); |
| } |
| } |
| if (!fehlend.isEmpty()) { |
| return manuell( |
| befehl, |
| Grund.ZIELTEXT_FEHLT, |
| "Fußnote " + String.join(", ", fehlend) + " kommt in " + norm.enbez() + " nicht vor."); |
| } |
| normen.set(aufloesung.normIndex(), norm.mitAbsaetzen(absaetze)); |
| return angewandt(befehl, norm.enbez()); |
| } |
| |
| /** „In Satz 1 wird die Satznummerierung „1“ gestrichen.“ */ |
| private static AngewandteAenderung wendeSatznummerierungStreichungAn( |
| List<Norm> normen, SatznummerierungStreichung befehl) { |
| return bearbeiteBereich( |
| normen, |
| befehl, |
| (text, bereich) -> { |
| var marke = Superskript.zuSuperskript(befehl.nummer()); |
| if (!text.startsWith(marke, bereich.von())) { |
| return TextErgebnis.fehler( |
| "Die Satznummerierung „" + befehl.nummer() + "“ steht nicht am Anfang des Ziels.", |
| Grund.ZIELTEXT_FEHLT); |
| } |
| return TextErgebnis.ok( |
| text.substring(0, bereich.von()) + text.substring(bereich.von() + marke.length())); |
| }); |
| } |
| |
| /** |
| * Ein Mehrfachziel-Befehl („In A und B wird jeweils …“): wendet jeden Teilbefehl nacheinander an |
| * (jeder mutiert den fortlaufenden Zwischenstand) und fasst sie zu einem Protokolleintrag |
| * zusammen. Nur wenn alle Teile gelingen, gilt der Befehl als angewandt; sonst wird er zur |
| * manuellen Prüfung markiert (bereits angewandte Teile bleiben wirksam). |
| */ |
| /** |
| * Ein Verbund, der erst hier auftaucht (in einem anderen Verbund geschachtelt), wird für sich |
| * genommen aufgefaltet und geordnet. Der Regelfall — der Verbund als eigener Gliederungspunkt — |
| * ist dagegen schon in {@link #anwenden} aufgefaltet und läuft nie hier durch. |
| */ |
| private static AngewandteAenderung wendeSammelAn( |
| List<Norm> normen, List<Gliederung> gliederungen, Sammelbefehl befehl) { |
| var schritte = new ArrayList<Schritt>(befehl.teilbefehle().size()); |
| falte(befehl, 0, schritte); |
| var ergebnisse = new AngewandteAenderung[schritte.size()]; |
| for (int index : anwendungsReihenfolge(schritte)) { |
| ergebnisse[index] = wendeAn(normen, gliederungen, schritte.get(index).teil()); |
| } |
| return verbund(befehl, Arrays.asList(ergebnisse), schritte, 0); |
| } |
| |
| // --- Gemeinsame Helfer --------------------------------------------------------------------- |
| |
| private record TextErgebnis( |
| @Nullable String text, @Nullable String fehler, @Nullable Grund grund) { |
| static TextErgebnis ok(String text) { |
| return new TextErgebnis(text, null, null); |
| } |
| |
| static TextErgebnis fehler(String begruendung, Grund grund) { |
| return new TextErgebnis(null, begruendung, grund); |
| } |
| } |
| |
| private interface TextOperation { |
| TextErgebnis wende(String text); |
| } |
| |
| // Fußnoten-Definitionszeile am Zeilenanfang („⁶) [Amtl. Anm.:] …“), die dem tragenden Absatz |
| // anhängt. Für wortweise Operationen (Streichen/Ersetzen/Einfügen) ist sie kein Inhalt: der |
| // eingebettete Marker „Gesetzbuchs²)“ soll getroffen werden, der gleichlautende Definitionskopf |
| // „²)“ nicht. Fußnoten werden daher vor der Operation abgetrennt und danach unverändert |
| // wieder angehängt (ihre eigene Aufhebung läuft über FussnotenAufhebung). |
| private static final Pattern FUSSNOTEN_DEFINITION = Pattern.compile("(?m)^[⁰¹²³⁴⁵⁶⁷⁸⁹]+\\)"); |
| |
| private static TextOperation ohneFussnoten(TextOperation operation) { |
| return text -> { |
| var m = FUSSNOTEN_DEFINITION.matcher(text); |
| if (!m.find()) { |
| return operation.wende(text); |
| } |
| int grenze = m.start(); |
| var ergebnis = operation.wende(text.substring(0, grenze)); |
| if (ergebnis.fehler() != null) { |
| return ergebnis; |
| } |
| return TextErgebnis.ok(ergebnis.text() + text.substring(grenze)); |
| }; |
| } |
| |
| private interface BereichsOperation { |
| TextErgebnis wende(String text, SatzTeiler.SatzBereich bereich); |
| } |
| |
| /** |
| * Wendet eine Textoperation auf den durch die Stelle bestimmten Bereich an. Ohne |
| * Bereichs-/Absatzangabe wird die Operation auf jeden Absatz der Norm versucht; sie muss dann in |
| * genau einem Absatz anwendbar sein. |
| */ |
| private static AngewandteAenderung bearbeiteText( |
| List<Norm> normen, Aenderungsbefehl befehl, TextOperation operation) { |
| // „In der Überschrift …“: die Operation wirkt auf den Titel der Norm, nicht auf ihren Text. |
| if (befehl.stelle().betrifftUeberschrift()) { |
| var aufloesung = loeseNormAuf(normen, befehl.stelle()); |
| if (aufloesung.fehler() != null) { |
| return manuell(befehl, aufloesung.grund(), aufloesung.fehler()); |
| } |
| var norm = normen.get(aufloesung.normIndex()); |
| if (norm.titel() == null) { |
| return manuell( |
| befehl, Grund.BESTAND_WIDERSPRICHT, norm.enbez() + " hat keine Überschrift."); |
| } |
| var titelErgebnis = operation.wende(norm.titel()); |
| if (titelErgebnis.fehler() != null) { |
| return manuell(befehl, titelErgebnis.grund(), titelErgebnis.fehler()); |
| } |
| normen.set(aufloesung.normIndex(), norm.mitTitel(titelErgebnis.text())); |
| return angewandt(befehl, norm.enbez()); |
| } |
| |
| var ergebnis = StellenAufloeser.aufloese(gesetzAus(normen), befehl.stelle()); |
| if (ergebnis instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) { |
| return manuell(befehl, nicht.grund(), nicht.begruendung()); |
| } |
| var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle(); |
| var norm = normen.get(fundstelle.normIndex()); |
| |
| if (fundstelle.absatzIndex() != null) { |
| var absaetze = new ArrayList<>(norm.absaetze()); |
| var absatz = absaetze.get(fundstelle.absatzIndex()); |
| var neuerText = wendeAufBereichAn(absatz.text(), fundstelle.bereich(), operation); |
| if (neuerText.fehler() != null) { |
| return manuell(befehl, neuerText.grund(), neuerText.fehler()); |
| } |
| absaetze.set(fundstelle.absatzIndex(), absatz.mitText(neuerText.text())); |
| normen.set(fundstelle.normIndex(), norm.mitAbsaetzen(absaetze)); |
| return angewandt(befehl, norm.enbez()); |
| } |
| |
| // Ganze Norm als Wirkungsbereich: Operation muss in genau einem Absatz gelingen. |
| var absaetze = new ArrayList<>(norm.absaetze()); |
| Integer trefferIndex = null; |
| TextErgebnis treffer = null; |
| String letzterFehler = "Norm hat keine Absätze."; |
| Grund letzterGrund = Grund.BESTAND_WIDERSPRICHT; |
| for (int i = 0; i < absaetze.size(); i++) { |
| var versuch = operation.wende(absaetze.get(i).text()); |
| if (versuch.fehler() == null) { |
| if (trefferIndex != null) { |
| return manuell( |
| befehl, |
| Grund.MEHRDEUTIG, |
| "Mehrere Absätze von " + norm.enbez() + " kommen infrage; mehrdeutig."); |
| } |
| trefferIndex = i; |
| treffer = versuch; |
| } else { |
| letzterFehler = versuch.fehler(); |
| letzterGrund = versuch.grund(); |
| } |
| } |
| if (trefferIndex == null) { |
| return manuell(befehl, letzterGrund, letzterFehler); |
| } |
| absaetze.set(trefferIndex, absaetze.get(trefferIndex).mitText(treffer.text())); |
| normen.set(fundstelle.normIndex(), norm.mitAbsaetzen(absaetze)); |
| return angewandt(befehl, norm.enbez()); |
| } |
| |
| /** Wie {@link #bearbeiteText}, aber die Operation braucht den konkreten Zeichenbereich. */ |
| private static AngewandteAenderung bearbeiteBereich( |
| List<Norm> normen, Aenderungsbefehl befehl, BereichsOperation operation) { |
| var ergebnis = StellenAufloeser.aufloese(gesetzAus(normen), befehl.stelle()); |
| if (ergebnis instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) { |
| return manuell(befehl, nicht.grund(), nicht.begruendung()); |
| } |
| var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle(); |
| if (fundstelle.absatzIndex() == null || fundstelle.bereich() == null) { |
| return manuell( |
| befehl, |
| Grund.STELLE_NICHT_AUFLOESBAR, |
| "„" + befehl.stelle().anzeigeText() + "“ bezeichnet keinen konkreten Textbereich."); |
| } |
| var norm = normen.get(fundstelle.normIndex()); |
| var absaetze = new ArrayList<>(norm.absaetze()); |
| var absatz = absaetze.get(fundstelle.absatzIndex()); |
| var neuerText = operation.wende(absatz.text(), fundstelle.bereich()); |
| if (neuerText.fehler() != null) { |
| return manuell(befehl, neuerText.grund(), neuerText.fehler()); |
| } |
| absaetze.set(fundstelle.absatzIndex(), absatz.mitText(neuerText.text())); |
| normen.set(fundstelle.normIndex(), norm.mitAbsaetzen(absaetze)); |
| return angewandt(befehl, norm.enbez()); |
| } |
| |
| private static TextErgebnis wendeAufBereichAn( |
| String text, SatzTeiler.@Nullable SatzBereich bereich, TextOperation operation) { |
| if (bereich == null) { |
| return operation.wende(text); |
| } |
| var ausschnitt = text.substring(bereich.von(), bereich.bis()); |
| var ergebnis = operation.wende(ausschnitt); |
| if (ergebnis.fehler() != null) { |
| return ergebnis; |
| } |
| return TextErgebnis.ok( |
| text.substring(0, bereich.von()) + ergebnis.text() + text.substring(bereich.bis())); |
| } |
| |
| private record NormAufloesung(int normIndex, @Nullable String fehler, @Nullable Grund grund) {} |
| |
| private static NormAufloesung loeseNormAuf(List<Norm> normen, Stelle stelle) { |
| String enbez; |
| if (stelle.paragraph().isPresent()) { |
| enbez = stelle.paragraph().get().enbez(); |
| } else if (stelle.anlagenEnbez().isPresent()) { |
| enbez = stelle.anlagenEnbez().get(); |
| // Wie im StellenAufloeser: Die Nummer einer Anlage kann eine eigene Norm sein. |
| var nummer = stelle.nummer(); |
| if (nummer.isPresent() |
| && StellenAufloeser.normIndex( |
| gesetzAus(normen), enbez + " Nummer " + nummer.get().nummer()) |
| >= 0) { |
| enbez = enbez + " Nummer " + nummer.get().nummer(); |
| } |
| } else { |
| return new NormAufloesung( |
| -1, |
| "Stelle nennt keinen Paragraphen: " + stelle.anzeigeText(), |
| Grund.STELLE_NICHT_AUFLOESBAR); |
| } |
| int index = StellenAufloeser.normIndex(gesetzAus(normen), enbez); |
| if (index < 0) { |
| return new NormAufloesung( |
| -1, enbez + " existiert nicht im Gesetz.", Grund.BESTAND_WIDERSPRICHT); |
| } |
| return new NormAufloesung(index, null, null); |
| } |
| |
| private static boolean nurParagraph(Stelle stelle) { |
| return stelle.komponenten().size() == 1 |
| && stelle.komponenten().get(0) instanceof Stelle.Paragraph; |
| } |
| |
| /** Wahr, wenn die Stelle als Ganzes eine Norm meint: ein einzelner § oder ein Anhang/Anlage. */ |
| private static boolean nurNorm(List<Norm> normen, Stelle stelle) { |
| if (stelle.komponenten().size() == 1 |
| && (stelle.komponenten().get(0) instanceof Stelle.Paragraph |
| || stelle.anlagenEnbez().isPresent())) { |
| return true; |
| } |
| // „Nummer 31 wird wie folgt gefasst“ innerhalb einer Anlage: Trägt das Gesetz die Nummer als |
| // eigene Norm, so bezeichnet die Stelle diese ganz und nicht einen Teil der Anlage. |
| return stelle.komponenten().size() == 2 |
| && stelle.anlagenEnbez().isPresent() |
| && stelle.nummer().isPresent() |
| && StellenAufloeser.normIndex( |
| gesetzAus(normen), |
| stelle.anlagenEnbez().get() + " Nummer " + stelle.nummer().get().nummer()) |
| >= 0; |
| } |
| |
| /** |
| * Die feinste Komponente der Stelle ist ein Absatz — es geht also um den Absatz selbst und nicht |
| * um einen Satz, eine Nummer oder einen Buchstaben in ihm. |
| * |
| * <p>Maßgeblich ist die <em>letzte</em> Komponente, nicht das bloße Vorkommen einer feineren: Die |
| * Nummer einer Anlage steht vor der Absatzangabe und gehört zur Bezeichnung der Norm („Anlage |
| * Nummer 23 Absatz 9“), nicht zu ihrem Inneren. Wer sie mitzählte, hielte die Umnummerierung |
| * eines solchen Absatzes für die einer Aufzählungseinheit und tauschte eine Marke „23.“ im Text — |
| * die dort nicht steht. Der Befehl galt dann als angewandt und bewirkte nichts. |
| */ |
| private static boolean feinsteIstAbsatz(Stelle stelle) { |
| var komponenten = stelle.komponenten(); |
| return !komponenten.isEmpty() |
| && komponenten.get(komponenten.size() - 1) instanceof Stelle.AbsatzNr; |
| } |
| |
| /** |
| * Wahr, wenn die feinste Komponente der Stelle eine Aufzählungseinheit ist (Nummer/Buchstabe). |
| */ |
| private static boolean feinsteIstAufzaehlung(Stelle stelle) { |
| var komponenten = stelle.komponenten(); |
| if (komponenten.isEmpty()) { |
| return false; |
| } |
| var feinste = komponenten.get(komponenten.size() - 1); |
| return feinste instanceof Stelle.NummerNr || feinste instanceof Stelle.BuchstabeNr; |
| } |
| |
| /** |
| * Heilt den Weißraum an der Stelle, an der eine Streichung Text herausgenommen hat: Aus zwei |
| * Leerzeichen wird eines, vor einem Satzzeichen bleibt keines. Geheilt wird ausdrücklich nur die |
| * Naht und nicht der ganze Zieltext — dieser ist bei einer Stelle ohne Fein-Komponente der |
| * gesamte Absatz, und eine Heilung über ihn hinweg fräße die Einrückung der Aufzählungszeilen: |
| * Aus den zwei Leerzeichen vor jeder Marke würde eines. Traf die Streichung den Zeilenanfang, so |
| * ist der Weißraum vor der Naht Einrückung und bleibt unangetastet. |
| */ |
| private static String heileNaht(String text, int naht) { |
| int von = naht; |
| while (von > 0 && text.charAt(von - 1) == ' ') { |
| von--; |
| } |
| int bis = naht; |
| while (bis < text.length() && text.charAt(bis) == ' ') { |
| bis++; |
| } |
| if (bis == von) { |
| return text; |
| } |
| if (von == 0 || text.charAt(von - 1) == '\n') { |
| return text.substring(0, naht) + text.substring(bis); |
| } |
| if (bis < text.length() && ",;.".indexOf(text.charAt(bis)) >= 0) { |
| return text.substring(0, von) + text.substring(bis); |
| } |
| if (bis - von == 1) { |
| return text; |
| } |
| return text.substring(0, von) + " " + text.substring(bis); |
| } |
| |
| private static @Nullable String labelVon(Stelle stelle) { |
| for (var komponente : stelle.komponenten().reversed()) { |
| if (komponente instanceof Stelle.NummerNr n) { |
| return n.nummer() + "."; |
| } |
| if (komponente instanceof Stelle.BuchstabeNr b) { |
| return b.kennung() + ")"; |
| } |
| } |
| 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(); |
| while (i < text.length() && (text.charAt(i) == ' ' || text.charAt(i) == '\t')) { |
| sb.append(text.charAt(i)); |
| i++; |
| } |
| return sb.toString(); |
| } |
| |
| private record Fundpruefung(int index, @Nullable String fehler, @Nullable Grund grund) {} |
| |
| private static Fundpruefung eindeutigeFundstelle(String text, String woerter) { |
| int anzahl = zaehleVorkommen(text, woerter); |
| if (anzahl == 0) { |
| return new Fundpruefung( |
| -1, "„" + woerter + "“ kommt im Zieltext nicht vor.", Grund.ZIELTEXT_FEHLT); |
| } |
| if (anzahl > 1) { |
| return new Fundpruefung( |
| -1, "„" + woerter + "“ kommt " + anzahl + "-mal vor; mehrdeutig.", Grund.MEHRDEUTIG); |
| } |
| return new Fundpruefung(findeVorkommen(text, woerter, 0), null, null); |
| } |
| |
| private static int zaehleVorkommen(String text, String suchtext) { |
| if (suchtext.isEmpty()) { |
| return 0; |
| } |
| int anzahl = 0; |
| int 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. |
| private static final Pattern PARAGRAPH_UEBERSCHRIFT = |
| Pattern.compile( |
| "(?=(?:§|Art\\.)\\s*\\d+[a-z]?\\s+" |
| + "(?!Absatz|Absätze|Abs|Satz|Sätze|Nummer|Nummern|Nr|Buchstabe|Buchstaben|Buchst" |
| + "|und|bis|oder|sowie|des|der|dieses|genannten)" |
| + "\\p{Lu})"); |
| |
| /** |
| * Zerlegt einen Zitatblock mehrerer Paragraphen an den §-Überschriften (nicht an Querverweisen) |
| * und parst jeden Abschnitt zu einer {@link Norm}. Die Gliederung wird von der Vorlage |
| * übernommen. |
| */ |
| private static List<Norm> parseNormenBlock(String block, @Nullable Gliederung gliederung) { |
| var normen = new ArrayList<Norm>(); |
| for (var stueck : PARAGRAPH_UEBERSCHRIFT.split(block.strip())) { |
| var s = stueck.strip(); |
| var m = Pattern.compile("^(§|Art\\.)\\s*(\\d+[a-z]?)\\b").matcher(s); |
| if (s.isEmpty() || !m.find()) { |
| continue; |
| } |
| var enbez = m.group(1) + " " + m.group(2); |
| normen.add(parseNorm(s, enbez, new Norm(enbez, null, gliederung, List.of(), false))); |
| } |
| return normen; |
| } |
| |
| /** Zerlegt einen zitierten Normtext („§ 28a Titel (1) … (2) …“) in Titel und Absätze. */ |
| private static Norm parseNorm(String zitat, String enbez, Norm vorlage) { |
| var text = zitat.strip(); |
| String titel = vorlage.titel(); |
| |
| // Das Zitat einer neugefassten Anlagen-Nummer wiederholt deren Bezeichnung als erste Zeile |
| // („Nummer 31“ / „Landesamt …“ / „Zu den Ordnungsaufgaben …“). Sie gehört nicht in den |
| // Wortlaut — ebenso wenig, wie die Bezeichnung eines Paragraphen in seinen Text gehört. |
| var eigeneBezeichnung = enbez.replaceFirst("^.*?\\b(Nummer \\d+[a-z]?)$", "$1"); |
| if (!eigeneBezeichnung.equals(enbez)) { |
| var erste = text.lines().findFirst().orElse("").strip(); |
| if (erste.equals(eigeneBezeichnung)) { |
| text = text.substring(text.indexOf('\n') + 1).strip(); |
| } |
| } |
| |
| var absatzStart = ABSATZ_MARKER.matcher(text); |
| int erster = absatzStart.find() ? absatzStart.start() : -1; |
| |
| if (erster >= 0) { |
| var kopf = text.substring(0, erster).strip(); |
| if (!kopf.isEmpty()) { |
| titel = kopf.replaceFirst("^(?:§|Art\\.)\\s*\\S+\\s*", "").replaceAll("\\s+", " ").strip(); |
| if (titel.isEmpty()) { |
| titel = vorlage.titel(); |
| } |
| } |
| return new Norm( |
| enbez, titel, vorlage.gliederung(), parseAbsaetze(text.substring(erster)), false); |
| } |
| |
| // Ohne Absatzmarker (Einzelabsatz-Normen wie „§ 19 Außerkrafttreten Dieses Gesetz tritt …“): |
| // Steht „§ N“ allein auf der ersten Zeile, ist die folgende Zeile die Überschrift und der Rest |
| // der Normtext. Die Überschrift darf dabei über mehrere Zeilen laufen — der Satz bricht sie am |
| // Spaltenrand um („Zahlungen an den örtlichen Träger“ / „der öffentlichen Jugendhilfe“). |
| // Fortgesetzt wird sie nur über klein beginnende Zeilen: Ein Normtext beginnt großgeschrieben |
| // oder mit einem Marker. |
| var zeilen = text.lines().map(String::strip).filter(z -> !z.isEmpty()).toList(); |
| if (zeilen.size() >= 3 && zeilen.get(0).matches("(?:§|Art\\.)\\s*\\d+[a-z]?")) { |
| int nachTitel = titelEnde(zeilen, 1); |
| titel = String.join(" ", zeilen.subList(1, nachTitel)); |
| var rest = String.join("\n", zeilen.subList(nachTitel, zeilen.size())); |
| return new Norm( |
| enbez, |
| titel, |
| vorlage.gliederung(), |
| List.of(new Absatz(null, normalisiereZitatText(rest))), |
| false); |
| } |
| // Dieselbe Norm, deren Überschrift der Satz an den Kopf gezogen hat („§ 1 Zahlungen an die |
| // Wohnsitzgemeinde“ in einer Zeile). Überschrift ist der Zeilenrest nur dann, wenn er keinen |
| // Satz abschließt — sonst trägt die Zeile bereits den Normtext. |
| var kopfZeile = |
| zeilen.isEmpty() |
| ? null |
| : Pattern.compile("^(?:§|Art\\.)\\s*\\d+[a-z]?\\s+(\\S.*)$").matcher(zeilen.get(0)); |
| if (zeilen.size() >= 2 |
| && kopfZeile != null |
| && kopfZeile.matches() |
| && !kopfZeile.group(1).endsWith(".")) { |
| var kopf = new ArrayList<String>(); |
| kopf.add(kopfZeile.group(1).strip()); |
| // Die Überschrift steht hier schon in Zeile 0; fortgesetzt werden kann sie ab Zeile 1. |
| int nachTitel = titelEnde(zeilen, 0); |
| kopf.addAll(zeilen.subList(1, nachTitel)); |
| var rest = String.join("\n", zeilen.subList(nachTitel, zeilen.size())); |
| if (!rest.isBlank()) { |
| return new Norm( |
| enbez, |
| String.join(" ", kopf), |
| vorlage.gliederung(), |
| List.of(new Absatz(null, normalisiereZitatText(rest))), |
| false); |
| } |
| } |
| // Fallback: gesamter Text (ohne „§ N“-Präfix) als unnummerierter Absatz, Titel unverändert. |
| var inhalt = text.replaceFirst("^(?:§|Art\\.)\\s*\\S+\\s*", ""); |
| return new Norm( |
| enbez, |
| titel, |
| vorlage.gliederung(), |
| List.of(new Absatz(null, normalisiereZitatText(inhalt))), |
| false); |
| } |
| |
| /** |
| * Der Index hinter der Überschrift: Ab {@code von} laufen so lange Überschriftenzeilen, wie sie |
| * klein beginnen — ein umbrochener Titel setzt klein fort, ein Normtext beginnt groß oder mit |
| * einem Marker. |
| */ |
| private static int titelEnde(List<String> zeilen, int von) { |
| int i = von + 1; |
| while (i < zeilen.size() |
| && !zeilen.get(i).isEmpty() |
| && Character.isLowerCase(zeilen.get(i).codePointAt(0))) { |
| i++; |
| } |
| return i; |
| } |
| |
| /** Zerlegt zitierten Text in Absätze anhand der „(n)“-Marker. */ |
| static List<Absatz> parseAbsaetze(String zitat) { |
| var text = zitat.strip(); |
| var absaetze = new ArrayList<Absatz>(); |
| var matcher = ABSATZ_MARKER.matcher(text); |
| |
| int vorherigesEnde = 0; |
| String vorherigeNummer = null; |
| while (matcher.find()) { |
| if (matcher.start() > vorherigesEnde || vorherigeNummer != null) { |
| var inhalt = text.substring(vorherigesEnde, matcher.start()).strip(); |
| if (!inhalt.isEmpty() || vorherigeNummer != null) { |
| absaetze.add(new Absatz(vorherigeNummer, normalisiereZitatText(inhalt))); |
| } |
| } |
| vorherigeNummer = matcher.group(1); |
| vorherigesEnde = matcher.end(); |
| } |
| var rest = text.substring(vorherigesEnde).strip(); |
| if (!rest.isEmpty() || vorherigeNummer != null) { |
| absaetze.add(new Absatz(vorherigeNummer, normalisiereZitatText(rest))); |
| } |
| return absaetze; |
| } |
| |
| /** |
| * Zitattext in die kanonische Zeilenform bringen: Leerzeilen entfallen, Zeilen werden gestutzt, |
| * Aufzählungspunkte eingerückt. Zeilenumbrüche bleiben erhalten — der TextBereiniger hat weiche |
| * (Blocksatz-)Umbrüche bereits zu Fließtext zusammengezogen, verbleibende Umbrüche sind also |
| * beabsichtigt (z.B. die Kurzüberschrift über einer hängend eingerückten Definition im |
| * UWG-Anhang) und müssen dieselbe Form erhalten wie beim Flatten des Stammgesetz-XML. |
| */ |
| /** Eine Zeile, die mit einem Aufzählungsmarker beginnt („3. “, „d) “). */ |
| private static final Pattern AUFZAEHLUNGSZEILE = |
| Pattern.compile("^(\\d+[a-z]?\\.|[a-z]{1,3}\\))\\s.*"); |
| |
| // Der Zwischenraum zwischen der Aufzählungsmarke und ihrem Text: im Gesetzblattsatz steht dort |
| // die Marken-Tabulatorspalte, im kanonischen Klartext ein einzelnes Leerzeichen. |
| private static final Pattern MARKEN_ZWISCHENRAUM = |
| Pattern.compile("^(\\d+[a-z]?\\.|[a-z]{1,3}\\))[ \\t]+"); |
| |
| private static String normalisiereZitatText(String text) { |
| var zeilen = text.split("\n"); |
| var sb = new StringBuilder(); |
| // Fortsetzungszeilen innerhalb eines Aufzählungspunkts (z.B. der Definitionstext unter einer |
| // Kurzüberschrift) werden tiefer eingerückt als die Aufzählungszeile — dieselbe Form, die der |
| // ContentFlattener aus dem Stammgesetz-XML erzeugt, und Voraussetzung dafür, dass die |
| // Stellenauflösung sie als Kindzeilen der Einheit erkennt. |
| var fortsetzungsEinzug = ""; |
| for (var zeile : zeilen) { |
| var gestutzt = MARKEN_ZWISCHENRAUM.matcher(zeile.strip()).replaceFirst("$1 "); |
| if (gestutzt.isEmpty()) { |
| continue; |
| } |
| if (sb.length() == 0) { |
| sb.append(gestutzt); |
| if (AUFZAEHLUNGSZEILE.matcher(gestutzt).matches()) { |
| fortsetzungsEinzug = " "; |
| } |
| } else if (AUFZAEHLUNGSZEILE.matcher(gestutzt).matches()) { |
| // Aufzählungspunkt: eigene Zeile mit Einzug. |
| sb.append("\n ").append(gestutzt); |
| fortsetzungsEinzug = " "; |
| } else { |
| sb.append('\n').append(fortsetzungsEinzug).append(gestutzt); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| /** |
| * Rückt die kanonischen Zitatzeilen auf die Ziel-Einrückung um: Aufzählungszeilen (und die erste |
| * Zeile) auf {@code einrueckung}, Fortsetzungszeilen — etwa der Definitionstext unter einer |
| * Kurzüberschrift — zwei Zeichen tiefer, damit sie Kindzeilen der Einheit bleiben (dieselbe Form, |
| * die der ContentFlattener aus dem Stammgesetz-XML erzeugt). |
| */ |
| private static String rueckeZitatEin(String zitat, String einrueckung) { |
| var sb = new StringBuilder(); |
| boolean erste = true; |
| for (var zeile : zitat.split("\n", -1)) { |
| var inhalt = zeile.strip(); |
| if (!erste) { |
| sb.append('\n'); |
| } |
| sb.append(einrueckung); |
| if (!erste && !AUFZAEHLUNGSZEILE.matcher(inhalt).matches()) { |
| sb.append(" "); |
| } |
| sb.append(inhalt); |
| erste = false; |
| } |
| return sb.toString(); |
| } |
| |
| private static AngewandteAenderung angewandt(Aenderungsbefehl befehl, String enbez) { |
| return new AngewandteAenderung( |
| befehl, Status.ANGEWANDT, "", new LinkedHashSet<>(List.of(enbez)), null); |
| } |
| |
| private static AngewandteAenderung angewandt(Aenderungsbefehl befehl, List<String> enbezliste) { |
| return new AngewandteAenderung( |
| befehl, Status.ANGEWANDT, "", new LinkedHashSet<>(enbezliste), null); |
| } |
| |
| /** |
| * Der Befehl bleibt liegen. Die Art des Grundes darf durchgereicht sein ({@link NormAufloesung}, |
| * {@link TextErgebnis}, {@link StellenAufloeser.Ergebnis.NichtGefunden} tragen sie mit ihrem |
| * Wortlaut, weil nur die erzeugende Stelle sie kennt); fehlt sie, gilt die unauffindbare Stelle. |
| */ |
| private static AngewandteAenderung manuell( |
| Aenderungsbefehl befehl, @Nullable Grund grund, String begruendung) { |
| return new AngewandteAenderung( |
| befehl, |
| Status.MANUELL_PRUEFEN, |
| begruendung, |
| Set.of(), |
| grund == null ? Grund.STELLE_NICHT_AUFLOESBAR : grund); |
| } |
| |
| private static Gesetz gesetzAus(List<Norm> normen) { |
| return new Gesetz("", null, null, normen); |
| } |
| } |