aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java7
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java238
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java5
-rw-r--r--src/main/resources/sampledata/Landesrecht-Beispiele.adoc8
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java69
-rw-r--r--src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java100
6 files changed, 339 insertions, 88 deletions
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
index bda8e94..e63cfd8 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
@@ -587,11 +587,16 @@ final class BefehlErkenner {
// „nach Satz 2 wird …“). Sie erbt nach einer Umnummerierung deren neue Bezeichnung als Kontext.
// Wortanker („nach der Angabe „…““) zählen ausdrücklich nicht dazu: sie suchen ihren Text
// normweit und dürfen nicht auf die neue Bezeichnung festgenagelt werden.
+ // Der Abschluss ist „kein Buchstabe mehr“ und nicht \b: hinter einem abgekürzten
+ // Bezeichnungswort steht bereits der Abkürzungspunkt, und zwischen ihm und dem folgenden
+ // Leerzeichen liegt keine Wortgrenze — mit \b entginge der Klausel jede bayerische
+ // Kurzform („in Abs. 2 …“, „in Buchst. b …“). Vor der Ausschreibung steht die Kurzform
+ // nur dann zurück, wenn diese länger passt (Alternativen sind absteigend geordnet).
private static final Pattern LOKATIVE_KLAUSEL =
Pattern.compile(
"(?:in|im|nach|vor) (?:dem |der |den )?"
+ "(?:Satzteil|Satz|Sätze[n]?|Absatz|Abs\\.|Absätze[n]?|Nummer[n]?|Nrn?\\."
- + "|Buchstabe[n]?|Buchst\\.|Halbsatz)\\b");
+ + "|Buchstabe[n]?|Buchst\\.|Halbsatz)(?!\\p{L})");
// „In <Stelle> wird nach den Wörtern «1» ein Komma eingefügt.“ (Satzzeichen statt Wörter
// einfügen)
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
index b8540c5..32350ff 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
@@ -73,20 +73,25 @@ public final class BefehlAnwender {
var protokoll = new ArrayList<AngewandteAenderung>();
String neuerLangtitel = null;
- // Angewandt wird in Sachreihenfolge, protokolliert in der Reihenfolge des Änderungsgesetzes.
- var ergebnisse = new AngewandteAenderung[befehle.size()];
- for (int index : anwendungsReihenfolge(befehle)) {
- var befehl = befehle.get(index);
+ // 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.
- if (befehl instanceof Neufassung n && istNurUeberschrift(n.stelle())) {
+ // 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(befehl, "(Gesetzesüberschrift)");
+ ergebnisse[index] = angewandt(schritt.teil(), "(Gesetzesüberschrift)");
continue;
}
- ergebnisse[index] = wendeAn(normen, gliederungen, befehl);
+ ergebnisse[index] = wendeAn(normen, gliederungen, schritt.teil());
}
- protokoll.addAll(Arrays.asList(ergebnisse));
+ protokoll.addAll(verdichte(befehle, schritte, ergebnisse));
var neu = alt.mitNormen(normen).mitGliederungen(gliederungen);
if (neuerLangtitel != null) {
@@ -96,46 +101,160 @@ public final class BefehlAnwender {
}
/**
- * Reihenfolge, in der die Befehle anzuwenden sind: grundsätzlich die des Änderungsgesetzes.
- *
- * <p>Eine Umnummerierung „Der bisherige § 13 wird § 14“ wird jedoch vor eine vorangehende
- * Einfügung gezogen, die denselben Paragraphen neu besetzt („In Kapitel 4 wird nach § 12 der
- * folgende neue § 13 angefügt“). „Bisherig“ bezeichnet den Stand vor der Änderung; die
- * Umnummerierung geht der Neubesetzung sachlich also voraus. In der Textreihenfolge angewandt
- * liefen beide Befehle dagegen auf zwei Paragraphen gleicher Bezeichnung hinaus.
+ * 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 Befehle. 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
+ * 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: ein Befehl rückt vor den ersten, mit dem er kollidiert.
- * So bleibt jede Folgeänderung hinter der Umnummerierung, auf deren neue Bezeichnung sie zeigt
- * („Der bisherige Absatz 3 wird Absatz 4 und wie folgt geändert: …“).
+ * <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.
*/
- private static List<Integer> anwendungsReihenfolge(List<Aenderungsbefehl> befehle) {
- var reihenfolge = new ArrayList<Integer>(befehle.size());
- for (int i = 0; i < befehle.size(); i++) {
- reihenfolge.add(i);
+ 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);
}
- for (int j = 0; j < befehle.size(); j++) {
- var raeumt = geraeumteBezeichnungen(befehle.get(j));
- if (raeumt.isEmpty()) {
+ 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), "Verbund ohne Teilbefehle."));
continue;
}
- for (int i = 0; i < j; i++) {
- if (belegteBezeichnungen(befehle.get(i)).stream().anyMatch(raeumt::contains)) {
- reihenfolge.remove(Integer.valueOf(j));
- reihenfolge.add(reihenfolge.indexOf(i), j);
- break;
- }
+ 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 reihenfolge;
+ 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>();
+ for (int k = 0; k < teilErgebnisse.size(); k++) {
+ var ergebnis = teilErgebnisse.get(k);
+ betroffene.addAll(ergebnis.betroffeneEnbez());
+ if (ergebnis.status() != Status.ANGEWANDT) {
+ fehler.add(
+ "Teil "
+ + (k + 1)
+ + " ("
+ + schritte.get(von + k).teil().stelle().anzeigeText()
+ + "): "
+ + ergebnis.begruendung());
+ }
+ }
+ if (fehler.isEmpty()) {
+ return new AngewandteAenderung(befehl, Status.ANGEWANDT, "", betroffene);
+ }
+ return new AngewandteAenderung(
+ befehl, Status.MANUELL_PRUEFEN, String.join(" ", fehler), betroffene);
}
/** Bezeichnungen, die ein Befehl freigibt — die Ausgangsstellen seiner Umnummerierungen. */
@@ -1486,39 +1605,20 @@ public final class BefehlAnwender {
* 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 betroffene = new LinkedHashSet<String>();
- var fehler = new ArrayList<String>();
- // Auch innerhalb eines Verbunds gilt die Bezeichnungs-Reihenfolge: „Die bisherigen Nrn. 13 und
- // 14 werden die Nrn. 14 und 15“ zerfällt in zwei Umnummerierungen, die aufsteigend angewandt
- // eine doppelte Nr. 14 erzeugten.
- var teile = befehl.teilbefehle();
- var meldungen = new String[teile.size()];
- for (int index : anwendungsReihenfolge(teile)) {
- var teil = teile.get(index);
- var ergebnis = wendeAn(normen, gliederungen, teil);
- betroffene.addAll(ergebnis.betroffeneEnbez());
- if (ergebnis.status() != Status.ANGEWANDT) {
- meldungen[index] =
- "Teil "
- + (index + 1)
- + " ("
- + teil.stelle().anzeigeText()
- + "): "
- + ergebnis.begruendung();
- }
- }
- for (var meldung : meldungen) {
- if (meldung != null) {
- fehler.add(meldung);
- }
- }
- if (fehler.isEmpty()) {
- return new AngewandteAenderung(befehl, Status.ANGEWANDT, "", betroffene);
+ 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 new AngewandteAenderung(
- befehl, Status.MANUELL_PRUEFEN, String.join(" ", fehler), betroffene);
+ return verbund(befehl, Arrays.asList(ergebnisse), schritte, 0);
}
// --- Gemeinsame Helfer ---------------------------------------------------------------------
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java b/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java
index ff77279..d2fe02a 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java
@@ -252,7 +252,10 @@ final class StellenAufloeser {
*/
private static SatzTeiler.@Nullable SatzBereich zeilenBlock(
String text, String labelRegex, SatzTeiler.SatzBereich suchbereich) {
- var muster = Pattern.compile("(?m)^([ \\t]*)" + labelRegex + "[ \\t].*$");
+ // Der Text hinter dem Label ist wahlfrei: eine Aufzählungseinheit, die sich vollständig in
+ // ihre Untergliederung ergießt, führt ihre Marke allein auf der Zeile (BayJG Art. 56 Abs. 2
+ // Nr. 12 — darunter nur die Buchstaben a und b). Ihr Block sind dann eben nur die Kindzeilen.
+ var muster = Pattern.compile("(?m)^([ \\t]*)" + labelRegex + "(?:[ \\t].*)?$");
var matcher = muster.matcher(text).region(suchbereich.von(), suchbereich.bis());
SatzTeiler.SatzBereich gefunden = null;
int einrueckung = 0;
diff --git a/src/main/resources/sampledata/Landesrecht-Beispiele.adoc b/src/main/resources/sampledata/Landesrecht-Beispiele.adoc
index be70dfc..4de907f 100644
--- a/src/main/resources/sampledata/Landesrecht-Beispiele.adoc
+++ b/src/main/resources/sampledata/Landesrecht-Beispiele.adoc
@@ -232,10 +232,16 @@ Nach absteigendem Nutzen:
angefasst — ein Sammelheft mit mehreren Verkündungen, also zugleich ein Test der Artikel-/
Verkündungsauswahl.
. *Berlin, ganzseitenbreite Rahmen* (XY-Cut) — die letzte offene Layout-Frage, siehe „Noch offen“.
+. *Stellung eines eingefügten Blocks* — seit die Umnummerierungs-Kaskade des BayJG aufgeht, ist
+ dies der sichtbare Rest an Art. 56: „Nach Nr. 4 werden die folgenden Nrn. 5 bis 7 eingefügt“ gilt
+ als angewandt, der Block tritt aber ans Ende des Absatzes statt hinter die Nr. 4, und der leere
+ Platzhalter „7. (aufgehoben)“ bleibt zwischen den Nrn. 9 und 10 stehen. Beides ist im
+ Akzeptanztest festgehalten, damit es nicht unbemerkt bleibt.
Regressionsschutz: `./mvnw verify` prüft die bestehenden Belegfälle mit. Gepinnt ist dabei nicht
überall dieselbe Art von Zahl — für UWG, AGG und ProdHaftG, dass *kein* Befehl manuell bleibt; für
-das BayJG die 154 erkannten Befehle und namentlich die drei Umnummerierungs-Residuen; für Sachsen,
+das BayJG die 154 erkannten Befehle, die vollständige Anwendung und den Wortlaut des
+Bußgeldkatalogs an drei Stellen; für Sachsen,
Niedersachsen und die vier NRW-Artikel die vollständige Anwendung bis auf das eine benannte
Residuum im WDR-Gesetz; für Schleswig-Holstein, Berlin und Hessen die vollständige *Erkennung*
(dort 21 Befehle). Wer eine dieser Zahlen
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index 55579f1..865afe6 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -260,18 +260,19 @@ class EndToEndTest {
assertThat(parseErgebnis.befehle()).hasSize(154);
assertThat(parseErgebnis.befehle()).noneMatch(b -> b instanceof UnbekannterBefehl);
- // 4. Anwenden auf die alte Fassung. Von 154 Befehlen bleiben genau drei als „manuell prüfen“
- // stehen — Folgeänderungen innerhalb einer mehrschrittigen Umnummerierungssequenz, die
- // ÄndGgner bewusst nicht automatisch auflöst (statt fehlerhaft zu raten):
- // * Art. 56 (§ 1 Nr. 48): eine umfangreiche Neunummerierung des Bußgeldkatalogs
- // (Einfügen der Nrn. 5–7 und 13, Verschieben von Nr. 6→9, 11→12 …) verschiebt die
- // Zielnummern der begleitenden Buchstaben-/Wortänderungen.
- // Die Sequenz in Art. 29a (§ 1 Nr. 23) löst sich dagegen seit der Kaskaden-Ordnung des
- // BefehlAnwenders auf: „Der bisherige Abs. 4 wird Abs. 5“ läuft vor der Bereichs-
- // Umnummerierung „Die bisherigen Abs. 1 bis 3 werden die Abs. 2 bis 4“, sodass die auf den
- // neuen Abs. 5 zielenden Wort- und Satzbefehle ihren Alttext finden.
- // Diese Residuen sind exakt gepinnt; sie landen mit Begründung im Abschnitt „Manuell prüfen“
- // der Synopse und werden nie stillschweigend verworfen.
+ // 4. Anwenden auf die alte Fassung. Alle 154 Befehle greifen; kein Rest bleibt manuell.
+ // Das Heft trägt zwei mehrschrittige Umnummerierungssequenzen, an denen sich die
+ // Schritt-Ordnung des BefehlAnwenders bewährt:
+ // * Art. 29a (§ 1 Nr. 23): „Der bisherige Abs. 4 wird Abs. 5“ läuft vor der Bereichs-
+ // Umnummerierung „Die bisherigen Abs. 1 bis 3 werden die Abs. 2 bis 4“, sodass die auf
+ // den neuen Abs. 5 zielenden Wort- und Satzbefehle ihren Alttext finden.
+ // * Art. 56 (§ 1 Nr. 48): die Neunummerierung des Bußgeldkatalogs verschränkt Einfügungen
+ // und Umnummerierungen über zehn Unterpunkte. Sie geht auf, weil die Umnummerierungen
+ // vorrücken, ihre Begleitänderungen aber an ihrer Dokumentstelle bleiben — die Wortfolge
+ // „schriftliche“ etwa steht erst nach dem vorangehenden Punkt nur noch einmal im
+ // Artikel und ist damit eindeutig.
+ // „Kein Rest“ heißt: jeder Befehl hat gegriffen. Es heißt nicht, dass die Norm in allem der
+ // amtlichen Nachfassung gleicht — was daran noch fehlt, steht weiter unten an Art. 56.
var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
assertThat(anwendung.protokoll()).hasSameSizeAs(parseErgebnis.befehle());
var manuellPfade =
@@ -279,7 +280,34 @@ class EndToEndTest {
.filter(a -> a.status() == BefehlAnwender.Status.MANUELL_PRUEFEN)
.map(a -> a.befehl().provenienz().gliederungsPfad())
.toList();
- assertThat(manuellPfade).containsExactlyInAnyOrder("48. a) ee)", "48. a) gg)", "48. b) cc)");
+ assertThat(manuellPfade).isEmpty();
+
+ // Der Bußgeldkatalog des Art. 56 trägt jetzt an den drei Stellen, an denen die Kaskade zuvor
+ // gescheitert war, den Wortlaut der amtlichen Nachfassung (BayJG.pdf):
+ var art56 = anwendung.neu().norm("Art. 56").orElseThrow();
+ var katalog = art56.absaetze().get(0).text();
+ assertThat(katalog)
+ // aus „Die bisherige Nr. 6 wird Nr. 9 und in Buchst. b …“ — die Begleitklausel findet ihre
+ // Stelle erst, seit die lokative Kurzform („in Buchst. b“) erkannt wird.
+ .contains("9. vorsätzlich oder fahrlässig entgegen Art. 32 Abs. 2 Satz 1, Abs. 4 oder 5")
+ .contains("b) die Abschussmeldung oder die Streckenliste")
+ // aus „Die bisherige Nr. 11 wird Nr. 12 und die Angabe „schriftliche“ wird gestrichen“ —
+ // eindeutig nur, weil die Streichung an ihrer Dokumentstelle bleibt.
+ .contains("12. ohne Begleitung oder Erlaubnis des Revierinhabers")
+ .doesNotContain("schriftliche");
+ // Abs. 2 Nr. 12 Buchst. b führt ihre Marke allein auf der Zeile — sie ist trotzdem auflösbar.
+ assertThat(art56.absaetze().get(1).text()).contains("(§ 2 Abs. 3 BJagdG)");
+
+ // Offen bleibt an dieser Norm eine Frage, die mit der Reihenfolge nichts zu tun hat und schon
+ // vorher so stand: Der eingefügte Block „Nach Nr. 4 werden die folgenden Nrn. 5 bis 7
+ // eingefügt“ landet am Ende des Absatzes statt hinter der Nr. 4, und der leere Platzhalter
+ // „7. (aufgehoben)“ der Altfassung bleibt zwischen den Nrn. 9 und 10 stehen. Beides ist hier
+ // festgehalten, damit es nicht unbemerkt bleibt — der Befehl gilt als angewandt, die Stellung
+ // seines Blocks ist aber falsch.
+ assertThat(katalog).contains("7. (aufgehoben)");
+ assertThat(katalog.indexOf("5.\t den Verboten des Art. 29 Abs. 2"))
+ .as("die eingefügten Nrn. 5 bis 7 stehen (noch) hinter der Nr. 16")
+ .isGreaterThan(katalog.indexOf("16."));
// Art. 29a Abs. 5 trägt nach der Kaskade die neue Behördenbezeichnung und den eingefügten Satz.
var art29a = anwendung.neu().norm("Art. 29a").orElseThrow();
@@ -975,8 +1003,8 @@ class EndToEndTest {
var ohne = Pipeline.erzeugeSynopse(alt, List.of(entwurfPdf), null, false);
var mit = Pipeline.erzeugeSynopse(alt, List.of(entwurfPdf, antragPdf), null, false);
- assertThat(mit.anzahlAngewandt()).isEqualTo(ohne.anzahlAngewandt()).isEqualTo(151);
- assertThat(mit.anzahlManuell()).isEqualTo(ohne.anzahlManuell()).isEqualTo(3);
+ assertThat(mit.anzahlAngewandt()).isEqualTo(ohne.anzahlAngewandt()).isEqualTo(154);
+ assertThat(mit.anzahlManuell()).isEqualTo(ohne.anzahlManuell()).isEqualTo(0);
// Beide Läufe zeigen eine Entwurfsfassung, nicht geltendes Recht.
assertThat(mit.html()).contains("Entwurfsfassung");
assertThat(mit.html()).contains("[Änderungsantrag Drs. 19/10365]");
@@ -1033,6 +1061,15 @@ class EndToEndTest {
* wird eine Fassung von 2023. Maßgeblich ist deshalb der Vergleich mit dem Entwurf — die
* Ausschussfassung muss <em>mehr</em> Befehle tragen, denn der Ausschuss hat zwei Artikel
* hinzugefügt (BGB und Betriebskostenverordnung).
+ *
+ * <p>Die Zahl stand bei 67, bevor die Anwendungsreihenfolge auf Schritte umgestellt wurde. Der
+ * hinzugekommene Befehl ist die Bereichs-Umnummerierung des Bußgeldkatalogs („Die bisherigen
+ * Nummern 19 bis 21 werden die Nummern 30 bis 32“, § 108 Absatz 1): Sie muss vor die Einfügung
+ * rücken, die ihre Ausgangsnummern neu vergibt, und dorthin trug die frühere einmalige
+ * Verschiebung nur ihr erstes Glied. Was in § 108 dabei herauskommt, sagt über die Ordnung nichts
+ * — die Norm liegt mitten im beschriebenen Stamm-Mismatch. Belegt ist die Ordnung an den
+ * Kaskaden, deren Stammfassung stimmt: BayJG Art. 29a und Art. 56 sowie WDR-Gesetz §§ 3 und 15,
+ * beide gegen die amtliche Nachfassung abgeglichen.
*/
@Test
void beschlussempfehlungLiefertDieAusschussfassung() throws Exception {
@@ -1046,7 +1083,7 @@ class EndToEndTest {
var ausEmpfehlung = Pipeline.erzeugeSynopse(xml, List.of(empfehlung), null, false);
var ausEntwurf = Pipeline.erzeugeSynopse(xml, List.of(entwurf), null, false);
- assertThat(ausEmpfehlung.anzahlAngewandt()).isEqualTo(67);
+ assertThat(ausEmpfehlung.anzahlAngewandt()).isEqualTo(68);
assertThat(ausEmpfehlung.anzahlAngewandt() + ausEmpfehlung.anzahlManuell())
.as("die Ausschussfassung trägt mehr Befehle als der Entwurf")
.isGreaterThan(ausEntwurf.anzahlAngewandt() + ausEntwurf.anzahlManuell());
diff --git a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
index c12f118..7e7da85 100644
--- a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
+++ b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
@@ -1378,4 +1378,104 @@ class BefehlAnwenderTest {
assertThat(ergebnis.neu().norm("§ 3").orElseThrow().titel()).isEqualTo("Zwischennorm");
assertThat(ergebnis.neu().norm("§ 4").orElseThrow().titel()).isEqualTo("Schlussvorschriften");
}
+
+ @Test
+ void zieehtEineGanzeUmnummerierungsketteVorDieEinfuegung() {
+ // Eine Einfügung besetzt die Nr. 2, die eine Kette von Umnummerierungen erst räumen muss:
+ // „Nr. 3 wird Nr. 4“ hält „Nr. 2 wird Nr. 3“ auf, und diese hält die Einfügung auf. Vorgezogen
+ // werden muss deshalb nicht nur das letzte Glied der Kette, sondern die ganze Kette — sonst
+ // träfe „Nr. 2 wird Nr. 3“ auf eine noch besetzte Nr. 3 (BayJG Art. 56 Abs. 1 Buchst. hh/ii).
+ // Solange der Verbund als Ganzes vorrückte, war das umsonst zu haben; seit die Schritte
+ // einzeln geordnet werden, hängt es an der Mitnahme der Vorgänger.
+ var einfuegung =
+ new StrukturEinfuegung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("2"), new Stelle.NummerNr("1")),
+ false,
+ Ebene.NUMMER,
+ "2",
+ "2. die Zulassung von Anträgen,",
+ PROV);
+ // So liefert der Erkenner eine Bereichs-Umnummerierung: das höhere Paar zuerst.
+ var kette =
+ new Sammelbefehl(
+ List.of(umnummerierungNummer("2", "3", "4"), umnummerierungNummer("2", "2", "3")));
+
+ var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(einfuegung, kette));
+
+ assertThat(ergebnis.anzahlManuell()).isZero();
+ assertThat(absatzText(ergebnis.neu(), "§ 1", 1))
+ .isEqualTo(
+ "Die Erprobung umfasst\n"
+ + " 1. das Einlesen von Gesetzen,\n"
+ + " 2. die Zulassung von Anträgen,\n"
+ + " 3. die Anwendung von Befehlen und\n"
+ + " 4. die Ausgabe von Synopsen.");
+ }
+
+ @Test
+ void laesstDieBegleitaenderungAnIhrerDokumentstelle() {
+ // „Die bisherige Nr. 3 wird Nr. 4 und die Angabe „schriftliche “ wird gestrichen“: Die
+ // Umnummerierung muss vor den Befehl rücken, der die Nr. 3 neu besetzt — ihre Begleitänderung
+ // aber nicht. Vorgezogen träfe die Streichung noch zwei Fundstellen und bliebe mehrdeutig; an
+ // ihrem Platz trifft sie genau eine, weil der Punkt davor die andere längst getilgt hat
+ // (BayJG Art. 56 Abs. 1 Buchst. gg).
+ var neuBesetzung = umnummerierungNummer("1", "2", "3");
+ var tilgtDieErsteFundstelle =
+ new Ersetzung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("1")),
+ "die schriftliche Anmeldung",
+ "die Anmeldung",
+ false,
+ false,
+ PROV);
+ var verbund =
+ new Sammelbefehl(
+ List.of(
+ umnummerierungNummer("1", "3", "4"),
+ new Streichung(stelle(new Stelle.Paragraph("1")), "schriftliche ", PROV)));
+
+ var ergebnis =
+ BefehlAnwender.anwenden(
+ kaskadenGesetz(), List.of(neuBesetzung, tilgtDieErsteFundstelle, verbund));
+
+ // Beide Teile des Verbunds greifen — die Streichung ist an ihrer Dokumentstelle eindeutig.
+ assertThat(ergebnis.anzahlManuell()).isZero();
+ var text = absatzText(ergebnis.neu(), "§ 1", 0);
+ assertThat(text).contains("1. die Anmeldung,").contains("3. die Prüfung und");
+ // Die Umnummerierung ist vorgerückt, ihre Begleitänderung nicht: Nr. 4 trägt den Text der
+ // bisherigen Nr. 3, und zwar ohne „schriftliche“.
+ assertThat(text).contains("4. die Bestätigung.").doesNotContain("schriftliche");
+ }
+
+ private static Umnummerierung umnummerierungNummer(String absatz, String alt, String neu) {
+ return new Umnummerierung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr(absatz), new Stelle.NummerNr(alt)),
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr(absatz), new Stelle.NummerNr(neu)),
+ PROV);
+ }
+
+ /**
+ * Ein Gesetz für die Kaskadenprobe: Das Wort „schriftliche“ steht zweimal in derselben Norm, in
+ * der ersten und in der letzten Nummer. Die Nummer 2, die die Kette frei macht, bleibt hier
+ * unbesetzt — die Einfügung, die sie im echten Fall füllt, gehört nicht zur Ordnungsfrage.
+ */
+ private static Gesetz kaskadenGesetz() {
+ return new Gesetz(
+ "TestG",
+ "Gesetz zur Erprobung",
+ "Testgesetz",
+ List.of(
+ new Norm(
+ "§ 1",
+ "Verfahren",
+ null,
+ List.of(
+ new Absatz(
+ "1",
+ "Die Erprobung umfasst\n"
+ + " 1. die schriftliche Anmeldung,\n"
+ + " 2. die Prüfung und\n"
+ + " 3. die schriftliche Bestätigung.")),
+ false)));
+ }
}