diff options
Diffstat (limited to 'src/main/java')
10 files changed, 323 insertions, 40 deletions
diff --git a/src/main/java/eu/mulk/aendggner/Pipeline.java b/src/main/java/eu/mulk/aendggner/Pipeline.java index c7b22bf..fee0ce9 100644 --- a/src/main/java/eu/mulk/aendggner/Pipeline.java +++ b/src/main/java/eu/mulk/aendggner/Pipeline.java @@ -16,6 +16,7 @@ import eu.mulk.aendggner.aenderung.parse.TextBereiniger; import eu.mulk.aendggner.anwendung.BefehlAnwender; import eu.mulk.aendggner.anwendung.Grund; import eu.mulk.aendggner.anwendung.Nachfassungsabgleich; +import eu.mulk.aendggner.gesetz.Fortschreibung; import eu.mulk.aendggner.gesetz.Gesetz; import eu.mulk.aendggner.gesetz.Superskript; import eu.mulk.aendggner.gesetz.gii.GiiXmlLoader; @@ -170,7 +171,9 @@ public final class Pipeline { var parser = new AenderungsgesetzParser(); var warnungen = new ArrayList<String>(); - var dokumente = wendeAntraegeAn(leseDokumente(patches, extraktor, warnungen), warnungen); + var dokumente = + ordneNachAusfertigung( + wendeAntraegeAn(leseDokumente(patches, extraktor, warnungen), warnungen), warnungen); var gesetz = altesGesetz; var protokoll = new ArrayList<BefehlAnwender.AngewandteAenderung>(); @@ -179,6 +182,10 @@ public final class Pipeline { Inkrafttreten inkrafttreten = null; for (var dokument : dokumente) { + var heft = new Fortschreibung(dokument.kopf().anzeigeName(), dokument.kopf().ausfertigung()); + if (gesetz.traegt(heft)) { + warnungen.add(doppelanwendungsRuege(dokument, heft)); + } var parseErgebnis = parser.parse(dokument.text(), gesetz, artikel, entwurfsGrenzen(dokument)); if (parseErgebnis.befehle().isEmpty()) { warnungen.add( @@ -191,7 +198,13 @@ public final class Pipeline { } var geteilt = teileNachStichtag(parseErgebnis, stichtag, warnungen); var anwendung = BefehlAnwender.anwenden(gesetz, geteilt.anzuwenden()); - gesetz = anwendung.neu(); + // Vermerkt wird nur, was auch gewirkt hat: Ein Heft, dessen Befehle sämtlich liegenblieben, + // hat den Wortlaut nicht fortgeschrieben und darf ihn deshalb nicht als fortgeschrieben + // ausweisen. + gesetz = + anwendung.anzahlAngewandt() > 0 && !gesetz.traegt(heft) + ? anwendung.neu().mitFortschreibung(heft) + : anwendung.neu(); protokoll.addAll(anwendung.protokoll()); protokoll.addAll(geteilt.zurueckgestellt()); warnungen.addAll(parseErgebnis.warnungen()); @@ -232,6 +245,50 @@ public final class Pipeline { abgleich); } + /** + * Die Rüge, wenn ein Heft ein zweites Mal auf denselben Wortlaut trifft. + * + * <p>Ein Änderungsbefehl ist keine Zustandsbeschreibung, sondern eine Anordnung: Wer zweimal + * anfügt, fügt zweimal an. Am Wortlaut allein ist das nicht zu erkennen — die Befehle greifen ein + * zweites Mal anstandslos —, wohl aber an dem, was die Fassung über sich selbst mitführt. + * Abgebrochen wird gleichwohl nicht: Es kann Gründe geben, dasselbe Heft erneut anzuwenden, und + * verworfen wird hier nichts (§ 1 Absatz 4 des Handbuchs). Gesagt wird es. + */ + private static String doppelanwendungsRuege(Quelldokument dokument, Fortschreibung heft) { + return ("Die Fassung trägt „%s“ bereits; %s wird damit ein zweites Mal angewandt. Ein Befehl" + + " ist eine Anordnung und keine Zustandsbeschreibung: Was angefügt wird, wird dann" + + " zweimal angefügt. Gemeint ist die Kette wohl mit einem anderen Heft.") + .formatted(heft.bezeichnung(), dokument.quelle().name()); + } + + /** + * Bringt die Hefte in die Reihenfolge ihrer Ausfertigung. + * + * <p>Die Kette wendet Heft auf Heft an; jedes setzt den Stand voraus, den das vorige hinterlassen + * hat. Die Reihenfolge der Aufrufargumente ist dafür kein Maßstab, das Ausfertigungsdatum schon. + * Umgestellt wird nur, wenn <em>jedes</em> Dokument ein Datum trägt — sonst fehlt der gemeinsame + * Maßstab, und geraten wird nicht. Und umgestellt wird nicht stillschweigend. + */ + private static List<Quelldokument> ordneNachAusfertigung( + List<Quelldokument> dokumente, List<String> warnungen) { + if (dokumente.size() < 2 || dokumente.stream().anyMatch(d -> d.kopf().ausfertigung() == null)) { + return dokumente; + } + var geordnet = new ArrayList<>(dokumente); + geordnet.sort(java.util.Comparator.comparing(d -> d.kopf().ausfertigung())); + if (geordnet.equals(dokumente)) { + return dokumente; + } + warnungen.add( + "Die Änderungsdokumente wurden nach ihrem Ausfertigungsdatum geordnet und in dieser Folge" + + " angewandt: " + + geordnet.stream() + .map(d -> d.quelle().name()) + .collect(java.util.stream.Collectors.joining(", ")) + + ". Jedes Heft setzt den Stand voraus, den das vorige hinterlässt."); + return List.copyOf(geordnet); + } + /** Die Befehle eines Dokuments, geschieden nach dem, was am Stichtag schon galt. */ private record Geteilt( List<eu.mulk.aendggner.aenderung.Aenderungsbefehl> anzuwenden, diff --git a/src/main/java/eu/mulk/aendggner/aenderung/DokumentKopf.java b/src/main/java/eu/mulk/aendggner/aenderung/DokumentKopf.java index 9dde246..de607db 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/DokumentKopf.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/DokumentKopf.java @@ -2,6 +2,8 @@ // SPDX-License-Identifier: AGPL-3.0-or-later package eu.mulk.aendggner.aenderung; +import eu.mulk.aendggner.aenderung.parse.DeutschesDatum; +import java.time.LocalDate; import java.util.List; import org.jspecify.annotations.Nullable; @@ -19,21 +21,45 @@ import org.jspecify.annotations.Nullable; * null} bei Dokumenten ohne Drucksachenkopf (Gesetzblätter, Referentenentwürfe). * @param bezugsDrucksachen die Nummern der Drucksachen, auf die sich das Dokument bezieht. * @param titel eine kurze Bezeichnung für Quellen- und Warnzeilen. + * @param ausfertigung das Datum, das die Zeile „Vom …“ nennt, oder {@code null}. Entwürfe tragen + * keines, und ein Sammelheft mit mehreren Verkündungen trägt mehrere — dann bleibt es + * gleichfalls leer, denn welche Verkündung gemeint ist, steht dort nicht fest. Geraten wird + * nicht. */ public record DokumentKopf( DokumentArt art, @Nullable String eigeneDrucksache, List<String> bezugsDrucksachen, - String titel) { + String titel, + @Nullable LocalDate ausfertigung) { public DokumentKopf { bezugsDrucksachen = List.copyOf(bezugsDrucksachen); } - /** Die Bezeichnung für die Quellenzeile der Synopse, z.B. „Änderungsantrag Drs. 19/10365“. */ + public DokumentKopf( + DokumentArt art, + @Nullable String eigeneDrucksache, + List<String> bezugsDrucksachen, + String titel) { + this(art, eigeneDrucksache, bezugsDrucksachen, titel, null); + } + + /** + * Die Bezeichnung für die Quellenzeile der Synopse, z.B. „Änderungsantrag Drs. 19/10365“ oder + * „Änderungsgesetz vom 22. April 2026“. + * + * <p>Sie ist zugleich der Schlüssel, an dem die Fassung wiedererkennt, welches Heft sie schon + * trägt (siehe {@link eu.mulk.aendggner.gesetz.Fortschreibung}). Deshalb steht hier, was das + * Dokument von sich selbst sagt — Drucksachennummer oder Ausfertigungsdatum —, und nicht der + * Dateiname, den jeder anders schreibt. + */ public String anzeigeName() { - return eigeneDrucksache == null + if (eigeneDrucksache != null) { + return art.anzeigeName() + " Drs. " + eigeneDrucksache; + } + return ausfertigung == null ? art.anzeigeName() - : art.anzeigeName() + " Drs. " + eigeneDrucksache; + : art.anzeigeName() + " vom " + DeutschesDatum.schreibe(ausfertigung); } } diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParser.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParser.java index a01442f..2d6f0d6 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParser.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParser.java @@ -8,6 +8,7 @@ import eu.mulk.aendggner.aenderung.Inkrafttreten; import eu.mulk.aendggner.aenderung.Provenienz; import eu.mulk.aendggner.aenderung.Stelle; import eu.mulk.aendggner.gesetz.Gesetz; +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -383,8 +384,8 @@ public final class AenderungsgesetzParser { * geraten wird nicht. */ private static Optional<String> standWarnung(String vorspann, Gesetz ziel) { - var stand = ziel.stand(); - if (stand == null || stand.juengsteAenderung() == null) { + var wortlautStand = ziel.wortlautStand(); + if (wortlautStand == null) { return Optional.empty(); } var satzDatum = EINLEITUNGS_DATUM.matcher(vorspann.replaceAll("\\s+", " ")); @@ -392,14 +393,14 @@ public final class AenderungsgesetzParser { return Optional.empty(); } var genannt = DeutschesDatum.lies(satzDatum.group(1), satzDatum.group(2), satzDatum.group(3)); - if (genannt == null || !stand.juengsteAenderung().isAfter(genannt)) { + if (genannt == null || !wortlautStand.isAfter(genannt)) { return Optional.empty(); } return Optional.of( "Das Stammgesetz ist jünger als das Änderungsgesetz: Sein Wortlaut ist bis zum " - + DeutschesDatum.schreibe(stand.juengsteAenderung()) + + DeutschesDatum.schreibe(wortlautStand) + " fortgeschrieben („" - + stand.kommentar() + + standHerkunft(ziel, wortlautStand) + "“), während der Einleitungssatz die Fassung vom " + satzDatum.group(1) + ". " @@ -412,6 +413,20 @@ public final class AenderungsgesetzParser { } /** + * Woher der Wortlautstand rührt: aus einem Heft, das das Erzeugnis selbst angewandt hat, sonst + * aus der Standangabe der Quelle. In der Kette ist die Fassung nämlich jünger als ihre eigene + * Standzeile — sie trägt die Hefte, die seither auf sie angewandt worden sind. + */ + private static String standHerkunft(Gesetz ziel, LocalDate wortlautStand) { + for (var heft : ziel.fortschreibungen()) { + if (wortlautStand.equals(heft.datum())) { + return heft.bezeichnung(); + } + } + return ziel.stand() != null ? ziel.stand().kommentar() : ""; + } + + /** * „… zuletzt durch Artikel 5 des Gesetzes vom 19. Juni 2020 (BGBl. I S. 1385) geändert worden * ist“ — das Datum der <em>letzten Änderung</em>, nicht das der Ausfertigung. Deshalb muss ihm * die Änderungsklausel vorausgehen: Der Einleitungssatz nennt zuerst das Ausfertigungsdatum („Das diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/DokumentErkenner.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/DokumentErkenner.java index 0ac3b2f..6f926bf 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/DokumentErkenner.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/DokumentErkenner.java @@ -4,6 +4,7 @@ package eu.mulk.aendggner.aenderung.parse; import eu.mulk.aendggner.aenderung.DokumentArt; import eu.mulk.aendggner.aenderung.DokumentKopf; +import java.time.LocalDate; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; @@ -78,6 +79,9 @@ public final class DokumentErkenner { /** So viele Zeilen darf ein Titel überspannen. */ private static final int TITELZEILEN = 6; + /** Die Ausfertigungszeile eines verkündeten Werkes: „Vom 22. April 2026“. */ + private static final Pattern VOM_ZEILE = Pattern.compile("^Vom " + DeutschesDatum.MUSTER + "$"); + /** * @param rohText der unbereinigte Extraktionstext des Dokuments. */ @@ -85,7 +89,47 @@ public final class DokumentErkenner { var zeilen = kopfZeilen(rohText); var art = bestimmeArt(zeilen, rohText); return new DokumentKopf( - art, eigeneDrucksache(zeilen), bezugsDrucksachen(zeilen), titel(zeilen)); + art, + eigeneDrucksache(zeilen), + bezugsDrucksachen(zeilen), + titel(zeilen), + ausfertigung(rohText)); + } + + /** + * Eine Rohtextzeile, wie die Erkennung sie liest: ohne die Zeilenend- und Schriftgrößenmarken aus + * dem privaten Unicode-Bereich, die erst der {@link TextBereiniger} auswertet, und ohne + * geschützte Leerzeichen, die für {@code \s} nicht als Leerraum zählen. Beides hinge sonst + * unsichtbar an Titeln, Nummern und Daten. + */ + private static String normiere(String zeile) { + return ZU_LEERRAUM.matcher(zeile).replaceAll(" ").replaceAll("\\s+", " ").strip(); + } + + /** + * Das Ausfertigungsdatum, das die Zeile „Vom …“ nennt. + * + * <p>Nur wenn das <em>ganze</em> Dokument genau eine solche Zeile führt. Ein Sammelheft stellt + * mehrere Verkündungen nebeneinander (so das thüringische GVBl. Nr. 2/2026 mit vieren), und + * welche von ihnen das Dokument ausmacht, sagt es nicht; dann bleibt das Datum lieber leer, als + * falsch zu sein. Denn an ihm hängt die Wiedererkennung des Heftes, und ein falsch bezeichnetes + * Heft wäre schlimmer als ein unbezeichnetes. Gesucht wird deshalb über den vollen Text und nicht + * bloß über die {@link #KOPFZEILEN} des Kopfes: Die zweite Verkündung stünde sonst außerhalb des + * Blickfeldes, und das Heft trüge den Namen der ersten. + */ + private static @Nullable LocalDate ausfertigung(String rohText) { + LocalDate gefunden = null; + for (var zeile : rohText.split("\n", -1)) { + var m = VOM_ZEILE.matcher(normiere(zeile)); + if (!m.matches()) { + continue; + } + if (gefunden != null) { + return null; + } + gefunden = DeutschesDatum.lies(m.group(1), m.group(2), m.group(3)); + } + return gefunden; } /** @@ -94,10 +138,7 @@ public final class DokumentErkenner { private static List<String> kopfZeilen(String rohText) { var zeilen = new ArrayList<String>(); for (var zeile : rohText.split("\n", -1)) { - // Der Rohtext trägt noch die Zeilenend- und Schriftgrößenmarken aus dem privaten - // Unicode-Bereich, die erst der TextBereiniger auswertet, dazu geschützte Leerzeichen, die - // für \s nicht als Leerraum zählen. Beides hinge sonst unsichtbar an Titeln und Nummern. - var normiert = ZU_LEERRAUM.matcher(zeile).replaceAll(" ").replaceAll("\\s+", " ").strip(); + var normiert = normiere(zeile); if (normiert.isEmpty()) { continue; } diff --git a/src/main/java/eu/mulk/aendggner/gesetz/Fortschreibung.java b/src/main/java/eu/mulk/aendggner/gesetz/Fortschreibung.java new file mode 100644 index 0000000..3d2c3b1 --- /dev/null +++ b/src/main/java/eu/mulk/aendggner/gesetz/Fortschreibung.java @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2026 Matthias Andreas Benkard <code@mail.matthias.benkard.de> +// SPDX-License-Identifier: AGPL-3.0-or-later +package eu.mulk.aendggner.gesetz; + +import eu.mulk.aendggner.aenderung.parse.DeutschesDatum; +import java.time.LocalDate; +import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; + +/** + * Ein Änderungsheft, das auf diese Fassung bereits angewandt worden ist. + * + * <p>Ein Befehl ist keine Zustandsbeschreibung, sondern eine Anordnung: Wer zweimal anfügt, fügt + * zweimal an. Dasselbe Heft ein zweites Mal auf seine eigene Ausgabe anzuwenden ist deshalb keine + * folgenlose Wiederholung — und dem Wortlaut allein ist das nicht anzusehen. Die Fassung führt + * darum mit, was auf ihr schon geschehen ist; im kanonischen Klartext steht das als Zeile + * „Fortgeschrieben durch: …“. + * + * <p>Die Bezeichnung ist zugleich der Schlüssel: Zwei Hefte gelten als dasselbe, wenn sie + * gleichlautend bezeichnet sind. Das trägt, weil die Bezeichnung aus dem Dokument selbst stammt + * (Art und Ausfertigungsdatum, bei Drucksachen deren Nummer) und nicht aus dem Dateinamen. + * + * @param bezeichnung wie das Heft sich nennt, z.B. „Änderungsgesetz vom 22. April 2026“. + * @param datum sein Ausfertigungsdatum, soweit die Bezeichnung eines nennt. Daran hängt die + * Altersrüge: Nach der Fortschreibung ist der Wortlaut so jung wie das jüngste angewandte Heft. + */ +public record Fortschreibung(String bezeichnung, @Nullable LocalDate datum) { + + /** „… vom 22. April 2026“ — das Ausfertigungsdatum, wie die Bezeichnung es führt. */ + private static final Pattern VOM = Pattern.compile("\\bvom " + DeutschesDatum.MUSTER + "\\b"); + + /** + * Liest eine Bezeichnung, wie der Klartext sie führt, und gewinnt das Datum aus ihr zurück. Die + * Bezeichnung bleibt dabei unangetastet — ausgegeben wird, was eingelesen wurde. + */ + public static Fortschreibung aus(String bezeichnung) { + var m = VOM.matcher(bezeichnung); + if (!m.find()) { + return new Fortschreibung(bezeichnung, null); + } + return new Fortschreibung(bezeichnung, DeutschesDatum.lies(m.group(1), m.group(2), m.group(3))); + } +} diff --git a/src/main/java/eu/mulk/aendggner/gesetz/Gesetz.java b/src/main/java/eu/mulk/aendggner/gesetz/Gesetz.java index f7e94c0..c2f5dd8 100644 --- a/src/main/java/eu/mulk/aendggner/gesetz/Gesetz.java +++ b/src/main/java/eu/mulk/aendggner/gesetz/Gesetz.java @@ -2,6 +2,8 @@ // SPDX-License-Identifier: AGPL-3.0-or-later package eu.mulk.aendggner.gesetz; +import java.time.LocalDate; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import org.jspecify.annotations.Nullable; @@ -13,6 +15,10 @@ import org.jspecify.annotations.Nullable; * @param stand der Stand der Quelle, soweit sie einen angibt; {@code null} bei handgepflegtem * Klartext. An ihm hängt die Auskunft, ob das Stammgesetz jünger ist als das Änderungsgesetz, * das darauf angewandt wird. + * @param fortschreibungen die Änderungshefte, die auf diesen Wortlaut bereits angewandt worden sind + * — leer bei jeder Fassung, die aus einer fremden Quelle stammt. Gefüllt wird die Liste erst, + * wenn das Erzeugnis selbst fortschreibt; sie überdauert die Kette, weil der kanonische + * Klartext sie mitführt (siehe {@link Fortschreibung}). */ public record Gesetz( String jurabk, @@ -20,11 +26,23 @@ public record Gesetz( String kurzue, List<Norm> normen, List<Gliederung> gliederungen, - @Nullable Stand stand) { + @Nullable Stand stand, + List<Fortschreibung> fortschreibungen) { public Gesetz { normen = List.copyOf(normen); gliederungen = List.copyOf(gliederungen); + fortschreibungen = List.copyOf(fortschreibungen); + } + + public Gesetz( + String jurabk, + String langue, + String kurzue, + List<Norm> normen, + List<Gliederung> gliederungen, + @Nullable Stand stand) { + this(jurabk, langue, kurzue, normen, gliederungen, stand, List.of()); } public Gesetz( @@ -45,14 +63,42 @@ public record Gesetz( } public Gesetz mitNormen(List<Norm> neueNormen) { - return new Gesetz(jurabk, langue, kurzue, neueNormen, gliederungen, stand); + return new Gesetz(jurabk, langue, kurzue, neueNormen, gliederungen, stand, fortschreibungen); } public Gesetz mitGliederungen(List<Gliederung> neueGliederungen) { - return new Gesetz(jurabk, langue, kurzue, normen, neueGliederungen, stand); + return new Gesetz(jurabk, langue, kurzue, normen, neueGliederungen, stand, fortschreibungen); } public Gesetz mitLangue(String neuerLangtitel) { - return new Gesetz(jurabk, neuerLangtitel, kurzue, normen, gliederungen, stand); + return new Gesetz( + jurabk, neuerLangtitel, kurzue, normen, gliederungen, stand, fortschreibungen); + } + + /** Vermerkt ein angewandtes Heft; die Reihenfolge ist die der Anwendung. */ + public Gesetz mitFortschreibung(Fortschreibung heft) { + var erweitert = new ArrayList<>(fortschreibungen); + erweitert.add(heft); + return new Gesetz(jurabk, langue, kurzue, normen, gliederungen, stand, erweitert); + } + + /** Ob dieses Heft auf den Wortlaut bereits angewandt worden ist. */ + public boolean traegt(Fortschreibung heft) { + return fortschreibungen.stream().anyMatch(f -> f.bezeichnung().equals(heft.bezeichnung())); + } + + /** + * Wie jung der Wortlaut wirklich ist: das späteste Datum, das entweder die Standangabe der Quelle + * oder eines der angewandten Hefte nennt. An ihm hängt die Altersrüge — und zwar auch in der + * Kette, denn wer ein Heft anwendet, macht den Wortlaut so jung wie dieses Heft. + */ + public @Nullable LocalDate wortlautStand() { + LocalDate juengste = stand != null ? stand.juengsteAenderung() : null; + for (var heft : fortschreibungen) { + if (heft.datum() != null && (juengste == null || heft.datum().isAfter(juengste))) { + juengste = heft.datum(); + } + } + return juengste; } } diff --git a/src/main/java/eu/mulk/aendggner/gesetz/Stand.java b/src/main/java/eu/mulk/aendggner/gesetz/Stand.java index 7541f5f..92f41be 100644 --- a/src/main/java/eu/mulk/aendggner/gesetz/Stand.java +++ b/src/main/java/eu/mulk/aendggner/gesetz/Stand.java @@ -3,6 +3,7 @@ package eu.mulk.aendggner.gesetz; import java.time.LocalDate; +import java.util.regex.Pattern; import org.jspecify.annotations.Nullable; /** @@ -19,4 +20,39 @@ import org.jspecify.annotations.Nullable; * @param juengsteAenderung das späteste Datum, das irgendeine Standangabe nennt — der wirkliche * Stand des Wortlauts; {@code null}, wenn keine Angabe ein Datum trägt. */ -public record Stand(String kommentar, @Nullable LocalDate juengsteAenderung) {} +public record Stand(String kommentar, @Nullable LocalDate juengsteAenderung) { + + /** „… v. 19.6.2020 I 1385“ — das Datum, das eine Standangabe nennt. */ + private static final Pattern STANDDATUM = + Pattern.compile("v\\. (\\d{1,2})\\.(\\d{1,2})\\.(\\d{4})"); + + /** Eine Standzeile mitsamt dem spätesten Datum, das in ihr steht. */ + public static Stand aus(String kommentar) { + return new Stand(kommentar, juengstesDatum(kommentar)); + } + + /** + * Das späteste Datum, das {@code text} nennt; {@code null}, wenn keines darin steht. Eine + * Standangabe nennt mitunter mehrere, und ein verschriebenes Datum („19.5..2020“) kommt vor — es + * bleibt außer Betracht, statt den ganzen Stand zu verwerfen. + */ + public static @Nullable LocalDate juengstesDatum(String text) { + LocalDate juengste = null; + var m = STANDDATUM.matcher(text); + while (m.find()) { + try { + var datum = + LocalDate.of( + Integer.parseInt(m.group(3)), + Integer.parseInt(m.group(2)), + Integer.parseInt(m.group(1))); + if (juengste == null || datum.isAfter(juengste)) { + juengste = datum; + } + } catch (RuntimeException verschrieben) { + // Ein unmögliches Datum bleibt außer Betracht. + } + } + return juengste; + } +} diff --git a/src/main/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoader.java b/src/main/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoader.java index 35561c6..c0fd0b9 100644 --- a/src/main/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoader.java +++ b/src/main/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoader.java @@ -165,10 +165,6 @@ public final class GiiXmlLoader { return null; } - /** „… v. 19.6.2020 I 1385“ — das Datum, das eine Standangabe nennt. */ - private static final Pattern STANDDATUM = - Pattern.compile("v\\. (\\d{1,2})\\.(\\d{1,2})\\.(\\d{4})"); - /** * Der Stand des Gesetzes: die Standzeile im Wortlaut und das späteste Datum, das * <em>irgendeine</em> Standangabe nennt. Die Hinweise („Änderung durch Art. 1 G v. 18.11.2020 … @@ -186,20 +182,9 @@ public final class GiiXmlLoader { if (zeile == null && "Stand".equals(kindText(angabe, "standtyp"))) { zeile = kommentar; } - var m = STANDDATUM.matcher(kommentar); - while (m.find()) { - try { - var datum = - LocalDate.of( - Integer.parseInt(m.group(3)), - Integer.parseInt(m.group(2)), - Integer.parseInt(m.group(1))); - if (juengste == null || datum.isAfter(juengste)) { - juengste = datum; - } - } catch (RuntimeException ignoriert) { - // Ein verschriebenes Datum („19.5..2020“) kommt vor; es bleibt außer Betracht. - } + var datum = Stand.juengstesDatum(kommentar); + if (datum != null && (juengste == null || datum.isAfter(juengste))) { + juengste = datum; } } return zeile == null ? null : new Stand(zeile, juengste); diff --git a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextAusgeber.java b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextAusgeber.java index 67434ca..3a82945 100644 --- a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextAusgeber.java +++ b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextAusgeber.java @@ -59,6 +59,11 @@ public final class LandesRechtTextAusgeber { if (gesetz.stand() != null) { sb.append("Stand: ").append(gesetz.stand().kommentar()).append('\n'); } + // Was das Erzeugnis selbst getan hat, steht mit im Kopf: Ohne diese Zeilen wüsste die nächste + // Stufe der Kette nicht, welche Hefte der Wortlaut schon trägt (siehe Fortschreibung). + for (var heft : gesetz.fortschreibungen()) { + sb.append("Fortgeschrieben durch: ").append(heft.bezeichnung()).append('\n'); + } var gliederungen = gesetz.gliederungen(); int gliederungsZeiger = 0; diff --git a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java index bef8a2c..7f7e2f4 100644 --- a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java +++ b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java @@ -3,9 +3,11 @@ package eu.mulk.aendggner.gesetz.land; import eu.mulk.aendggner.gesetz.Absatz; +import eu.mulk.aendggner.gesetz.Fortschreibung; import eu.mulk.aendggner.gesetz.Gesetz; import eu.mulk.aendggner.gesetz.Gliederung; import eu.mulk.aendggner.gesetz.Norm; +import eu.mulk.aendggner.gesetz.Stand; import eu.mulk.aendggner.gesetz.Superskript; import java.util.ArrayList; import java.util.List; @@ -38,6 +40,13 @@ final class LandesRechtTextParser { private static final Pattern ABSATZ_MARKER = Pattern.compile("^\\((\\d+[a-z]?)\\)\\s+"); + /** Die Standangabe der Quelle, wie der {@link LandesRechtTextAusgeber} sie schreibt. */ + private static final Pattern STAND_ZEILE = Pattern.compile("^Stand: (\\S.*)$"); + + /** Ein bereits angewandtes Änderungsheft, gleichfalls aus der Feder des Ausgebers. */ + private static final Pattern FORTSCHREIBUNGS_ZEILE = + Pattern.compile("^Fortgeschrieben durch: (\\S.*)$"); + // Juris-Abkürzung direkt hinter dem Langtitel. Bayern führt einteilige Kürzel („(BayJG)“), die // übrigen Länder oft mehrteilige („(GO NRW)“); Nur die Zeile unmittelbar nach dem Titel wird // geprüft, sodass spätere Fundstellen-Klammern („(BayRS V S. 595)“) nicht getroffen werden. @@ -189,7 +198,20 @@ final class LandesRechtTextParser { } } // Rest des Titelblocks (Datum, Fundstellen, Vollzitat) bis zur ersten Struktur überspringen. + // Zwei Zeilenformen werden dabei aufgenommen statt verworfen: die Standangabe der Quelle und + // die Hefte, die das Erzeugnis selbst schon angewandt hat. Beides gibt der Ausgeber aus; würde + // es hier fallen, so verlöre die Kette bei jedem Schritt ihr Gedächtnis. + Stand stand = null; + var fortschreibungen = new ArrayList<Fortschreibung>(); while (i < zeilen.size() && !istStrukturZeile(zeilen, i)) { + var kopfzeile = zeilen.get(i).strip(); + var standZeile = STAND_ZEILE.matcher(kopfzeile); + var heftZeile = FORTSCHREIBUNGS_ZEILE.matcher(kopfzeile); + if (standZeile.matches()) { + stand = Stand.aus(standZeile.group(1).strip()); + } else if (heftZeile.matches()) { + fortschreibungen.add(Fortschreibung.aus(heftZeile.group(1).strip())); + } i++; } @@ -347,7 +369,14 @@ final class LandesRechtTextParser { "Kein „§ N“- oder „Art. N“-Normkopf gefunden — ist das eine konsolidierte Fassung im" + " kanonischen Klartextformat?"); } - return new Gesetz(jurabk != null ? jurabk : langue, langue, kurzue, normen, gliederungen); + return new Gesetz( + jurabk != null ? jurabk : langue, + langue, + kurzue, + normen, + gliederungen, + stand, + fortschreibungen); } /** Eröffnet die Zeile eine Struktureinheit (Gliederung, Inhaltsübersicht oder Normkopf)? */ |
