aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/eu
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/eu')
-rw-r--r--src/main/java/eu/mulk/aendggner/AendGgner.java23
-rw-r--r--src/main/java/eu/mulk/aendggner/Pipeline.java129
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/Inkrafttreten.java110
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/AenderungsgesetzParser.java70
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/DeutschesDatum.java60
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/InkrafttretensLeser.java154
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java4
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/Grund.java5
-rw-r--r--src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java35
-rw-r--r--src/main/java/eu/mulk/aendggner/synopse/Synopse.java36
-rw-r--r--src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java29
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);
}
/**