diff options
| -rw-r--r-- | FASSUNGEN.txt | 85 | ||||
| -rw-r--r-- | README.adoc | 35 | ||||
| -rw-r--r-- | src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java | 7 | ||||
| -rw-r--r-- | src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java | 238 | ||||
| -rw-r--r-- | src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java | 5 | ||||
| -rw-r--r-- | src/main/resources/sampledata/Landesrecht-Beispiele.adoc | 8 | ||||
| -rw-r--r-- | src/test/java/eu/mulk/aendggner/EndToEndTest.java | 69 | ||||
| -rw-r--r-- | src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java | 100 |
8 files changed, 451 insertions, 96 deletions
diff --git a/FASSUNGEN.txt b/FASSUNGEN.txt index 5b1a08f..ec8aeb5 100644 --- a/FASSUNGEN.txt +++ b/FASSUNGEN.txt @@ -21,6 +21,91 @@ Vorbemerkung zur Führung dieses Verzeichnisses ════════════════════════════════════════════════════════════════════════════════ + Fassung vom 15. August 2026, + zuletzt geändert durch die am 15. August 2026 vorgenommenen Änderungen +════════════════════════════════════════════════════════════════════════════════ + +Das Erzeugnis hat die mehrschrittige Umnummerierungssequenz bislang als eine ihm +wesenseigene Grenze behandelt und ihre Folgeänderungen dem Abschnitt „Manuell +prüfen“ zugewiesen. Die Prüfung des bayerischen Belegfalls (Art. 56 des +Bayerischen Jagdgesetzes, Neunummerierung des Bußgeldkatalogs durch § 1 Nr. 48 +des Gesetzes vom 31. März 2026) hat ergeben, dass es sich um keine Grenze der +Sache, sondern um drei voneinander unabhängige Mängel gehandelt hat. Sie werden +mit dieser Fassung behoben. + + +Artikel 1 +Ordnung der Anwendung nach Schritten + +(1) Der Anwender (BefehlAnwender) faltet die Befehlsliste vor der Anwendung zu + einer Liste von Schritten auf; ein Verbund (Sammelbefehl) liefert je + Teilbefehl einen Schritt, jeder andere Befehl einen einzigen. Geordnet + werden fortan die Schritte, protokolliert wird unverändert je Befehl. + +(2) Der Grund der Trennung ist folgender: Ein Verbund aus einer Umnummerierung + und einer Begleitänderung trägt zwei gegenläufige zeitliche Ansprüche. Die + Umnummerierung gehört vor denjenigen Befehl, der ihre Bezeichnung neu + besetzt; die Begleitänderung gehört an ihre Stelle im Dokument, denn sie + setzt die vorangegangenen Punkte als vollzogen voraus. Bislang setzte sich + der erste Anspruch für die ganze Einheit durch und zog die Begleitänderung + mit sich; sie traf dann auf einen Stand, den das Änderungsgesetz nicht + voraussetzt. + +(3) Die Ordnungsregel selbst bleibt unverändert („wer eine Bezeichnung räumt, + kommt vor demjenigen, der sie neu besetzt“), wird aber nunmehr vollständig + durchgesetzt: Vor jedem Schritt laufen erst diejenigen Schritte, die ihm + eine Bezeichnung räumen, und vor diesen wiederum die ihren. Eine ringförmige + Abhängigkeit bricht ab; die beteiligten Schritte behalten dann ihre + Dokumentreihenfolge. + + +Artikel 2 +Erkennung der abgekürzten lokativen Folgeklausel + +Das Muster der lokativen Klausel (BefehlErkenner) schloss mit einer Wortgrenze. +Hinter einem abgekürzten Bezeichnungswort steht jedoch bereits der +Abkürzungspunkt, und zwischen ihm und dem folgenden Leerzeichen liegt keine +Wortgrenze; der Klausel entging deshalb jede Kurzform („in Abs. 2 …“, „in +Buchst. b …“). An die Stelle der Wortgrenze tritt die Bedingung, dass kein +Buchstabe folgt. + + +Artikel 3 +Auflösung der Aufzählungseinheit ohne eigenen Text + +Der Stellenaufloeser (StellenAufloeser) verlangte hinter der Aufzählungsmarke +Text auf derselben Zeile. Eine Einheit, die sich vollständig in ihre +Untergliederung ergießt, führt ihre Marke aber allein auf der Zeile (Art. 56 +Abs. 2 Nr. 12 des Bayerischen Jagdgesetzes, darunter nur die Buchstaben a und +b); sie galt deshalb als nicht auffindbar. Der Text hinter der Marke ist fortan +wahlfrei. + + +Schlussbestimmung + +(1) Die vorstehenden Änderungen sind durch die Prüfung sämtlicher Testfälle + (dreihundertvier an der Zahl) sowie durch die vollständige Erstellung + (mvnw verify) bestätigt worden. + +(2) Der bayerische Belegfall weist nunmehr sämtliche einhundertvierundfünfzig + Anweisungen als angewandt aus (zuvor einhunderteinundfünfzig); die + Ausschussfassung der Beschlussempfehlung zum Gebäudeenergiegesetz weist + achtundsechzig statt siebenundsechzig aus. Die übrigen Bezugszahlen des + Bundes und der Länder sind unverändert geblieben. + +(3) Nicht behoben ist die Stellung eines eingefügten Blocks: Die Anweisung „Nach + Nr. 4 werden die folgenden Nrn. 5 bis 7 eingefügt“ (Art. 56 Abs. 1 des + Bayerischen Jagdgesetzes) wird angewandt, ihr Block tritt aber an das Ende + des Absatzes statt hinter die Nummer 4; ebenso verbleibt der leere + Platzhalter „7. (aufgehoben)“ der Altfassung zwischen den Nummern 9 und 10. + Beides bestand schon vor dieser Fassung und ist im Belegfall festgehalten. + +(4) Ferner ist festgehalten: Die Heilung doppelter Leerzeichen nach einer + Streichung (BefehlAnwender) wirkt auf den gesamten Zieltext und verkürzt + dabei die Einrückung der Aufzählungszeilen. Auch dies bestand schon zuvor. + + +════════════════════════════════════════════════════════════════════════════════ Fassung vom 14. August 2026, zuletzt geändert durch die am 14. August 2026 vorgenommenen Änderungen ════════════════════════════════════════════════════════════════════════════════ diff --git a/README.adoc b/README.adoc index 75b66f6..586b17f 100644 --- a/README.adoc +++ b/README.adoc @@ -131,12 +131,28 @@ deshalb vor den, der sie neu besetzt. Aus dieser einen Regel folgt die absteigende Abarbeitung einer aufsteigenden Kaskade („Der bisherige Absatz 3 wird Absatz 4“, „Der bisherige Absatz 4 wird Absatz 5“, …) ebenso wie der Vorrang einer Umnummerierung vor der Einfügung, die deren -Bezeichnung neu vergibt. Verschoben wird dabei nur nach vorn, sodass -jede Folgeänderung hinter der Umnummerierung bleibt, auf deren neue -Bezeichnung sie zeigt. Die PDF-Aufbereitung toleriert dabei -Drucksachen-Artefakte (Seitenköpfe und -füße, Vorabfassungs-Wasserzeichen, -vertauschte oder gerade Anführungszeichen, verklebte Wortgrenzen, zerlegt -kodierte Umlaute) und bestimmt die Brotschrift seitenweise, sodass auch +Bezeichnung neu vergibt. Wer vorrückt, nimmt dabei seine eigenen Räumer +mit: Auch eine über mehrere Punkte verschränkte Kaskade („Die bisherige +Nr. 15 wird Nr. 16“, „Die bisherigen Nrn. 13 und 14 werden die Nrn. 14 +und 15“, „Nach Nr. 12 wird folgende Nr. 13 eingefügt“) tritt deshalb +geschlossen vor die Einfügung, nicht bloß mit ihrem letzten Glied. + +Geordnet werden nicht die Befehle, sondern die einzelnen +Anwendungsschritte. Ein Verbund aus Umnummerierung und Folgeänderung +trägt nämlich zwei gegenläufige zeitliche Ansprüche: Die Umnummerierung +gehört nach vorn, die Folgeänderung dagegen an ihre Dokumentstelle, weil +sie die vorangegangenen Punkte als vollzogen voraussetzt — „Die +bisherige Nr. 11 wird Nr. 12 und die Angabe „schriftliche“ wird +gestrichen“ ist vorgezogen mehrdeutig, weil erst ein früherer Punkt die +zweite Fundstelle des Wortes beseitigt. Verschoben wird stets nur nach +vorn. Das Protokoll bleibt gleichwohl befehlsweise: Ein Verbund gilt nur +dann als angewandt, wenn jeder seiner Teile gegriffen hat, sonst nennt +die Meldung den Teil und den Grund. + +Die PDF-Aufbereitung toleriert Drucksachen-Artefakte (Seitenköpfe +und -füße, Vorabfassungs-Wasserzeichen, vertauschte oder gerade +Anführungszeichen, verklebte Wortgrenzen, zerlegt kodierte Umlaute) und +bestimmt die Brotschrift seitenweise, sodass auch Ministeriumsentwürfe mit gemischten Layouts vollständig extrahiert werden. Fehlt im amtlichen Satz ein schließendes Anführungszeichen, endet das Zitat an der nächsten Strukturgrenze — an einer Artikel-Überschrift @@ -161,7 +177,10 @@ wird aufgehoben“, „In Satz 1 wird die Satznummerierung „1“ gestrichen“ „Dem Wortlaut werden die folgenden Abs. 1 bis 4 vorangestellt“, „Der bisherige Wortlaut wird Abs. 5“, Halbsatz-Ziele, Klauselketten mit gemeinsamem Schlussverb) sowie das Fortführungszeichen des GVBl (jedes -neugefasste Aufzählungsglied öffnet erneut mit „). +neugefasste Aufzählungsglied öffnet erneut mit „). Der Fall ist gegen +die amtliche Nachfassung belegt: Alle 154 auf das BayJG zielenden +Befehle des Heftes 6/2026 werden an 54 Normen selbsttätig angewandt, +auch die verschränkte Neunummerierung des Bußgeldkatalogs in Art. 56. Die übrigen Länder gliedern ihre Stammgesetze wie der Bund in Paragraphen; die Unterschiede liegen im Gesetzblatt-Satz und in @@ -169,7 +188,7 @@ Befehlsidiomen. Belegt sind Sachsen (SächsBeamtVG), Niedersachsen (NEFG) und Nordrhein-Westfalen — dort alle vier ändernden Artikel eines Heftes: Telemedienzuständigkeitsgesetz, Landesmediengesetz, Ausführungsgesetz zum 17. Rundfunkänderungsstaatsvertrag und, mit 101 -Befehlen an 31 Normen der größte Landesrechtsfall, das WDR-Gesetz — +Befehlen an 31 Normen der größte Fall des Heftes, das WDR-Gesetz — mit Akzeptanztests gegen die amtlichen Nachfassungen; für Schleswig-Holstein, Berlin und Hessen reicht die Prüfung bis zur Befehlserkennung — dort vollständig, aber ohne 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))); + } } |
