diff options
Diffstat (limited to 'src')
3 files changed, 252 insertions, 35 deletions
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java index e931901..a46defb 100644 --- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java +++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java @@ -1001,6 +1001,19 @@ public final class BefehlAnwender { var stelle = befehl.stelle(); if (stelle.betrifftUeberschrift()) { + // Die Neufassung einer Überschrift trifft den Titel der Norm. Nennt die Stelle eine feinere + // Einheit („die Überschrift der Nummer 1 der Anlage 8“), so ist deren Kopfzeile gemeint — + // und die trägt außer der Überschrift auch die Aufzählungsmarke, die eine Neufassung nicht + // mitliefert. Was hier zu setzen wäre, steht deshalb nicht fest; gerügt wird es, statt die + // feinere Angabe zu verwerfen und den Normtitel zu überschreiben. + if (nenntFeinereEinheit(stelle)) { + return manuell( + befehl, + Grund.NICHT_UNTERSTUETZT, + "Die Neufassung der Überschrift von „" + + stelle.ohne(new Stelle.Ueberschrift()).anzeigeText() + + "“ ist nicht umgesetzt: Deren Kopfzeile trägt zugleich die Aufzählungsmarke."); + } var aufloesung = loeseNormAuf(normen, stelle); if (aufloesung.fehler() != null) { return manuell(befehl, aufloesung.grund(), aufloesung.fehler()); @@ -2145,23 +2158,14 @@ public final class BefehlAnwender { */ private static AngewandteAenderung bearbeiteText( List<Norm> normen, Aenderungsbefehl befehl, TextOperation operation) { - // „In der Überschrift …“: die Operation wirkt auf den Titel der Norm, nicht auf ihren Text. + // „In der Überschrift …“ meint nicht immer den Titel der Norm. Nennt die Stelle eine feinere + // Einheit („In Anlage 8 Nummer 1 werden in der Überschrift die Wörter … gestrichen“), so ist + // die Überschrift jener Einheit gemeint — im gii-XML die Kopfzeile ihres Aufzählungsblocks. + // Zuvor verwarf dieser Zweig die feinere Angabe schweigend und griff auf den Normtitel; stand + // der Zieltext zufällig dort, so wurde die falsche Einheit geändert, ohne dass es jemand + // erfuhr. if (befehl.stelle().betrifftUeberschrift()) { - var aufloesung = loeseNormAuf(normen, befehl.stelle()); - if (aufloesung.fehler() != null) { - return manuell(befehl, aufloesung.grund(), aufloesung.fehler()); - } - var norm = normen.get(aufloesung.normIndex()); - if (norm.titel() == null) { - return manuell( - befehl, Grund.BESTAND_WIDERSPRICHT, norm.enbez() + " hat keine Überschrift."); - } - var titelErgebnis = operation.wende(norm.titel()); - if (titelErgebnis.fehler() != null) { - return manuell(befehl, titelErgebnis.grund(), titelErgebnis.fehler()); - } - normen.set(aufloesung.normIndex(), norm.mitTitel(titelErgebnis.text())); - return angewandt(befehl, norm.enbez()); + return bearbeiteUeberschrift(normen, befehl, operation); } var ergebnis = StellenAufloeser.aufloese(gesetzAus(normen), befehl.stelle()); @@ -2213,6 +2217,98 @@ public final class BefehlAnwender { return angewandt(befehl, norm.enbez()); } + /** + * Wendet eine Textoperation auf eine Überschrift an. Welche Überschrift gemeint ist, entscheidet + * nicht der Wortlaut des Befehls, sondern die Auflösung der Stelle ohne die + * Überschrift-Komponente: + * + * <ul> + * <li>Löst sie auf eine ganze Norm auf, so ist deren Titel gemeint — der Regelfall („In der + * Überschrift des § 5 …“), und ebenso dort, wo die Nummer einer Anlage eine eigene Norm ist + * (Landesrecht seit Welle 20c). + * <li>Löst sie auf einen Textbereich auf, so ist dessen <em>Kopfzeile</em> gemeint. Im gii-XML + * des Bundes stehen die Nummern einer Anlage als Aufzählungsmarken im Wortlaut, und die + * Kopfzeile trägt Marke und Überschrift („1. Wärmedämmung von … in den Fällen des § 69 und + * § 71 Absatz 1“); die Kindzeilen sind ihr Inhalt. + * </ul> + * + * <p>Ein Block aus einer einzigen Zeile hat keine Überschrift, sondern nur Text. Dort wird nichts + * angewandt, sondern gerügt: Andernfalls träfe die Operation den ganzen Wortlaut und gäbe vor, + * eine Überschrift geändert zu haben. + */ + private static AngewandteAenderung bearbeiteUeberschrift( + List<Norm> normen, Aenderungsbefehl befehl, TextOperation operation) { + var ohneUeberschrift = befehl.stelle().ohne(new Stelle.Ueberschrift()); + var ergebnis = StellenAufloeser.aufloese(gesetzAus(normen), ohneUeberschrift); + if (ergebnis instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) { + return manuell(befehl, nicht.grund(), nicht.begruendung()); + } + var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle(); + var norm = normen.get(fundstelle.normIndex()); + + if (fundstelle.bereich() == null) { + if (norm.titel() == null) { + return manuell( + befehl, Grund.BESTAND_WIDERSPRICHT, norm.enbez() + " hat keine Überschrift."); + } + var titelErgebnis = operation.wende(norm.titel()); + if (titelErgebnis.fehler() != null) { + return manuell(befehl, titelErgebnis.grund(), titelErgebnis.fehler()); + } + normen.set(fundstelle.normIndex(), norm.mitTitel(titelErgebnis.text())); + return angewandt(befehl, norm.enbez()); + } + + var absaetze = new ArrayList<>(norm.absaetze()); + var absatz = absaetze.get(fundstelle.absatzIndex()); + var kopfzeile = kopfzeileVon(absatz.text(), fundstelle.bereich()); + if (kopfzeile == null) { + return manuell( + befehl, + Grund.STELLE_NICHT_AUFLOESBAR, + "„" + ohneUeberschrift.anzeigeText() + "“ trägt keine eigene Überschrift."); + } + var kopfErgebnis = operation.wende(absatz.text().substring(kopfzeile.von(), kopfzeile.bis())); + if (kopfErgebnis.fehler() != null) { + return manuell(befehl, kopfErgebnis.grund(), kopfErgebnis.fehler()); + } + var neuerText = + absatz.text().substring(0, kopfzeile.von()) + + kopfErgebnis.text() + + absatz.text().substring(kopfzeile.bis()); + absaetze.set(fundstelle.absatzIndex(), absatz.mitText(neuerText)); + normen.set(fundstelle.normIndex(), norm.mitAbsaetzen(absaetze)); + return angewandt(befehl, norm.enbez()); + } + + /** + * Nennt die Stelle eine Einheit unterhalb der Norm — einen Absatz, einen Satz, eine Nummer oder + * einen Buchstaben? Nur dann meint „die Überschrift“ etwas anderes als den Titel der Norm. + */ + private static boolean nenntFeinereEinheit(Stelle stelle) { + return stelle.komponenten().stream() + .anyMatch( + k -> + k instanceof Stelle.AbsatzNr + || k instanceof Stelle.SatzNr + || k instanceof Stelle.HalbsatzNr + || k instanceof Stelle.NummerNr + || k instanceof Stelle.BuchstabeNr); + } + + /** + * Die erste Zeile eines Textbereichs, sofern ihr wenigstens eine weitere folgt; sonst {@code + * null}. Der Zeilenumbruch selbst bleibt außerhalb. + */ + private static SatzTeiler.@Nullable SatzBereich kopfzeileVon( + String text, SatzTeiler.SatzBereich bereich) { + int umbruch = text.indexOf('\n', bereich.von()); + if (umbruch < 0 || umbruch >= bereich.bis()) { + return null; + } + return new SatzTeiler.SatzBereich(bereich.von(), umbruch); + } + /** Wie {@link #bearbeiteText}, aber die Operation braucht den konkreten Zeichenbereich. */ private static AngewandteAenderung bearbeiteBereich( List<Norm> normen, Aenderungsbefehl befehl, BereichsOperation operation) { diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java index 91a0cca..7a6dcee 100644 --- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java +++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java @@ -261,16 +261,17 @@ class EndToEndTest { * auch eine Aufhebung als Räumende zählt, läuft sie vor der Umnummerierung, und § 108 Absatz 1 * trägt danach die lückenlose Folge der Nummern 1 bis 32. * - * <ol> - * <li>{@code 40. a) hh)} „Die bisherige Nummer 18 wird Nummer 29 und nach der Angabe ‚Absatz 1‘ - * werden die Wörter ‚oder Absatz 4‘ eingefügt.“ — Die Begleitklausel löst norm-weit auf - * (bewusst so, siehe {@code BefehlErkenner}) und trifft dort 21 Vorkommen. Gemeint ist die - * soeben umnummerierte Einheit. - * <li>{@code 43. b) aa)} „In der Überschrift werden die Wörter … gestrichen.“ — Ziel ist die - * Überschrift der <em>Nummer 1 der Anlage 8</em>. Das ist dieselbe offene Modellfrage wie - * bei Berlins Anlage: Die Nummern einer Anlage brauchen einen eigenen Rang zwischen Norm - * und Absatz. - * </ol> + * <p>Auch {@code 43. b) aa)} ist keiner mehr („In der Überschrift werden die Wörter … in den + * Fällen des § 69 und § 71 Absatz 1 … gestrichen.“). Ziel ist die Überschrift der <em>Nummer 1 + * der Anlage 8</em>, und es brauchte dafür keinen eigenen Rang zwischen Norm und Absatz: Im + * gii-XML ist die Überschrift einer Anlagen-Nummer die Kopfzeile ihres Aufzählungsblocks. Der + * Fehler saß darin, dass der Überschrift-Zweig die feinere Angabe schweigend verwarf und stets + * auf den Normtitel griff. + * + * <p>Ein Rest bleibt: {@code 40. a) hh)} „Die bisherige Nummer 18 wird Nummer 29 und nach der + * Angabe ‚Absatz 1‘ werden die Wörter ‚oder Absatz 4‘ eingefügt.“ — Die Begleitklausel löst + * norm-weit auf (bewusst so, siehe {@code BefehlErkenner}) und trifft dort 21 Vorkommen. Gemeint + * ist die soeben umnummerierte Einheit. */ @Test void gegGegenZeitrichtigenStamm() throws Exception { @@ -292,8 +293,8 @@ class EndToEndTest { .filter(a -> a.status() == BefehlAnwender.Status.MANUELL_PRUEFEN) .map(a -> a.befehl().provenienz().gliederungsPfad()) .toList(); - assertThat(manuellPfade).containsExactly("40. a) hh)", "43. b) aa)"); - assertThat(anwendung.anzahlAngewandt()).isEqualTo(117); + assertThat(manuellPfade).containsExactly("40. a) hh)"); + assertThat(anwendung.anzahlAngewandt()).isEqualTo(118); // Die Kaskade des § 108 Absatz 1 geht lückenlos auf: 32 Nummern, keine doppelt, keine fehlend. var nummern = @@ -311,6 +312,15 @@ class EndToEndTest { assertThat(nummern).hasSize(32).endsWith("32"); var neu = anwendung.neu(); + // Die Überschrift der Nummer 1 der Anlage 8 ist geändert — und nur sie: Der Titel der Anlage + // trägt seinen eigenen Wortlaut unversehrt, den Punkt 43. a) ihm gegeben hat. + var anlage8 = neu.norm("Anlage 8").orElseThrow(); + assertThat(anlage8.gesamtText()) + .contains("1. Wärmedämmung von Wärmeverteilungs- und Warmwasserleitungen sowie Armaturen") + .doesNotContain("sowie Armaturen in den Fällen des § 69 und § 71 Absatz 1"); + assertThat(anlage8.titel()) + .contains("Anforderungen an die Wärmedämmung von Rohrleitungen und Armaturen"); + // Der neue § 9a steht im Gesetz und in der Inhaltsübersicht. assertThat(neu.norm("§ 9a")).isPresent(); assertThat(neu.norm("Inhaltsübersicht").orElseThrow().gesamtText()) @@ -1702,8 +1712,8 @@ class EndToEndTest { /** * Dasselbe am großen Fall, und zugleich die Probe auf die Schritt-Ordnung: Im GEG bleibt am 1. * Januar 2024 allein der Befehl der Nummer 22 zurück. Er steht in keiner Umnummerierungs-Kaskade, - * sodass die übrigen 115 unverändert durchlaufen — die drei liegengebliebenen Befehle sind - * dieselben wie ohne Stichtag und werden von der Auswahl nicht vermehrt. + * sodass die übrigen 116 unverändert durchlaufen — die liegengebliebenen Befehle sind dieselben + * wie ohne Stichtag und werden von der Auswahl nicht vermehrt. */ @Test void gegStichtagLaesstDieUebrigenBefehleUnberuehrt() throws Exception { @@ -1712,14 +1722,14 @@ class EndToEndTest { assumeTrue(Files.exists(xml) && Files.exists(pdf), "GEG-Beispieldaten fehlen"); var vollstaendig = Pipeline.erzeugeSynopse(Pipeline.Auftrag.von(xml, List.of(pdf))); - assertThat(vollstaendig.anzahlAngewandt()).isEqualTo(117); - assertThat(vollstaendig.anzahlManuell()).isEqualTo(2); + assertThat(vollstaendig.anzahlAngewandt()).isEqualTo(118); + assertThat(vollstaendig.anzahlManuell()).isEqualTo(1); var anfang2024 = Pipeline.erzeugeSynopse( Pipeline.Auftrag.von(xml, List.of(pdf)).mitStichtag(LocalDate.of(2024, 1, 1))); - assertThat(anfang2024.anzahlAngewandt()).isEqualTo(116); - assertThat(anfang2024.anzahlManuell()).isEqualTo(2); + assertThat(anfang2024.anzahlAngewandt()).isEqualTo(117); + assertThat(anfang2024.anzahlManuell()).isEqualTo(1); assertThat(anfang2024.html()) .contains("Am Stichtag noch nicht in Kraft") .contains("Tritt erst am 1. Oktober 2024 in Kraft"); @@ -1727,7 +1737,7 @@ class EndToEndTest { var oktober2024 = Pipeline.erzeugeSynopse( Pipeline.Auftrag.von(xml, List.of(pdf)).mitStichtag(LocalDate.of(2024, 10, 1))); - assertThat(oktober2024.anzahlAngewandt()).isEqualTo(117); + assertThat(oktober2024.anzahlAngewandt()).isEqualTo(118); } /** diff --git a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java index 629ec49..0e186ad 100644 --- a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java +++ b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java @@ -1666,4 +1666,115 @@ class BefehlAnwenderTest { assertThat(ergebnis.anzahlManuell()).isEqualTo(1); assertThat(ergebnis.protokoll().get(0).begruendung()).contains("nicht so viele Einheiten"); } + + /** + * „In der Überschrift der Nummer 1 …“ meint die Kopfzeile jenes Aufzählungsblocks, nicht den + * Titel der Norm. Zuvor verwarf der Überschrift-Zweig die Nummer schweigend und griff auf den + * Normtitel — stand der Zieltext zufällig dort, so änderte er die falsche Einheit. + */ + @Test + void ueberschriftEinerNummerIstDieKopfzeileIhresBlocks() { + var anlage = + new Norm( + "Anlage 8", + "Anforderungen an die Wärmedämmung in den Fällen des § 69", + null, + List.of( + new Absatz( + null, + "1. Wärmedämmung von Leitungen in den Fällen des § 69\n" + + " a) Leitungen sind in den Fällen des § 69 zu dämmen.\n" + + "2. Sonstiges\n" + + " a) Im Übrigen gilt nichts.")), + false); + var gesetz = new Gesetz("TestG", "Testgesetz", "TestG", List.of(anlage)); + + var befehl = + new Streichung( + stelle( + new Stelle.Gliederungseinheit("Anlage", "8"), + new Stelle.NummerNr("1"), + new Stelle.Ueberschrift()), + " in den Fällen des § 69", + PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz, List.of(befehl)); + assertThat(ergebnis.protokoll()).allMatch(a -> a.status() == Status.ANGEWANDT); + + var text = ergebnis.neu().norm("Anlage 8").orElseThrow(); + // Die Kopfzeile ist gestutzt … + assertThat(text.absaetze().get(0).text()).startsWith("1. Wärmedämmung von Leitungen\n"); + // … der Buchstabe a) darunter und der Titel der Anlage dagegen unberührt: Beide führen + // denselben Wortlaut, und eine norm-weite Streichung hätte sie mitgenommen. + assertThat(text.absaetze().get(0).text()) + .contains("a) Leitungen sind in den Fällen des § 69 zu dämmen."); + assertThat(text.titel()).isEqualTo("Anforderungen an die Wärmedämmung in den Fällen des § 69"); + } + + /** + * Ein Aufzählungsglied aus einer einzigen Zeile hat keine Überschrift, sondern nur Text. Dort + * wird nichts angewandt, sondern gerügt: Andernfalls träfe die Operation den ganzen Wortlaut und + * gäbe vor, eine Überschrift geändert zu haben. + */ + @Test + void einzeiligesAufzaehlungsgliedTraegtKeineUeberschrift() { + var anlage = + new Norm( + "Anlage 8", + "Anforderungen", + null, + List.of(new Absatz(null, "1. Kurz und bündig\n2. Ebenso kurz")), + false); + var gesetz = new Gesetz("TestG", "Testgesetz", "TestG", List.of(anlage)); + + var befehl = + new Streichung( + stelle( + new Stelle.Gliederungseinheit("Anlage", "8"), + new Stelle.NummerNr("1"), + new Stelle.Ueberschrift()), + " und bündig", + PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz, List.of(befehl)); + assertThat(ergebnis.protokoll()).hasSize(1); + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.MANUELL_PRUEFEN); + assertThat(ergebnis.protokoll().get(0).grund()).isEqualTo(Grund.STELLE_NICHT_AUFLOESBAR); + assertThat(ergebnis.protokoll().get(0).begruendung()) + .contains("trägt keine eigene Überschrift"); + assertThat(ergebnis.neu().norm("Anlage 8").orElseThrow().absaetze().get(0).text()) + .isEqualTo("1. Kurz und bündig\n2. Ebenso kurz"); + } + + /** + * Die Neufassung der Überschrift einer Aufzählungseinheit bleibt eine bewusst gezogene Grenze: + * Deren Kopfzeile trägt zugleich die Aufzählungsmarke, die eine Neufassung nicht mitliefert. + * Gerügt wird sie deshalb — nicht auf den Normtitel umgebogen. + */ + @Test + void neufassungDerUeberschriftEinerNummerWirdGeruegt() { + var anlage = + new Norm( + "Anlage 8", + "Anforderungen", + null, + List.of(new Absatz(null, "1. Wärmedämmung\n a) Näheres.")), + false); + + var gesetz = new Gesetz("TestG", "Testgesetz", "TestG", List.of(anlage)); + + var befehl = + new Neufassung( + stelle( + new Stelle.Gliederungseinheit("Anlage", "8"), + new Stelle.NummerNr("1"), + new Stelle.Ueberschrift()), + "Wärmedämmung und Kältedämmung", + 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.NICHT_UNTERSTUETZT); + assertThat(ergebnis.neu().norm("Anlage 8").orElseThrow().titel()).isEqualTo("Anforderungen"); + } } |
