aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FASSUNGEN.txt81
-rw-r--r--README.md13
-rw-r--r--src/main/java/eu/mulk/aendggner/AendGgner.java24
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParser.java93
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java337
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/Grund.java56
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtAnwender.java72
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java28
-rw-r--r--src/main/java/eu/mulk/aendggner/gesetz/Gesetz.java29
-rw-r--r--src/main/java/eu/mulk/aendggner/gesetz/Stand.java22
-rw-r--r--src/main/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoader.java46
-rw-r--r--src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java63
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java10
-rw-r--r--src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java31
-rw-r--r--src/test/java/eu/mulk/aendggner/synopse/HtmlRendererTest.java2
15 files changed, 763 insertions, 144 deletions
diff --git a/FASSUNGEN.txt b/FASSUNGEN.txt
index e4ca4c7..2083a3f 100644
--- a/FASSUNGEN.txt
+++ b/FASSUNGEN.txt
@@ -738,6 +738,87 @@ Fortschreibung der Prüfung
(3) Der Prüfbestand umfasst hiernach dreihundertdreißig Prüfungen.
+
+Artikel 30
+Arten der Gründe
+
+(1) Neben den ausformulierten Grund tritt seine Art (Klasse „Grund“): Befehl
+ nicht erkannt, Stelle nicht auffindbar, Zieltext nicht vorhanden, Fundstelle
+ mehrdeutig, Bereich unbrauchbar, Zitat unbrauchbar, Bestand widerspricht dem
+ Befehl, nicht unterstützt, Anwendung fehlgeschlagen.
+
+(2) Die Art wird dort vergeben, wo der Grund entsteht. Die Hilfsergebnisse der
+ Stellenauflösung, der Textoperation und der Zeilensuche in der Inhalts-
+ übersicht führen sie mit ihrem Wortlaut, weil allein die erzeugende Stelle
+ sie kennt. Bleibt ein Verbund liegen, gilt die Art seines ersten
+ gescheiterten Teiles.
+
+(3) Der ausformulierte Grund bleibt unberührt und maßgeblich. Der Grundsatz der
+ Nichtverwerfung erfordert die genaue Auskunft im Einzelfall; die Art ordnet
+ sie nur.
+
+
+Artikel 31
+Darstellung und Auszählung
+
+(1) Der Abschnitt „Manuell prüfen“ der Synopse bündelt die Befehle nach der Art
+ des Grundes und stellt jeder Gruppe ihre Häufigkeit voran. Innerhalb der
+ Gruppe bleibt die Reihenfolge des Dokuments.
+
+(2) Der Schalter „--dump-befehle“ schließt mit der Tabelle „Gründe nach
+ Häufigkeit“.
+
+
+Artikel 32
+Rüge des jüngeren Stammgesetzes
+
+(1) Das Stammgesetz führt fortan den Stand, den seine Quelle angibt (Klasse
+ „Stand“). Maßgeblich ist nicht die Standzeile allein, sondern das späteste
+ Datum, das irgendeine Standangabe nennt: Die Hinweise der Form „Änderung
+ durch … textlich nachgewiesen, dokumentarisch noch nicht abschließend
+ bearbeitet“ bezeugen, dass der Wortlaut die Änderung bereits trägt, während
+ die Standzeile noch die vorige nennt.
+
+(2) Ist der Wortlaut des Stammgesetzes jünger als die Fassung, die der
+ Einleitungssatz des Änderungsgesetzes fortschreibt, so ergeht eine Warnung.
+ Sie nennt beide Daten und weist darauf hin, dass Befehle, deren Zieltext
+ „im Zieltext nicht vorkommt“, wahrscheinlich hierauf beruhen.
+
+(3) Die Warnung ergeht einmal je Dokument, auch wenn mehrere Artikel dasselbe
+ Stammgesetz betreffen. Bei fehlender Standangabe, bei unlesbarem
+ Einleitungssatz und bei gleichem oder früherem Datum schweigt die Prüfung;
+ geraten wird nicht.
+
+
+Artikel 33
+Anfügen ganzer Paragraphen
+
+Die ankerlose Anfügung ganzer Paragraphen wird angewandt, statt als nicht
+unterstützt gemeldet zu werden. Angefügt wird ans Ende des Gesetzes, bei
+benannter Gliederungseinheit ans Ende ihres Blocks; trägt das Zitat keinen
+Normkopf, bleibt der Befehl liegen. Die verankerte Form („Nach § 114 wird
+folgender § 115 angefügt“) war schon bisher eine Struktureinfügung und bleibt es.
+
+
+Artikel 34
+Einfügung am Ende einer Einheit
+
+Die Zurückweisung des Einfügeankers „am Ende“ für ganze Einheiten wird als
+bewusst gezogene Grenze kenntlich gemacht und ihre Meldung neu gefasst: Wo eine
+Einheit ans Ende tritt, sagt das Gesetzblatt „angefügt“. Die Verbindung kommt im
+gesamten Prüfbestand kein einziges Mal vor; Vorschriften auf Verdacht werden
+nicht geschrieben.
+
+
+Artikel 35
+Fortschreibung der Prüfung
+
+Der Prüfbestand umfasst hiernach zweiunddreißig Prüfungen mehr als vor dem
+Artikel 24, nämlich dreihundertzweiunddreißig. Neu sind namentlich die Prüfung
+des Anfügens ganzer Paragraphen samt der Begründung bei fehlendem Normkopf, die
+Prüfung der Bündelung im Abschnitt „Manuell prüfen“ und die Prüfung, dass die
+Altersrüge beim zeitrichtigen Stamm unterbleibt und beim heutigen einmal ergeht.
+
════════════════════════════════════════════════════════════════════════════════
Fassung vom 22. August 2026,
zuletzt geändert durch die am 22. August 2026 vorgenommenen Änderungen
diff --git a/README.md b/README.md
index 71c5c13..3d69818 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,19 @@ gerichtet ist, deren Zieltext nicht mehr besteht —, wird mitsamt der Begründu
seines Scheiterns in den Abschnitt **Manuell prüfen** der Synopse aufgenommen. Er
wird niemals stillschweigend übergangen.
+(5) Die Begründungen sind dort nach der Art des Grundes gebündelt und ausgezählt
+(Befehl nicht erkannt, Stelle nicht auffindbar, Zieltext nicht vorhanden,
+Fundstelle mehrdeutig, Bereich unbrauchbar, Zitat unbrauchbar, Bestand
+widerspricht dem Befehl, nicht unterstützt, Anwendung fehlgeschlagen). Der
+ausformulierte Grund bleibt daneben stehen; gebündelt ist nur, was ohne Ordnung
+eine bloße Liste wäre. Dieselbe Auszählung gibt `--dump-befehle` unter der
+Aufschrift „Gründe nach Häufigkeit“ aus.
+
+(6) Gibt die Quelle des Stammgesetzes ihren Stand an und ist ihr Wortlaut jünger
+als die Fassung, die der Einleitungssatz des Änderungsgesetzes fortschreibt, so
+wird das eigens gerügt. Das ist die häufigste Ursache liegengebliebener Befehle
+und aus der Begründung „kommt im Zieltext nicht vor“ allein nicht zu erkennen.
+
## § 2 Begriffsbestimmungen
Im Sinne dieses Handbuchs ist
diff --git a/src/main/java/eu/mulk/aendggner/AendGgner.java b/src/main/java/eu/mulk/aendggner/AendGgner.java
index a55358e..a12515a 100644
--- a/src/main/java/eu/mulk/aendggner/AendGgner.java
+++ b/src/main/java/eu/mulk/aendggner/AendGgner.java
@@ -7,10 +7,12 @@ import eu.mulk.aendggner.aenderung.parse.DokumentErkenner;
import eu.mulk.aendggner.aenderung.parse.PatchTextExtraktor;
import eu.mulk.aendggner.aenderung.parse.TextBereiniger;
import eu.mulk.aendggner.anwendung.BefehlAnwender;
+import eu.mulk.aendggner.anwendung.Grund;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.EnumMap;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.logging.LogManager;
@@ -182,6 +184,7 @@ public class AendGgner implements Callable<Integer> {
System.out.printf(
"Angewandt: %d, manuell prüfen: %d%n",
anwendung.anzahlAngewandt(), anwendung.anzahlManuell());
+ haeufigkeitDerGruende(anwendung);
gesetz = anwendung.neu();
}
return 0;
@@ -212,6 +215,27 @@ public class AendGgner implements Callable<Integer> {
return ergebnis.anzahlAngewandt() == 0 && ergebnis.anzahlProtokollEintraege() > 0 ? 2 : 0;
}
+ /**
+ * „Gründe nach Häufigkeit“ — die Auszählung der liegengebliebenen Befehle nach Art des Grundes.
+ * Bei fünfzig Resten sagt sie mit einem Blick, woran es liegt; der ausformulierte Grund steht bei
+ * jedem Befehl darüber.
+ */
+ private static void haeufigkeitDerGruende(BefehlAnwender.AnwendungsErgebnis anwendung) {
+ var haeufigkeit = new EnumMap<Grund, Integer>(Grund.class);
+ for (var eintrag : anwendung.protokoll()) {
+ if (eintrag.status() == BefehlAnwender.Status.MANUELL_PRUEFEN && eintrag.grund() != null) {
+ haeufigkeit.merge(eintrag.grund(), 1, Integer::sum);
+ }
+ }
+ if (haeufigkeit.isEmpty()) {
+ return;
+ }
+ System.out.println("Gründe nach Häufigkeit:");
+ haeufigkeit.entrySet().stream()
+ .sorted(java.util.Map.Entry.<Grund, Integer>comparingByValue().reversed())
+ .forEach(e -> System.out.printf(" %4d %s%n", e.getValue(), e.getKey().bezeichnung()));
+ }
+
private static String kuerze(String text) {
var einzeilig = text.replaceAll("\\s+", " ");
return einzeilig.length() <= 160 ? einzeilig : einzeilig.substring(0, 157) + "…";
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParser.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParser.java
index 75dac0a..06186c7 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParser.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParser.java
@@ -7,8 +7,12 @@ import eu.mulk.aendggner.aenderung.Aenderungsbefehl.UnbekannterBefehl;
import eu.mulk.aendggner.aenderung.Provenienz;
import eu.mulk.aendggner.aenderung.Stelle;
import eu.mulk.aendggner.gesetz.Gesetz;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
import java.util.regex.Pattern;
import org.jboss.logging.Logger;
import org.jspecify.annotations.Nullable;
@@ -77,6 +81,11 @@ public final class AenderungsgesetzParser {
continue;
}
log.infof("Artikel %s betrifft %s.", artikel.label, ziel.jurabk());
+ // Nur einmal je Dokument: Ein Gesetzblatt kann mehrere Artikel auf dasselbe Stammgesetz
+ // richten, die Auskunft über dessen Stand gilt aber für alle zusammen.
+ standWarnung(GliederungsScanner.scanne(artikel.zeilen).vorspann(), ziel)
+ .filter(w -> !warnungen.contains(w))
+ .ifPresent(warnungen::add);
if (paragraphenModus && artikelFilter != null && betroffeneArtikel.contains(artikel.label)) {
// Ein GVBl-Heft enthält mehrere Gesetze mit je eigener §-Zählung.
warnungen.add(
@@ -338,6 +347,90 @@ public final class AenderungsgesetzParser {
|| vorspann.matches(".*\\b" + Pattern.quote(ziel.jurabk()) + "\\b.*");
}
+ /**
+ * Der Einleitungssatz eines Änderungsgesetzes nennt die Fassung, die es fortschreibt („das
+ * zuletzt durch Artikel 5 des Gesetzes vom 19. Juni 2020 (BGBl. I S. 1385) geändert worden ist“),
+ * und die Quelle des Stammgesetzes nennt ihren eigenen Stand („Zuletzt geändert durch Art. 5 G v.
+ * 19.6.2020 I 1385“). Weichen beide voneinander ab, so rechnet das Werkzeug auf der falschen
+ * Fassung — und zwar meist auf einer <em>jüngeren</em>, weil die Portale die heute geltende
+ * ausliefern.
+ *
+ * <p>Das ist die häufigste Ursache liegengebliebener Befehle überhaupt und aus der Meldung „kommt
+ * im Zieltext nicht vor“ allein nicht zu erkennen. Verglichen werden die beiden Daten; ist das
+ * des Stammes das spätere, ergeht eine Warnung. Bei gleichem oder früherem Datum, bei fehlender
+ * Standangabe (handgepflegter Klartext) und bei unlesbarem Einleitungssatz schweigt die Prüfung —
+ * geraten wird nicht.
+ */
+ private static Optional<String> standWarnung(String vorspann, Gesetz ziel) {
+ var stand = ziel.stand();
+ if (stand == null || stand.juengsteAenderung() == null) {
+ return Optional.empty();
+ }
+ var satzDatum = EINLEITUNGS_DATUM.matcher(vorspann.replaceAll("\\s+", " "));
+ if (!satzDatum.find()) {
+ return Optional.empty();
+ }
+ var genannt =
+ datum(satzDatum.group(3), MONATSNAMEN.indexOf(satzDatum.group(2)) + 1, satzDatum.group(1));
+ if (genannt == null || !stand.juengsteAenderung().isAfter(genannt)) {
+ return Optional.empty();
+ }
+ return Optional.of(
+ "Das Stammgesetz ist jünger als das Änderungsgesetz: Sein Wortlaut ist bis zum "
+ + stand.juengsteAenderung().format(DATUM)
+ + " fortgeschrieben („"
+ + stand.kommentar()
+ + "“), während der Einleitungssatz die Fassung vom "
+ + satzDatum.group(1)
+ + ". "
+ + satzDatum.group(2)
+ + " "
+ + satzDatum.group(3)
+ + " fortschreibt. Befehle, deren Zieltext „im Zieltext nicht vorkommt“, beruhen"
+ + " wahrscheinlich hierauf und nicht auf einem Mangel des Werkzeugs; abzuhelfen ist"
+ + " ihnen nur mit der zeitrichtigen Fassung des Stammgesetzes.");
+ }
+
+ private static final DateTimeFormatter DATUM =
+ DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.GERMAN);
+
+ /**
+ * „… zuletzt durch Artikel 5 des Gesetzes vom 19. Juni 2020 (BGBl. I S. 1385) geändert worden
+ * ist“ — das Datum der <em>letzten Änderung</em>, nicht das der Ausfertigung. Deshalb muss ihm
+ * die Änderungsklausel vorausgehen: Der Einleitungssatz nennt zuerst das Ausfertigungsdatum („Das
+ * Infektionsschutzgesetz vom 20. Juli 2000“), und dieses zu vergleichen ergäbe bei jedem je
+ * geänderten Gesetz eine Warnung.
+ */
+ private static final Pattern EINLEITUNGS_DATUM =
+ Pattern.compile(
+ "durch (?:Artikel|Art\\.)[^.]{0,80}?vom (\\d{1,2})\\. (\\p{Lu}\\p{L}+) (\\d{4})");
+
+ private static final List<String> MONATSNAMEN =
+ List.of(
+ "Januar",
+ "Februar",
+ "März",
+ "April",
+ "Mai",
+ "Juni",
+ "Juli",
+ "August",
+ "September",
+ "Oktober",
+ "November",
+ "Dezember");
+
+ private static @Nullable LocalDate datum(String jahr, int monat, String tag) {
+ if (monat < 1 || monat > 12) {
+ return null;
+ }
+ try {
+ return LocalDate.of(Integer.parseInt(jahr), monat, Integer.parseInt(tag));
+ } catch (RuntimeException e) {
+ return null;
+ }
+ }
+
private static final List<String> STAMM_SUFFIXE = List.of("es", "er", "en", "em", "e", "s", "n");
/** Reduziert jedes Wort grob auf seinen Stamm, um Deklinationsendungen zu neutralisieren. */
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
index 145334d..cefb285 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
@@ -49,8 +49,16 @@ public final class BefehlAnwender {
MANUELL_PRUEFEN
}
+ /**
+ * 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) {}
+ Aenderungsbefehl befehl,
+ Status status,
+ String begruendung,
+ Set<String> betroffeneEnbez,
+ @Nullable Grund grund) {}
public record AnwendungsErgebnis(Gesetz neu, List<AngewandteAenderung> protokoll) {
@@ -303,7 +311,7 @@ public final class BefehlAnwender {
}
if (von == schritt) {
// Ein Verbund ohne Teilbefehle — nichts anzuwenden, aber auch nichts zu verschweigen.
- protokoll.add(manuell(befehle.get(i), "Verbund ohne Teilbefehle."));
+ protokoll.add(manuell(befehle.get(i), Grund.NICHT_ERKANNT, "Verbund ohne Teilbefehle."));
continue;
}
if (schritte.get(von).ganzerBefehl()) {
@@ -323,10 +331,14 @@ public final class BefehlAnwender {
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)
@@ -337,10 +349,12 @@ public final class BefehlAnwender {
}
}
if (fehler.isEmpty()) {
- return new AngewandteAenderung(befehl, Status.ANGEWANDT, "", betroffene);
+ 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);
+ befehl, Status.MANUELL_PRUEFEN, String.join(" ", fehler), betroffene, ersterGrund);
}
/** Bezeichnungen, die ein Befehl freigibt — die Ausgangsstellen seiner Umnummerierungen. */
@@ -517,7 +531,7 @@ public final class BefehlAnwender {
private static AngewandteAenderung wendeAn(
List<Norm> normen, List<Gliederung> gliederungen, Aenderungsbefehl befehl) {
if (befehl instanceof UnbekannterBefehl) {
- return manuell(befehl, "Befehl nicht erkannt.");
+ return manuell(befehl, Grund.NICHT_ERKANNT, "Befehl nicht erkannt.");
}
// Sammelbefehle vor den Spezialweichen dispatchen (jeder Teil wird einzeln geroutet).
if (befehl instanceof Sammelbefehl s) {
@@ -539,7 +553,11 @@ public final class BefehlAnwender {
case Neufassung n -> wendeGliederungNeufassungAn(gliederungen, n);
case Aufhebung a -> wendeGliederungStreichungAn(gliederungen, a);
case Umnummerierung u -> wendeGliederungUmnummerierungAn(gliederungen, u);
- default -> manuell(befehl, "Strukturänderung wird nicht automatisch angewandt.");
+ default ->
+ manuell(
+ befehl,
+ Grund.NICHT_UNTERSTUETZT,
+ "Strukturänderung wird nicht automatisch angewandt.");
};
}
return switch (befehl) {
@@ -557,6 +575,7 @@ public final class BefehlAnwender {
case BereichsUmnummerierung b ->
manuell(
befehl,
+ Grund.BEREICH_UNGUELTIG,
"Der Bereich "
+ b.stelle().anzeigeText()
+ " bis "
@@ -570,10 +589,10 @@ public final class BefehlAnwender {
case GliederungsUeberschriften g ->
wendeGliederungsUeberschriftenAn(normen, gliederungen, g);
case Sammelbefehl s -> wendeSammelAn(normen, gliederungen, s);
- case UnbekannterBefehl u -> manuell(befehl, "Befehl nicht erkannt.");
+ case UnbekannterBefehl u -> manuell(befehl, Grund.NICHT_ERKANNT, "Befehl nicht erkannt.");
};
} catch (RuntimeException e) {
- return manuell(befehl, "Anwendung fehlgeschlagen: " + e);
+ return manuell(befehl, Grund.FEHLGESCHLAGEN, "Anwendung fehlgeschlagen: " + e);
}
}
@@ -583,7 +602,7 @@ public final class BefehlAnwender {
List<Gliederung> gliederungen, Neufassung befehl) {
int idx = findeGliederung(gliederungen, befehl.stelle().gliederungsPfad());
if (idx < 0) {
- return manuell(befehl, "Gliederungseinheit nicht gefunden.");
+ return manuell(befehl, Grund.STELLE_NICHT_AUFLOESBAR, "Gliederungseinheit nicht gefunden.");
}
var alt = gliederungen.get(idx);
var titel = befehl.neuerText().replaceAll("\\s+", " ").strip();
@@ -603,7 +622,7 @@ public final class BefehlAnwender {
List<Gliederung> gliederungen, Aufhebung befehl) {
int idx = findeGliederung(gliederungen, befehl.stelle().gliederungsPfad());
if (idx < 0) {
- return manuell(befehl, "Gliederungseinheit nicht gefunden.");
+ return manuell(befehl, Grund.STELLE_NICHT_AUFLOESBAR, "Gliederungseinheit nicht gefunden.");
}
var alt = gliederungen.get(idx);
gliederungen.set(idx, alt.mitTitel("(weggefallen)"));
@@ -617,11 +636,14 @@ public final class BefehlAnwender {
List<Gliederung> gliederungen, Umnummerierung befehl) {
int idx = findeGliederung(gliederungen, befehl.stelle().gliederungsPfad());
if (idx < 0) {
- return manuell(befehl, "Gliederungseinheit nicht gefunden: " + befehl.stelle().anzeigeText());
+ return manuell(
+ befehl,
+ Grund.STELLE_NICHT_AUFLOESBAR,
+ "Gliederungseinheit nicht gefunden: " + befehl.stelle().anzeigeText());
}
var neuPfad = befehl.neu().gliederungsPfad();
if (neuPfad.isEmpty()) {
- return manuell(befehl, "Neue Gliederungsbezeichnung fehlt.");
+ return manuell(befehl, Grund.NICHT_ERKANNT, "Neue Gliederungsbezeichnung fehlt.");
}
var neueBezeichnung = neuPfad.get(neuPfad.size() - 1).bezeichnung();
var alt = gliederungen.get(idx);
@@ -646,7 +668,10 @@ public final class BefehlAnwender {
var bezeichnung = befehl.neue().get(i).bezeichnung();
starts[i] = flach.indexOf(bezeichnung, suchAb);
if (starts[i] < 0) {
- return manuell(befehl, "Die Überschrift zu „" + bezeichnung + "“ fehlt im Zitat.");
+ return manuell(
+ befehl,
+ Grund.ZITAT_UNBRAUCHBAR,
+ "Die Überschrift zu „" + bezeichnung + "“ fehlt im Zitat.");
}
suchAb = starts[i] + bezeichnung.length();
}
@@ -668,6 +693,7 @@ public final class BefehlAnwender {
if (idx < 0) {
return manuell(
befehl,
+ Grund.STELLE_NICHT_AUFLOESBAR,
"Gliederungseinheit nicht gefunden: " + pfad.get(pfad.size() - 1).bezeichnung());
}
indizes.add(idx);
@@ -692,7 +718,7 @@ public final class BefehlAnwender {
// Einfügeform: hinter dem Anker-§.
var aufloesung = loeseNormAuf(normen, befehl.stelle());
if (aufloesung.fehler() != null) {
- return manuell(befehl, aufloesung.fehler());
+ return manuell(befehl, aufloesung.grund(), aufloesung.fehler());
}
var anker = normen.get(aufloesung.normIndex());
int gliederungsPos =
@@ -766,7 +792,8 @@ public final class BefehlAnwender {
if (befehl.amEnde()) {
var gestutzt = text.stripTrailing();
if (!gestutzt.endsWith(befehl.alt())) {
- return TextErgebnis.fehler("Der Text endet nicht mit „" + 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 =
@@ -779,7 +806,8 @@ public final class BefehlAnwender {
}
int anzahl = zaehleVorkommen(text, befehl.alt());
if (anzahl == 0) {
- return TextErgebnis.fehler("„" + befehl.alt() + "“ kommt im Zieltext nicht vor.");
+ return TextErgebnis.fehler(
+ "„" + befehl.alt() + "“ kommt im Zieltext nicht vor.", Grund.ZIELTEXT_FEHLT);
}
if (anzahl > 1 && !befehl.jeweils()) {
return TextErgebnis.fehler(
@@ -787,7 +815,8 @@ public final class BefehlAnwender {
+ befehl.alt()
+ "“ kommt "
+ anzahl
- + "-mal vor (ohne „jeweils“ mehrdeutig).");
+ + "-mal vor (ohne „jeweils“ mehrdeutig).",
+ Grund.MEHRDEUTIG);
}
if (brauchtFuge(befehl.alt(), befehl.neu())) {
return TextErgebnis.ok(ersetzeMitFuge(text, befehl.alt(), befehl.neu()));
@@ -845,11 +874,12 @@ public final class BefehlAnwender {
int anzahl = zaehleVorkommen(text, befehl.woerter());
if (anzahl == 0) {
return TextErgebnis.fehler(
- "„" + befehl.woerter() + "“ kommt im Zieltext nicht vor.");
+ "„" + befehl.woerter() + "“ kommt im Zieltext nicht vor.",
+ Grund.ZIELTEXT_FEHLT);
}
if (anzahl > 1) {
return TextErgebnis.fehler(
- "„" + befehl.woerter() + "“ kommt " + anzahl + "-mal vor.");
+ "„" + befehl.woerter() + "“ kommt " + anzahl + "-mal vor.", Grund.MEHRDEUTIG);
}
int naht = findeVorkommen(text, befehl.woerter(), 0);
return TextErgebnis.ok(
@@ -870,7 +900,7 @@ public final class BefehlAnwender {
case WortAnker.NachWoertern nach -> {
var pruefung = eindeutigeFundstelle(text, nach.woerter());
if (pruefung.fehler() != null) {
- yield TextErgebnis.fehler(pruefung.fehler());
+ yield TextErgebnis.fehler(pruefung.fehler(), pruefung.grund());
}
int ende = pruefung.index() + nach.woerter().length();
yield TextErgebnis.ok(
@@ -882,7 +912,7 @@ public final class BefehlAnwender {
case WortAnker.VorWoertern vor -> {
var pruefung = eindeutigeFundstelle(text, vor.woerter());
if (pruefung.fehler() != null) {
- yield TextErgebnis.fehler(pruefung.fehler());
+ yield TextErgebnis.fehler(pruefung.fehler(), pruefung.grund());
}
yield TextErgebnis.ok(
text.substring(0, pruefung.index())
@@ -893,7 +923,8 @@ public final class BefehlAnwender {
case WortAnker.VorKommaAmEnde ignoriert -> {
var gestutzt = text.stripTrailing();
if (!gestutzt.endsWith(",")) {
- yield TextErgebnis.fehler("Der Zieltext endet nicht mit einem Komma.");
+ yield TextErgebnis.fehler(
+ "Der Zieltext endet nicht mit einem Komma.", Grund.ZIELTEXT_FEHLT);
}
yield TextErgebnis.ok(
gestutzt.substring(0, gestutzt.length() - 1)
@@ -926,7 +957,7 @@ public final class BefehlAnwender {
if (stelle.betrifftUeberschrift()) {
var aufloesung = loeseNormAuf(normen, stelle);
if (aufloesung.fehler() != null) {
- return manuell(befehl, aufloesung.fehler());
+ return manuell(befehl, aufloesung.grund(), aufloesung.fehler());
}
var norm = normen.get(aufloesung.normIndex());
var titel = befehl.neuerText().replaceFirst("^(?:§|Art\\.)\\s*\\S+\\s+", "").strip();
@@ -937,7 +968,7 @@ public final class BefehlAnwender {
if (nurNorm(stelle)) {
var aufloesung = loeseNormAuf(normen, stelle);
if (aufloesung.fehler() != null) {
- return manuell(befehl, aufloesung.fehler());
+ return manuell(befehl, aufloesung.grund(), aufloesung.fehler());
}
var alteNorm = normen.get(aufloesung.normIndex());
var neueNorm = parseNorm(befehl.neuerText(), alteNorm.enbez(), alteNorm);
@@ -948,7 +979,7 @@ public final class BefehlAnwender {
if (stelle.absatz().isPresent() && feinsteIstAbsatz(stelle)) {
var ergebnis = StellenAufloeser.aufloese(gesetzAus(normen), stelle);
if (ergebnis instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) {
- return manuell(befehl, nicht.begruendung());
+ return manuell(befehl, nicht.grund(), nicht.begruendung());
}
var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle();
var norm = normen.get(fundstelle.normIndex());
@@ -1001,11 +1032,14 @@ public final class BefehlAnwender {
case ABSATZ -> {
var stelle = befehl.stelle();
if (stelle.absatz().isEmpty()) {
- yield manuell(befehl, "Ersetzungsziel nennt keinen Absatz: " + stelle.anzeigeText());
+ 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.begruendung());
+ yield manuell(befehl, nicht.grund(), nicht.begruendung());
}
var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle();
var norm = normen.get(fundstelle.normIndex());
@@ -1015,16 +1049,20 @@ public final class BefehlAnwender {
if (befehl.bisStelle() != null) {
var e2 = StellenAufloeser.aufloese(gesetzAus(normen), befehl.bisStelle());
if (e2 instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) {
- yield manuell(befehl, nicht.begruendung());
+ 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, "Ersetzungsbereich liegt nicht in einer einzigen Norm.");
+ yield manuell(
+ befehl,
+ Grund.BEREICH_UNGUELTIG,
+ "Ersetzungsbereich liegt nicht in einer einzigen Norm.");
}
bisIndex = f2.absatzIndex();
}
if (bisIndex < vonIndex) {
- yield manuell(befehl, "Ersetzungsbereich ist leer oder absteigend.");
+ yield manuell(
+ befehl, Grund.BEREICH_UNGUELTIG, "Ersetzungsbereich ist leer oder absteigend.");
}
var absaetze = new ArrayList<>(norm.absaetze());
for (int k = bisIndex; k >= vonIndex; k--) {
@@ -1066,23 +1104,25 @@ public final class BefehlAnwender {
// wird entfernt und durch die Paragraphen des Blocks ersetzt.
var aufloesung = loeseNormAuf(normen, befehl.stelle());
if (aufloesung.fehler() != null) {
- yield manuell(befehl, aufloesung.fehler());
+ 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.fehler());
+ yield manuell(befehl, a2.grund(), a2.fehler());
}
bisIndex = a2.normIndex();
}
if (bisIndex < vonIndex) {
- yield manuell(befehl, "Ersetzungsbereich ist leer oder absteigend.");
+ 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, "Im Ersetzungsblock wurde kein Paragraph erkannt.");
+ yield manuell(
+ befehl, Grund.ZITAT_UNBRAUCHBAR, "Im Ersetzungsblock wurde kein Paragraph erkannt.");
}
for (int k = bisIndex; k >= vonIndex; k--) {
normen.remove(k);
@@ -1102,11 +1142,11 @@ public final class BefehlAnwender {
List<Norm> normen, StrukturErsetzung befehl) {
var e1 = StellenAufloeser.aufloese(gesetzAus(normen), befehl.stelle());
if (e1 instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) {
- return manuell(befehl, nicht.begruendung());
+ 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.begruendung());
+ return manuell(befehl, nicht.grund(), nicht.begruendung());
}
var f1 = ((StellenAufloeser.Ergebnis.Gefunden) e1).fundstelle();
var f2 = ((StellenAufloeser.Ergebnis.Gefunden) e2).fundstelle();
@@ -1115,12 +1155,13 @@ public final class BefehlAnwender {
|| !f1.absatzIndex().equals(f2.absatzIndex())
|| f1.bereich() == null
|| f2.bereich() == null) {
- return manuell(befehl, "Satz-Bereich liegt nicht in einem einzigen Absatz.");
+ 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, "Satz-Bereich ist leer oder absteigend.");
+ return manuell(befehl, Grund.BEREICH_UNGUELTIG, "Satz-Bereich ist leer oder absteigend.");
}
var norm = normen.get(f1.normIndex());
var absaetze = new ArrayList<>(norm.absaetze());
@@ -1144,11 +1185,11 @@ public final class BefehlAnwender {
List<Norm> normen, StrukturErsetzung befehl) {
var e1 = StellenAufloeser.aufloese(gesetzAus(normen), befehl.stelle());
if (e1 instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) {
- return manuell(befehl, nicht.begruendung());
+ 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.begruendung());
+ return manuell(befehl, nicht.grund(), nicht.begruendung());
}
var f1 = ((StellenAufloeser.Ergebnis.Gefunden) e1).fundstelle();
var f2 = ((StellenAufloeser.Ergebnis.Gefunden) e2).fundstelle();
@@ -1157,12 +1198,16 @@ public final class BefehlAnwender {
|| !f1.absatzIndex().equals(f2.absatzIndex())
|| f1.bereich() == null
|| f2.bereich() == null) {
- return manuell(befehl, "Ersetzungsbereich liegt nicht in einem einzigen Absatz.");
+ 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, "Ersetzungsbereich ist leer oder absteigend.");
+ return manuell(
+ befehl, Grund.BEREICH_UNGUELTIG, "Ersetzungsbereich ist leer oder absteigend.");
}
var norm = normen.get(f1.normIndex());
var absaetze = new ArrayList<>(norm.absaetze());
@@ -1186,7 +1231,7 @@ public final class BefehlAnwender {
case PARAGRAPH -> {
var aufloesung = loeseNormAuf(normen, befehl.stelle());
if (aufloesung.fehler() != null) {
- yield manuell(befehl, aufloesung.fehler());
+ yield manuell(befehl, aufloesung.grund(), aufloesung.fehler());
}
var anker = normen.get(aufloesung.normIndex());
int position = aufloesung.normIndex() + (befehl.vorher() ? 0 : 1);
@@ -1195,11 +1240,15 @@ public final class BefehlAnwender {
if (befehl.bezeichnung() == null) {
var neue = parseNormenBlock(befehl.text(), anker.gliederung());
if (neue.isEmpty()) {
- yield manuell(befehl, "Im Einfügeblock wurde kein Paragraph erkannt.");
+ 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, n.enbez() + " existiert bereits im Stammgesetz.");
+ yield manuell(
+ befehl,
+ Grund.BESTAND_WIDERSPRICHT,
+ n.enbez() + " existiert bereits im Stammgesetz.");
}
}
normen.addAll(position, neue);
@@ -1209,7 +1258,8 @@ public final class BefehlAnwender {
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, enbezNeu + " existiert bereits im Stammgesetz.");
+ yield manuell(
+ befehl, Grund.BESTAND_WIDERSPRICHT, enbezNeu + " existiert bereits im Stammgesetz.");
}
var neueNorm =
parseNorm(
@@ -1222,11 +1272,14 @@ public final class BefehlAnwender {
case ABSATZ -> {
var stelle = befehl.stelle();
if (stelle.absatz().isEmpty()) {
- yield manuell(befehl, "Einfügeanker nennt keinen Absatz: " + stelle.anzeigeText());
+ 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.begruendung());
+ yield manuell(befehl, nicht.grund(), nicht.begruendung());
}
var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle();
var norm = normen.get(fundstelle.normIndex());
@@ -1296,18 +1349,23 @@ public final class BefehlAnwender {
switch (anker) {
case WortAnker.NachWoertern nach -> nach.woerter();
case WortAnker.VorWoertern vor -> vor.woerter();
- // „am Ende“ ist kein Einfügeanker für ganze Einheiten — dafür gibt es die
- // Anfügung.
+ // „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(
- "Einfügeanker ohne Wortlaut wird für Struktureinfügungen nicht unterstützt.");
+ "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());
+ return TextErgebnis.fehler(pruefung.fehler(), pruefung.grund());
}
int zeilenAnfang = text.lastIndexOf('\n', pruefung.index()) + 1;
int zeilenEnde = text.indexOf('\n', pruefung.index());
@@ -1332,7 +1390,7 @@ public final class BefehlAnwender {
case ABSATZ -> {
var aufloesung = loeseNormAuf(normen, befehl.stelle());
if (aufloesung.fehler() != null) {
- yield manuell(befehl, aufloesung.fehler());
+ yield manuell(befehl, aufloesung.grund(), aufloesung.fehler());
}
var norm = normen.get(aufloesung.normIndex());
var absaetze = new ArrayList<>(norm.absaetze());
@@ -1360,17 +1418,71 @@ public final class BefehlAnwender {
.orElse("");
return TextErgebnis.ok(text.stripTrailing() + "\n" + block);
});
- case PARAGRAPH -> manuell(befehl, "Anfügen ganzer Paragraphen wird nicht unterstützt.");
+ 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.fehler());
+ return manuell(befehl, aufloesung.grund(), aufloesung.fehler());
}
var norm = normen.get(aufloesung.normIndex());
var nummer = stelle.absatzbezeichnung().get().nummer();
@@ -1389,13 +1501,14 @@ public final class BefehlAnwender {
return angewandt(befehl, norm.enbez());
}
}
- return manuell(befehl, "Absatz (" + nummer + ") nicht gefunden.");
+ return manuell(
+ befehl, Grund.STELLE_NICHT_AUFLOESBAR, "Absatz (" + nummer + ") nicht gefunden.");
}
if (nurNorm(stelle)) {
var aufloesung = loeseNormAuf(normen, stelle);
if (aufloesung.fehler() != null) {
- return manuell(befehl, aufloesung.fehler());
+ return manuell(befehl, aufloesung.grund(), aufloesung.fehler());
}
var norm = normen.get(aufloesung.normIndex());
if (norm.weggefallen()) {
@@ -1409,7 +1522,7 @@ public final class BefehlAnwender {
if (stelle.absatz().isPresent() && feinsteIstAbsatz(stelle)) {
var ergebnis = StellenAufloeser.aufloese(gesetzAus(normen), stelle);
if (ergebnis instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) {
- return manuell(befehl, nicht.begruendung());
+ return manuell(befehl, nicht.grund(), nicht.begruendung());
}
var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle();
var norm = normen.get(fundstelle.normIndex());
@@ -1458,12 +1571,14 @@ public final class BefehlAnwender {
var enbezNeu = befehl.neu().paragraph().get().enbez();
int idx = StellenAufloeser.normIndex(gesetzAus(normen), enbezAlt);
if (idx < 0) {
- return manuell(befehl, enbezAlt + " existiert nicht im Gesetz.");
+ 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, enbezNeu + " existiert bereits im Gesetz.");
+ return manuell(
+ befehl, Grund.BESTAND_WIDERSPRICHT, enbezNeu + " existiert bereits im Gesetz.");
}
// Eine bereits weggefallene Zielnorm wird durch die Umnummerierung überschrieben.
if (zielIdx >= 0) {
@@ -1487,7 +1602,7 @@ public final class BefehlAnwender {
&& feinsteIstAbsatz(befehl.neu())) {
var ergebnis = StellenAufloeser.aufloese(gesetzAus(normen), befehl.stelle());
if (ergebnis instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) {
- return manuell(befehl, nicht.begruendung());
+ return manuell(befehl, nicht.grund(), nicht.begruendung());
}
var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle();
var norm = normen.get(fundstelle.normIndex());
@@ -1532,7 +1647,8 @@ public final class BefehlAnwender {
+ alteMarke
+ "“ steht nicht am Anfang von "
+ befehl.stelle().anzeigeText()
- + ".");
+ + ".",
+ Grund.ZIELTEXT_FEHLT);
}
var umbenannt =
text.substring(0, bereich.von())
@@ -1550,7 +1666,7 @@ public final class BefehlAnwender {
if (altSatz != null && neuSatz != null) {
var ergebnis = StellenAufloeser.aufloese(gesetzAus(normen), befehl.stelle());
if (ergebnis instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) {
- return manuell(befehl, nicht.begruendung());
+ return manuell(befehl, nicht.grund(), nicht.begruendung());
}
var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle();
if (fundstelle.bereich() != null && fundstelle.absatzIndex() != null) {
@@ -1661,7 +1777,7 @@ public final class BefehlAnwender {
List<Norm> normen, WortlautZuAbsatz befehl) {
var aufloesung = loeseNormAuf(normen, befehl.stelle());
if (aufloesung.fehler() != null) {
- return manuell(befehl, aufloesung.fehler());
+ return manuell(befehl, aufloesung.grund(), aufloesung.fehler());
}
var norm = normen.get(aufloesung.normIndex());
// Steht genau ein unnummerierter Absatz zwischen nummerierten (bayerische Folge „Dem Wortlaut
@@ -1693,7 +1809,7 @@ public final class BefehlAnwender {
List<Norm> normen, WortlautZuSatz befehl) {
var ergebnis = StellenAufloeser.aufloese(gesetzAus(normen), befehl.stelle());
if (ergebnis instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) {
- return manuell(befehl, nicht.begruendung());
+ return manuell(befehl, nicht.grund(), nicht.begruendung());
}
var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle();
var norm = normen.get(fundstelle.normIndex());
@@ -1701,7 +1817,9 @@ public final class BefehlAnwender {
if (absatzIndex == null) {
if (norm.absaetze().size() != 1) {
return manuell(
- befehl, norm.enbez() + " hat " + norm.absaetze().size() + " Absätze; Ziel unklar.");
+ befehl,
+ Grund.MEHRDEUTIG,
+ norm.enbez() + " hat " + norm.absaetze().size() + " Absätze; Ziel unklar.");
}
absatzIndex = 0;
}
@@ -1720,7 +1838,7 @@ public final class BefehlAnwender {
List<Norm> normen, WortlautVoranstellung befehl) {
var aufloesung = loeseNormAuf(normen, befehl.stelle());
if (aufloesung.fehler() != null) {
- return manuell(befehl, aufloesung.fehler());
+ return manuell(befehl, aufloesung.grund(), aufloesung.fehler());
}
var norm = normen.get(aufloesung.normIndex());
@@ -1729,7 +1847,7 @@ public final class BefehlAnwender {
if (befehl.stelle().absatz().isPresent()) {
var ergebnis = StellenAufloeser.aufloese(gesetzAus(normen), befehl.stelle());
if (ergebnis instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) {
- return manuell(befehl, nicht.begruendung());
+ return manuell(befehl, nicht.grund(), nicht.begruendung());
}
var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle();
var absaetze = new ArrayList<>(norm.absaetze());
@@ -1744,7 +1862,7 @@ public final class BefehlAnwender {
var neue = parseAbsaetze(befehl.text());
if (neue.isEmpty()) {
- return manuell(befehl, "Im Zitat wurde kein Absatz erkannt.");
+ return manuell(befehl, Grund.ZITAT_UNBRAUCHBAR, "Im Zitat wurde kein Absatz erkannt.");
}
var absaetze = new ArrayList<>(neue);
absaetze.addAll(norm.absaetze());
@@ -1757,7 +1875,7 @@ public final class BefehlAnwender {
List<Norm> normen, FussnotenAufhebung befehl) {
var aufloesung = loeseNormAuf(normen, befehl.stelle());
if (aufloesung.fehler() != null) {
- return manuell(befehl, aufloesung.fehler());
+ return manuell(befehl, aufloesung.grund(), aufloesung.fehler());
}
var norm = normen.get(aufloesung.normIndex());
var absaetze = new ArrayList<>(norm.absaetze());
@@ -1783,6 +1901,7 @@ public final class BefehlAnwender {
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));
@@ -1799,7 +1918,8 @@ public final class BefehlAnwender {
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.");
+ "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()));
@@ -1830,13 +1950,14 @@ public final class BefehlAnwender {
// --- Gemeinsame Helfer ---------------------------------------------------------------------
- private record TextErgebnis(@Nullable String text, @Nullable String fehler) {
+ private record TextErgebnis(
+ @Nullable String text, @Nullable String fehler, @Nullable Grund grund) {
static TextErgebnis ok(String text) {
- return new TextErgebnis(text, null);
+ return new TextErgebnis(text, null, null);
}
- static TextErgebnis fehler(String begruendung) {
- return new TextErgebnis(null, begruendung);
+ static TextErgebnis fehler(String begruendung, Grund grund) {
+ return new TextErgebnis(null, begruendung, grund);
}
}
@@ -1881,15 +2002,16 @@ public final class BefehlAnwender {
if (befehl.stelle().betrifftUeberschrift()) {
var aufloesung = loeseNormAuf(normen, befehl.stelle());
if (aufloesung.fehler() != null) {
- return manuell(befehl, aufloesung.fehler());
+ return manuell(befehl, aufloesung.grund(), aufloesung.fehler());
}
var norm = normen.get(aufloesung.normIndex());
if (norm.titel() == null) {
- return manuell(befehl, norm.enbez() + " hat keine Überschrift.");
+ 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.fehler());
+ return manuell(befehl, titelErgebnis.grund(), titelErgebnis.fehler());
}
normen.set(aufloesung.normIndex(), norm.mitTitel(titelErgebnis.text()));
return angewandt(befehl, norm.enbez());
@@ -1897,7 +2019,7 @@ public final class BefehlAnwender {
var ergebnis = StellenAufloeser.aufloese(gesetzAus(normen), befehl.stelle());
if (ergebnis instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) {
- return manuell(befehl, nicht.begruendung());
+ return manuell(befehl, nicht.grund(), nicht.begruendung());
}
var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle();
var norm = normen.get(fundstelle.normIndex());
@@ -1907,7 +2029,7 @@ public final class BefehlAnwender {
var absatz = absaetze.get(fundstelle.absatzIndex());
var neuerText = wendeAufBereichAn(absatz.text(), fundstelle.bereich(), operation);
if (neuerText.fehler() != null) {
- return manuell(befehl, neuerText.fehler());
+ return manuell(befehl, neuerText.grund(), neuerText.fehler());
}
absaetze.set(fundstelle.absatzIndex(), absatz.mitText(neuerText.text()));
normen.set(fundstelle.normIndex(), norm.mitAbsaetzen(absaetze));
@@ -1919,21 +2041,25 @@ public final class BefehlAnwender {
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, "Mehrere Absätze von " + norm.enbez() + " kommen infrage; mehrdeutig.");
+ 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, letzterFehler);
+ return manuell(befehl, letzterGrund, letzterFehler);
}
absaetze.set(trefferIndex, absaetze.get(trefferIndex).mitText(treffer.text()));
normen.set(fundstelle.normIndex(), norm.mitAbsaetzen(absaetze));
@@ -1945,12 +2071,13 @@ public final class BefehlAnwender {
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.begruendung());
+ 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());
@@ -1958,7 +2085,7 @@ public final class BefehlAnwender {
var absatz = absaetze.get(fundstelle.absatzIndex());
var neuerText = operation.wende(absatz.text(), fundstelle.bereich());
if (neuerText.fehler() != null) {
- return manuell(befehl, neuerText.fehler());
+ return manuell(befehl, neuerText.grund(), neuerText.fehler());
}
absaetze.set(fundstelle.absatzIndex(), absatz.mitText(neuerText.text()));
normen.set(fundstelle.normIndex(), norm.mitAbsaetzen(absaetze));
@@ -1979,7 +2106,7 @@ public final class BefehlAnwender {
text.substring(0, bereich.von()) + ergebnis.text() + text.substring(bereich.bis()));
}
- private record NormAufloesung(int normIndex, @Nullable String fehler) {}
+ private record NormAufloesung(int normIndex, @Nullable String fehler, @Nullable Grund grund) {}
private static NormAufloesung loeseNormAuf(List<Norm> normen, Stelle stelle) {
String enbez;
@@ -1988,13 +2115,17 @@ public final class BefehlAnwender {
} else if (stelle.anlagenEnbez().isPresent()) {
enbez = stelle.anlagenEnbez().get();
} else {
- return new NormAufloesung(-1, "Stelle nennt keinen Paragraphen: " + stelle.anzeigeText());
+ 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.");
+ return new NormAufloesung(
+ -1, enbez + " existiert nicht im Gesetz.", Grund.BESTAND_WIDERSPRICHT);
}
- return new NormAufloesung(index, null);
+ return new NormAufloesung(index, null, null);
}
private static boolean nurParagraph(Stelle stelle) {
@@ -2105,17 +2236,19 @@ public final class BefehlAnwender {
return sb.toString();
}
- private record Fundpruefung(int index, @Nullable String fehler) {}
+ 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.");
+ 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.");
+ return new Fundpruefung(
+ -1, "„" + woerter + "“ kommt " + anzahl + "-mal vor; mehrdeutig.", Grund.MEHRDEUTIG);
}
- return new Fundpruefung(findeVorkommen(text, woerter, 0), null);
+ return new Fundpruefung(findeVorkommen(text, woerter, 0), null, null);
}
private static int zaehleVorkommen(String text, String suchtext) {
@@ -2382,15 +2515,27 @@ public final class BefehlAnwender {
private static AngewandteAenderung angewandt(Aenderungsbefehl befehl, String enbez) {
return new AngewandteAenderung(
- befehl, Status.ANGEWANDT, "", new LinkedHashSet<>(List.of(enbez)));
+ 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));
+ return new AngewandteAenderung(
+ befehl, Status.ANGEWANDT, "", new LinkedHashSet<>(enbezliste), null);
}
- private static AngewandteAenderung manuell(Aenderungsbefehl befehl, String begruendung) {
- return new AngewandteAenderung(befehl, Status.MANUELL_PRUEFEN, begruendung, Set.of());
+ /**
+ * 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) {
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/Grund.java b/src/main/java/eu/mulk/aendggner/anwendung/Grund.java
new file mode 100644
index 0000000..065567c
--- /dev/null
+++ b/src/main/java/eu/mulk/aendggner/anwendung/Grund.java
@@ -0,0 +1,56 @@
+// SPDX-FileCopyrightText: 2020 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+// SPDX-License-Identifier: AGPL-3.0-or-later
+package eu.mulk.aendggner.anwendung;
+
+/**
+ * Die Art eines Grundes, aus dem ein Befehl nicht angewandt wurde.
+ *
+ * <p>Der ausformulierte Grund bleibt daneben stehen und wird nicht ersetzt — der Grundsatz, nichts
+ * stillschweigend zu verwerfen, verlangt die genaue Auskunft im Einzelfall. Die Art ordnet sie nur:
+ * Eine Synopse mit fünfzig Resten ist ohne Ordnung eine Liste, aus der niemand einen Schluss zieht;
+ * nach Arten gebündelt und ausgezählt zeigt sie auf einen Blick, ob eine Vorlage am Werkzeug
+ * scheitert, am Wortlaut des Änderungsgesetzes oder am falschen Stand des Stammgesetzes.
+ *
+ * <p>Die Reihenfolge ist die der Darstellung: erst, was am Befehl liegt, dann, was am Stammgesetz
+ * liegt, zuletzt die bewusst gezogenen Grenzen.
+ */
+public enum Grund {
+
+ /** Der Befehl wurde nicht als Änderungsbefehl erkannt. */
+ NICHT_ERKANNT("Befehl nicht erkannt"),
+
+ /** Die benannte Stelle ist im Stammgesetz nicht auffindbar. */
+ STELLE_NICHT_AUFLOESBAR("Stelle nicht auffindbar"),
+
+ /** Der zu ersetzende oder zu streichende Wortlaut steht nicht im Zieltext. */
+ ZIELTEXT_FEHLT("Zieltext nicht vorhanden"),
+
+ /** Die Stelle oder der Zieltext kommt mehrfach vor; welche Fundstelle gemeint ist, ist offen. */
+ MEHRDEUTIG("Fundstelle mehrdeutig"),
+
+ /** Ein Bereich ist leer, absteigend oder überschreitet die Einheit, in der er liegen müsste. */
+ BEREICH_UNGUELTIG("Bereich unbrauchbar"),
+
+ /** Das Zitat trägt nicht, was der Befehl ihm entnehmen will (Normkopf, Absatz, Überschrift). */
+ ZITAT_UNBRAUCHBAR("Zitat unbrauchbar"),
+
+ /** Der Bestand des Stammgesetzes widerspricht dem Befehl (Bezeichnung belegt oder fehlt). */
+ BESTAND_WIDERSPRICHT("Bestand widerspricht dem Befehl"),
+
+ /** Die Anwendung dieser Befehlsart ist nicht umgesetzt; die Grenze ist bewusst gezogen. */
+ NICHT_UNTERSTUETZT("Nicht unterstützt"),
+
+ /** Die Anwendung ist mit einem Fehler abgebrochen. */
+ FEHLGESCHLAGEN("Anwendung fehlgeschlagen");
+
+ private final String bezeichnung;
+
+ Grund(String bezeichnung) {
+ this.bezeichnung = bezeichnung;
+ }
+
+ /** Die Aufschrift der Gruppe in der Synopse. */
+ public String bezeichnung() {
+ return bezeichnung;
+ }
+}
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtAnwender.java
index 5ffe0b7..af1034e 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtAnwender.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/InhaltsuebersichtAnwender.java
@@ -47,21 +47,28 @@ final class InhaltsuebersichtAnwender {
var von = zielKette(s.stelle());
var bis = s.bisStelle() != null ? zielKette(s.bisStelle()) : von;
if (von.isEmpty() || bis.isEmpty()) {
- yield manuell(befehl, "Angabe-Bereich nennt kein auflösbares Ziel.");
+ yield manuell(
+ befehl, Grund.STELLE_NICHT_AUFLOESBAR, "Angabe-Bereich nennt kein auflösbares Ziel.");
}
yield ersetzeZeilen(normen, befehl, von, bis, s.text());
}
case Aufhebung a -> {
var ziel = zielKette(a.stelle());
if (ziel.isEmpty()) {
- yield manuell(befehl, "Zu streichende Angabe nennt kein auflösbares Ziel.");
+ yield manuell(
+ befehl,
+ Grund.STELLE_NICHT_AUFLOESBAR,
+ "Zu streichende Angabe nennt kein auflösbares Ziel.");
}
yield ersetzeZeilen(normen, befehl, ziel, ziel, null);
}
case StrukturEinfuegung e -> {
var anker = zielKette(e.stelle());
if (anker.isEmpty()) {
- yield manuell(befehl, "Einfügeanker in der Inhaltsübersicht nennt kein Ziel.");
+ yield manuell(
+ befehl,
+ Grund.STELLE_NICHT_AUFLOESBAR,
+ "Einfügeanker in der Inhaltsübersicht nennt kein Ziel.");
}
yield fuegeZeilenEin(normen, befehl, anker, e.vorher(), e.text());
}
@@ -69,7 +76,10 @@ final class InhaltsuebersichtAnwender {
case Aenderungsbefehl.Streichung ignoriert -> null;
case Aenderungsbefehl.WoerterEinfuegung ignoriert -> null;
default ->
- manuell(befehl, "Änderungen an der Inhaltsübersicht werden nicht automatisch angewandt.");
+ manuell(
+ befehl,
+ Grund.NICHT_UNTERSTUETZT,
+ "Änderungen an der Inhaltsübersicht werden nicht automatisch angewandt.");
};
}
@@ -105,19 +115,23 @@ final class InhaltsuebersichtAnwender {
@Nullable String zitat) {
int normIndex = StellenAufloeser.normIndex(gesetzAus(normen), ENBEZ);
if (normIndex < 0) {
- return manuell(befehl, "Das Gesetz enthält keine Inhaltsübersicht.");
+ return manuell(
+ befehl, Grund.BESTAND_WIDERSPRICHT, "Das Gesetz enthält keine Inhaltsübersicht.");
}
var norm = normen.get(normIndex);
var vonFund = findeZeile(norm, von);
if (vonFund.fehler() != null) {
- return manuell(befehl, vonFund.fehler());
+ return manuell(befehl, vonFund.grund(), vonFund.fehler());
}
var bisFund = von.equals(bis) ? vonFund : findeZeile(norm, bis);
if (bisFund.fehler() != null) {
- return manuell(befehl, bisFund.fehler());
+ return manuell(befehl, bisFund.grund(), bisFund.fehler());
}
if (vonFund.absatzIndex() != bisFund.absatzIndex() || bisFund.bisZeile() < vonFund.vonZeile()) {
- return manuell(befehl, "Angabe-Bereich liegt nicht zusammenhängend in der Inhaltsübersicht.");
+ return manuell(
+ befehl,
+ Grund.BEREICH_UNGUELTIG,
+ "Angabe-Bereich liegt nicht zusammenhängend in der Inhaltsübersicht.");
}
var zeilen = new ArrayList<>(zeilenVon(norm, vonFund.absatzIndex()));
var einrueckung = einrueckungVon(zeilen.get(vonFund.vonZeile()));
@@ -139,12 +153,13 @@ final class InhaltsuebersichtAnwender {
String zitat) {
int normIndex = StellenAufloeser.normIndex(gesetzAus(normen), ENBEZ);
if (normIndex < 0) {
- return manuell(befehl, "Das Gesetz enthält keine Inhaltsübersicht.");
+ return manuell(
+ befehl, Grund.BESTAND_WIDERSPRICHT, "Das Gesetz enthält keine Inhaltsübersicht.");
}
var norm = normen.get(normIndex);
var fund = findeZeile(norm, anker);
if (fund.fehler() != null) {
- return manuell(befehl, fund.fehler());
+ return manuell(befehl, fund.grund(), fund.fehler());
}
var zeilen = new ArrayList<>(zeilenVon(norm, fund.absatzIndex()));
var einrueckung = einrueckungVon(zeilen.get(fund.vonZeile()));
@@ -156,9 +171,10 @@ final class InhaltsuebersichtAnwender {
// --- Zeilenmodell ----------------------------------------------------------------------------
- private record Zeilenfund(int absatzIndex, int vonZeile, int bisZeile, @Nullable String fehler) {
- static Zeilenfund fehlgeschlagen(String begruendung) {
- return new Zeilenfund(-1, -1, -1, begruendung);
+ private record Zeilenfund(
+ int absatzIndex, int vonZeile, int bisZeile, @Nullable String fehler, @Nullable Grund grund) {
+ static Zeilenfund fehlgeschlagen(String begruendung, Grund grund) {
+ return new Zeilenfund(-1, -1, -1, begruendung, grund);
}
}
@@ -190,19 +206,22 @@ final class InhaltsuebersichtAnwender {
int treffer = eindeutigeZeile(zeilen, zeilenMuster(ziel), von, bis);
if (treffer == -2) {
return Zeilenfund.fehlgeschlagen(
- "Die Angabe zu „" + anzeige(ziel) + "“ ist in der Inhaltsübersicht mehrdeutig.");
+ "Die Angabe zu „" + anzeige(ziel) + "“ ist in der Inhaltsübersicht mehrdeutig.",
+ Grund.MEHRDEUTIG);
}
if (treffer >= 0) {
if (gefunden != null) {
return Zeilenfund.fehlgeschlagen(
- "Die Angabe zu „" + anzeige(ziel) + "“ ist in der Inhaltsübersicht mehrdeutig.");
+ "Die Angabe zu „" + anzeige(ziel) + "“ ist in der Inhaltsübersicht mehrdeutig.",
+ Grund.MEHRDEUTIG);
}
- gefunden = new Zeilenfund(a, treffer, treffer, null);
+ gefunden = new Zeilenfund(a, treffer, treffer, null, null);
}
}
if (gefunden == null) {
return Zeilenfund.fehlgeschlagen(
- "Die Angabe zu „" + anzeige(ziel) + "“ ist in der Inhaltsübersicht nicht auffindbar.");
+ "Die Angabe zu „" + anzeige(ziel) + "“ ist in der Inhaltsübersicht nicht auffindbar.",
+ Grund.STELLE_NICHT_AUFLOESBAR);
}
return gefunden;
}
@@ -275,7 +294,8 @@ final class InhaltsuebersichtAnwender {
List<Norm> normen, Neufassung befehl) {
int normIndex = StellenAufloeser.normIndex(gesetzAus(normen), ENBEZ);
if (normIndex < 0) {
- return manuell(befehl, "Das Gesetz enthält keine Inhaltsübersicht.");
+ return manuell(
+ befehl, Grund.BESTAND_WIDERSPRICHT, "Das Gesetz enthält keine Inhaltsübersicht.");
}
var flach = befehl.neuerText().replaceAll("\\s+", " ").strip();
flach = flach.replaceFirst("^Inhaltsübersicht\\s*", "");
@@ -284,6 +304,7 @@ final class InhaltsuebersichtAnwender {
if (flach.contains("wie folgt geändert") || flach.contains(" wird wie folgt gefasst")) {
return manuell(
befehl,
+ Grund.ZITAT_UNBRAUCHBAR,
"Das Zitat der neuen Inhaltsübersicht enthält Befehlstext — vermutlich ist ein"
+ " Anführungszeichen unbalanciert; bitte manuell prüfen.");
}
@@ -305,7 +326,8 @@ final class InhaltsuebersichtAnwender {
}
}
if (zeilen.size() < 2) {
- return manuell(befehl, "Das Zitat enthält keine erkennbare Inhaltsübersicht.");
+ return manuell(
+ befehl, Grund.ZITAT_UNBRAUCHBAR, "Das Zitat enthält keine erkennbare Inhaltsübersicht.");
}
var norm = normen.get(normIndex);
normen.set(
@@ -387,11 +409,17 @@ final class InhaltsuebersichtAnwender {
private static AngewandteAenderung angewandt(Aenderungsbefehl befehl) {
return new AngewandteAenderung(
- befehl, Status.ANGEWANDT, "", new LinkedHashSet<>(List.of(ENBEZ)));
+ befehl, Status.ANGEWANDT, "", new LinkedHashSet<>(List.of(ENBEZ)), null);
}
- private static AngewandteAenderung manuell(Aenderungsbefehl befehl, String begruendung) {
- return new AngewandteAenderung(befehl, Status.MANUELL_PRUEFEN, begruendung, Set.of());
+ 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 eu.mulk.aendggner.gesetz.Gesetz gesetzAus(List<Norm> normen) {
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java b/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java
index b8d22d6..fbc3f65 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java
@@ -26,14 +26,19 @@ final class StellenAufloeser {
sealed interface Ergebnis {
record Gefunden(Fundstelle fundstelle) implements Ergebnis {}
- record NichtGefunden(String begruendung) implements Ergebnis {}
+ /**
+ * @param grund die Art des Grundes; der ausformulierte Grund steht daneben und bleibt
+ * maßgeblich (siehe {@link Grund}).
+ */
+ record NichtGefunden(String begruendung, Grund grund) implements Ergebnis {}
}
private StellenAufloeser() {}
static Ergebnis aufloese(Gesetz gesetz, Stelle stelle) {
if (stelle.istLeer()) {
- return new Ergebnis.NichtGefunden("Stelle nennt keine Fundstelle im Gesetz.");
+ return new Ergebnis.NichtGefunden(
+ "Stelle nennt keine Fundstelle im Gesetz.", Grund.STELLE_NICHT_AUFLOESBAR);
}
// 1. Norm bestimmen.
@@ -45,11 +50,14 @@ final class StellenAufloeser {
} else if (stelle.anlagenEnbez().isPresent()) {
enbez = stelle.anlagenEnbez().get();
} else {
- return new Ergebnis.NichtGefunden("Stelle nennt keinen Paragraphen: " + stelle.anzeigeText());
+ return new Ergebnis.NichtGefunden(
+ "Stelle nennt keinen Paragraphen: " + stelle.anzeigeText(),
+ Grund.STELLE_NICHT_AUFLOESBAR);
}
int normIndex = normIndex(gesetz, enbez);
if (normIndex < 0) {
- return new Ergebnis.NichtGefunden(enbez + " existiert nicht im Gesetz.");
+ return new Ergebnis.NichtGefunden(
+ enbez + " existiert nicht im Gesetz.", Grund.BESTAND_WIDERSPRICHT);
}
var norm = gesetz.normen().get(normIndex);
@@ -59,7 +67,8 @@ final class StellenAufloeser {
var nummer = stelle.absatz().get().nummer();
absatzIndex = absatzIndex(norm, nummer);
if (absatzIndex < 0) {
- return new Ergebnis.NichtGefunden(enbez + " hat keinen Absatz " + nummer + ".");
+ return new Ergebnis.NichtGefunden(
+ enbez + " hat keinen Absatz " + nummer + ".", Grund.STELLE_NICHT_AUFLOESBAR);
}
}
@@ -86,7 +95,8 @@ final class StellenAufloeser {
+ norm.absaetze().size()
+ " Absätze; „"
+ stelle.anzeigeText()
- + "“ ist ohne Absatzangabe nicht eindeutig.");
+ + "“ ist ohne Absatzangabe nicht eindeutig.",
+ Grund.MEHRDEUTIG);
}
trefferAbsatz = i;
trefferBereich = kandidat;
@@ -94,7 +104,8 @@ final class StellenAufloeser {
}
if (trefferAbsatz == null) {
return new Ergebnis.NichtGefunden(
- "„" + stelle.anzeigeText() + "“ ist im Text von " + enbez + " nicht auffindbar.");
+ "„" + stelle.anzeigeText() + "“ ist im Text von " + enbez + " nicht auffindbar.",
+ Grund.STELLE_NICHT_AUFLOESBAR);
}
return new Ergebnis.Gefunden(new Fundstelle(normIndex, trefferAbsatz, trefferBereich));
}
@@ -104,7 +115,8 @@ final class StellenAufloeser {
var bereich = loeseFeinKomponentenAuf(stelle, text);
if (bereich == null) {
return new Ergebnis.NichtGefunden(
- "„" + stelle.anzeigeText() + "“ ist im Text von " + enbez + " nicht auffindbar.");
+ "„" + stelle.anzeigeText() + "“ ist im Text von " + enbez + " nicht auffindbar.",
+ Grund.STELLE_NICHT_AUFLOESBAR);
}
return new Ergebnis.Gefunden(new Fundstelle(normIndex, absatzIndex, bereich));
}
diff --git a/src/main/java/eu/mulk/aendggner/gesetz/Gesetz.java b/src/main/java/eu/mulk/aendggner/gesetz/Gesetz.java
index bbbf47c..112247d 100644
--- a/src/main/java/eu/mulk/aendggner/gesetz/Gesetz.java
+++ b/src/main/java/eu/mulk/aendggner/gesetz/Gesetz.java
@@ -4,21 +4,40 @@ package eu.mulk.aendggner.gesetz;
import java.util.List;
import java.util.Optional;
+import org.jspecify.annotations.Nullable;
/**
* Ein Stammgesetz, bestehend aus einer geordneten Liste von Einzelnormen und den (ebenfalls
* geordneten) Gliederungseinheiten (Teil/Abschnitt/Unterabschnitt/…), die die Normen strukturieren.
+ *
+ * @param stand der Stand der Quelle, soweit sie einen angibt; {@code null} bei handgepflegtem
+ * Klartext. An ihm hängt die Auskunft, ob das Stammgesetz jünger ist als das Änderungsgesetz,
+ * das darauf angewandt wird.
*/
public record Gesetz(
- String jurabk, String langue, String kurzue, List<Norm> normen, List<Gliederung> gliederungen) {
+ String jurabk,
+ String langue,
+ String kurzue,
+ List<Norm> normen,
+ List<Gliederung> gliederungen,
+ @Nullable Stand stand) {
public Gesetz {
normen = List.copyOf(normen);
gliederungen = List.copyOf(gliederungen);
}
+ public Gesetz(
+ String jurabk,
+ String langue,
+ String kurzue,
+ List<Norm> normen,
+ List<Gliederung> gliederungen) {
+ this(jurabk, langue, kurzue, normen, gliederungen, null);
+ }
+
public Gesetz(String jurabk, String langue, String kurzue, List<Norm> normen) {
- this(jurabk, langue, kurzue, normen, List.of());
+ this(jurabk, langue, kurzue, normen, List.of(), null);
}
public Optional<Norm> norm(String enbez) {
@@ -26,14 +45,14 @@ public record Gesetz(
}
public Gesetz mitNormen(List<Norm> neueNormen) {
- return new Gesetz(jurabk, langue, kurzue, neueNormen, gliederungen);
+ return new Gesetz(jurabk, langue, kurzue, neueNormen, gliederungen, stand);
}
public Gesetz mitGliederungen(List<Gliederung> neueGliederungen) {
- return new Gesetz(jurabk, langue, kurzue, normen, neueGliederungen);
+ return new Gesetz(jurabk, langue, kurzue, normen, neueGliederungen, stand);
}
public Gesetz mitLangue(String neuerLangtitel) {
- return new Gesetz(jurabk, neuerLangtitel, kurzue, normen, gliederungen);
+ return new Gesetz(jurabk, neuerLangtitel, kurzue, normen, gliederungen, stand);
}
}
diff --git a/src/main/java/eu/mulk/aendggner/gesetz/Stand.java b/src/main/java/eu/mulk/aendggner/gesetz/Stand.java
new file mode 100644
index 0000000..26a6ee4
--- /dev/null
+++ b/src/main/java/eu/mulk/aendggner/gesetz/Stand.java
@@ -0,0 +1,22 @@
+// SPDX-FileCopyrightText: 2020 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+// SPDX-License-Identifier: AGPL-3.0-or-later
+package eu.mulk.aendggner.gesetz;
+
+import java.time.LocalDate;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Der Stand einer konsolidierten Fassung, wie ihn die Quelle selbst angibt.
+ *
+ * <p>Die Standzeile allein trägt nicht weit genug. Das gii-XML führt neben ihr Hinweise der Form
+ * „Änderung durch Art. 1 G v. 18.11.2020 I 2397 … textlich nachgewiesen, dokumentarisch noch nicht
+ * abschließend bearbeitet“: Der Wortlaut ist dann bereits fortgeschrieben, die Standzeile nennt
+ * aber noch die vorige Änderung. Wer nur sie liest, hält eine Fassung für älter, als sie ist —
+ * genau der Irrtum, an dem im Infektionsschutzgesetz siebenundzwanzig Befehle scheiterten, ohne
+ * dass jemand den Grund benennen konnte.
+ *
+ * @param kommentar die Standzeile im Wortlaut der Quelle, für die Anzeige.
+ * @param juengsteAenderung das späteste Datum, das irgendeine Standangabe nennt — der wirkliche
+ * Stand des Wortlauts; {@code null}, wenn keine Angabe ein Datum trägt.
+ */
+public record Stand(String kommentar, @Nullable LocalDate juengsteAenderung) {}
diff --git a/src/main/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoader.java b/src/main/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoader.java
index 65e005c..8eeb91f 100644
--- a/src/main/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoader.java
+++ b/src/main/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoader.java
@@ -7,10 +7,12 @@ 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.Stand;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.nio.file.Path;
+import java.time.LocalDate;
import java.util.ArrayList;
import java.util.regex.Pattern;
import javax.xml.XMLConstants;
@@ -47,6 +49,7 @@ public final class GiiXmlLoader {
String jurabk = null;
String langue = null;
String kurzue = null;
+ Stand stand = null;
var normen = new ArrayList<Norm>();
var gliederungen = new ArrayList<Gliederung>();
Gliederung aktuelleGliederung = null;
@@ -61,6 +64,7 @@ public final class GiiXmlLoader {
jurabk = kindText(metadaten, "jurabk");
langue = kindText(metadaten, "langue");
kurzue = kindText(metadaten, "kurzue");
+ stand = standangabe(metadaten);
}
var gliederungselement = erstesKind(metadaten, "gliederungseinheit");
@@ -91,7 +95,7 @@ public final class GiiXmlLoader {
if (jurabk == null) {
throw new SAXException("Keine <norm>-Elemente mit Metadaten gefunden: " + quelle.name());
}
- return new Gesetz(jurabk, langue, kurzue, normen, gliederungen);
+ return new Gesetz(jurabk, langue, kurzue, normen, gliederungen, stand);
}
private static ArrayList<Absatz> leseAbsaetze(Element normElement) {
@@ -161,6 +165,46 @@ public final class GiiXmlLoader {
return null;
}
+ /** „… v. 19.6.2020 I 1385“ — das Datum, das eine Standangabe nennt. */
+ private static final Pattern STANDDATUM =
+ Pattern.compile("v\\. (\\d{1,2})\\.(\\d{1,2})\\.(\\d{4})");
+
+ /**
+ * Der Stand des Gesetzes: die Standzeile im Wortlaut und das späteste Datum, das
+ * <em>irgendeine</em> Standangabe nennt. Die Hinweise („Änderung durch Art. 1 G v. 18.11.2020 …
+ * textlich nachgewiesen“) gehören dazu — sie bezeugen, dass der Wortlaut die Änderung bereits
+ * trägt, während die Standzeile noch die vorige nennt (siehe {@link Stand}).
+ */
+ private static @Nullable Stand standangabe(Element metadaten) {
+ String zeile = null;
+ LocalDate juengste = null;
+ for (var angabe : kindElemente(metadaten, "standangabe")) {
+ var kommentar = kindText(angabe, "standkommentar");
+ if (kommentar == null) {
+ continue;
+ }
+ if (zeile == null && "Stand".equals(kindText(angabe, "standtyp"))) {
+ zeile = kommentar;
+ }
+ var m = STANDDATUM.matcher(kommentar);
+ while (m.find()) {
+ try {
+ var datum =
+ LocalDate.of(
+ Integer.parseInt(m.group(3)),
+ Integer.parseInt(m.group(2)),
+ Integer.parseInt(m.group(1)));
+ if (juengste == null || datum.isAfter(juengste)) {
+ juengste = datum;
+ }
+ } catch (RuntimeException ignoriert) {
+ // Ein verschriebenes Datum („19.5..2020“) kommt vor; es bleibt außer Betracht.
+ }
+ }
+ }
+ return zeile == null ? null : new Stand(zeile, juengste);
+ }
+
private static @Nullable String kindText(Element parent, String name) {
var element = erstesKind(parent, name);
if (element == null) {
diff --git a/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java b/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java
index 03f61eb..9b8f406 100644
--- a/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java
+++ b/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java
@@ -3,8 +3,11 @@
package eu.mulk.aendggner.synopse;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.UnbekannterBefehl;
+import eu.mulk.aendggner.anwendung.BefehlAnwender;
+import eu.mulk.aendggner.anwendung.Grund;
import eu.mulk.aendggner.gesetz.Norm;
import java.time.LocalDate;
+import java.util.LinkedHashMap;
import java.util.List;
/**
@@ -228,20 +231,38 @@ public final class HtmlRenderer {
sb.append("</ul>\n");
}
if (!synopse.manuellZuPruefen().isEmpty()) {
- sb.append("<h3>Nicht automatisch angewandte Befehle</h3>\n<ol>\n");
- for (var eintrag : synopse.manuellZuPruefen()) {
- var befehl = eintrag.befehl();
- sb.append("<li><strong>")
- .append(esc(befehl.provenienz().anzeigeText()))
- .append("</strong>");
- if (!(befehl instanceof UnbekannterBefehl) || !eintrag.begruendung().isEmpty()) {
- sb.append(" — ").append(esc(eintrag.begruendung()));
+ sb.append("<h3>Nicht automatisch angewandte Befehle</h3>\n");
+ // Nach Art des Grundes gebündelt und ausgezählt. Der ausformulierte Grund bleibt bei jedem
+ // Befehl stehen — gebündelt ist nur, was ohne Ordnung eine bloße Liste wäre. Innerhalb der
+ // Gruppe bleibt die Reihenfolge des Dokuments.
+ var gruppen = new LinkedHashMap<Grund, List<BefehlAnwender.AngewandteAenderung>>();
+ for (var grund : Grund.values()) {
+ var eintraege =
+ synopse.manuellZuPruefen().stream().filter(e -> e.grund() == grund).toList();
+ if (!eintraege.isEmpty()) {
+ gruppen.put(grund, eintraege);
}
- sb.append("<br><span class=\"originaltext\">")
- .append(esc(befehl.provenienz().originalText()))
- .append("</span></li>\n");
}
- sb.append("</ol>\n");
+ for (var gruppe : gruppen.entrySet()) {
+ sb.append("<h4>")
+ .append(esc(gruppe.getKey().bezeichnung()))
+ .append(" <span class=\"anzahl\">")
+ .append(gruppe.getValue().size())
+ .append("</span></h4>\n<ol>\n");
+ for (var eintrag : gruppe.getValue()) {
+ var befehl = eintrag.befehl();
+ sb.append("<li><strong>")
+ .append(esc(befehl.provenienz().anzeigeText()))
+ .append("</strong>");
+ if (!(befehl instanceof UnbekannterBefehl) || !eintrag.begruendung().isEmpty()) {
+ sb.append(" — ").append(esc(eintrag.begruendung()));
+ }
+ sb.append("<br><span class=\"originaltext\">")
+ .append(esc(befehl.provenienz().originalText()))
+ .append("</span></li>\n");
+ }
+ sb.append("</ol>\n");
+ }
}
sb.append("</section>\n");
}
@@ -485,6 +506,24 @@ public final class HtmlRenderer {
section.gliederung-aenderungen { padding-top: 1.25rem; }
section.manuell { margin-top: 1.5rem; padding-top: 1.25rem; }
section.manuell h3 { margin: 1rem 0 0.4rem; font-size: 0.6875rem; }
+ /* Die Gruppenaufschrift trägt ihre Häufigkeit in einem Kästchen wie die Randziffer der
+ Normen — sie ist eine Zahl und keine Zierde. */
+ section.manuell h4 {
+ margin: 0.9rem 0 0.3rem;
+ font-size: 0.6875rem;
+ font-weight: bold;
+ color: var(--muted);
+ }
+ section.manuell h4 .anzahl {
+ display: inline-block;
+ min-width: 1.5rem;
+ margin-left: 0.35rem;
+ padding: 0 0.25rem;
+ border: 1px solid var(--linie);
+ background: var(--grund);
+ color: var(--fg);
+ text-align: center;
+ }
section.manuell li { margin-bottom: 0.6rem; font-size: 0.75rem; }
.originaltext { font-family: var(--antiqua); color: var(--muted); font-size: 0.8125rem; }
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index 66b7f00..97bcffb 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -66,6 +66,13 @@ class EndToEndTest {
// Das Original-BGBl enthält ein überzähliges Anführungszeichen (Artikel 3) — muss als
// Warnung gemeldet werden, nicht als Abbruch.
assertThat(parseErgebnis.warnungen()).isNotEmpty();
+ // Und der Leser erfährt, woran die Reste liegen: Das XML trägt die Änderung dieses
+ // Gesetzes bereits als „textlich nachgewiesen“ und ist damit jünger als das Gesetz selbst.
+ assertThat(parseErgebnis.warnungen())
+ .anyMatch(w -> w.contains("Das Stammgesetz ist jünger als das Änderungsgesetz"))
+ // Die Rüge ergeht einmal, obwohl zwei Artikel dasselbe Stammgesetz betreffen.
+ .filteredOn(w -> w.contains("ist jünger als"))
+ .hasSize(1);
// 4. Anwenden: wirft nicht; jeder Befehl erhält einen Protokolleintrag. Auch hier ist das XML
// jünger als das Änderungsgesetz — die Sollzahlen stehen in ifsgGegenZeitrichtigenStamm().
@@ -102,6 +109,9 @@ class EndToEndTest {
assertThat(parseErgebnis.artikel()).containsExactly("1", "2");
assertThat(parseErgebnis.befehle()).hasSize(75);
+ // Gegen den zeitrichtigen Stamm ergeht keine Altersrüge; gegen den heutigen schon (siehe
+ // gesamtePipelineAufIfSgBeispiel).
+ assertThat(parseErgebnis.warnungen()).noneMatch(w -> w.contains("ist jünger als"));
var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
var manuellPfade =
diff --git a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
index 8b60017..1650d1b 100644
--- a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
+++ b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
@@ -342,6 +342,37 @@ class BefehlAnwenderTest {
}
@Test
+ void haengtGanzeParagraphenAnsGesetzAn() {
+ var befehl =
+ new Anfuegung(
+ Stelle.LEER,
+ Ebene.PARAGRAPH,
+ null,
+ "§ 4\nÜbergang\nWer zuvor erprobt hat, erprobt weiter.\n\n"
+ + "§ 5\nSchluss\nDiese Vorschrift gilt zuletzt.",
+ PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl));
+
+ assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT);
+ assertThat(ergebnis.neu().normen().stream().map(Norm::enbez))
+ .containsExactly("§ 1", "§ 2", "§ 3", "§ 4", "§ 5");
+ assertThat(ergebnis.neu().norm("§ 4").orElseThrow().titel()).isEqualTo("Übergang");
+ }
+
+ @Test
+ void haengtNichtAnUndBegruendetEsWennDasZitatKeinenNormkopfTraegt() {
+ var befehl =
+ new Anfuegung(Stelle.LEER, Ebene.PARAGRAPH, null, "Wer zuvor erprobt hat, …", PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl));
+
+ assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.MANUELL_PRUEFEN);
+ assertThat(ergebnis.protokoll().get(0).grund()).isEqualTo(Grund.ZITAT_UNBRAUCHBAR);
+ assertThat(ergebnis.neu().normen()).hasSize(3);
+ }
+
+ @Test
void hebtParagraphenAuf() {
var befehl = new Aufhebung(stelle(new Stelle.Paragraph("2")), PROV);
diff --git a/src/test/java/eu/mulk/aendggner/synopse/HtmlRendererTest.java b/src/test/java/eu/mulk/aendggner/synopse/HtmlRendererTest.java
index b80fc88..6d8c840 100644
--- a/src/test/java/eu/mulk/aendggner/synopse/HtmlRendererTest.java
+++ b/src/test/java/eu/mulk/aendggner/synopse/HtmlRendererTest.java
@@ -72,6 +72,8 @@ class HtmlRendererTest {
assertThat(html).contains("Manuell prüfen");
assertThat(html).contains("Die Nummern 1 bis 3 werden aufgehoben.");
+ // Nach Art des Grundes gebündelt, mit vorangestellter Häufigkeit.
+ assertThat(html).contains("Befehl nicht erkannt").contains("<span class=\"anzahl\">1</span>");
}
@Test