aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FASSUNGEN.txt47
-rw-r--r--README.md23
-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
-rw-r--r--src/main/resources/eu/mulk/aendggner/web/app.js2
-rw-r--r--src/main/resources/eu/mulk/aendggner/web/index.html8
-rw-r--r--src/main/resources/eu/mulk/aendggner/web/worker.js3
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java98
-rw-r--r--src/test/java/eu/mulk/aendggner/aenderung/parse/InkrafttretensLeserTest.java143
-rw-r--r--src/wasm/java/eu/mulk/aendggner/wasm/BrowserMain.java13
19 files changed, 934 insertions, 58 deletions
diff --git a/FASSUNGEN.txt b/FASSUNGEN.txt
index 691e916..b739390 100644
--- a/FASSUNGEN.txt
+++ b/FASSUNGEN.txt
@@ -107,6 +107,53 @@ Lesereihenfolge nach dem Satzbild
(5) Der Prüfbestand umfasst dreihundertneununddreißig Prüfungen.
+Artikel 5
+Zeitliche Geltung der Änderungen
+
+Ein Änderungsgesetz tritt nicht notwendig auf einen Schlag in Kraft. Das Dritte
+Gesetz zur Änderung des Gesetzes gegen den unlauteren Wettbewerb lässt seinen
+Artikel 1 Nummer 2 Buchstabe c am 19. Juni 2026 wirksam werden, alles Übrige
+erst am 27. September 2026; das Infektionsschutz-Änderungsgesetz von 2020 ändert
+dasselbe Stammgesetz in zwei Artikeln zu zwei Zeitpunkten. Wer alle Befehle
+zugleich anwendet, erhält in solchen Fällen eine Fassung, die an keinem einzigen
+Tage gegolten hat. Bislang tat das Erzeugnis eben dies, und zwar schweigend.
+
+(1) Der Schlussartikel des Änderungsgesetzes wird gelesen (Inkrafttreten,
+ InkrafttretensLeser). Er ist von der Artikelwahl nicht erfaßt, weil er weder
+ eine Änderungsformel noch ein Stammgesetz nennt; maßgeblich ist der erste
+ Artikel hinter dem letzten ändernden, damit in einem Gesetzblatt mit mehreren
+ Gesetzen jedes seine eigene Schlussvorschrift erhält.
+
+(2) Erkannt werden die Grundregel, der Vorbehalt zugunsten späterer Absätze, die
+ abweichende Anordnung für einzelne Artikel, Nummern, Buchstaben und Doppel-
+ buchstaben sowie das rückwirkende Inkrafttreten („mit Wirkung vom“). Das
+ Außerkrafttreten bleibt außer Betracht.
+
+(3) Nennt der Wortlaut kein bestimmtes Datum, sondern knüpft an die Verkündung an,
+ so wird keines erfunden: Es bleibt beim Wortlaut, und die betroffenen Befehle
+ gelten als am Stichtag bereits wirksam. Der Umstand wird ausgewiesen.
+
+(4) Jeder Befehl wird der besondersten Anordnung zugeordnet, die ihn erfaßt; im
+ übrigen gilt die Grundregel. Zugeordnet wird über die Herkunft des Befehls
+ (Artikel und Gliederungspfad).
+
+(5) Es wird der Schalter „--stichtag“ eingeführt, dem im Vordruck der Browser-
+ fassung das Feld „Stichtag“ entspricht. Mit ihm ergibt sich die an jenem Tage
+ geltende Fassung; die noch nicht in Kraft getretenen Befehle bleiben unange-
+ wandt und werden gesondert ausgewiesen (Status NICHT_IN_KRAFT, Grund
+ NOCH_NICHT_IN_KRAFT).
+
+(6) Ohne Stichtag bleibt es bei der Anwendung aller Befehle. Tritt das Gesetz
+ jedoch gestaffelt in Kraft, so wird dies unter Angabe der Wortlaute gerügt;
+ ferner weist der Vorspann der Synopse das Inkrafttreten und den etwa gewählten
+ Stichtag aus.
+
+(7) Die Ausdrucksweise der ausgeschriebenen Daten wird in einer gemeinsamen Klasse
+ (DeutschesDatum) zusammengefaßt; die Altersrüge bedient sich fortan derselben.
+
+(8) Der Prüfbestand umfasst dreihundertachtundvierzig Prüfungen.
+
+
════════════════════════════════════════════════════════════════════════════════
Fassung vom 23. August 2026,
zuletzt geändert durch die am 23. August 2026 vorgenommenen Änderungen
diff --git a/README.md b/README.md
index f832666..1dcbb29 100644
--- a/README.md
+++ b/README.md
@@ -207,6 +207,12 @@ java -jar target/aendggner-0.1.0-SNAPSHOT.jar \
: Nur den bezeichneten Artikel des Änderungsgesetzes anwenden. Ohne diese Angabe
werden alle Artikel angewandt, deren Einleitung das Stammgesetz nennt.
+**`--stichtag <JJJJ-MM-TT>`**
+: Die an diesem Tage geltende Fassung erzeugen. Tritt das Änderungsgesetz
+ gestaffelt in Kraft, so bleiben die an jenem Tage noch nicht geltenden Befehle
+ unangewandt und werden gesondert ausgewiesen (§ 8 Absatz 5). Ohne diese Angabe
+ werden alle Befehle angewandt.
+
**`--extract-only`**
: Nur den bereinigten Lineartext des Änderungsdokuments ausgeben. Dies ist
angezeigt, wenn die PDF-Aufbereitung (§ 9) fehlerhaft bleibt: Der Text ist zu
@@ -291,6 +297,18 @@ Verschoben wird stets nur nach vorn.
angewandt, wenn jeder seiner Teile gegriffen hat; sonst nennt die Meldung den
Teil und den Grund.
+(5) Ein Änderungsgesetz tritt nicht notwendig auf einen Schlag in Kraft. Der
+Schlussartikel wird deshalb gelesen und jeder Befehl der besondersten Anordnung
+zugeordnet, die ihn erfasst („Artikel 1 Nummer 2 Buchstabe c tritt am 19. Juni
+2026 in Kraft“); im Übrigen gilt die Grundregel. Mit dem Schalter `--stichtag`
+(§ 6 Absatz 2) ergibt sich die an einem bestimmten Tage geltende Fassung; die
+noch nicht in Kraft getretenen Befehle bleiben dann unangewandt und werden
+gesondert ausgewiesen. Ohne diese Angabe werden alle Befehle angewandt; tritt das
+Gesetz gestaffelt in Kraft, so rügt die Synopse, dass die gezeigte Fassung an
+keinem einzigen Tage so gegolten hat. Nennt eine Anordnung kein bestimmtes Datum,
+sondern knüpft an die Verkündung an, so wird keines erfunden — der Verkündungstag
+steht nicht im Gesetzestext.
+
## § 9 Aufbereitung der Druckwerke
(1) Die PDF-Aufbereitung toleriert die üblichen Drucksachen-Artefakte, nämlich
@@ -500,8 +518,9 @@ wählen, Synopse erhalten.
(2) Der Vordruck stellt außer den beiden Dateifeldern dieselben Angaben zur
Verfügung wie die Befehlszeile nach § 6 Absatz 2, nämlich das Feld
-„Anzuwendender Artikel“ (`--artikel`), das Ankreuzfeld „Unveränderte
-Vorschriften mit in die Synopse aufnehmen“ (`--vollstaendig`) und das
+„Anzuwendender Artikel“ (`--artikel`), das Feld „Stichtag“ (`--stichtag`), das
+Ankreuzfeld „Unveränderte Vorschriften mit in die Synopse aufnehmen“
+(`--vollstaendig`) und das
Ankreuzfeld „Statt der Synopse nur den maschinell gelesenen Text der
Änderungsdokumente ausgeben“ (`--extract-only`). Das letztgenannte ist kein
Zierat: Ohne es hätte, wer im Browser arbeitet, keine Möglichkeit, einem
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);
}
/**
diff --git a/src/main/resources/eu/mulk/aendggner/web/app.js b/src/main/resources/eu/mulk/aendggner/web/app.js
index 97eca52..d75482b 100644
--- a/src/main/resources/eu/mulk/aendggner/web/app.js
+++ b/src/main/resources/eu/mulk/aendggner/web/app.js
@@ -50,6 +50,7 @@ formular.addEventListener("submit", async (ereignis) => {
}
const artikel = document.querySelector("#artikel").value.trim();
+ const stichtag = document.querySelector("#stichtag").value.trim();
const nurText = document.querySelector("#nurtext").checked;
knopf.disabled = true;
@@ -73,6 +74,7 @@ formular.addEventListener("submit", async (ereignis) => {
patches,
vollstaendig: document.querySelector("#vollstaendig").checked,
artikel: artikel === "" ? null : artikel,
+ stichtag: stichtag === "" ? null : stichtag,
nurText,
});
});
diff --git a/src/main/resources/eu/mulk/aendggner/web/index.html b/src/main/resources/eu/mulk/aendggner/web/index.html
index edca09e..c5f7f47 100644
--- a/src/main/resources/eu/mulk/aendggner/web/index.html
+++ b/src/main/resources/eu/mulk/aendggner/web/index.html
@@ -140,6 +140,14 @@
<input type="text" id="artikel" name="artikel" inputmode="numeric"
autocomplete="off" placeholder="z. B. 3">
</div>
+ <div class="feld">
+ <label for="stichtag">Stichtag
+ <span class="erl">(nur auszufüllen, wenn die Fassung eines bestimmten Tages
+ gewünscht ist: Tritt das Änderungsgesetz gestaffelt in Kraft, so bleiben die an
+ diesem Tag noch nicht geltenden Befehle unangewandt und werden gesondert
+ ausgewiesen. Ohne Angabe werden alle Befehle angewandt)</span></label>
+ <input type="date" id="stichtag" name="stichtag" autocomplete="off">
+ </div>
<div class="feld ankreuzfeld">
<label>
<input type="checkbox" id="vollstaendig" name="vollstaendig">
diff --git a/src/main/resources/eu/mulk/aendggner/web/worker.js b/src/main/resources/eu/mulk/aendggner/web/worker.js
index 827358e..e7f2161 100644
--- a/src/main/resources/eu/mulk/aendggner/web/worker.js
+++ b/src/main/resources/eu/mulk/aendggner/web/worker.js
@@ -19,7 +19,7 @@ const gestartet = GraalVM.run([], Object.assign(new GraalVM.Config(), {
}));
self.onmessage = async (nachricht) => {
- const { stamm, patches, vollstaendig, artikel, nurText } = nachricht.data;
+ const { stamm, patches, vollstaendig, artikel, stichtag, nurText } = nachricht.data;
try {
await gestartet;
await bereit;
@@ -27,6 +27,7 @@ self.onmessage = async (nachricht) => {
stamm,
patches,
artikel: artikel ?? null,
+ stichtag: stichtag ?? null,
vollstaendig: Boolean(vollstaendig),
nurText: Boolean(nurText),
});
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index d9f36d7..10cfb8d 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -24,6 +24,7 @@ import eu.mulk.aendggner.synopse.HtmlRenderer;
import eu.mulk.aendggner.synopse.SynopseBuilder;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.time.LocalDate;
import java.util.List;
import org.junit.jupiter.api.Test;
@@ -1650,4 +1651,101 @@ class EndToEndTest {
.findFirst()
.orElseThrow();
}
+
+ /**
+ * Die Fassung eines bestimmten Tages. Das 3. UWGÄndG tritt gestaffelt in Kraft: Artikel 1 Nummer
+ * 2 Buchstabe c am 19. Juni 2026, alles Übrige erst am 27. September 2026. Wer alle Befehle auf
+ * einen Schlag anwendet, erhält eine Fassung, die an keinem einzigen Tag gegolten hat — deshalb
+ * warnt die Synopse davor, und mit einem Stichtag ergibt sich die wirklich geltende Fassung.
+ */
+ @Test
+ void uwgTrittGestaffeltInKraft() throws Exception {
+ var xml = SAMPLEDATA.resolve("UWG/BJNR141400004.xml");
+ var pdf = SAMPLEDATA.resolve("UWG/bgbl126s0043_regelungstext.pdf");
+ assumeTrue(Files.exists(xml) && Files.exists(pdf), "UWG-Beispieldaten fehlen");
+
+ // Ohne Stichtag bleibt es beim vollen Bestand — aber nicht stillschweigend.
+ var ohne = Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false);
+ assertThat(ohne.anzahlAngewandt()).isEqualTo(19);
+ assertThat(ohne.html())
+ .contains("Das Änderungsgesetz tritt gestaffelt in Kraft")
+ .contains("Artikel 1 Nummer 2 Buchstabe c tritt am 19. Juni 2026 in Kraft.")
+ .contains("<dt>Inkrafttreten</dt><dd>27. September 2026 (gestaffelt, siehe unten)</dd>");
+
+ // Am 19. Juni 2026 galt genau ein Befehl: der Buchstabe c der Nummer 2.
+ var frueh = Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false, LocalDate.of(2026, 6, 19));
+ assertThat(frueh.anzahlAngewandt()).isEqualTo(1);
+ assertThat(frueh.anzahlManuell()).isZero();
+ assertThat(frueh.html())
+ .contains("<dt>Stichtag</dt><dd>19. Juni 2026</dd>")
+ .contains("Am Stichtag noch nicht in Kraft")
+ .contains("Tritt erst am 27. September 2026 in Kraft");
+ // Der Tag davor ändert noch gar nichts.
+ var davor = Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false, LocalDate.of(2026, 6, 18));
+ assertThat(davor.anzahlAngewandt()).isZero();
+
+ // Am 27. September 2026 ist das Gesetz vollständig in Kraft; dann deckt sich die Fassung mit
+ // der ungefilterten, und der Abschnitt „noch nicht in Kraft“ entfällt.
+ var spaet = Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false, LocalDate.of(2026, 9, 27));
+ assertThat(spaet.anzahlAngewandt()).isEqualTo(19);
+ assertThat(spaet.html()).doesNotContain("Am Stichtag noch nicht in Kraft");
+ }
+
+ /**
+ * Dasselbe am großen Fall, und zugleich die Probe auf die Schritt-Ordnung: Im GEG bleibt am 1.
+ * Januar 2024 allein der Befehl der Nummer 22 zurück. Er steht in keiner Umnummerierungs-Kaskade,
+ * sodass die übrigen 115 unverändert durchlaufen — die drei liegengebliebenen Befehle sind
+ * dieselben wie ohne Stichtag und werden von der Auswahl nicht vermehrt.
+ */
+ @Test
+ void gegStichtagLaesstDieUebrigenBefehleUnberuehrt() throws Exception {
+ var xml = SAMPLEDATA.resolve("GEG/BJNR172810020-2023.xml");
+ var pdf = SAMPLEDATA.resolve("GEG/bgbl123s0280_regelungstext.pdf");
+ assumeTrue(Files.exists(xml) && Files.exists(pdf), "GEG-Beispieldaten fehlen");
+
+ var vollstaendig = Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false);
+ assertThat(vollstaendig.anzahlAngewandt()).isEqualTo(116);
+ assertThat(vollstaendig.anzahlManuell()).isEqualTo(3);
+
+ var anfang2024 =
+ Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false, LocalDate.of(2024, 1, 1));
+ assertThat(anfang2024.anzahlAngewandt()).isEqualTo(115);
+ assertThat(anfang2024.anzahlManuell()).isEqualTo(3);
+ assertThat(anfang2024.html())
+ .contains("Am Stichtag noch nicht in Kraft")
+ .contains("Tritt erst am 1. Oktober 2024 in Kraft");
+
+ var oktober2024 =
+ Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false, LocalDate.of(2024, 10, 1));
+ assertThat(oktober2024.anzahlAngewandt()).isEqualTo(116);
+ }
+
+ /**
+ * Das IfSG-Gesetz von 2020 ändert dasselbe Stammgesetz in zwei Artikeln zu zwei Zeitpunkten:
+ * Artikel 1 sogleich, Artikel 2 erst am 1. April 2021. Genau dort ist die auf einen Schlag
+ * gerechnete Fassung eine, die es nie gab.
+ *
+ * <p>Zugleich der Fall der unbestimmten Grundregel: „am Tag nach der Verkündung“ nennt kein
+ * Datum, das im Gesetzestext stünde. Erfunden wird keines; die betroffenen Befehle gelten als am
+ * Stichtag bereits wirksam, und das wird gesagt.
+ */
+ @Test
+ void ifsgZweiArtikelZuZweiZeitpunkten() throws Exception {
+ var xml = SAMPLEDATA.resolve("IfSG/BJNR104510000-2020.xml");
+ var pdf = SAMPLEDATA.resolve("IfSG/bgbl120s2397_78991.pdf");
+ assumeTrue(Files.exists(xml) && Files.exists(pdf), "IfSG-Beispieldaten fehlen");
+
+ var novemberFassung =
+ Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false, LocalDate.of(2020, 11, 19));
+ assertThat(novemberFassung.anzahlAngewandt()).isEqualTo(65);
+ assertThat(novemberFassung.html())
+ .contains("Am Stichtag noch nicht in Kraft")
+ .contains("Eine Inkrafttretens-Anordnung nennt kein bestimmtes Datum");
+
+ // Am 1. April 2021 kommen die zehn Befehle des Artikels 2 und der Doppelbuchstabe hinzu.
+ var aprilFassung =
+ Pipeline.erzeugeSynopse(xml, List.of(pdf), null, false, LocalDate.of(2021, 4, 1));
+ assertThat(aprilFassung.anzahlAngewandt()).isEqualTo(75);
+ assertThat(aprilFassung.html()).doesNotContain("Am Stichtag noch nicht in Kraft");
+ }
}
diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/InkrafttretensLeserTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/InkrafttretensLeserTest.java
new file mode 100644
index 0000000..819cd95
--- /dev/null
+++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/InkrafttretensLeserTest.java
@@ -0,0 +1,143 @@
+// 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 static org.assertj.core.api.Assertions.assertThat;
+
+import eu.mulk.aendggner.aenderung.Provenienz;
+import java.time.LocalDate;
+import org.junit.jupiter.api.Test;
+
+/** Der Schlussartikel, gelesen — die Wortlaute stammen sämtlich aus den Beispieldaten. */
+class InkrafttretensLeserTest {
+
+ private static Provenienz punkt(String artikel, String pfad) {
+ return new Provenienz(artikel, pfad, "");
+ }
+
+ @Test
+ void schlichtesInkrafttretenOhneStaffelung() {
+ var inkrafttreten =
+ InkrafttretensLeser.lies("Dieses Gesetz tritt am 12. Juni 2026 in Kraft.\nBerlin, den 3.");
+
+ assertThat(inkrafttreten).isNotNull();
+ assertThat(inkrafttreten.gestaffelt()).isFalse();
+ assertThat(inkrafttreten.grundregel().orElseThrow().datum())
+ .isEqualTo(LocalDate.of(2026, 6, 12));
+ // Ohne eigenen Bezug gilt für jeden Punkt die Grundregel.
+ assertThat(inkrafttreten.fuer(punkt("1", "3. a)")).orElseThrow().datum())
+ .isEqualTo(LocalDate.of(2026, 6, 12));
+ }
+
+ /** UWG 2026: Ein einzelner Buchstabe tritt drei Monate vor dem übrigen Gesetz in Kraft. */
+ @Test
+ void gestaffeltMitBuchstabenBezug() {
+ var inkrafttreten =
+ InkrafttretensLeser.lies(
+ """
+ (1) Dieses Gesetz tritt vorbehaltlich des Absatzes 2 am 27. September 2026 in Kraft.
+ (2) Artikel 1 Nummer 2 Buchstabe c tritt am 19. Juni 2026 in Kraft.
+ Die verfassungsmäßigen Rechte des Bundesrates sind gewahrt.
+ """);
+
+ assertThat(inkrafttreten).isNotNull();
+ assertThat(inkrafttreten.gestaffelt()).isTrue();
+ // Der Vorbehalt („vorbehaltlich des Absatzes 2“) verweist auf den Schlussartikel selbst; er
+ // ist kein Punktbezug und macht die Grundregel nicht zur Sonderregel.
+ assertThat(inkrafttreten.grundregel().orElseThrow().datum())
+ .isEqualTo(LocalDate.of(2026, 9, 27));
+ assertThat(inkrafttreten.fuer(punkt("1", "2. c)")).orElseThrow().datum())
+ .isEqualTo(LocalDate.of(2026, 6, 19));
+ // Der Nachbarbuchstabe ist nicht gemeint, und die Nummer selbst auch nicht.
+ assertThat(inkrafttreten.fuer(punkt("1", "2. d)")).orElseThrow().datum())
+ .isEqualTo(LocalDate.of(2026, 9, 27));
+ assertThat(inkrafttreten.fuer(punkt("1", "2.")).orElseThrow().datum())
+ .isEqualTo(LocalDate.of(2026, 9, 27));
+ }
+
+ /**
+ * GEG 2023: „Abweichend von Absatz 1 treten Artikel 1 Nummer 22 sowie Artikel 3 am … in Kraft.“ —
+ * zwei Bezüge in einem Satz, davon einer auf einen ganzen Artikel. Der Schlusssatz über die
+ * Rechte des Bundesrates klebt im Gesetzblatt unmittelbar am Anordnungssatz.
+ */
+ @Test
+ void zweiBezuegeInEinemSatzUndAngeklebterSchlusssatz() {
+ var inkrafttreten =
+ InkrafttretensLeser.lies(
+ """
+ (1) Dieses Gesetz tritt vorbehaltlich des Absatzes 2 am 1. Januar 2024 in Kraft.
+ (2) Abweichend von Absatz 1 treten Artikel 1 Nummer 22 sowie Artikel 3 am 1. Oktober \
+ 2024 in Kraft. Die verfassungsmäßigen Rechte des Bundesrates sind gewahrt.
+ """);
+
+ assertThat(inkrafttreten).isNotNull();
+ var sonder = inkrafttreten.sonderregeln();
+ assertThat(sonder).hasSize(1);
+ assertThat(sonder.get(0).wortlaut()).endsWith("am 1. Oktober 2024 in Kraft.");
+ assertThat(inkrafttreten.fuer(punkt("1", "22.")).orElseThrow().datum())
+ .isEqualTo(LocalDate.of(2024, 10, 1));
+ // Ein ganzer Artikel als Bezug erfasst jeden seiner Punkte.
+ assertThat(inkrafttreten.fuer(punkt("3", "5. b)")).orElseThrow().datum())
+ .isEqualTo(LocalDate.of(2024, 10, 1));
+ assertThat(inkrafttreten.fuer(punkt("1", "21.")).orElseThrow().datum())
+ .isEqualTo(LocalDate.of(2024, 1, 1));
+ }
+
+ /**
+ * IfSG 2020: Rückwirkendes Inkrafttreten („mit Wirkung vom“), ein Doppelbuchstabe als Bezug und
+ * eine Grundregel, die kein Datum nennt — der Verkündungstag steht nicht im Gesetzestext, also
+ * wird auch keiner erfunden.
+ */
+ @Test
+ void rueckwirkungDoppelbuchstabeUndUnbestimmteGrundregel() {
+ var inkrafttreten =
+ InkrafttretensLeser.lies(
+ """
+ (1) Dieses Gesetz tritt vorbehaltlich der Absätze 2 und 3 am Tag nach der Verkündung \
+ in Kraft.
+ (2) Artikel 4a Nummer 1 tritt mit Wirkung vom
+ 18. November 2020 in Kraft.
+ (3) Artikel 1 Nummer 11 Buchstabe a Doppelbuchstabe bb und Artikel 2 treten am \
+ 1. April 2021 in Kraft.
+ """);
+
+ assertThat(inkrafttreten).isNotNull();
+ var grundregel = inkrafttreten.grundregel().orElseThrow();
+ assertThat(grundregel.datum()).isNull();
+ assertThat(grundregel.anzeige()).isEqualTo(grundregel.wortlaut());
+ // Der Zeilenumbruch mitten im Datum („mit Wirkung vom\n18. November 2020“) stört nicht.
+ assertThat(inkrafttreten.fuer(punkt("4a", "1.")).orElseThrow().datum())
+ .isEqualTo(LocalDate.of(2020, 11, 18));
+ assertThat(inkrafttreten.fuer(punkt("1", "11. a) bb)")).orElseThrow().datum())
+ .isEqualTo(LocalDate.of(2021, 4, 1));
+ assertThat(inkrafttreten.fuer(punkt("1", "11. a) aa)")).orElseThrow().datum()).isNull();
+ assertThat(inkrafttreten.fuer(punkt("2", "6. b)")).orElseThrow().datum())
+ .isEqualTo(LocalDate.of(2021, 4, 1));
+ }
+
+ /** Das Außerkrafttreten sagt nichts darüber, wann eine Änderung wirkt — es bleibt außer Acht. */
+ @Test
+ void ausserkrafttretenIstKeineAnordnungDesInkrafttretens() {
+ assertThat(InkrafttretensLeser.lies("Diese Verordnung tritt am 31. Dezember 2027 außer Kraft."))
+ .isNull();
+ }
+
+ /**
+ * Ein Gesetzblatt trägt mehrere Gesetze mit je eigener Schlussvorschrift. Maßgeblich ist die, die
+ * auf den eigenen ändernden Artikel folgt — nicht die eines fremden Gesetzes davor.
+ */
+ @Test
+ void waehltDenSchlussartikelHinterDemEigenenGesetz() {
+ var bloecke =
+ java.util.List.of(
+ "Das Erste Gesetz wird wie folgt geändert: …",
+ "Dieses Gesetz tritt am 1. März 2026 in Kraft.",
+ "Das Zweite Gesetz wird wie folgt geändert: …",
+ "Dieses Gesetz tritt am 1. Juli 2026 in Kraft.");
+
+ assertThat(InkrafttretensLeser.waehle(bloecke, 1).grundregel().orElseThrow().datum())
+ .isEqualTo(LocalDate.of(2026, 3, 1));
+ assertThat(InkrafttretensLeser.waehle(bloecke, 3).grundregel().orElseThrow().datum())
+ .isEqualTo(LocalDate.of(2026, 7, 1));
+ }
+}
diff --git a/src/wasm/java/eu/mulk/aendggner/wasm/BrowserMain.java b/src/wasm/java/eu/mulk/aendggner/wasm/BrowserMain.java
index c318f86..6d7a664 100644
--- a/src/wasm/java/eu/mulk/aendggner/wasm/BrowserMain.java
+++ b/src/wasm/java/eu/mulk/aendggner/wasm/BrowserMain.java
@@ -4,6 +4,7 @@ package eu.mulk.aendggner.wasm;
import eu.mulk.aendggner.Pipeline;
import eu.mulk.aendggner.Quelle;
+import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
@@ -22,10 +23,10 @@ import org.graalvm.webimage.api.JSValue;
* damit dieselbe {@link Pipeline} auf wie Befehlszeile und Tests.
*
* <p>Erwartet ein JS-Objekt <code>{stamm: {name, base64}, patches: [{name, base64}], artikel,
- * vollstaendig, nurText}</code> und liefert <code>{html, angewandt, manuell, normen}</code> — bei
- * <code>nurText</code> stattdessen <code>{text}</code> — oder <code>{fehler}</code> zurück.
- * Geworfen wird nichts: Eine Ausnahme im Wasm hinterlässt auf der JS-Seite nur einen unlesbaren
- * Stapel, also wird jeder Fehler als Text zurückgereicht.
+ * stichtag, vollstaendig, nurText}</code> und liefert <code>{html, angewandt, manuell,
+ * normen}</code> — bei <code>nurText</code> stattdessen <code>{text}</code> — oder <code>{fehler}
+ * </code> zurück. Geworfen wird nichts: Eine Ausnahme im Wasm hinterlässt auf der JS-Seite nur
+ * einen unlesbaren Stapel, also wird jeder Fehler als Text zurückgereicht.
*
* <p>Der Dateiinhalt wandert als Base64-Text über die Grenze, nicht als {@code Uint8Array}: Die
* Umsetzung typisierter Felder nach {@code byte[]} ist in Web Image derzeit defekt („byteArrayHub
@@ -70,6 +71,7 @@ public final class BrowserMain {
}
var artikel = text(eingabe.get("artikel"));
+ var stichtag = text(eingabe.get("stichtag"));
var vollstaendig = Boolean.TRUE.equals(wahrheitswert(eingabe.get("vollstaendig")));
if (Boolean.TRUE.equals(wahrheitswert(eingabe.get("nurText")))) {
@@ -85,7 +87,8 @@ public final class BrowserMain {
stamm,
List.copyOf(patches),
artikel == null || artikel.isBlank() ? null : artikel,
- vollstaendig);
+ vollstaendig,
+ stichtag == null || stichtag.isBlank() ? null : LocalDate.parse(stichtag.strip()));
// Java-Werte kämen auf der JS-Seite als undurchsichtige Proxys an; JSString/JSNumber
// erzeugen echte JS-Werte.