diff options
Diffstat (limited to 'src/main/java')
11 files changed, 605 insertions, 50 deletions
diff --git a/src/main/java/eu/mulk/aendggner/AendGgner.java b/src/main/java/eu/mulk/aendggner/AendGgner.java index 58b0d50..fbe80fd 100644 --- a/src/main/java/eu/mulk/aendggner/AendGgner.java +++ b/src/main/java/eu/mulk/aendggner/AendGgner.java @@ -12,6 +12,8 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.EnumMap; import java.util.List; @@ -103,6 +105,14 @@ public class AendGgner implements Callable<Integer> { + "(default: all articles whose introduction names the base law).") private String artikel; + @Option( + names = "--stichtag", + paramLabel = "<JJJJ-MM-TT>", + description = + "Produce the version in force on this date. Commands of the amendment act that had not " + + "yet entered into force are listed separately instead of being applied.") + private String stichtag; + public static void main(String... args) { int exitCode = new CommandLine(new AendGgner()).execute(args); System.exit(exitCode); @@ -199,7 +209,18 @@ public class AendGgner implements Callable<Integer> { return 1; } - var ergebnis = Pipeline.erzeugeSynopse(baseFile, patches, artikel, vollstaendig); + LocalDate tag = null; + if (stichtag != null) { + try { + tag = LocalDate.parse(stichtag); + } catch (DateTimeParseException e) { + System.err.println( + "Fehler: „" + stichtag + "“ ist kein Datum in der Form JJJJ-MM-TT (z.B. 2024-01-01)."); + return 1; + } + } + + var ergebnis = Pipeline.erzeugeSynopse(baseFile, patches, artikel, vollstaendig, tag); if (output.equals("-")) { System.out.println(ergebnis.html()); diff --git a/src/main/java/eu/mulk/aendggner/Pipeline.java b/src/main/java/eu/mulk/aendggner/Pipeline.java index 7949e7f..abb19d5 100644 --- a/src/main/java/eu/mulk/aendggner/Pipeline.java +++ b/src/main/java/eu/mulk/aendggner/Pipeline.java @@ -4,14 +4,17 @@ package eu.mulk.aendggner; import eu.mulk.aendggner.aenderung.DokumentArt; import eu.mulk.aendggner.aenderung.DokumentKopf; +import eu.mulk.aendggner.aenderung.Inkrafttreten; import eu.mulk.aendggner.aenderung.parse.AenderungsantragParser; import eu.mulk.aendggner.aenderung.parse.AenderungsgesetzParser; +import eu.mulk.aendggner.aenderung.parse.DeutschesDatum; import eu.mulk.aendggner.aenderung.parse.DokumentErkenner; import eu.mulk.aendggner.aenderung.parse.EntwurfsPatcher; import eu.mulk.aendggner.aenderung.parse.PatchTextExtraktor; import eu.mulk.aendggner.aenderung.parse.SuperskriptModus; import eu.mulk.aendggner.aenderung.parse.TextBereiniger; import eu.mulk.aendggner.anwendung.BefehlAnwender; +import eu.mulk.aendggner.anwendung.Grund; import eu.mulk.aendggner.gesetz.Gesetz; import eu.mulk.aendggner.gesetz.Superskript; import eu.mulk.aendggner.gesetz.gii.GiiXmlLoader; @@ -19,8 +22,10 @@ import eu.mulk.aendggner.gesetz.land.LandesRechtLoader; import eu.mulk.aendggner.synopse.HtmlRenderer; import eu.mulk.aendggner.synopse.SynopseBuilder; import java.nio.file.Path; +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; +import java.util.Set; import org.jspecify.annotations.Nullable; /** @@ -85,16 +90,41 @@ public final class Pipeline { /** Bequemlichkeit für Befehlszeile und Tests; im Browser gibt es keine {@link Path}e. */ public static Ergebnis erzeugeSynopse( Path baseFile, List<Path> patches, String artikel, boolean vollstaendig) throws Exception { + return erzeugeSynopse(baseFile, patches, artikel, vollstaendig, null); + } + + public static Ergebnis erzeugeSynopse( + Path baseFile, + List<Path> patches, + String artikel, + boolean vollstaendig, + @Nullable LocalDate stichtag) + throws Exception { var patchQuellen = new ArrayList<Quelle>(); for (var patch : patches) { patchQuellen.add(Quelle.lies(patch)); } - return erzeugeSynopse(Quelle.lies(baseFile), patchQuellen, artikel, vollstaendig); + return erzeugeSynopse(Quelle.lies(baseFile), patchQuellen, artikel, vollstaendig, stichtag); } public static Ergebnis erzeugeSynopse( Quelle baseFile, List<Quelle> patches, String artikel, boolean vollstaendig) throws Exception { + return erzeugeSynopse(baseFile, patches, artikel, vollstaendig, null); + } + + /** + * @param stichtag die Fassung dieses Tages erzeugen: Befehle, die an ihm noch nicht in Kraft + * waren, bleiben unangewandt und werden gesondert ausgewiesen. {@code null} = alle Befehle + * anwenden (dann warnt die Synopse, wenn das Gesetz gestaffelt in Kraft tritt). + */ + public static Ergebnis erzeugeSynopse( + Quelle baseFile, + List<Quelle> patches, + String artikel, + boolean vollstaendig, + @Nullable LocalDate stichtag) + throws Exception { var altesGesetz = ladeStammgesetz(baseFile); var extraktor = new PatchTextExtraktor(superskriptModus(altesGesetz)); var parser = new AenderungsgesetzParser(); @@ -106,6 +136,7 @@ public final class Pipeline { var protokoll = new ArrayList<BefehlAnwender.AngewandteAenderung>(); var quellen = new ArrayList<String>(); boolean entwurfsfassung = false; + Inkrafttreten inkrafttreten = null; for (var dokument : dokumente) { var parseErgebnis = parser.parse(dokument.text(), gesetz, artikel, entwurfsGrenzen(dokument)); @@ -115,16 +146,26 @@ public final class Pipeline { .formatted( dokument.quelle().name(), dokument.kopf().anzeigeName(), gesetz.jurabk())); } - var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle()); + if (parseErgebnis.inkrafttreten() != null) { + inkrafttreten = parseErgebnis.inkrafttreten(); + } + var geteilt = teileNachStichtag(parseErgebnis, stichtag, warnungen); + var anwendung = BefehlAnwender.anwenden(gesetz, geteilt.anzuwenden()); gesetz = anwendung.neu(); protokoll.addAll(anwendung.protokoll()); + protokoll.addAll(geteilt.zurueckgestellt()); warnungen.addAll(parseErgebnis.warnungen()); entwurfsfassung |= dokument.kopf().art().istEntwurfsfassung(); quellen.add(dokument.quellenAngabe(parseErgebnis.artikel())); } + if (stichtag == null && inkrafttreten != null && inkrafttreten.gestaffelt()) { + warnungen.add(staffelungsWarnung(inkrafttreten)); + } var gesamtErgebnis = new BefehlAnwender.AnwendungsErgebnis(gesetz, protokoll); - var synopse = SynopseBuilder.baue(altesGesetz, gesamtErgebnis, warnungen, vollstaendig); + var synopse = + SynopseBuilder.baue( + altesGesetz, gesamtErgebnis, warnungen, vollstaendig, inkrafttreten, stichtag); var quellenZeile = baseFile.name() + " + " + String.join(" + ", quellen); var html = HtmlRenderer.rendere(synopse, quellenZeile, entwurfsfassung); @@ -136,6 +177,88 @@ public final class Pipeline { protokoll.size()); } + /** Die Befehle eines Dokuments, geschieden nach dem, was am Stichtag schon galt. */ + private record Geteilt( + List<eu.mulk.aendggner.aenderung.Aenderungsbefehl> anzuwenden, + List<BefehlAnwender.AngewandteAenderung> zurueckgestellt) {} + + /** + * Scheidet die Befehle nach dem Stichtag. Ohne Stichtag bleibt alles beisammen — dann ist die + * Fassung die des vollständigen Inkrafttretens, worauf die Staffelungswarnung hinweist. + * + * <p>Eine Anordnung ohne bestimmbares Datum („am Tag nach der Verkündung“ — der Verkündungstag + * steht nicht im Gesetzestext) gilt als am Stichtag bereits wirksam; geraten wird nicht, aber es + * wird gesagt. + */ + private static Geteilt teileNachStichtag( + AenderungsgesetzParser.ParseErgebnis parseErgebnis, + @Nullable LocalDate stichtag, + List<String> warnungen) { + var befehle = parseErgebnis.befehle(); + var inkrafttreten = parseErgebnis.inkrafttreten(); + if (stichtag == null || befehle.isEmpty()) { + return new Geteilt(befehle, List.of()); + } + if (inkrafttreten == null) { + warnungen.add( + "Es wurde ein Stichtag gewählt, das Änderungsdokument trägt aber keine lesbare" + + " Inkrafttretens-Vorschrift; angewandt wurden deshalb alle Befehle."); + return new Geteilt(befehle, List.of()); + } + var anzuwenden = new ArrayList<eu.mulk.aendggner.aenderung.Aenderungsbefehl>(); + var zurueckgestellt = new ArrayList<BefehlAnwender.AngewandteAenderung>(); + boolean unbestimmt = false; + for (var befehl : befehle) { + var regel = inkrafttreten.fuer(befehl.provenienz()).orElse(null); + if (regel == null || regel.datum() == null) { + unbestimmt |= regel != null; + anzuwenden.add(befehl); + continue; + } + if (regel.datum().isAfter(stichtag)) { + zurueckgestellt.add( + new BefehlAnwender.AngewandteAenderung( + befehl, + BefehlAnwender.Status.NICHT_IN_KRAFT, + "Tritt erst am " + + DeutschesDatum.schreibe(regel.datum()) + + " in Kraft („" + + regel.wortlaut() + + "“).", + Set.of(), + Grund.NOCH_NICHT_IN_KRAFT)); + continue; + } + anzuwenden.add(befehl); + } + if (unbestimmt) { + warnungen.add( + "Eine Inkrafttretens-Anordnung nennt kein bestimmtes Datum, sondern knüpft an die" + + " Verkündung an; der Verkündungstag steht nicht im Gesetzestext. Die betroffenen" + + " Befehle wurden als am Stichtag bereits geltend behandelt."); + } + return new Geteilt(List.copyOf(anzuwenden), List.copyOf(zurueckgestellt)); + } + + /** + * Wird ein Änderungsgesetz gestaffelt wirksam, so ist die auf einen Schlag gerechnete Fassung + * eine, die an keinem Tag gegolten hat. Das darf nicht unausgesprochen bleiben. + */ + private static String staffelungsWarnung(Inkrafttreten inkrafttreten) { + var sb = new StringBuilder("Das Änderungsgesetz tritt gestaffelt in Kraft"); + inkrafttreten + .grundregel() + .ifPresent(r -> sb.append(" — im Grundsatz: „").append(r.wortlaut()).append("“")); + for (var sonder : inkrafttreten.sonderregeln()) { + sb.append("; abweichend: „").append(sonder.wortlaut()).append("“"); + } + return sb.append( + ". Die gezeigte Fassung wendet alle Befehle an und gilt daher erst, wenn das Gesetz" + + " vollständig in Kraft getreten ist; die Fassung eines bestimmten Tages ergibt" + + " sich nur mit einem Stichtag.") + .toString(); + } + /** * Liest die Änderungsdokumente ein, bestimmt ihre Art und sortiert die aus, aus denen keine * Synopse zu gewinnen ist. Verworfen wird nichts stillschweigend: Jedes ausgesonderte Dokument diff --git a/src/main/java/eu/mulk/aendggner/aenderung/Inkrafttreten.java b/src/main/java/eu/mulk/aendggner/aenderung/Inkrafttreten.java new file mode 100644 index 0000000..0dfe754 --- /dev/null +++ b/src/main/java/eu/mulk/aendggner/aenderung/Inkrafttreten.java @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: 2020 Matthias Andreas Benkard <code@mail.matthias.benkard.de> +// 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.ArrayList; +import java.util.List; +import java.util.Optional; +import org.jspecify.annotations.Nullable; + +/** + * Was der Schlussartikel eines Änderungsgesetzes über sein Inkrafttreten sagt. + * + * <p>Ein Änderungsgesetz tritt nicht notwendig auf einen Schlag in Kraft. Das UWG-Änderungsgesetz + * von 2026 etwa lässt seinen Artikel 1 Nummer 2 Buchstabe c am 19. Juni 2026 wirksam werden und + * alles Übrige erst am 27. September 2026 — wer alle Befehle zugleich anwendet, erhält eine + * Fassung, die an keinem einzigen Tag gegolten hat. Deshalb trägt jeder Befehl seinen Stichtag. + * + * <p>Erfunden wird nichts: Nennt der Wortlaut kein bestimmtes Datum („am Tag nach der Verkündung“ — + * der Verkündungstag steht nicht im Gesetzestext), so bleibt {@link Regel#datum()} leer und es + * bleibt beim Wortlaut. + */ +public record Inkrafttreten(List<Regel> regeln) { + + /** + * Eine Inkrafttretens-Anordnung. + * + * @param datum der Tag, an dem sie wirkt; {@code null}, wenn der Wortlaut keinen nennt. + * @param wortlaut der Satz, aus dem sie stammt — er bleibt maßgeblich. + * @param giltFuer die Punkte, die sie erfasst; leer heißt: das ganze Gesetz (Grundregel). + */ + public record Regel(@Nullable LocalDate datum, String wortlaut, List<Punktbezug> giltFuer) { + + public boolean istGrundregel() { + return giltFuer.isEmpty(); + } + + /** „am 1. Oktober 2024“ bzw. der Wortlaut, wo kein Datum bestimmbar ist. */ + public String anzeige() { + return datum == null ? wortlaut : DeutschesDatum.schreibe(datum); + } + } + + /** + * Ein Verweis des Schlussartikels auf einen Punkt des eigenen Gesetzes: „Artikel 1 Nummer 11 + * Buchstabe a Doppelbuchstabe bb“. + * + * @param artikel die Artikel- bzw. Paragraphennummer, wie {@link Provenienz#artikel()} sie führt. + * @param pfadPraefix der Anfang des Gliederungspfades („11. a) bb)“); leer heißt: der ganze + * Artikel. + */ + public record Punktbezug(String artikel, String pfadPraefix) { + + boolean trifft(Provenienz provenienz) { + if (!artikel.equals(provenienz.artikel())) { + return false; + } + var pfad = provenienz.gliederungsPfad(); + return pfadPraefix.isEmpty() + || pfad.equals(pfadPraefix) + || pfad.startsWith(pfadPraefix + " "); + } + + /** Je länger der Pfad, desto besonderer der Bezug. */ + int besonderheit() { + return pfadPraefix.length(); + } + } + + /** + * Die Regel, die für diesen Befehl gilt: die besonderste, die ihn erfasst, sonst die Grundregel. + */ + public Optional<Regel> fuer(Provenienz provenienz) { + Regel beste = null; + int besteBesonderheit = -1; + for (var regel : regeln) { + for (var bezug : regel.giltFuer()) { + if (bezug.trifft(provenienz) && bezug.besonderheit() > besteBesonderheit) { + beste = regel; + besteBesonderheit = bezug.besonderheit(); + } + } + } + return beste != null ? Optional.of(beste) : grundregel(); + } + + public Optional<Regel> grundregel() { + return regeln.stream().filter(Regel::istGrundregel).findFirst(); + } + + /** Die Sonderregeln, in der Reihenfolge des Schlussartikels. */ + public List<Regel> sonderregeln() { + var sonder = new ArrayList<Regel>(); + for (var regel : regeln) { + if (!regel.istGrundregel()) { + sonder.add(regel); + } + } + return List.copyOf(sonder); + } + + /** + * Wahr, wenn das Gesetz an mehr als einem Tag wirksam wird — nur dann ist die auf einen Schlag + * gerechnete Fassung eine, die es so nie gab. + */ + public boolean gestaffelt() { + return !sonderregeln().isEmpty(); + } +} 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 06186c7..7cced90 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParser.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParser.java @@ -4,14 +4,12 @@ package eu.mulk.aendggner.aenderung.parse; import eu.mulk.aendggner.aenderung.Aenderungsbefehl; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.UnbekannterBefehl; +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.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; -import java.util.Locale; import java.util.Optional; import java.util.regex.Pattern; import org.jboss.logging.Logger; @@ -37,8 +35,21 @@ public final class AenderungsgesetzParser { private static final Pattern PARAGRAPH_UEBERSCHRIFT_AUSSEN = Pattern.compile("^§\\s*(\\d+[a-z]?)$"); + /** + * @param inkrafttreten was der Schlussartikel über das Inkrafttreten sagt; {@code null}, wenn das + * Dokument keinen lesbaren trägt (Entwürfe ohne Schlussartikel, Auszüge). + */ public record ParseErgebnis( - List<Aenderungsbefehl> befehle, List<String> artikel, List<String> warnungen) {} + List<Aenderungsbefehl> befehle, + List<String> artikel, + List<String> warnungen, + @Nullable Inkrafttreten inkrafttreten) { + + public ParseErgebnis( + List<Aenderungsbefehl> befehle, List<String> artikel, List<String> warnungen) { + this(befehle, artikel, warnungen, null); + } + } /** * @param text der bereinigte Lineartext des Änderungsgesetzes. @@ -70,8 +81,10 @@ public final class AenderungsgesetzParser { var befehle = new ArrayList<Aenderungsbefehl>(); var betroffeneArtikel = new ArrayList<String>(); var warnungen = new ArrayList<>(zitate.warnungen()); + int letzterBetroffen = -1; - for (var artikel : artikelBloecke) { + for (int artikelIndex = 0; artikelIndex < artikelBloecke.size(); artikelIndex++) { + var artikel = artikelBloecke.get(artikelIndex); var relevant = artikelFilter != null ? artikel.label.equals(artikelFilter) @@ -95,6 +108,7 @@ public final class AenderungsgesetzParser { + " Namen des Stammgesetzes)."); } betroffeneArtikel.add(artikel.label); + letzterBetroffen = artikelIndex; var scan = GliederungsScanner.scanne(artikel.zeilen); if (scan.punkte().isEmpty()) { @@ -114,7 +128,14 @@ public final class AenderungsgesetzParser { } } - return new ParseErgebnis(befehle, betroffeneArtikel, warnungen); + // Der Schlussartikel steht hinter dem letzten ändernden Artikel desselben Gesetzes. + var inkrafttreten = + letzterBetroffen < 0 + ? null + : InkrafttretensLeser.waehle( + artikelBloecke.stream().map(b -> String.join("\n", b.zeilen())).toList(), + letzterBetroffen + 1); + return new ParseErgebnis(befehle, betroffeneArtikel, warnungen, inkrafttreten); } private static final String AENDERUNGSFORMEL = "wird wie folgt geändert:"; @@ -370,14 +391,13 @@ public final class AenderungsgesetzParser { if (!satzDatum.find()) { return Optional.empty(); } - var genannt = - datum(satzDatum.group(3), MONATSNAMEN.indexOf(satzDatum.group(2)) + 1, satzDatum.group(1)); + var genannt = DeutschesDatum.lies(satzDatum.group(1), satzDatum.group(2), satzDatum.group(3)); if (genannt == null || !stand.juengsteAenderung().isAfter(genannt)) { return Optional.empty(); } return Optional.of( "Das Stammgesetz ist jünger als das Änderungsgesetz: Sein Wortlaut ist bis zum " - + stand.juengsteAenderung().format(DATUM) + + DeutschesDatum.schreibe(stand.juengsteAenderung()) + " fortgeschrieben („" + stand.kommentar() + "“), während der Einleitungssatz die Fassung vom " @@ -391,9 +411,6 @@ public final class AenderungsgesetzParser { + " ihnen nur mit der zeitrichtigen Fassung des Stammgesetzes."); } - private static final DateTimeFormatter DATUM = - DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.GERMAN); - /** * „… 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 @@ -402,34 +419,7 @@ public final class AenderungsgesetzParser { * geänderten Gesetz eine Warnung. */ private static final Pattern EINLEITUNGS_DATUM = - Pattern.compile( - "durch (?:Artikel|Art\\.)[^.]{0,80}?vom (\\d{1,2})\\. (\\p{Lu}\\p{L}+) (\\d{4})"); - - private static final List<String> MONATSNAMEN = - List.of( - "Januar", - "Februar", - "März", - "April", - "Mai", - "Juni", - "Juli", - "August", - "September", - "Oktober", - "November", - "Dezember"); - - private static @Nullable LocalDate datum(String jahr, int monat, String tag) { - if (monat < 1 || monat > 12) { - return null; - } - try { - return LocalDate.of(Integer.parseInt(jahr), monat, Integer.parseInt(tag)); - } catch (RuntimeException e) { - return null; - } - } + Pattern.compile("durch (?:Artikel|Art\\.)[^.]{0,80}?vom " + DeutschesDatum.MUSTER); private static final List<String> STAMM_SUFFIXE = List.of("es", "er", "en", "em", "e", "s", "n"); diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/DeutschesDatum.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/DeutschesDatum.java new file mode 100644 index 0000000..6ecb161 --- /dev/null +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/DeutschesDatum.java @@ -0,0 +1,60 @@ +// 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 java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Locale; +import org.jspecify.annotations.Nullable; + +/** + * Ausgeschriebene Daten, wie Gesetzestexte sie führen: „19. Juni 2020“. + * + * <p>Gebraucht an zwei Stellen, die nichts miteinander zu tun haben — die Altersrüge liest das + * Datum aus dem Einleitungssatz des Änderungsgesetzes, der {@link InkrafttretensLeser} liest es aus + * dessen Schlussartikel. Damit beide dieselbe Schreibweise verstehen, steht die Erkennung hier. + */ +public final class DeutschesDatum { + + /** Tag, Monatsname, Jahr — als Gruppen 1 bis 3. Ohne Anker; zum Einbetten gedacht. */ + public static final String MUSTER = "(\\d{1,2})\\. (\\p{Lu}\\p{L}+) (\\d{4})"; + + private static final DateTimeFormatter AUSGABE = + DateTimeFormatter.ofPattern("d. MMMM yyyy", Locale.GERMAN); + + private static final List<String> MONATSNAMEN = + List.of( + "Januar", + "Februar", + "März", + "April", + "Mai", + "Juni", + "Juli", + "August", + "September", + "Oktober", + "November", + "Dezember"); + + private DeutschesDatum() {} + + /** „19“, „Juni“, „2020“ → 2020-06-19; {@code null} bei unbekanntem Monat oder unmöglichem Tag. */ + public static @Nullable LocalDate lies(String tag, String monatsname, String jahr) { + int monat = MONATSNAMEN.indexOf(monatsname) + 1; + if (monat < 1) { + return null; + } + try { + return LocalDate.of(Integer.parseInt(jahr), monat, Integer.parseInt(tag)); + } catch (RuntimeException e) { + return null; + } + } + + /** 2020-06-19 → „19. Juni 2020“. */ + public static String schreibe(LocalDate datum) { + return datum.format(AUSGABE); + } +} diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/InkrafttretensLeser.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/InkrafttretensLeser.java new file mode 100644 index 0000000..2892466 --- /dev/null +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/InkrafttretensLeser.java @@ -0,0 +1,154 @@ +// 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 eu.mulk.aendggner.aenderung.Inkrafttreten; +import eu.mulk.aendggner.aenderung.Inkrafttreten.Punktbezug; +import eu.mulk.aendggner.aenderung.Inkrafttreten.Regel; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; + +/** + * Liest den Schlussartikel eines Änderungsgesetzes — den, der das Inkrafttreten regelt. + * + * <p>Er wird von der Artikelwahl des {@link AenderungsgesetzParser} nicht erfasst, denn er trägt + * keine Änderungsformel und nennt kein Stammgesetz. Gelesen werden muss er trotzdem: Ohne ihn weiß + * niemand, ab wann die errechnete Fassung gilt, und bei gestaffeltem Inkrafttreten gilt sie + * womöglich nie im ganzen. + */ +public final class InkrafttretensLeser { + + // Absatzmarke „(1) “ am Anfang oder nach Leerraum. + private static final Pattern ABSATZ = Pattern.compile("(?:^|(?<=\\s))\\((\\d+[a-z]?)\\)\\s+"); + + // „… tritt/treten … in Kraft.“ — das Außerkrafttreten bleibt außer Betracht (es sagt nichts + // darüber, wann die Änderung wirkt), deshalb der Ausschluss von „außer“. + private static final Pattern IN_KRAFT = + Pattern.compile("\\b(?:tritt|treten)\\b.{0,400}?(?<!außer )\\bin Kraft\\b", Pattern.DOTALL); + + private static final Pattern SATZ_ENDE = Pattern.compile("(?<!außer )\\bin Kraft\\b\\s*\\.?"); + + /** „am 1. Oktober 2024“, „mit Wirkung vom 18. November 2020“. */ + private static final Pattern DATUM = Pattern.compile("\\b(?:am|vom)\\s+" + DeutschesDatum.MUSTER); + + /** + * „Artikel 1 Nummer 11 Buchstabe a Doppelbuchstabe bb“ — ein Verweis des Schlussartikels auf + * einen Punkt des eigenen Gesetzes. Bayerische Änderungsgesetze gliedern sich in Paragraphen; + * deren Schlussvorschrift verweist entsprechend („§ 3 Nr. 2“). + */ + private static final Pattern PUNKTBEZUG = + Pattern.compile( + "(?:Artikel|Art\\.|§)\\s*(\\d+[a-z]?)" + + "(?:\\s+(?:Nummer|Nr\\.)\\s+(\\d+[a-z]?))?" + + "(?:\\s+Buchstabe\\s+(\\p{Ll})\\b)?" + + "(?:\\s+Doppelbuchstabe\\s+(\\p{Ll}{2})\\b)?"); + + private static final Pattern AENDERUNGSFORMEL = Pattern.compile("wird wie folgt geändert"); + + private InkrafttretensLeser() {} + + /** + * Sucht unter den Artikelblöcken den Schlussartikel und liest ihn. + * + * <p>Maßgeblich ist der <em>erste</em> Block ab {@code abIndex}, der vom Inkrafttreten handelt + * und selbst nichts ändert. Der Index ist der des letzten Artikels, der das Stammgesetz betrifft: + * Ein Gesetzblatt enthält mehrere Gesetze mit je eigener Schlussvorschrift, und maßgeblich ist + * die, die auf das eigene Gesetz folgt — nicht die eines fremden weiter vorn. + * + * @return {@code null}, wenn kein solcher Block da ist oder er keine lesbare Anordnung enthält. + */ + public static @Nullable Inkrafttreten waehle(List<String> blockTexte, int abIndex) { + for (int i = Math.max(abIndex, 0); i < blockTexte.size(); i++) { + var text = blockTexte.get(i); + if (AENDERUNGSFORMEL.matcher(text).find()) { + continue; + } + var gelesen = lies(text); + if (gelesen != null) { + return gelesen; + } + } + return null; + } + + /** Liest die Anordnungen eines einzelnen Blocks; {@code null}, wenn er keine trägt. */ + public static @Nullable Inkrafttreten lies(String blockText) { + var text = blockText.replaceAll("\\s+", " ").strip(); + var regeln = new ArrayList<Regel>(); + for (var absatz : teileInAbsaetze(text)) { + var regel = liesAbsatz(absatz); + if (regel != null) { + regeln.add(regel); + } + } + return regeln.isEmpty() ? null : new Inkrafttreten(List.copyOf(regeln)); + } + + /** + * Zerlegt den Schlussartikel an seinen Absatzmarken. Ohne Marken ist der ganze Text ein Absatz + * („Dieses Gesetz tritt am Tage nach seiner Verkündung in Kraft.“). + */ + private static List<String> teileInAbsaetze(String text) { + var marken = ABSATZ.matcher(text); + var grenzen = new ArrayList<Integer>(); + while (marken.find()) { + grenzen.add(marken.start()); + } + if (grenzen.isEmpty()) { + return List.of(text); + } + var absaetze = new ArrayList<String>(grenzen.size()); + for (int i = 0; i < grenzen.size(); i++) { + int ende = i + 1 < grenzen.size() ? grenzen.get(i + 1) : text.length(); + absaetze.add(text.substring(grenzen.get(i), ende)); + } + return absaetze; + } + + private static @Nullable Regel liesAbsatz(String absatz) { + if (!IN_KRAFT.matcher(absatz).find()) { + return null; + } + // Der Anordnungssatz endet bei „in Kraft“; was danach steht, gehört nicht mehr dazu (im GEG + // klebt der Schlusssatz über die Rechte des Bundesrates unmittelbar an ihm). + var ende = SATZ_ENDE.matcher(absatz); + var satz = ende.find() ? absatz.substring(0, ende.end()).strip() : absatz.strip(); + // Die Absatzmarke selbst gehört nicht zum Wortlaut der Anordnung. + var wortlaut = ABSATZ.matcher(satz).replaceFirst("").strip(); + + var datumTreffer = DATUM.matcher(satz); + var datum = + datumTreffer.find() + ? DeutschesDatum.lies( + datumTreffer.group(1), datumTreffer.group(2), datumTreffer.group(3)) + : null; + + return new Regel(datum, wortlaut, bezuege(satz)); + } + + /** + * Die Punkte, die der Satz benennt. Nennt er keine, so ist es die Grundregel („Dieses Gesetz + * tritt … in Kraft“) — der Vorbehalt zugunsten späterer Absätze („vorbehaltlich des Absatzes 2“) + * verweist auf den Schlussartikel selbst und ist kein Punktbezug. + */ + private static List<Punktbezug> bezuege(String satz) { + var bezuege = new ArrayList<Punktbezug>(); + var treffer = PUNKTBEZUG.matcher(satz); + while (treffer.find()) { + var pfad = new StringBuilder(); + if (treffer.group(2) != null) { + pfad.append(treffer.group(2)).append('.'); + if (treffer.group(3) != null) { + pfad.append(' ').append(treffer.group(3)).append(')'); + if (treffer.group(4) != null) { + pfad.append(' ').append(treffer.group(4)).append(')'); + } + } + } + bezuege.add(new Punktbezug(treffer.group(1), pfad.toString())); + } + return List.copyOf(bezuege); + } +} diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java index 677b0ee..cccba63 100644 --- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java +++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java @@ -46,7 +46,9 @@ public final class BefehlAnwender { public enum Status { ANGEWANDT, - MANUELL_PRUEFEN + MANUELL_PRUEFEN, + /** Der Befehl gehört zu einem Teil des Änderungsgesetzes, der am Stichtag noch nicht galt. */ + NICHT_IN_KRAFT } /** diff --git a/src/main/java/eu/mulk/aendggner/anwendung/Grund.java b/src/main/java/eu/mulk/aendggner/anwendung/Grund.java index 065567c..d463846 100644 --- a/src/main/java/eu/mulk/aendggner/anwendung/Grund.java +++ b/src/main/java/eu/mulk/aendggner/anwendung/Grund.java @@ -41,7 +41,10 @@ public enum Grund { NICHT_UNTERSTUETZT("Nicht unterstützt"), /** Die Anwendung ist mit einem Fehler abgebrochen. */ - FEHLGESCHLAGEN("Anwendung fehlgeschlagen"); + FEHLGESCHLAGEN("Anwendung fehlgeschlagen"), + + /** Der Befehl war am gewählten Stichtag noch nicht in Kraft; angewandt wurde er deshalb nicht. */ + NOCH_NICHT_IN_KRAFT("Noch nicht in Kraft"); private final String bezeichnung; diff --git a/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java b/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java index 9b8f406..d2586c2 100644 --- a/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java +++ b/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java @@ -3,6 +3,7 @@ package eu.mulk.aendggner.synopse; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.UnbekannterBefehl; +import eu.mulk.aendggner.aenderung.parse.DeutschesDatum; import eu.mulk.aendggner.anwendung.BefehlAnwender; import eu.mulk.aendggner.anwendung.Grund; import eu.mulk.aendggner.gesetz.Norm; @@ -98,6 +99,19 @@ public final class HtmlRenderer { + " geänderte Normen, " + synopse.manuellZuPruefen().size() + " manuell zu prüfende Befehle"); + if (synopse.inkrafttreten() != null) { + var grundregel = synopse.inkrafttreten().grundregel(); + if (grundregel.isPresent()) { + vorspannZeile( + sb, + "Inkrafttreten", + grundregel.get().anzeige() + + (synopse.inkrafttreten().gestaffelt() ? " (gestaffelt, siehe unten)" : "")); + } + } + if (synopse.stichtag() != null) { + vorspannZeile(sb, "Stichtag", DeutschesDatum.schreibe(synopse.stichtag())); + } vorspannZeile(sb, "Geltung", entwurfsfassung ? "nichtamtlich, Entwurfsstand" : "nichtamtlich"); sb.append("</dl>\n"); } @@ -219,7 +233,9 @@ public final class HtmlRenderer { } private static void rendereManuellZuPruefen(StringBuilder sb, Synopse synopse) { - if (synopse.manuellZuPruefen().isEmpty() && synopse.warnungen().isEmpty()) { + if (synopse.manuellZuPruefen().isEmpty() + && synopse.warnungen().isEmpty() + && synopse.nichtInKraft().isEmpty()) { return; } sb.append("<section class=\"manuell\">\n<h2>Manuell prüfen</h2>\n"); @@ -264,6 +280,23 @@ public final class HtmlRenderer { sb.append("</ol>\n"); } } + if (!synopse.nichtInKraft().isEmpty()) { + // Nicht angewandt, aber auch nicht zu beanstanden: Diese Befehle galten am Stichtag noch + // nicht. Sie stehen deshalb für sich und nicht unter den Gründen des Scheiterns. + sb.append("<h3>Am Stichtag noch nicht in Kraft <span class=\"anzahl\">") + .append(synopse.nichtInKraft().size()) + .append("</span></h3>\n<ol>\n"); + for (var eintrag : synopse.nichtInKraft()) { + sb.append("<li><strong>") + .append(esc(eintrag.befehl().provenienz().anzeigeText())) + .append("</strong> — ") + .append(esc(eintrag.begruendung())) + .append("<br><span class=\"originaltext\">") + .append(esc(eintrag.befehl().provenienz().originalText())) + .append("</span></li>\n"); + } + sb.append("</ol>\n"); + } sb.append("</section>\n"); } diff --git a/src/main/java/eu/mulk/aendggner/synopse/Synopse.java b/src/main/java/eu/mulk/aendggner/synopse/Synopse.java index 489366e..91f5482 100644 --- a/src/main/java/eu/mulk/aendggner/synopse/Synopse.java +++ b/src/main/java/eu/mulk/aendggner/synopse/Synopse.java @@ -2,21 +2,53 @@ // SPDX-License-Identifier: AGPL-3.0-or-later package eu.mulk.aendggner.synopse; +import eu.mulk.aendggner.aenderung.Inkrafttreten; import eu.mulk.aendggner.anwendung.BefehlAnwender.AngewandteAenderung; import eu.mulk.aendggner.gesetz.Gesetz; import eu.mulk.aendggner.gesetz.Gliederung; import eu.mulk.aendggner.gesetz.Norm; +import java.time.LocalDate; import java.util.List; import org.jspecify.annotations.Nullable; -/** Eine Gegenüberstellung alter und neuer Fassung, normweise. */ +/** + * Eine Gegenüberstellung alter und neuer Fassung, normweise. + * + * @param inkrafttreten was der Schlussartikel des Änderungsgesetzes anordnet; {@code null}, wo + * keiner gelesen werden konnte. + * @param stichtag der Tag, dessen Fassung gezeigt wird; {@code null} heißt: alle Befehle, ohne + * Rücksicht auf das Inkrafttreten. + * @param nichtInKraft die Befehle, die am Stichtag noch nicht galten und deshalb unterblieben. + */ public record Synopse( Gesetz alt, Gesetz neu, List<Eintrag> eintraege, List<GliederungsAenderung> gliederungsAenderungen, List<AngewandteAenderung> manuellZuPruefen, - List<String> warnungen) { + List<String> warnungen, + @Nullable Inkrafttreten inkrafttreten, + @Nullable LocalDate stichtag, + List<AngewandteAenderung> nichtInKraft) { + + public Synopse( + Gesetz alt, + Gesetz neu, + List<Eintrag> eintraege, + List<GliederungsAenderung> gliederungsAenderungen, + List<AngewandteAenderung> manuellZuPruefen, + List<String> warnungen) { + this( + alt, + neu, + eintraege, + gliederungsAenderungen, + manuellZuPruefen, + warnungen, + null, + null, + List.of()); + } /** Eine geänderte Gliederungs-Überschrift (Teil/Abschnitt/…); {@code alt == null} bei neuen. */ public record GliederungsAenderung(@Nullable Gliederung alt, Gliederung neu) {} diff --git a/src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java b/src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java index bfe7df3..5dd65cd 100644 --- a/src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java +++ b/src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java @@ -2,11 +2,14 @@ // SPDX-License-Identifier: AGPL-3.0-or-later package eu.mulk.aendggner.synopse; +import eu.mulk.aendggner.aenderung.Inkrafttreten; import eu.mulk.aendggner.anwendung.BefehlAnwender.AnwendungsErgebnis; import eu.mulk.aendggner.anwendung.BefehlAnwender.Status; import eu.mulk.aendggner.gesetz.Gesetz; +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; /** Paart die Normen alter und neuer Fassung zu Synopse-Einträgen. */ public final class SynopseBuilder { @@ -21,6 +24,20 @@ public final class SynopseBuilder { */ public static Synopse baue( Gesetz alt, AnwendungsErgebnis anwendung, List<String> parseWarnungen, boolean vollstaendig) { + return baue(alt, anwendung, parseWarnungen, vollstaendig, null, null); + } + + /** + * @param inkrafttreten was der Schlussartikel des Änderungsgesetzes anordnet, sofern gelesen. + * @param stichtag der Tag, dessen Fassung gezeigt wird; {@code null} = ohne Rücksicht darauf. + */ + public static Synopse baue( + Gesetz alt, + AnwendungsErgebnis anwendung, + List<String> parseWarnungen, + boolean vollstaendig, + @Nullable Inkrafttreten inkrafttreten, + @Nullable LocalDate stichtag) { var neu = anwendung.neu(); var eintraege = new ArrayList<Synopse.Eintrag>(); @@ -51,9 +68,19 @@ public final class SynopseBuilder { var manuell = anwendung.protokoll().stream().filter(a -> a.status() == Status.MANUELL_PRUEFEN).toList(); + var nichtInKraft = + anwendung.protokoll().stream().filter(a -> a.status() == Status.NICHT_IN_KRAFT).toList(); return new Synopse( - alt, neu, eintraege, gliederungsAenderungen(alt, neu), manuell, parseWarnungen); + alt, + neu, + eintraege, + gliederungsAenderungen(alt, neu), + manuell, + parseWarnungen, + inkrafttreten, + stichtag, + nichtInKraft); } /** |
