diff options
| author | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2026-08-28 07:57:41 +0200 |
|---|---|---|
| committer | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2026-08-28 07:57:41 +0200 |
| commit | 3203725daa2c8db9ce25ff4541dd4ea438ac5cd7 (patch) | |
| tree | 76a76393cfc5abab8a79e9b9c6f31dc006f24edf /src/test | |
| parent | eb998fbfc324151b0b712adb37525caf2320211e (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')
| -rw-r--r-- | src/test/java/eu/mulk/aendggner/EndToEndTest.java | 54 | ||||
| -rw-r--r-- | src/test/java/eu/mulk/aendggner/aenderung/parse/SeitenkonkordanzTest.java | 84 |
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(); + } +} |
