diff options
| author | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2026-08-26 08:24:40 +0200 |
|---|---|---|
| committer | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2026-08-26 08:24:40 +0200 |
| commit | 916dc34893dd9b3d62e7c65566ef14a5545ae4b2 (patch) | |
| tree | fea7ddcad57019a9d39f592e7e99dd01cdd2d7bc /src/test/java | |
| parent | 5efff363d45cf03f334bf10145367e7bc43d2754 (diff) | |
Hamburg zählt in Dezimalen und gliedert sein Heft in Paragraphen
Das hamburgische Gesetzblatt bringt zwei Gliederungen, die im übrigen Landesrecht
nicht vorkommen. Die erste: Ein Sammelheft trägt Verkündungen beider Art
nebeneinander — die eine teilt sich in Artikel, die nächste in Paragraphen. Die
§-Teilung erst dann zu versuchen, wenn ein Heft überhaupt keinen Artikel führt, war
darum zu grob; sie ist auch dann zu versuchen, wenn kein Artikel das Stammgesetz
betrifft. Die zweite: Die Befehle sind dezimal gegliedert (6.1, 7.1.1, 7.2.3), wo
die übrigen Blätter a)/aa) setzen. Die Ebene steht dabei in der Zahl selbst, und
weil das Label seine Herkunft schon trägt, wiederholt der Gliederungspfad sie
nicht.
Dazu drei Funde am Satz des Blattes: Es setzt zwischen Paragraphenzeichen und
Nummer ein schmales Leerzeichen, das Javas \s nicht kennt — ohne dessen
Normalisierung ist „§ 1“ dort kein Normkopf, und das ganze Heft bliebe ungelesen.
Es führt seinen Kolumnentitel im Inhaltsstrom, wo er hinter das Zitat eines
Befehls fällt. Und es lässt das Schlusswort der Eingangsformel in die rechte
Spalte fallen, mitten in einen Absatz; die Spalte folgt deshalb fortan ihrer
Grundlinie statt dem Strom — stabil sortiert, im Regelfall folgenlos.
Drei Funde an der Anwendung: Ein Satzzeichen, das an die Stelle eines Wortes
tritt, nimmt dessen Zwischenraum mit (die Umkehrung der schon geregelten Fuge).
Ein Satz, der an eine Aufzählung angefügt wird, gehört dem Absatz und nicht dem
letzten Glied — er tritt auf eine eigene Zeile, und zwar in der Einrückung des
Blockes, denn bündig gesetzt beendete er ihn. Und eine Neufassung, deren
Bezeichnung eine aufsteigende Umnummerierung räumt, tritt hinter diese zurück:
Sonst verlöre die bisherige Nummer 3 ihren Wortlaut und die Umnummerierung
benennte alsdann die neue — ein stiller Verlust.
Zwei Befehle des Heftes bleiben unerkannt, und das ist richtig so: Sie schreiben
„wir die Textstelle … ersetzt“ statt „wird“. Ein Werkzeug, das das errät, läse
morgen auch anderes, was dort nicht steht.
Geprüft: mvnw verify, 425 Tests (zuvor 415), reuse lint 200/200. Hamburg: 21
Befehle gelesen, 17 angewandt, 10 von 14 Normen gleich der amtlichen Nachfassung;
drei der vier Abweichungen liegen an der Vorlage.
Change-Id: Ibc61b4a38d5df5604b8c8efe2d69b7b6d27512d6
Diffstat (limited to 'src/test/java')
| -rw-r--r-- | src/test/java/eu/mulk/aendggner/EndToEndTest.java | 78 | ||||
| -rw-r--r-- | src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java | 88 |
2 files changed, 166 insertions, 0 deletions
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java index 8c9743d..bb67978 100644 --- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java +++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java @@ -1700,6 +1700,84 @@ class EndToEndTest { .containsOnlyOnce("Fortgeschrieben durch: Änderungsgesetz vom 12. Februar 2026"); } + /** + * Hamburg: Die Verordnung zur Änderung der Verordnung über den Gutachterausschuss für + * Grundstückswerte (HmbGVBl. Nr. 17/2026, S. 147). + * + * <p>Der Fall bringt eine Gliederung, die im übrigen Landesrecht nicht vorkommt: Das + * Änderungsdokument selbst teilt sich in <em>Paragraphen</em> (§ 1 Änderung, § 2 Inkrafttreten), + * obgleich dasselbe Heft davor eine Verkündung führt, die sich in Artikel teilt; und seine + * Befehle sind <em>dezimal</em> gegliedert (6.1, 7.1.1, 7.2.3) statt a)/aa)/aaa). Dazu vier + * Idiome — „die Textstelle“, „Hinter § 7 wird … eingefügt“, „Es wird folgender Absatz angefügt“, + * „§§ 6 und 7 erhalten folgende Fassung“ — und ein schmales Leerzeichen zwischen + * Paragraphenzeichen und Nummer. + * + * <p>Vier Normen weichen von der amtlichen Nachfassung ab; drei davon liegen an der Vorlage und + * nicht am Werkzeug (siehe {@code Hamburg/SOURCES}). + */ + @Test + void gutachterausschussVOHamburg() throws Exception { + var alt = SAMPLEDATA.resolve("Hamburg/GAusschV-HH-alt.txt"); + var pdf = SAMPLEDATA.resolve("Hamburg/HmbGVBl-2026-17_GutachterausschussVO-AendVO.pdf"); + var neu = SAMPLEDATA.resolve("Hamburg/GAusschV-HH-neu.txt"); + assumeTrue(Files.exists(alt) && Files.exists(pdf), "Hamburger Beispieldaten fehlen"); + + var gesetz = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader().load(alt); + assertThat(gesetz.jurabk()).isEqualTo("GAusschV HA 2009"); + assertThat(gesetz.normen()).hasSize(13); + + var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf)); + // Das schmale Leerzeichen (U+2009) zwischen § und Nummer ist normalisiert; ohne das wäre + // „§ 1“ kein Kopf, und das ganze Heft bliebe ungelesen. + assertThat(text).contains("\n§ 1\n").doesNotContain("§\u2009"); + // Der Kolumnentitel steht im Inhaltsstrom und fiele sonst hinter das Zitat eines Befehls. + assertThat(text).doesNotContain("Dienstag, den 26. Mai 2026 149"); + + var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null); + // Der Artikel-Modus findet nichts, das die Verordnung beträfe; erst die §-Teilung tut es. + assertThat(parseErgebnis.artikel()).containsExactly("1"); + assertThat(parseErgebnis.befehle()).hasSize(21); + + // Die Dezimalgliederung trägt ihre Herkunft im Label; der Pfad wiederholt sie nicht. + assertThat(befehlAn(parseErgebnis, "7.1.4")) + .isInstanceOf(Aenderungsbefehl.Umnummerierung.class); + assertThat(befehlAn(parseErgebnis, "8.1")).isInstanceOf(Aenderungsbefehl.Neufassung.class); + + // Zwei Befehle bleiben unerkannt, und zwar zu Recht: Die Nummern 2 und 3 des Heftes schreiben + // „wir die Textstelle … ersetzt“ statt „wird“. Das ist ein Verkündungsfehler, und geraten wird + // nicht. + var unerkannt = + parseErgebnis.befehle().stream() + .filter(b -> b instanceof UnbekannterBefehl) + .map(b -> b.provenienz().gliederungsPfad()) + .toList(); + assertThat(unerkannt).containsExactly("2.", "3."); + + var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle()); + assertThat(anwendung.anzahlAngewandt()).isEqualTo(17); + + // Der angefügte Schlusssatz steht hinter der Aufzählung auf eigener Zeile und nicht am Ende + // des letzten Aufzählungsgliedes. + var neunter = anwendung.neu().norm("§ 9").orElseThrow().absaetze().get(0).text(); + assertThat(neunter) + .contains("\nDie DIN EN ISO/IEC 17024 ist zur kostenfreien Einsicht") + // Das Satzzeichen, das an die Stelle eines Wortes tritt, nimmt dessen Zwischenraum mit. + .contains("zur Erfüllung ihrer Aufgaben,\n") + // Die bisherige Nummer 3 ist erhalten: Die Umnummerierung geht der Neufassung vor, die + // ihre Bezeichnung neu vergäbe — sonst ginge jener Wortlaut verloren. + .contains("4. weitere Personen und Stellen"); + + assumeTrue(Files.exists(neu), "Hamburger Nachfassung fehlt"); + var soll = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader().load(neu); + var abgleich = Nachfassungsabgleich.vergleiche(soll, anwendung.neu()); + assertThat(abgleich.fehlende()).isEmpty(); + assertThat(abgleich.ueberzaehlige()).isEmpty(); + // § 3 und § 4: der Verkündungsfehler „wir“. § 6: das Gesetzblatt setzt „1. in Präsenz“ ohne + // Komma, das Portal mit. § 9: die Neufassung der soeben geräumten Nummer 3. + assertThat(abgleich.abweichungen().stream().map(a -> a.enbez()).toList()) + .containsExactly("§ 3", "§ 4", "§ 6", "§ 9"); + } + private static Aenderungsbefehl befehlZu( AenderungsgesetzParser.ParseErgebnis ergebnis, String gliederungsPfad) { return ergebnis.befehle().stream() diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java index 327c8db..4c55154 100644 --- a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java +++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java @@ -1544,4 +1544,92 @@ class BefehlErkennerTest { Stelle.LEER); assertThat(befehl).containsInstanceOf(Ersetzung.class); } + + // --- Hamburger Idiome (HmbGVBl. Nr. 17/2026) -------------------------------------------------- + + /** „Hinter“ steht dem „nach“ gleich; das hamburgische Blatt setzt es durchweg. */ + @Test + void erkenntEinfuegungHinterEinerNorm() { + var befehl = erkenne("Hinter § 7 wird folgender § 7a eingefügt: „§ 7a Aufgaben“", Stelle.LEER); + + assertThat(befehl).containsInstanceOf(StrukturEinfuegung.class); + var einfuegung = (StrukturEinfuegung) befehl.orElseThrow(); + assertThat(einfuegung.vorher()).isFalse(); + assertThat(einfuegung.stelle().anzeigeText()).isEqualTo("§ 7"); + assertThat(einfuegung.bezeichnung()).isEqualTo("7a"); + } + + @Test + void erkenntEinfuegungHinterEinemAbsatz() { + var befehl = + erkenne( + "Hinter Absatz 3 wird folgender neuer Absatz 4 eingefügt: „(4) Nicht" + + " grundstücksbezogene Auskünfte sind zu erteilen.“", + PARAGRAPH_5); + + assertThat(befehl).containsInstanceOf(StrukturEinfuegung.class); + var einfuegung = (StrukturEinfuegung) befehl.orElseThrow(); + assertThat(einfuegung.vorher()).isFalse(); + assertThat(einfuegung.bezeichnung()).isEqualTo("4"); + } + + /** Die Anfügung ohne Subjekt: Das Ziel steht im Rahmen, der Satz nennt nur das Angefügte. */ + @Test + void erkenntAnfuegungOhneSubjekt() { + var befehl = + erkenne( + "Es wird folgender Absatz 3 angefügt: „(3) Personenbezogene Daten sind zu löschen.“", + PARAGRAPH_5); + + assertThat(befehl).isPresent(); + assertThat(befehl.orElseThrow().stelle().anzeigeText()).isEqualTo("§ 5"); + } + + @Test + void erkenntSatzAnfuegungOhneSubjekt() { + var befehl = + erkenne("Es wird folgender Satz angefügt: „Die Norm ist niedergelegt.“", PARAGRAPH_5); + + assertThat(befehl).isPresent(); + } + + /** „Textstelle“ ist die hamburgische Nebenform zu „Wörter“ und „Angabe“. */ + @Test + void erkenntTextstellenErsetzung() { + var befehl = + erkenne( + "In § 3 Absatz 2 Nummer 1 wird die Textstelle „Absatz 5“ durch die Textstelle" + + " „Absatz 8“ ersetzt.", + Stelle.LEER); + + assertThat(befehl).containsInstanceOf(Ersetzung.class); + var ersetzung = (Ersetzung) befehl.orElseThrow(); + assertThat(ersetzung.alt()).isEqualTo("Absatz 5"); + assertThat(ersetzung.neu()).isEqualTo("Absatz 8"); + } + + @Test + void erkenntEinfuegungHinterEinerTextstelle() { + var befehl = + erkenne( + "In Satz 3 wird hinter der Textstelle „Sachverständige nach Absatz 1 Nummer 2“ die" + + " Textstelle „und 3“ eingefügt.", + PARAGRAPH_5); + + assertThat(befehl).containsInstanceOf(WoerterEinfuegung.class); + var einfuegung = (WoerterEinfuegung) befehl.orElseThrow(); + assertThat(einfuegung.woerter()).isEqualTo("und 3"); + } + + /** „§§ 6 und 7 erhalten folgende Fassung“ — die Neufassung einer Paragraphenfolge. */ + @Test + void erkenntNeufassungMehrererParagraphen() { + var befehl = + erkenne( + "§§ 6 und 7 erhalten folgende Fassung: „§ 6 Verfahren (1) Beraten wird gemeinsam." + + " § 7 Aufgaben Das vorsitzende Mitglied ist verantwortlich.“", + Stelle.LEER); + + assertThat(befehl).isPresent(); + } } |
