aboutsummaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-07-13 21:33:47 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-07-13 21:33:47 +0200
commite486b1f4efb034ed83247f4d4a84964ef9887221 (patch)
tree1fed779c320a9c1b7c750073f461bfe6fea0c971 /src/main/java
parent9a29b384b84fac20f3c417ff9dcf3d461627e185 (diff)
Model and apply structural (Teil/Abschnitt) and TOC commands
Introduce the Gliederungsbaum into the model and support the structural command families that were the bulk of the remaining unknowns: - Gliederung gains a kennzahl; Gesetz carries the ordered list of Gliederungseinheiten, which the loader now collects. - New Stelle components Gliederungseinheit (Teil/Abschnitt/Unterabschnitt/ Anlage/…) and Absatzbezeichnung; StellenParser parses these plus "Überschrift von <Gliederung>" and drops "Satzteil/Angabe vor Nummer N" chapeau qualifiers. - Gliederungs-Überschrift Neufassung/Streichung apply to the tree and render as a "Geänderte Gliederungs-Überschriften" diff section; Absatzbezeichnung-Streichung removes an Absatz number; Inhaltsübersicht "Angabe(n) zu …" commands are recognized (applied via the existing TOC path). Reuses Neufassung/Aufhebung with structural Stellen rather than adding new command types. Final UnbekannterBefehl counts: GEG 50->5, IfSG 11->3, AGG 2->0 (§ 1 Alters->Lebensalters now applies), UWG 2->0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Change-Id: I0cc91bfe65798140dd982b19f1884dfe60be87c4
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/Stelle.java40
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java44
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java36
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java103
-rw-r--r--src/main/java/eu/mulk/aendggner/gesetz/Gesetz.java19
-rw-r--r--src/main/java/eu/mulk/aendggner/gesetz/Gliederung.java20
-rw-r--r--src/main/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoader.java7
-rw-r--r--src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java18
-rw-r--r--src/main/java/eu/mulk/aendggner/synopse/Synopse.java5
-rw-r--r--src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java16
10 files changed, 293 insertions, 15 deletions
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/Stelle.java b/src/main/java/eu/mulk/aendggner/aenderung/Stelle.java
index 0bcceb6..eb135fe 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/Stelle.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/Stelle.java
@@ -17,11 +17,29 @@ public record Stelle(List<Komponente> komponenten) {
}
public sealed interface Komponente
- permits Paragraph, AbsatzNr, SatzNr, NummerNr, BuchstabeNr, Ueberschrift, Inhaltsuebersicht {}
+ permits Paragraph,
+ AbsatzNr,
+ SatzNr,
+ NummerNr,
+ BuchstabeNr,
+ Ueberschrift,
+ Inhaltsuebersicht,
+ Gliederungseinheit,
+ Absatzbezeichnung {}
/** „§ 5“, „§ 28a“ — die Paragraphennummer ohne „§ “. */
public record Paragraph(String nummer) implements Komponente {}
+ /** Eine Gliederungseinheit oberhalb des Paragraphen: „Teil 2“, „Abschnitt 3“, „Anlage 8“. */
+ public record Gliederungseinheit(String art, String nummer) implements Komponente {
+ public String bezeichnung() {
+ return nummer.isEmpty() ? art : art + " " + nummer;
+ }
+ }
+
+ /** „Die Absatzbezeichnung „(2)““ — die reine Absatznummer als Ziel einer Streichung. */
+ public record Absatzbezeichnung(String nummer) implements Komponente {}
+
/** „Absatz 2“ */
public record AbsatzNr(String nummer) implements Komponente {}
@@ -75,6 +93,24 @@ public record Stelle(List<Komponente> komponenten) {
return komponenten.stream().anyMatch(Ueberschrift.class::isInstance);
}
+ public List<Gliederungseinheit> gliederungsPfad() {
+ return komponenten.stream()
+ .filter(Gliederungseinheit.class::isInstance)
+ .map(Gliederungseinheit.class::cast)
+ .toList();
+ }
+
+ public boolean betrifftGliederung() {
+ return komponenten.stream().anyMatch(Gliederungseinheit.class::isInstance);
+ }
+
+ public Optional<Absatzbezeichnung> absatzbezeichnung() {
+ return komponenten.stream()
+ .filter(Absatzbezeichnung.class::isInstance)
+ .map(Absatzbezeichnung.class::cast)
+ .findFirst();
+ }
+
public String anzeigeText() {
if (komponenten.isEmpty()) {
return "(gesamtes Gesetz)";
@@ -93,6 +129,8 @@ public record Stelle(List<Komponente> komponenten) {
case BuchstabeNr b -> "Buchstabe " + b.kennung();
case Ueberschrift u -> "Überschrift";
case Inhaltsuebersicht i -> "Inhaltsübersicht";
+ case Gliederungseinheit g -> g.bezeichnung();
+ case Absatzbezeichnung a -> "Absatzbezeichnung (" + a.nummer() + ")";
});
}
return sb.toString();
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
index 80f664e..7283d90 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
@@ -128,6 +128,26 @@ final class BefehlErkenner {
private static final Pattern AUFHEBUNG = Pattern.compile("^(.+?) (?:wird|werden) aufgehoben\\.$");
+ // „Die Überschrift von Teil 3 Abschnitt 2 wird gestrichen.“ — Streichung einer Gliederungs-
+ // Überschrift (die Wörter-Streichung STREICHUNG erfordert dagegen ein Zitat).
+ private static final Pattern UEBERSCHRIFT_STREICHUNG =
+ Pattern.compile("^(Die Überschrift (?:von|des|der|zu) .+?) wird gestrichen\\.$");
+
+ // „Die Absatzbezeichnung „(2)“ wird gestrichen.“
+ private static final Pattern ABSATZBEZEICHNUNG_STREICHUNG =
+ Pattern.compile("^Die Absatzbezeichnung " + Z + " wird gestrichen\\.$");
+
+ // Inhaltsübersicht: „Die Angabe(n) zu <…> wird/werden wie folgt gefasst / durch … ersetzt /
+ // gestrichen.“ Wird als Änderung der Inhaltsübersicht typisiert (Anwendung erfolgt gesondert).
+ private static final Pattern INHALTSUEBERSICHT_ANGABE =
+ Pattern.compile(
+ "^Die Angaben? (?:zu|zur) .+? (?:wird|werden) "
+ + "(?:wie folgt gefasst: "
+ + Z
+ + "|durch (?:die )?folgende[nrs]? Angaben? ersetzt: "
+ + Z
+ + "|gestrichen)\\.?$");
+
private static final Pattern STREICHUNG =
Pattern.compile(
"^(?:In )?(.+?) (?:wird|werden) (?:jeweils )?" + WOERTER + " " + Z + " gestrichen\\.$");
@@ -284,6 +304,19 @@ final class BefehlErkenner {
Matcher m;
+ // Inhaltsübersichts-Angaben zuerst prüfen, bevor NEUFASSUNG/STRUKTUR_ERSETZUNG die Phrase
+ // strukturell (aber mit unparsbarer Stelle) an sich ziehen.
+ if ((m = INHALTSUEBERSICHT_ANGABE.matcher(text)).matches()) {
+ var stelle = kontext.plus(new Stelle(List.of(new Stelle.Inhaltsuebersicht())));
+ if (m.group(1) != null) {
+ return Optional.of(new Neufassung(stelle, zitat(zitate, m.group(1)), provenienz));
+ }
+ if (m.group(2) != null) {
+ return Optional.of(new Neufassung(stelle, zitat(zitate, m.group(2)), provenienz));
+ }
+ return Optional.of(new Aufhebung(stelle, provenienz));
+ }
+
if ((m = PARAGRAPH_BEREICH_NEUFASSUNG.matcher(text)).matches()) {
return paragraphBereichNeufassung(zitat(zitate, m.group(3)), kontext, provenienz);
}
@@ -497,6 +530,17 @@ final class BefehlErkenner {
return ausStellen(m.group(1), s -> new Aufhebung(kontext.plus(s), provenienz));
}
+ if ((m = UEBERSCHRIFT_STREICHUNG.matcher(text)).matches()) {
+ return StellenParser.parse(m.group(1))
+ .map(s -> new Aufhebung(kontext.plus(s), provenienz));
+ }
+
+ if ((m = ABSATZBEZEICHNUNG_STREICHUNG.matcher(text)).matches()) {
+ var nummer = zitat(zitate, m.group(1)).replaceAll("[^0-9a-z]", "");
+ var stelle = new Stelle(List.of(new Stelle.Absatzbezeichnung(nummer)));
+ return Optional.of(new Aufhebung(kontext.plus(stelle), provenienz));
+ }
+
if ((m = STREICHUNG.matcher(text)).matches()) {
var woerter = wortZitat(zitate, m.group(2));
return ausStellen(m.group(1), s -> new Streichung(kontext.plus(s), woerter, provenienz));
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java b/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java
index 2b2b80e..4660343 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java
@@ -30,6 +30,9 @@ public final class StellenParser {
"dem",
"den",
"des",
+ "von",
+ "zu",
+ "zur",
"neue",
"neuen",
"bisherige",
@@ -41,7 +44,15 @@ public final class StellenParser {
private StellenParser() {}
+ // „in dem Satzteil vor Nummer 1“, „in der Angabe vor Nummer 1“ — verfeinernde Chapeau-Angaben
+ // ohne eigene Stelle-Komponente; die Ersetzung sucht ohnehin im Text der umgebenden Stelle.
+ private static final Pattern CHAPEAU_QUALIFIER =
+ Pattern.compile(
+ "(?i)(?:in dem |im |in der )?(?:Satzteil|Angabe) vor "
+ + "(?:Nummer|Buchstabe|Satz|Absatz) \\S+");
+
public static Optional<Stelle> parse(String phrase) {
+ phrase = CHAPEAU_QUALIFIER.matcher(phrase).replaceAll(" ").strip();
var woerter = phrase.strip().split("\\s+");
var komponenten = new ArrayList<Stelle.Komponente>();
@@ -91,6 +102,16 @@ public final class StellenParser {
komponenten.add(new Stelle.BuchstabeNr(wert));
i++;
}
+ case "Teil", "Teils", "Buch", "Buches", "Kapitel", "Kapitels", "Abschnitt", "Abschnitts",
+ "Unterabschnitt", "Unterabschnitts", "Anlage", "Anlagen" -> {
+ var wert = naechstesWort(woerter, i);
+ if (wert == null || !NUMMER_WERT.matcher(wert).matches()) {
+ return Optional.empty();
+ }
+ komponenten.add(new Stelle.Gliederungseinheit(gliederungsArt(wort), wert));
+ i++;
+ }
+ case "Anhang" -> komponenten.add(new Stelle.Gliederungseinheit("Anhang", ""));
case "Inhaltsübersicht" -> komponenten.add(new Stelle.Inhaltsuebersicht());
case "Überschrift" -> komponenten.add(new Stelle.Ueberschrift());
default -> {
@@ -251,8 +272,10 @@ public final class StellenParser {
case Stelle.SatzNr s -> new Stelle.SatzNr(label);
case Stelle.NummerNr n -> new Stelle.NummerNr(label);
case Stelle.BuchstabeNr b -> new Stelle.BuchstabeNr(label);
+ case Stelle.Gliederungseinheit g -> new Stelle.Gliederungseinheit(g.art(), label);
case Stelle.Ueberschrift u -> null;
case Stelle.Inhaltsuebersicht i -> null;
+ case Stelle.Absatzbezeichnung a -> null;
};
}
@@ -305,6 +328,19 @@ public final class StellenParser {
return Optional.of(new Stelle(komponenten));
}
+ /** Normalisiert Genitiv-/Pluralformen der Gliederungsart auf den Nominativ Singular. */
+ private static String gliederungsArt(String wort) {
+ return switch (wort) {
+ case "Teils" -> "Teil";
+ case "Buches" -> "Buch";
+ case "Kapitels" -> "Kapitel";
+ case "Abschnitts" -> "Abschnitt";
+ case "Unterabschnitts" -> "Unterabschnitt";
+ case "Anlagen" -> "Anlage";
+ default -> wort;
+ };
+ }
+
private static String naechstesWort(String[] woerter, int i) {
return i + 1 < woerter.length ? entfernePunktuation(woerter[i + 1]) : null;
}
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
index 94c5db1..86eccaf 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
@@ -17,6 +17,7 @@ import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortlautZuAbsatz;
import eu.mulk.aendggner.aenderung.Stelle;
import eu.mulk.aendggner.gesetz.Absatz;
import eu.mulk.aendggner.gesetz.Gesetz;
+import eu.mulk.aendggner.gesetz.Gliederung;
import eu.mulk.aendggner.gesetz.Norm;
import java.util.ArrayList;
import java.util.LinkedHashSet;
@@ -60,16 +61,19 @@ public final class BefehlAnwender {
public static AnwendungsErgebnis anwenden(Gesetz alt, List<Aenderungsbefehl> befehle) {
var normen = new ArrayList<>(alt.normen());
+ var gliederungen = new ArrayList<>(alt.gliederungen());
var protokoll = new ArrayList<AngewandteAenderung>();
for (var befehl : befehle) {
- protokoll.add(wendeAn(normen, befehl));
+ protokoll.add(wendeAn(normen, gliederungen, befehl));
}
- return new AnwendungsErgebnis(alt.mitNormen(normen), protokoll);
+ return new AnwendungsErgebnis(
+ alt.mitNormen(normen).mitGliederungen(gliederungen), protokoll);
}
- private static AngewandteAenderung wendeAn(List<Norm> normen, Aenderungsbefehl befehl) {
+ private static AngewandteAenderung wendeAn(
+ List<Norm> normen, List<Gliederung> gliederungen, Aenderungsbefehl befehl) {
if (befehl instanceof UnbekannterBefehl) {
return manuell(befehl, "Befehl nicht erkannt.");
}
@@ -78,6 +82,14 @@ public final class BefehlAnwender {
befehl, "Änderungen an der Inhaltsübersicht werden nicht automatisch angewandt.");
}
try {
+ // Änderungen an Gliederungs-Überschriften (Teil/Abschnitt/…) wirken auf den Gliederungsbaum.
+ if (befehl.stelle().betrifftGliederung()) {
+ return switch (befehl) {
+ case Neufassung n -> wendeGliederungNeufassungAn(gliederungen, n);
+ case Aufhebung a -> wendeGliederungStreichungAn(gliederungen, a);
+ default -> manuell(befehl, "Strukturänderung wird nicht automatisch angewandt.");
+ };
+ }
return switch (befehl) {
case Ersetzung e -> wendeErsetzungAn(normen, e);
case Streichung s -> wendeStreichungAn(normen, s);
@@ -89,7 +101,7 @@ public final class BefehlAnwender {
case Aufhebung a -> wendeAufhebungAn(normen, a);
case Umnummerierung u -> wendeUmnummerierungAn(normen, u);
case WortlautZuAbsatz w -> wendeWortlautZuAbsatzAn(normen, w);
- case Sammelbefehl s -> wendeSammelAn(normen, s);
+ case Sammelbefehl s -> wendeSammelAn(normen, gliederungen, s);
case UnbekannterBefehl u -> manuell(befehl, "Befehl nicht erkannt.");
};
} catch (RuntimeException e) {
@@ -97,6 +109,66 @@ public final class BefehlAnwender {
}
}
+ // --- Gliederungs-Überschriften -------------------------------------------------------------
+
+ private static AngewandteAenderung wendeGliederungNeufassungAn(
+ List<Gliederung> gliederungen, Neufassung befehl) {
+ int idx = findeGliederung(gliederungen, befehl.stelle().gliederungsPfad());
+ if (idx < 0) {
+ return manuell(befehl, "Gliederungseinheit nicht gefunden.");
+ }
+ var alt = gliederungen.get(idx);
+ var titel = befehl.neuerText().replaceAll("\\s+", " ").strip();
+ if (titel.startsWith(alt.bezeichnung())) {
+ titel = titel.substring(alt.bezeichnung().length()).strip();
+ }
+ gliederungen.set(idx, alt.mitTitel(titel.isEmpty() ? null : titel));
+ return angewandt(befehl, alt.bezeichnung());
+ }
+
+ private static AngewandteAenderung wendeGliederungStreichungAn(
+ List<Gliederung> gliederungen, Aufhebung befehl) {
+ int idx = findeGliederung(gliederungen, befehl.stelle().gliederungsPfad());
+ if (idx < 0) {
+ return manuell(befehl, "Gliederungseinheit nicht gefunden.");
+ }
+ var alt = gliederungen.get(idx);
+ gliederungen.set(idx, alt.mitTitel("(weggefallen)"));
+ return angewandt(befehl, alt.bezeichnung());
+ }
+
+ /**
+ * Findet die Gliederungseinheit zum Pfad („Teil 3 Abschnitt 2“): jede Ebene wird per Bezeichnung
+ * innerhalb des Kennzahl-Präfixes der übergeordneten Ebene aufgelöst.
+ */
+ private static int findeGliederung(
+ List<Gliederung> gliederungen, List<Stelle.Gliederungseinheit> pfad) {
+ if (pfad.isEmpty()) {
+ return -1;
+ }
+ String praefix = "";
+ int gefunden = -1;
+ for (var einheit : pfad) {
+ gefunden = -1;
+ for (int i = 0; i < gliederungen.size(); i++) {
+ var g = gliederungen.get(i);
+ if (g.bezeichnung().equals(einheit.bezeichnung())
+ && (g.kennzahl() == null || g.kennzahl().startsWith(praefix))) {
+ gefunden = i;
+ break;
+ }
+ }
+ if (gefunden < 0) {
+ return -1;
+ }
+ var kennzahl = gliederungen.get(gefunden).kennzahl();
+ if (kennzahl != null) {
+ praefix = kennzahl;
+ }
+ }
+ return gefunden;
+ }
+
// --- Wortweise Textoperationen -------------------------------------------------------------
private static AngewandteAenderung wendeErsetzungAn(List<Norm> normen, Ersetzung befehl) {
@@ -401,6 +473,24 @@ public final class BefehlAnwender {
private static AngewandteAenderung wendeAufhebungAn(List<Norm> normen, Aufhebung befehl) {
var stelle = befehl.stelle();
+ if (stelle.absatzbezeichnung().isPresent()) {
+ var aufloesung = loeseNormAuf(normen, stelle);
+ if (aufloesung.fehler() != null) {
+ return manuell(befehl, aufloesung.fehler());
+ }
+ var norm = normen.get(aufloesung.normIndex());
+ var nummer = stelle.absatzbezeichnung().get().nummer();
+ var absaetze = new ArrayList<>(norm.absaetze());
+ for (int i = 0; i < absaetze.size(); i++) {
+ if (nummer.equals(absaetze.get(i).nummer())) {
+ absaetze.set(i, new Absatz(null, absaetze.get(i).text()));
+ normen.set(aufloesung.normIndex(), norm.mitAbsaetzen(absaetze));
+ return angewandt(befehl, norm.enbez());
+ }
+ }
+ return manuell(befehl, "Absatz (" + nummer + ") nicht gefunden.");
+ }
+
if (nurParagraph(stelle)) {
var aufloesung = loeseNormAuf(normen, stelle);
if (aufloesung.fehler() != null) {
@@ -497,12 +587,13 @@ public final class BefehlAnwender {
* zusammen. Nur wenn alle Teile gelingen, gilt der Befehl als angewandt; sonst wird er zur
* manuellen Prüfung markiert (bereits angewandte Teile bleiben wirksam).
*/
- private static AngewandteAenderung wendeSammelAn(List<Norm> normen, Sammelbefehl befehl) {
+ private static AngewandteAenderung wendeSammelAn(
+ List<Norm> normen, List<Gliederung> gliederungen, Sammelbefehl befehl) {
var betroffene = new LinkedHashSet<String>();
var fehler = new ArrayList<String>();
int i = 1;
for (var teil : befehl.teilbefehle()) {
- var ergebnis = wendeAn(normen, teil);
+ var ergebnis = wendeAn(normen, gliederungen, teil);
betroffene.addAll(ergebnis.betroffeneEnbez());
if (ergebnis.status() != Status.ANGEWANDT) {
fehler.add("Teil " + i + " (" + teil.stelle().anzeigeText() + "): " + ergebnis.begruendung());
diff --git a/src/main/java/eu/mulk/aendggner/gesetz/Gesetz.java b/src/main/java/eu/mulk/aendggner/gesetz/Gesetz.java
index 5db0860..fc84172 100644
--- a/src/main/java/eu/mulk/aendggner/gesetz/Gesetz.java
+++ b/src/main/java/eu/mulk/aendggner/gesetz/Gesetz.java
@@ -3,11 +3,20 @@ package eu.mulk.aendggner.gesetz;
import java.util.List;
import java.util.Optional;
-/** Ein Stammgesetz, bestehend aus einer geordneten Liste von Einzelnormen. */
-public record Gesetz(String jurabk, String langue, String kurzue, List<Norm> normen) {
+/**
+ * Ein Stammgesetz, bestehend aus einer geordneten Liste von Einzelnormen und den (ebenfalls
+ * geordneten) Gliederungseinheiten (Teil/Abschnitt/Unterabschnitt/…), die die Normen strukturieren.
+ */
+public record Gesetz(
+ String jurabk, String langue, String kurzue, List<Norm> normen, List<Gliederung> gliederungen) {
public Gesetz {
normen = List.copyOf(normen);
+ gliederungen = List.copyOf(gliederungen);
+ }
+
+ public Gesetz(String jurabk, String langue, String kurzue, List<Norm> normen) {
+ this(jurabk, langue, kurzue, normen, List.of());
}
public Optional<Norm> norm(String enbez) {
@@ -15,6 +24,10 @@ public record Gesetz(String jurabk, String langue, String kurzue, List<Norm> nor
}
public Gesetz mitNormen(List<Norm> neueNormen) {
- return new Gesetz(jurabk, langue, kurzue, neueNormen);
+ return new Gesetz(jurabk, langue, kurzue, neueNormen, gliederungen);
+ }
+
+ public Gesetz mitGliederungen(List<Gliederung> neueGliederungen) {
+ return new Gesetz(jurabk, langue, kurzue, normen, neueGliederungen);
}
}
diff --git a/src/main/java/eu/mulk/aendggner/gesetz/Gliederung.java b/src/main/java/eu/mulk/aendggner/gesetz/Gliederung.java
index 21c9c4a..53eb6b9 100644
--- a/src/main/java/eu/mulk/aendggner/gesetz/Gliederung.java
+++ b/src/main/java/eu/mulk/aendggner/gesetz/Gliederung.java
@@ -2,10 +2,26 @@ package eu.mulk.aendggner.gesetz;
import org.jspecify.annotations.Nullable;
-/** Eine Gliederungseinheit (z.B. „2. Abschnitt — Koordinierung und Früherkennung“). */
-public record Gliederung(String bezeichnung, @Nullable String titel) {
+/**
+ * Eine Gliederungseinheit (z.B. „Teil 2 — Anforderungen an zu errichtende Gebäude“ oder „2.
+ * Abschnitt — Koordinierung und Früherkennung“).
+ *
+ * @param kennzahl die hierarchie-kodierende Gliederungskennzahl aus dem gii-XML (z.B. {@code 020020}
+ * für Abschnitt 2 in Teil 2); {@code null} bei anderweitig konstruierten Einheiten.
+ * @param bezeichnung die relative Bezeichnung („Teil 2“, „Abschnitt 2“).
+ * @param titel die Überschrift der Einheit ({@code null} bei titellosen Einheiten).
+ */
+public record Gliederung(@Nullable String kennzahl, String bezeichnung, @Nullable String titel) {
+
+ public Gliederung(String bezeichnung, @Nullable String titel) {
+ this(null, bezeichnung, titel);
+ }
public String anzeigeText() {
return titel == null ? bezeichnung : bezeichnung + " — " + titel;
}
+
+ public Gliederung mitTitel(@Nullable String neuerTitel) {
+ return new Gliederung(kennzahl, bezeichnung, neuerTitel);
+ }
}
diff --git a/src/main/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoader.java b/src/main/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoader.java
index ca7bdf6..b14ec1c 100644
--- a/src/main/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoader.java
+++ b/src/main/java/eu/mulk/aendggner/gesetz/gii/GiiXmlLoader.java
@@ -39,6 +39,7 @@ public final class GiiXmlLoader {
String langue = null;
String kurzue = null;
var normen = new ArrayList<Norm>();
+ var gliederungen = new ArrayList<Gliederung>();
Gliederung aktuelleGliederung = null;
for (var normElement : kindElemente(wurzel, "norm")) {
@@ -55,10 +56,12 @@ public final class GiiXmlLoader {
var gliederungselement = erstesKind(metadaten, "gliederungseinheit");
if (gliederungselement != null) {
+ var kennzahl = kindText(gliederungselement, "gliederungskennzahl");
var bez = kindText(gliederungselement, "gliederungsbez");
var titel = kindText(gliederungselement, "gliederungstitel");
if (bez != null) {
- aktuelleGliederung = new Gliederung(bez, titel);
+ aktuelleGliederung = new Gliederung(kennzahl, bez, titel);
+ gliederungen.add(aktuelleGliederung);
}
}
@@ -79,7 +82,7 @@ public final class GiiXmlLoader {
if (jurabk == null) {
throw new SAXException("Keine <norm>-Elemente mit Metadaten gefunden: " + datei);
}
- return new Gesetz(jurabk, langue, kurzue, normen);
+ return new Gesetz(jurabk, langue, kurzue, normen, gliederungen);
}
private static ArrayList<Absatz> leseAbsaetze(Element normElement) {
diff --git a/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java b/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java
index 2f5f735..46e1fa8 100644
--- a/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java
+++ b/src/main/java/eu/mulk/aendggner/synopse/HtmlRenderer.java
@@ -25,6 +25,8 @@ public final class HtmlRenderer {
rendereKopf(sb, synopse, quelleBeschreibung);
+ rendereGliederungsAenderungen(sb, synopse);
+
for (var eintrag : synopse.eintraege()) {
rendereEintrag(sb, eintrag);
}
@@ -58,6 +60,22 @@ public final class HtmlRenderer {
sb.append("</header>\n");
}
+ private static void rendereGliederungsAenderungen(StringBuilder sb, Synopse synopse) {
+ if (synopse.gliederungsAenderungen().isEmpty()) {
+ return;
+ }
+ sb.append("<section class=\"gliederung-aenderungen\">\n<h2>Geänderte Gliederungs-Überschriften</h2>\n");
+ for (var aenderung : synopse.gliederungsAenderungen()) {
+ var spalten = WortDiff.vergleiche(aenderung.alt().anzeigeText(), aenderung.neu().anzeigeText());
+ sb.append("<div class=\"vergleich\">\n<div class=\"alt\">")
+ .append(spalten.altHtml())
+ .append("</div>\n<div class=\"neu\">")
+ .append(spalten.neuHtml())
+ .append("</div>\n</div>\n");
+ }
+ sb.append("</section>\n");
+ }
+
private static void rendereEintrag(StringBuilder sb, Synopse.Eintrag eintrag) {
sb.append("<section class=\"norm ")
.append(eintrag.art().name().toLowerCase())
diff --git a/src/main/java/eu/mulk/aendggner/synopse/Synopse.java b/src/main/java/eu/mulk/aendggner/synopse/Synopse.java
index 26730a2..4f7a58f 100644
--- a/src/main/java/eu/mulk/aendggner/synopse/Synopse.java
+++ b/src/main/java/eu/mulk/aendggner/synopse/Synopse.java
@@ -2,6 +2,7 @@ package eu.mulk.aendggner.synopse;
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.util.List;
import org.jspecify.annotations.Nullable;
@@ -11,9 +12,13 @@ public record Synopse(
Gesetz alt,
Gesetz neu,
List<Eintrag> eintraege,
+ List<GliederungsAenderung> gliederungsAenderungen,
List<AngewandteAenderung> manuellZuPruefen,
List<String> warnungen) {
+ /** Eine geänderte Gliederungs-Überschrift (Teil/Abschnitt/…). */
+ public record GliederungsAenderung(Gliederung alt, Gliederung neu) {}
+
public enum Aenderungsart {
UNVERAENDERT,
GEAENDERT,
diff --git a/src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java b/src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java
index 0f88538..536bca4 100644
--- a/src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java
+++ b/src/main/java/eu/mulk/aendggner/synopse/SynopseBuilder.java
@@ -50,7 +50,21 @@ public final class SynopseBuilder {
var manuell =
anwendung.protokoll().stream().filter(a -> a.status() == Status.MANUELL_PRUEFEN).toList();
- return new Synopse(alt, neu, eintraege, manuell, parseWarnungen);
+ return new Synopse(alt, neu, eintraege, gliederungsAenderungen(alt, neu), manuell, parseWarnungen);
+ }
+
+ /** Paart die Gliederungseinheiten nach Kennzahl und sammelt die mit geänderter Überschrift. */
+ private static List<Synopse.GliederungsAenderung> gliederungsAenderungen(Gesetz alt, Gesetz neu) {
+ var aenderungen = new ArrayList<Synopse.GliederungsAenderung>();
+ for (var neuG : neu.gliederungen()) {
+ alt.gliederungen().stream()
+ .filter(a -> java.util.Objects.equals(a.kennzahl(), neuG.kennzahl()))
+ .filter(a -> a.kennzahl() != null)
+ .findFirst()
+ .filter(a -> !java.util.Objects.equals(a.titel(), neuG.titel()))
+ .ifPresent(a -> aenderungen.add(new Synopse.GliederungsAenderung(a, neuG)));
+ }
+ return aenderungen;
}
private static boolean gleicherInhalt(