diff options
Diffstat (limited to 'src')
4 files changed, 79 insertions, 3 deletions
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java index 6fb511d..d9ecf8a 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/TextBereiniger.java @@ -73,6 +73,17 @@ public final class TextBereiniger { private static final Pattern KONJUNKTION = Pattern.compile("^(und|oder|sowie|bzw\\.|beziehungsweise)\\b.*"); + /** Perzentil der Zeilenlängen, das als „volle Spaltenbreite“ gilt (siehe {@link #verbindeUmbrueche}). */ + private static final double VOLLZEILE_PERZENTIL = 0.9; + + /** Mindestanteil der vollen Spaltenbreite, ab dem ein markerloser Umbruch als Silbentrennung + * statt als bewusster Wortgrenzen-Umbruch gilt. */ + private static final double VOLLZEILE_MINDESTANTEIL = 0.7; + + /** Anzahl Zeilen vor/nach einer Kandidatenzeile, die für die lokale Spaltenbreiten-Schätzung + * herangezogen werden (siehe {@link #typischeZeilenlaenge}). */ + private static final int VOLLZEILE_FENSTER = 20; + // BMJV-Entwurfsvorlagen zeichnen das hängende öffnende Anführungszeichen im Content-Stream // NACH dem ersten Element der zitierten Passage: „(1) „ Ungeachtet…“ statt „„(1) Ungeachtet…“, // „§ 19„“ statt „„§ 19“. @@ -183,7 +194,11 @@ public final class TextBereiniger { * <li><b>Markerlos</b> (Bundestags-Drucksachen: „Schwel“ + „lenwertes“): Reguläre Umbrüche * enden dort mit Leerzeichen vor dem Zeilenumbruch; endet eine Zeile direkt mit einem * Buchstaben und beginnt die Folgezeile klein, ist es eine Trennung → ohne Leerzeichen - * zusammenziehen. + * zusammenziehen. Das trifft aber nur zu, wenn die Zeile (fast) die volle Spaltenbreite + * ausnutzt — sonst wäre der Umbruch dort nicht nötig gewesen. Kurze, bewusst + * abgebrochene Zeilen (z.B. ein Stichwort vor einer hängend eingerückten Definition: + * „…Nachhaltigkeitssiegels“ + „das Anbringen …“) werden deshalb ausgenommen — sie sind + * ein Wortgrenzen-Umbruch, keine Silbentrennung, auch wenn das Trailing-Space-Signal fehlt. * </ul> */ private static ArrayList<String> verbindeUmbrueche(List<String> zeilen) { @@ -198,7 +213,10 @@ public final class TextBereiniger { while (true) { var gestutzt = zeile.stripTrailing(); var mitTrennstrich = endetMitSilbentrennung(gestutzt); - var markerlos = markerlosAktiv && endetMarkerlos(zeile); + var markerlos = + markerlosAktiv + && endetMarkerlos(zeile) + && gestutzt.length() >= typischeZeilenlaenge(zeilen, i) * VOLLZEILE_MINDESTANTEIL; if (!mitTrennstrich && !markerlos) { break; } @@ -266,6 +284,37 @@ public final class TextBereiniger { return mitTrailingSpace > 0 && mitTrailingSpace * 4 >= nichtLeer; } + /** + * Typische „volle“ Zeilenlänge im Umfeld von {@code zentrum} ({@link #VOLLZEILE_PERZENTIL}- + * Perzentil der gestutzten Längen nichtleerer Zeilen in einem Fenster von {@link + * #VOLLZEILE_FENSTER} Zeilen davor/danach) — ein grober Näherungswert für die lokale + * Spaltenbreite, ohne auf PDF-Positionsdaten zugreifen zu müssen. Lokal statt dokumentweit, weil + * ein einziges Dokument Abschnitte mit unterschiedlicher Spaltenbreite mischen kann (z.B. + * schmalerer Regelungstext vs. breitere Begründung in Regierungsentwürfen) — eine dokumentweite + * Kennzahl würde dort die kürzere Spalte systematisch benachteiligen. Vereinzelte überlange + * Zeilen (z.B. selbst fälschlich verklebte Umbrüche) dürfen den Wert nicht verzerren, daher ein + * hohes Perzentil statt des reinen Maximums. + */ + private static int typischeZeilenlaenge(List<String> zeilen, int zentrum) { + var laengen = new ArrayList<Integer>(); + int von = Math.max(0, zentrum - VOLLZEILE_FENSTER); + int bis = Math.min(zeilen.size(), zentrum + VOLLZEILE_FENSTER + 1); + for (int i = von; i < bis; i++) { + var zeile = zeilen.get(i); + if (zeile.isBlank()) { + continue; + } + laengen.add(zeile.stripTrailing().length()); + } + if (laengen.isEmpty()) { + return 0; + } + laengen.sort(null); + int index = (int) (laengen.size() * VOLLZEILE_PERZENTIL); + index = Math.min(index, laengen.size() - 1); + return laengen.get(index); + } + private static String strippeZeilenenden(List<String> zeilen) { var sb = new StringBuilder(); for (var zeile : zeilen) { diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java index 4140357..68fe2b8 100644 --- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java +++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java @@ -460,7 +460,8 @@ public final class BefehlAnwender { } // Neufassung eines Satzes / einer Nummer / eines Buchstabens: Bereich ersetzen. - return bearbeiteText(normen, befehl, text -> TextErgebnis.ok(befehl.neuerText().strip())); + return bearbeiteText( + normen, befehl, text -> TextErgebnis.ok(normalisiereZitatText(befehl.neuerText()))); } /** Ein Ziel (Absatz, Satz, Nummer, Buchstabe) wird durch einen Block ersetzt (ggf. 1 → N). */ diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java index d4b0b17..a395fa1 100644 --- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java +++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java @@ -84,6 +84,11 @@ class EndToEndTest { assertThat(text).doesNotContain("Dieses Gesetz dient der Umsetzung der Richtlinie (EU)"); assertThat(text).contains("wird wie folgt geändert"); + // Regression: Stichwort/Definition-Umbruch im Anhang darf nicht ohne Leerzeichen verklebt + // werden (kurze Stichwort-Zeile vor hängend eingerückter Definition, kein Wort wird getrennt). + assertThat(text).contains("Nachhaltigkeitssiegels\ndas Anbringen"); + assertThat(text).doesNotContain("Nachhaltigkeitssiegelsdas"); + var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null); assertThat(parseErgebnis.artikel()).containsExactly("1"); assertThat(parseErgebnis.befehle().size()).isGreaterThanOrEqualTo(10); diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java index 98dc1d3..4ca86cf 100644 --- a/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java +++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/TextBereinigerTest.java @@ -100,6 +100,27 @@ class TextBereinigerTest { } @Test + void ziehtMarkerlosNichtBeiKurzemStichwortVorEingerueckterDefinitionZusammen() { + // Definitionslisten-Muster (neues BGBl-Format, „2a. …Siegel“ + hängend eingerückte + // Definition): die Stichwort-Zeile endet ohne Trailing-Space, ist aber deutlich kürzer als + // die umgebenden Volltextzeilen — kein Wort wird getrennt, der Umbruch ist bewusst. + var roh = + "1. „Betriebsstoff“ jeder Bestandteil einer Ware, der wiederholt verbraucht wird und ersetzt" + + " oder aufgefüllt werden muss, damit die Ware ordnungsgemäß funktioniert. \n" + + "2. „Haltbarkeit“ die Fähigkeit der Waren, ihre erforderlichen Funktionen und ihre" + + " Leistung bei normaler Verwendung über einen längeren Zeitraum zu bewahren. \n" + + "3. „Zertifizierungssystem“ ein System der Überprüfung durch Dritte, durch das" + + " bestätigt wird, dass ein Produkt bestimmten Anforderungen entspricht. \n" + + "2a. unzulässiges Anbringen eines Nachhaltigkeitssiegels\n" + + "das Anbringen eines Nachhaltigkeitssiegels, das weder auf einem Zertifizierungssystem" + + " beruht noch von \n" + + "staatlichen Stellen festgesetzt wurde;"; + + assertThat(TextBereiniger.bereinige(roh)) + .contains("Nachhaltigkeitssiegels\ndas Anbringen eines Nachhaltigkeitssiegels"); + } + + @Test void repariertInvertierteZitatzeichenAnAbsatzmarkern() { // BMJV-Vorlagen zeichnen das hängende „ nach dem Absatzmarker bzw. der Paragraphenangabe. assertThat(TextBereiniger.bereinige("(1) „ Ungeachtet des § 8 gilt.“")) |
