aboutsummaryrefslogtreecommitdiff
path: root/src/test
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-24 19:57:07 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-24 19:57:07 +0200
commit3d40c583308d3f26501a53547e6294dc2db20854 (patch)
tree67ecbb6b907b34837d50f88d3f141d5cc3c9ec82 /src/test
parent9feaf3b9eebd79a37a252c2ffdf241f4190b2637 (diff)
Gelesen wird, was im Satz steht, nicht was zuerst gezeichnet wurde
Das Berliner Gesetz- und Verordnungsblatt zeichnet den ganzseitenbreiten Titelblock des Gesetzes zuletzt — nach beiden Spalten, obgleich er über ihnen steht. Im Inhaltsstrom stand er damit mitten in einem Zitat, das über den Seitenwechsel läuft: „… zur Sicherung des Be-“, Titelblock, „triebs von Unterkünften …“. Der Wortlaut der Anlage Nummer 31 trug den Titel des Änderungsgesetzes in sich, und die Nummer wich als einzige aus einem Grunde von der amtlichen Nachfassung ab, der weder in der Erkennung noch in der Anwendung lag. Maßgeblich ist fortan das Satzbild in der einfachen Gestalt, die die Gesetzblätter durchweg haben: zwei Spalten, dazwischen eine Rinne, und darüber oder dazwischen einzelne ganzseitenbreite Zeilen. Gesucht wird die Rinne — die senkrechte Linie, die möglichst wenige Zeilen überschreiten, die nahe der Mitte des Satzspiegels liegt und zu deren beiden Seiten je eine Spalte von nennenswerter Breite steht. Die wenigen Zeilen, die sie gleichwohl überschreiten, sind die breiten; sie zerlegen die Seite in Bänder. Gelesen wird Band für Band von oben nach unten, in jedem Band erst die linke, dann die rechte Spalte. Der übliche XY-Schnitt der Literatur leistet das nicht, und der erste Versuch danach hat es bewiesen: Teilt man erst waagerecht am weitesten Weißraumband, so zerfällt eine zweispaltige Seite mit Kolumnentitel in oben und unten, ehe sie in links und rechts zerfällt; gelesen wird links oben, rechts oben, links unten, rechts unten. Im Infektionsschutzgesetz zerriss das eine Aufzählung mitten entzwei und ließ von fünfundsiebzig Befehlen einundvierzig übrig. Die Spalte hat deshalb den Vorrang vor dem Band. Zwei Vorsichtsmaßregeln begrenzen den Eingriff, der sonst jeden Extraktionslauf berührte: Innerhalb einer Spalte bleibt es bei der Reihenfolge des Inhaltsstroms, und findet sich keine Rinne, so bleibt die Seite unangetastet. Der Eingriff kann Spalten und breite Zeilen gegeneinander versetzen, niemals aber den Satz einer Spalte durcheinanderbringen, deren Strom schon stimmte. Ausgezählt über den ganzen Beispielbestand ändern fünfzehn der fünfundvierzig PDF-Dateien ihren Auszug; nachgesehen sind sie sämtlich, und wo sie den Fließtext betreffen, rücken sie einen Vorspann an die Stelle, an der er im Satz steht (Berlin, Thüringen, die Beispielsynopse zum Produkthaftungsrecht). Die Anlage Nummer 31 gleicht nunmehr der amtlichen Nachfassung; es verbleiben zwei benannte Abweichungen, keine davon in der Anwendung begründet. Alle dreihundertneununddreißig Prüfungen laufen durch, darunter jede gepinnte Zahl des Bestandes; REUSE meldet 176/176. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Change-Id: I80f100bba2b8e48d0d292fb310f4a9c98fc519ec
Diffstat (limited to 'src/test')
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java30
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/LesereihenfolgeTest.java114
-rw-r--r--src/test/resources/sampledata/Landesrecht-Beispiele.adoc29
3 files changed, 139 insertions, 34 deletions
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index fe98b84..d9f36d7 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -1034,7 +1034,7 @@ class EndToEndTest {
.containsExactly(null, "1", "2", "3", "4", "4a", "5", "6", "7", "8", "9", "10");
// Norm für Norm gegen die amtliche Nachfassung vom 12. Juni 2026. Es bleiben genau zwei
- // benannte Abweichungen — beide nicht in der Anwendung begründet:
+ // benannte Abweichungen — keine davon in der Anwendung begründet:
var sollAsog = SAMPLEDATA.resolve("Berlin/ASOG-Bln-neu.txt");
assumeTrue(Files.exists(sollAsog), "ASOG-Nachfassung fehlt");
var soll = new eu.mulk.aendggner.gesetz.land.LandesRechtLoader().load(sollAsog);
@@ -1049,27 +1049,23 @@ class EndToEndTest {
abweichend.add(normSoll.enbez());
}
}
- // Drei benannte Abweichungen, keine davon in der Anwendung begründet:
+ // Zwei benannte Abweichungen, keine davon in der Anwendung begründet:
//
// § 67 — Das Portal setzt in der neuen Fassung amtliche Satznummern („(2) ¹Gegen einen …“);
// das Gesetzblatt, aus dem der Wortlaut stammt, setzt keine.
// Anlage Nummer 23 — der oben benannte Rest: dort steht der Punkt, wo die Nachfassung ein
// Semikolon führt.
- // Anlage Nummer 31 — das Zitat läuft im Gesetzblatt über einen Seitenwechsel, und der
- // ganzseitenbreite Titelblock des Änderungsgesetzes steht im Inhaltsstrom mitten darin
- // („… zur Sicherung des Be-“ / Titelblock / „triebs von Unterkünften …“). Das ist die
- // bekannte Berliner Layout-Frage, die erst eine geometrische Lesereihenfolge löst; sie
- // betrifft die Extraktion, nicht die Anwendung.
- assertThat(abweichend).containsExactly("§ 67", "Anlage Nummer 23", "Anlage Nummer 31");
- assertThat(
- anwendung
- .neu()
- .norm("Anlage Nummer 31")
- .orElseThrow()
- .gesamtText()
- .replaceAll("\\s+", " "))
- .as("der Titelblock steht im Zitat — Beleg der offenen Layout-Frage")
- .contains("Gesetz zur Änderung des Allgemeinen Sicherheits- und Ordnungsgesetzes");
+ assertThat(abweichend).containsExactly("§ 67", "Anlage Nummer 23");
+ // Die Nummer 31 war die dritte: Ihr Zitat läuft über einen Seitenwechsel, und der
+ // ganzseitenbreite Titelblock des Änderungsgesetzes steht im Inhaltsstrom mitten darin
+ // („… zur Sicherung des Be-“ / Titelblock / „triebs von Unterkünften …“). Seit die
+ // Lesereihenfolge dem Satzbild folgt, steht der Titel, wo er hingehört, und das Zitat liest
+ // sich durch.
+ var nummer31 =
+ anwendung.neu().norm("Anlage Nummer 31").orElseThrow().gesamtText().replaceAll("\\s+", " ");
+ assertThat(nummer31)
+ .doesNotContain("Gesetz zur Änderung des Allgemeinen Sicherheits- und Ordnungsgesetzes")
+ .contains("die Ordnungsaufgaben zur Sicherung des Betriebs von Unterkünften");
assertThat(anwendung.neu().norm("§ 67").orElseThrow().gesamtText())
.contains("Gegen einen straßenverkehrsrechtlichen Verwaltungsakt");
}
diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/LesereihenfolgeTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/LesereihenfolgeTest.java
new file mode 100644
index 0000000..f213d07
--- /dev/null
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/LesereihenfolgeTest.java
@@ -0,0 +1,114 @@
+// SPDX-FileCopyrightText: 2020 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+// SPDX-License-Identifier: AGPL-3.0-or-later
+package eu.mulk.aendggner.aenderung.parse;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import eu.mulk.aendggner.aenderung.parse.FontgroessenFilter.Zeile;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+/** Die Lesereihenfolge am nachgestellten Satzbild eines Gesetzblatts. */
+class LesereihenfolgeTest {
+
+ /** Der Satzspiegel des Berliner Blattes: zwei Spalten von 45 bis 289 und von 332 bis 547. */
+ private static Zeile links(int seite, float y, String text) {
+ return new Zeile(seite, y, 45f, 289f, text);
+ }
+
+ private static Zeile rechts(int seite, float y, String text) {
+ return new Zeile(seite, y, 332f, 547f, text);
+ }
+
+ private static Zeile breit(int seite, float y, String text) {
+ return new Zeile(seite, y, 127f, 465f, text);
+ }
+
+ private static List<String> texte(List<Zeile> zeilen) {
+ var namen = new ArrayList<String>();
+ for (var zeile : zeilen) {
+ namen.add(zeile.text());
+ }
+ return namen;
+ }
+
+ /**
+ * Der Fall, der den Anstoß gab: Das Blatt zeichnet den ganzseitenbreiten Titelblock zuletzt,
+ * obgleich er über beiden Spalten steht. Er gehört vor sie — und die Spalten bleiben ganz.
+ */
+ @Test
+ void derZuletztGezeichneteTitelblockGehoertNachOben() {
+ var seite = new ArrayList<Zeile>();
+ seite.add(breit(1, 51, "Kolumnentitel"));
+ for (int i = 0; i < 5; i++) {
+ seite.add(links(1, 345 + 10 * i, "links " + i));
+ }
+ for (int i = 0; i < 5; i++) {
+ seite.add(rechts(1, 345 + 10 * i, "rechts " + i));
+ }
+ seite.add(breit(1, 280, "Gesetz zur Änderung"));
+ seite.add(breit(1, 293, "Vom 3. Juni 2026"));
+
+ assertThat(texte(Lesereihenfolge.ordne(seite)))
+ .containsExactly(
+ "Kolumnentitel",
+ "Gesetz zur Änderung",
+ "Vom 3. Juni 2026",
+ "links 0",
+ "links 1",
+ "links 2",
+ "links 3",
+ "links 4",
+ "rechts 0",
+ "rechts 1",
+ "rechts 2",
+ "rechts 3",
+ "rechts 4");
+ }
+
+ /**
+ * Die Falle des einfachen XY-Schnitts: Läge zwischen den Zeilen 4 und 5 ein weites Band, so
+ * zerfiele die Seite erst in oben und unten — gelesen würde links oben, rechts oben, links unten,
+ * rechts unten. Die Spalte hat Vorrang vor dem Band.
+ */
+ @Test
+ void eineLueckeAufGleicherHoeheZerreisstDieSpaltenNicht() {
+ var seite = new ArrayList<Zeile>();
+ seite.add(breit(1, 51, "Kolumnentitel"));
+ float[] hoehen = {100, 110, 120, 130, 300, 310, 320, 330};
+ for (float y : hoehen) {
+ seite.add(links(1, y, "links " + (int) y));
+ }
+ for (float y : hoehen) {
+ seite.add(rechts(1, y, "rechts " + (int) y));
+ }
+
+ var geordnet = texte(Lesereihenfolge.ordne(seite));
+ assertThat(geordnet).startsWith("Kolumnentitel", "links 100");
+ assertThat(geordnet.subList(1, 9)).allMatch(t -> t.startsWith("links"));
+ assertThat(geordnet.subList(9, 17)).allMatch(t -> t.startsWith("rechts"));
+ }
+
+ /** Einspaltiger Satz hat keine Rinne; da bleibt es beim Inhaltsstrom. */
+ @Test
+ void einspaltigerSatzBleibtUnangetastet() {
+ var seite = new ArrayList<Zeile>();
+ for (int i = 0; i < 10; i++) {
+ seite.add(breit(1, 100 + 10 * i, "zeile " + i));
+ }
+ assertThat(Lesereihenfolge.ordne(seite)).isEqualTo(seite);
+ }
+
+ /** Fehlt einer Zeile die Geometrie, so ist die ganze Seite nicht zu vermessen. */
+ @Test
+ void ohneGeometrieBleibtDieSeiteWieSieIst() {
+ var seite = new ArrayList<Zeile>();
+ seite.add(new Zeile(0, Float.NaN, Float.NaN, Float.NaN, "unbekannt"));
+ for (int i = 0; i < 5; i++) {
+ seite.add(links(0, 100 + 10 * i, "links " + i));
+ seite.add(rechts(0, 100 + 10 * i, "rechts " + i));
+ }
+ assertThat(Lesereihenfolge.ordne(seite)).isEqualTo(seite);
+ }
+}
diff --git a/src/test/resources/sampledata/Landesrecht-Beispiele.adoc b/src/test/resources/sampledata/Landesrecht-Beispiele.adoc
index fef01d9..ea218a0 100644
--- a/src/test/resources/sampledata/Landesrecht-Beispiele.adoc
+++ b/src/test/resources/sampledata/Landesrecht-Beispiele.adoc
@@ -26,11 +26,12 @@ Stammfassung kommt je Land aus dem Portal des Landes oder als handgepflegter Kla
|Berlin
|`Berlin/GVBl-2026-17_ASOG-LAF-AendG.pdf` — GVBl. für Berlin Nr. 17 vom 11.06.2026, S. 234
(Gesetz zur Änderung des ASOG und des LAF-Errichtungsgesetzes vom 03.06.2026)
-|*Zweispaltiges Layout* (Herausgabe über Wolters Kluwer). Der Spaltenspike ist erledigt: Die
-Reihenfolge des Inhaltsstroms *ist* bereits die Lesereihenfolge, eine koordinatenbasierte
-Spaltenerkennung ist nicht nötig. Verbleibende Layout-Eigenheit sind ganzseitenbreite Rahmen
-(Titelblock, laufender Seitenkopf), die an falscher Stelle im Strom stehen — der Seitenkopf wird
-herausgeschnitten, der Titelblock landet weiterhin mitten im Text. Tiefe Verschachtelung
+|*Zweispaltiges Layout* (Herausgabe über Wolters Kluwer). Innerhalb einer Spalte ist die
+Reihenfolge des Inhaltsstroms bereits die Lesereihenfolge; die ganzseitenbreiten Rahmen dagegen
+stehen an falscher Stelle im Strom — der Titelblock wird *zuletzt* gezeichnet, obgleich er über
+beiden Spalten steht, und landete damit mitten im Fließtext. Seit Welle 21 folgt die Reihenfolge
+dem Satzbild (Rinne zwischen den Spalten, breite Zeilen als Bandgrenzen), und der Titelblock steht,
+wo er hingehört. Tiefe Verschachtelung
a)/aa)/bb)/cc); Artikel 1 ändert eine *Anlage*, deren Einheiten als *Überschriften* („Nummer 6")
statt als Aufzählungsmarken gesetzt sind. Artikel 2 (LAF-Errichtungsgesetz) ist ein voller
Akzeptanzfall; das Portal setzt die amtlichen *Satznummern als angeklebte Ziffern* („(2) 1Gegen …").
@@ -152,16 +153,17 @@ vollständig belegt.
* *Berlin* ist umgesetzt (`EndToEndTest.asogLafAendGBerlin`). Artikel 2
(LAF-Errichtungsgesetz) ist ein *voller* Akzeptanzfall: beide Befehle angewandt, alle fünf
Normen gleich der amtlichen Nachfassung vom 12. Juni 2026. Artikel 1 (ASOG) wird vollständig
- erkannt und zu fünf Sechsteln angewandt; 168 der 171 Normen gleichen danach der Nachfassung.
+ erkannt und zu fünf Sechsteln angewandt; 169 der 171 Normen gleichen danach der Nachfassung.
Die Nummern der Anlage sind seit dieser Welle *eigene Normen* („Anlage Nummer 23“), wie das
Portal sie führt — damit trägt die vorhandene Absatz-Maschinerie ihre Befehle. Beide
Stammfassungen sind über die Browsersteuerung beschafft.
+
--
-Die drei Abweichungen sind benannt und liegen nicht in der Anwendung: § 67 (das Portal setzt
-amtliche Satznummern, das Gesetzblatt nicht), Anlage Nummer 23 (der eine Rest, siehe unten) und
-Anlage Nummer 31 (der ganzseitenbreite Titelblock steht im Inhaltsstrom mitten im Zitat — die
-offene Layout-Frage). Der eine liegengebliebene Befehl benennt eine Grenze des Klartextformats:
+Die zwei Abweichungen sind benannt und liegen nicht in der Anwendung: § 67 (das Portal setzt
+amtliche Satznummern, das Gesetzblatt nicht) und Anlage Nummer 23 (der eine Rest, siehe unten).
+Die Anlage Nummer 31 war die dritte, bis die Lesereihenfolge dem Satzbild folgte: Der
+ganzseitenbreite Titelblock stand im Inhaltsstrom mitten in ihrem Zitat. Der eine liegengebliebene
+Befehl benennt eine Grenze des Klartextformats:
Ein Zwischentitel ohne Absatzbezeichnung („Aus dem Bereich Verkehr:“) wird dem vorangehenden
Absatz zugeschlagen, weshalb dessen Text nicht auf den Punkt endet, den der Befehl meint.
--
@@ -253,9 +255,6 @@ Klartext-Zweig des Loaders normalisiert zur Sicherheit mit.
dagegen als Aufzählungsmarken im Wortlaut der Anlage; dort bleibt es beim Markensuchen, und
ein Befehl auf „die Überschrift der Nummer 1 der Anlage 8“ (GEG, Artikel 1 Nr. 43 b) aa))
findet seine Stelle nicht. Ob die Anlagen des Bundes ebenso zu zerlegen sind, ist offen.
-* *Berlin, ganzseitenbreite Rahmen*: Titelblock und Impressum stehen im Inhaltsstrom an anderer
- Stelle als im Satzbild (der Titelblock mitten im Fließtext der ersten Seite). Das zu beheben
- verlangt eine geometrische Lesereihenfolge (XY-Cut), nicht bloß eine Spaltenerkennung.
* *Rheinland-Pfalz*: Die Verkündungsplattform `verkuendung.rlp.de` verkündet erst seit dem
01.07.2026 elektronisch; ältere GVBl-Hefte liegen nicht als freie Direkt-PDF vor.
* Bremen, Hamburg, Mecklenburg-Vorpommern, Saarland, Brandenburg, Sachsen-Anhalt: noch nicht
@@ -266,10 +265,6 @@ Klartext-Zweig des Loaders normalisiert zur Sicherheit mit.
Kein Land scheitert noch an reiner Extraktionsschwäche, und jedes Land mit einem Änderungs-
dokument trägt nunmehr einen Belegfall bis zur Anwendung. Nach absteigendem Nutzen:
-. *Berlin, ganzseitenbreite Rahmen* (XY-Cut) — die letzte offene Layout-Frage, siehe „Noch
- offen“. Sie ist seit dieser Welle *belegt*: Die Anlage Nummer 31 des ASOG weicht allein
- deshalb von der Nachfassung ab, weil der Titelblock des Änderungsgesetzes im Inhaltsstrom
- mitten in ihrem Zitat steht.
. *Weitere Länder erfassen* — Rheinland-Pfalz (Verkündungsplattform erst ab 01.07.2026), Bremen,
Hamburg, Mecklenburg-Vorpommern, Saarland, Brandenburg, Sachsen-Anhalt.