aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java128
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java48
-rw-r--r--src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java111
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");
+ }
}