aboutsummaryrefslogtreecommitdiff
path: root/src/test/java
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-28 07:57:41 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-28 07:57:41 +0200
commit3203725daa2c8db9ce25ff4541dd4ea438ac5cd7 (patch)
tree76a76393cfc5abab8a79e9b9c6f31dc006f24edf /src/test/java
parenteb998fbfc324151b0b712adb37525caf2320211e (diff)
Jeder Befehl nennt fortan seine Seite im Heft
Die Synopse wies bislang Artikel und Gliederungspunkt aus. Wer einem liegengebliebenen Befehl nachgehen wollte, musste ihn in einem Heft von achtzig Seiten selbst suchen; die eigentliche Arbeit lag damit beim Leser. Fortan trägt jeder Befehl seine Fundstelle: „Artikel 1 23. b) (S. 8)“. Gezählt wird die Seite nicht, sondern am Wortlaut wiedergefunden. Eine Marke, die im Textstrom mitreiste, kam dafür nicht in Betracht: Der Bereiniger zieht weiche Umbrüche zusammen, entfernt Kolumnentitel und schneidet Seitenfüße heraus; die Marke geriete entweder vor einen Zeilenanfangs-Anker und nähme jedem Normkopf-Muster seine Grundlage, oder sie verschwände mit der Zeile, die sie trug. Stattdessen hält der Auszug seinen Wortbestand seitenweise fest (Seitenkonkordanz) — nur Buchstaben und Ziffern, kleingeschrieben — und der Befehlstext wird darin gesucht, ebenso heruntergebrochen. Silbentrennung, Anführungszeichen, Leerraum und Satzzeichen, also gerade das, woran die Aufbereitung arbeitet, stören den Vergleich dann nicht. Der Fontgrößenfilter erhebt die Konkordanz aus denselben Zeilen, aus denen er den Text fügt; ein zweiter Lauf über das Druckwerk unterbleibt. Gesucht wird von einem fortschreitenden Leser aus, der sich merkt, wie weit die Erschließung gediehen ist. Ohne ihn träfe ein mehrfach vorkommender Wortlaut („Absatz 3 wird aufgehoben“ steht in einem Heft dutzendfach) stets dessen erstes Vorkommen. Was kürzer ist als acht Zeichen, ist keine Wortfolge, sondern eine Marke; was sich nicht wiederfinden lässt, bleibt ohne Seitenangabe. Eine falsche Seite wäre schlimmer als keine. Ohne Angabe bleiben aus demselben Grund die Klartexteingabe, die kein Satzbild hat, und die beschlossene Fassung einer Beschlussempfehlung, die aus zwei Spalten zusammengesetzt ist und so auf keiner Seite des Heftes steht. Die Anzeige der Herkunft (Provenienz) hängt die Seite an; Synopse, Abschnitt „Manuell prüfen“ und „--dump-befehle“ tragen sie damit, ohne dass an ihnen etwas zu ändern gewesen wäre. Geprüft: mvnw verify (438 Testfälle) und reuse lint (209/209) gehen durch. Sechs Fälle prüfen die Konkordanz für sich, zwei am Druckwerk — dass jeder der neunzehn Befehle des Heftes BGBl. 2026 I Nr. 43 eine Seite zwischen eins und fünf trägt, dass die Seiten in der Reihenfolge der Erschließung steigen und dass eine Klartexteingabe keine trägt. Die Fundstelle „Artikel 1 23. b) (S. 8)“ des Gesetzes vom 16. Oktober 2023 ist am Druckbild nachgeschlagen und für richtig befunden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Change-Id: If9a9008311aaabf49b007d82f9c1aaaca495878a
Diffstat (limited to 'src/test/java')
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java54
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/SeitenkonkordanzTest.java84
2 files changed, 138 insertions, 0 deletions
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index eead6a0..34bc8e6 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -172,6 +172,60 @@ class EndToEndTest {
/** Neues digitales BGBl-Format (recht.bund.de, ab 2023): 3. UWGÄndG 2026. */
@Test
+ void fundstelleImHeft() throws Exception {
+ var xml = SAMPLEDATA.resolve("UWG/BJNR141400004.xml");
+ var pdf = SAMPLEDATA.resolve("UWG/bgbl126s0043_regelungstext.pdf");
+ assumeTrue(Files.exists(xml) && Files.exists(pdf), "UWG-Beispieldaten fehlen");
+
+ var gesetz = new GiiXmlLoader().load(xml);
+ var auszug = new PatchTextExtraktor().extrahiereMitSeiten(Quelle.lies(pdf));
+ var befehle =
+ new AenderungsgesetzParser()
+ .parse(TextBereiniger.bereinige(auszug.text()), gesetz, null, false, auszug.seiten())
+ .befehle();
+
+ // Jeder Befehl trägt seine Seite; das Heft hat fünf.
+ assertThat(befehle).allSatisfy(b -> assertThat(b.provenienz().seite()).isNotNull());
+ assertThat(befehle).allSatisfy(b -> assertThat(b.provenienz().seite()).isBetween(1, 5));
+
+ // Die Befehle stehen im Heft, wie sie erschlossen werden: Die Seiten steigen.
+ var seiten = befehle.stream().map(b -> b.provenienz().seite()).toList();
+ assertThat(seiten).isSorted();
+
+ // Gegenprobe am Druckwerk: Der erste Befehl steht auf Seite 1, „Nach § 5b Absatz 3 …“ auf
+ // Seite 3 (dort beginnt die Zeile „3. Nach § 5b Absatz 3 wird der folgende Absatz 3a
+ // eingefügt:“).
+ assertThat(befehle.get(0).provenienz().seite()).isEqualTo(1);
+ assertThat(
+ befehle.stream()
+ .filter(b -> b.provenienz().originalText().startsWith("Nach § 5b Absatz 3"))
+ .findFirst()
+ .orElseThrow()
+ .provenienz()
+ .seite())
+ .isEqualTo(3);
+
+ // Die Seite steht auch in der Anzeige — dort sucht sie, wer einen Rest von Hand prüft.
+ assertThat(befehle.get(0).provenienz().anzeigeText()).endsWith("(S. 1)");
+ }
+
+ @Test
+ void klartextTraegtKeineSeite() throws Exception {
+ // Eine Klartextdatei hat kein Satzbild; eine Seitenangabe wäre erfunden.
+ var xml = SAMPLEDATA.resolve("UWG/BJNR141400004.xml");
+ var pdf = SAMPLEDATA.resolve("UWG/bgbl126s0043_regelungstext.pdf");
+ assumeTrue(Files.exists(xml) && Files.exists(pdf), "UWG-Beispieldaten fehlen");
+
+ var gesetz = new GiiXmlLoader().load(xml);
+ var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf));
+ var befehle = new AenderungsgesetzParser().parse(text, gesetz, null).befehle();
+
+ assertThat(befehle).isNotEmpty();
+ assertThat(befehle).allSatisfy(b -> assertThat(b.provenienz().seite()).isNull());
+ assertThat(befehle.get(0).provenienz().anzeigeText()).doesNotContain("(S.");
+ }
+
+ @Test
void uwgNeuesBgblFormat() throws Exception {
var xml = SAMPLEDATA.resolve("UWG/BJNR141400004.xml");
var pdf = SAMPLEDATA.resolve("UWG/bgbl126s0043_regelungstext.pdf");
diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/SeitenkonkordanzTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/SeitenkonkordanzTest.java
new file mode 100644
index 0000000..8e30814
--- /dev/null
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/SeitenkonkordanzTest.java
@@ -0,0 +1,84 @@
+// SPDX-FileCopyrightText: 2026 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 java.util.List;
+import org.junit.jupiter.api.Test;
+
+class SeitenkonkordanzTest {
+
+ private static FontgroessenFilter.Zeile zeile(int seite, String text) {
+ return new FontgroessenFilter.Zeile(seite, Float.NaN, Float.NaN, Float.NaN, text);
+ }
+
+ private static Seitenkonkordanz konkordanz(FontgroessenFilter.Zeile... zeilen) {
+ return Seitenkonkordanz.aus(List.of(zeilen));
+ }
+
+ @Test
+ void findetDenBefehlAufSeinerSeite() {
+ var leser =
+ konkordanz(
+ zeile(1, "1. § 2 Absatz 2 wird durch die folgenden Absätze 2 und 3 ersetzt:"),
+ zeile(2, "2. In § 5 Absatz 3 Nummer 1 wird die Angabe „a“ durch „b“ ersetzt."))
+ .leser();
+
+ assertThat(leser.seiteVon("§ 2 Absatz 2 wird durch die folgenden Absätze 2 und 3 ersetzt:"))
+ .isEqualTo(1);
+ assertThat(leser.seiteVon("In § 5 Absatz 3 Nummer 1 wird die Angabe „a“ durch „b“ ersetzt."))
+ .isEqualTo(2);
+ }
+
+ @Test
+ void satzzeichenUndSilbentrennungStoerenNicht() {
+ // Der Bereiniger zieht weiche Umbrüche zusammen und heilt Trennstriche; der Wortbestand ist
+ // hernach derselbe, das Satzbild nicht. Genau darauf beruht die Suche.
+ var leser =
+ konkordanz(
+ zeile(4, "3. Nach Absatz 5 wird der folgende Ab-"),
+ zeile(5, "satz 6 eingefügt: „(6) Eine geschäftliche Handlung ist irreführend.“"))
+ .leser();
+
+ assertThat(leser.seiteVon("Nach Absatz 5 wird der folgende Absatz 6 eingefügt:")).isEqualTo(4);
+ }
+
+ @Test
+ void mehrfacherWortlautTrifftDieFortgeschritteneSeite() {
+ // „Absatz 3 wird aufgehoben“ steht in einem Heft dutzendfach. Maßgeblich ist, wie weit die
+ // Erschließung gediehen ist — deshalb der fortschreitende Leser.
+ var konkordanz =
+ konkordanz(
+ zeile(1, "a) Der Absatz 3 wird aufgehoben."),
+ zeile(7, "b) In § 40 wird ein Wort ersetzt."),
+ zeile(9, "c) Der Absatz 3 wird aufgehoben."));
+
+ var leser = konkordanz.leser();
+ assertThat(leser.seiteVon("Der Absatz 3 wird aufgehoben.")).isEqualTo(1);
+ assertThat(leser.seiteVon("In § 40 wird ein Wort ersetzt.")).isEqualTo(7);
+ assertThat(leser.seiteVon("Der Absatz 3 wird aufgehoben.")).isEqualTo(9);
+ }
+
+ @Test
+ void wasSichNichtWiederfindetBleibtOhneSeite() {
+ var leser =
+ konkordanz(zeile(1, "Ein Wortlaut, der nichts mit dem Gesuchten zu tun hat.")).leser();
+
+ assertThat(leser.seiteVon("Dieser Befehl steht in keinem Heft.")).isNull();
+ }
+
+ @Test
+ void zuKurzesIstKeinWortlaut() {
+ // „a)“ steht auf jeder zweiten Seite; daraus eine Fundstelle zu machen wäre geraten.
+ var leser = konkordanz(zeile(3, "a) und b) und c)")).leser();
+
+ assertThat(leser.seiteVon("a)")).isNull();
+ }
+
+ @Test
+ void ohneSatzbildKeineSeite() {
+ assertThat(Seitenkonkordanz.LEER.leser().seiteVon("Ein beliebiger Befehlstext von Länge."))
+ .isNull();
+ }
+}