aboutsummaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java22
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java92
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java69
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java27
4 files changed, 196 insertions, 14 deletions
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java b/src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java
index 60f5971..6b7c093 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java
@@ -88,6 +88,28 @@ public sealed interface Aenderungsbefehl {
record Umnummerierung(Stelle stelle, Stelle neu, Provenienz provenienz)
implements Aenderungsbefehl {}
+ /**
+ * „In A und B wird jeweils …“ — ein Befehl, der dieselbe Operation auf mehrere, per „und“
+ * koordinierte Stellen anwendet. Die Teilbefehle teilen sich Provenienz und Befehlszeile; der
+ * Applier fasst sie zu einem Protokolleintrag zusammen.
+ */
+ record Sammelbefehl(java.util.List<Aenderungsbefehl> teilbefehle) implements Aenderungsbefehl {
+
+ public Sammelbefehl {
+ teilbefehle = java.util.List.copyOf(teilbefehle);
+ }
+
+ @Override
+ public Stelle stelle() {
+ return teilbefehle.get(0).stelle();
+ }
+
+ @Override
+ public Provenienz provenienz() {
+ return teilbefehle.get(0).provenienz();
+ }
+ }
+
/** Fallback für alles, was der Parser nicht versteht — muss manuell geprüft werden. */
record UnbekannterBefehl(Stelle stelle, String originalText, Provenienz provenienz)
implements Aenderungsbefehl {}
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 22c2976..ca59b12 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java
@@ -6,6 +6,7 @@ import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Aufhebung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Ebene;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Ersetzung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Neufassung;
+import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Sammelbefehl;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Streichung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturEinfuegung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturErsetzung;
@@ -14,7 +15,10 @@ import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WoerterEinfuegung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortAnker;
import eu.mulk.aendggner.aenderung.Provenienz;
import eu.mulk.aendggner.aenderung.Stelle;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Optional;
+import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jspecify.annotations.Nullable;
@@ -84,7 +88,8 @@ final class BefehlErkenner {
private static final Pattern WOERTER_EINFUEGUNG =
Pattern.compile(
- "^(?:In )?(.+?) (?:wird|werden) (nach|vor) (?:dem Wort|den Wörtern|der Angabe|der Zahl) "
+ "^(?:In )?(.+?) (?:wird|werden) (?:jeweils )?(nach|vor) "
+ + "(?:dem Wort|den Wörtern|der Angabe|der Zahl) "
+ Z
+ " "
+ WOERTER
@@ -123,10 +128,18 @@ final class BefehlErkenner {
private static final Pattern AUFHEBUNG = Pattern.compile("^(.+?) (?:wird|werden) aufgehoben\\.$");
private static final Pattern STREICHUNG =
- Pattern.compile("^(?:In )?(.+?) (?:wird|werden) " + WOERTER + " " + Z + " gestrichen\\.$");
+ Pattern.compile(
+ "^(?:In )?(.+?) (?:wird|werden) (?:jeweils )?" + WOERTER + " " + Z + " gestrichen\\.$");
private static final Pattern UMNUMMERIERUNG =
- Pattern.compile("^(?:Der bisherige )?(.+?) wird (Absatz|Satz) (\\d+[a-z]?)\\.$");
+ Pattern.compile("^(?:Der bisherige )?(.+?) wird (?:zu )?(Absatz|Satz) (\\d+[a-z]?)\\.$");
+
+ // „Die bisherigen Absätze 2 bis 4 werden zu den Absätzen 3 bis 5.“ (Entwürfe) — Bereichs-
+ // Umnummerierung, die in einzelne Umnummerierungen aufgelöst wird.
+ private static final Pattern UMNUMMERIERUNG_BEREICH =
+ Pattern.compile(
+ "^Die bisherigen (?:Absätze|Sätze) (\\d+) bis (\\d+) "
+ + "werden zu den (Absätzen|Sätzen) (\\d+) bis (\\d+)\\.$");
private static final Pattern INHALTSUEBERSICHT_EINFUEGUNG =
Pattern.compile(
@@ -203,10 +216,8 @@ final class BefehlErkenner {
var jeweils = m.group(2) != null || text.contains(" jeweils durch ");
var alt = wortZitat(zitate, m.group(3));
var neu = wortZitat(zitate, m.group(4));
- var stelle = StellenParser.parse(m.group(1));
- var effektivesJeweils = jeweils;
- return stelle.map(
- s -> new Ersetzung(kontext.plus(s), alt, neu, effektivesJeweils, false, provenienz));
+ return ausStellen(
+ m.group(1), s -> new Ersetzung(kontext.plus(s), alt, neu, jeweils, false, provenienz));
}
if ((m = ERSETZUNG_OHNE_STELLE.matcher(text)).matches()) {
@@ -244,8 +255,8 @@ final class BefehlErkenner {
? new WortAnker.NachWoertern(ankerWoerter)
: new WortAnker.VorWoertern(ankerWoerter);
var woerter = wortZitat(zitate, m.group(4));
- return StellenParser.parse(m.group(1))
- .map(s -> new WoerterEinfuegung(kontext.plus(s), anker, woerter, provenienz));
+ return ausStellen(
+ m.group(1), s -> new WoerterEinfuegung(kontext.plus(s), anker, woerter, provenienz));
}
if ((m = WOERTER_EINFUEGUNG_VOR_KOMMA.matcher(text)).matches()) {
@@ -335,8 +346,12 @@ final class BefehlErkenner {
if ((m = STREICHUNG.matcher(text)).matches()) {
var woerter = wortZitat(zitate, m.group(2));
- return StellenParser.parse(m.group(1))
- .map(s -> new Streichung(kontext.plus(s), woerter, provenienz));
+ return ausStellen(m.group(1), s -> new Streichung(kontext.plus(s), woerter, provenienz));
+ }
+
+ if ((m = UMNUMMERIERUNG_BEREICH.matcher(text)).matches()) {
+ return bereichsUmnummerierung(
+ m.group(3), m.group(1), m.group(2), m.group(4), m.group(5), kontext, provenienz);
}
if ((m = UMNUMMERIERUNG.matcher(text)).matches()) {
@@ -345,14 +360,63 @@ final class BefehlErkenner {
.map(
alt ->
new Umnummerierung(
- kontext.plus(alt),
- kontext.plus(new Stelle(java.util.List.of(neu))),
- provenienz));
+ kontext.plus(alt), kontext.plus(new Stelle(List.of(neu))), provenienz));
}
return Optional.empty();
}
+ /**
+ * Löst „Die bisherigen Absätze X bis Y werden zu den Absätzen X′ bis Y′.“ in einzelne
+ * Umnummerierungen auf, angewandt in absteigender Reihenfolge (Y→Y′ zuerst), damit die
+ * sequenzielle Anwendung keine Labels kollidieren lässt.
+ */
+ private static Optional<Aenderungsbefehl> bereichsUmnummerierung(
+ String ebeneWort,
+ String altVon,
+ String altBis,
+ String neuVon,
+ String neuBis,
+ Stelle kontext,
+ Provenienz provenienz) {
+ var ebene = ebeneWort.equals("Absätzen") ? "Absatz" : "Satz";
+ int av = Integer.parseInt(altVon);
+ int ab = Integer.parseInt(altBis);
+ int nv = Integer.parseInt(neuVon);
+ int nb = Integer.parseInt(neuBis);
+ if (ab - av != nb - nv || ab < av) {
+ return Optional.empty();
+ }
+ var teile = new ArrayList<Aenderungsbefehl>();
+ for (int k = ab - av; k >= 0; k--) {
+ var alt = komponenteFuer(ebene, String.valueOf(av + k));
+ var neu = komponenteFuer(ebene, String.valueOf(nv + k));
+ teile.add(
+ new Umnummerierung(
+ kontext.plus(new Stelle(List.of(alt))),
+ kontext.plus(new Stelle(List.of(neu))),
+ provenienz));
+ }
+ return Optional.of(new Sammelbefehl(teile));
+ }
+
+ /**
+ * Wendet einen Stellen-basierten Befehlsbauer auf eine (ggf. koordinierte) Stellenangabe an: bei
+ * einer einzelnen Stelle das gewohnte Verhalten, bei mehreren per „und“ verbundenen Stellen ein
+ * {@link Sammelbefehl}, der die Operation auf jede Stelle anwendet.
+ */
+ private static Optional<Aenderungsbefehl> ausStellen(
+ String phrase, Function<Stelle, Aenderungsbefehl> bauer) {
+ var stellen = StellenParser.parseMehrfach(phrase);
+ if (stellen.isEmpty()) {
+ return Optional.empty();
+ }
+ if (stellen.size() == 1) {
+ return Optional.of(bauer.apply(stellen.get(0)));
+ }
+ return Optional.of(new Sammelbefehl(stellen.stream().map(bauer).toList()));
+ }
+
private record EbeneBezeichnung(Ebene ebene, String bezeichnung) {}
private static Optional<EbeneBezeichnung> ebeneUndBezeichnung(String phrase) {
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 ad794b5..07702b2 100644
--- a/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java
+++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java
@@ -2,6 +2,7 @@ package eu.mulk.aendggner.aenderung.parse;
import eu.mulk.aendggner.aenderung.Stelle;
import java.util.ArrayList;
+import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
@@ -103,6 +104,74 @@ public final class StellenParser {
return Optional.of(new Stelle(komponenten));
}
+ private static final Pattern KOORDINATION = Pattern.compile(",\\s*|\\s+und\\s+|\\s+sowie\\s+");
+
+ /**
+ * Parst eine ggf. per „und“/„sowie“/Komma koordinierte Stellenangabe in eine Liste von Stellen.
+ * Nachfolgende Segmente teilen sich den gemeinsamen Präfix des vorigen Segments: „§ 3 Absatz 1
+ * Satz 2 und Absatz 4“ → [§ 3 Absatz 1 Satz 2, § 3 Absatz 4].
+ *
+ * <p>Für eine einfache (nicht koordinierte) Angabe liefert die Methode genau ein Element (bzw.
+ * eine leere Liste, wenn {@link #parse} sie ablehnt) und ist damit ein Drop-in für {@code
+ * parse(...).map(List::of)}. Kann ein Segment nicht geparst werden oder findet der Präfix-Merge
+ * keine Anknüpfung, wird eine leere Liste geliefert — der Aufrufer stuft den Befehl dann als
+ * unbekannt ein (konservativ: lieber manuell prüfen als falsch anwenden).
+ */
+ public static List<Stelle> parseMehrfach(String phrase) {
+ var segmente = KOORDINATION.split(phrase.strip());
+ if (segmente.length <= 1) {
+ return parse(phrase).map(List::of).orElseGet(List::of);
+ }
+
+ var ergebnis = new ArrayList<Stelle>();
+ Stelle vorige = null;
+ for (var segment : segmente) {
+ if (segment.isBlank()) {
+ return List.of();
+ }
+ var teil = parse(segment);
+ if (teil.isEmpty()) {
+ return List.of();
+ }
+ Stelle voll;
+ if (vorige == null) {
+ voll = teil.get();
+ } else {
+ var gemergt = mitGemeinsamemPraefix(vorige, teil.get());
+ if (gemergt.isEmpty()) {
+ return List.of();
+ }
+ voll = gemergt.get();
+ }
+ ergebnis.add(voll);
+ vorige = voll;
+ }
+ return ergebnis;
+ }
+
+ /**
+ * Ergänzt {@code segment} um die Präfix-Komponenten von {@code vorige}, die feiner-granular als
+ * die führende Komponente des Segments sind. Die führende Komponentenklasse des Segments wird in
+ * {@code vorige} gesucht; alle davor stehenden Komponenten bilden den gemeinsamen Präfix.
+ */
+ private static Optional<Stelle> mitGemeinsamemPraefix(Stelle vorige, Stelle segment) {
+ var fuehrende = segment.komponenten().get(0).getClass();
+ var vorKomp = vorige.komponenten();
+ int ankerIndex = -1;
+ for (int i = 0; i < vorKomp.size(); i++) {
+ if (vorKomp.get(i).getClass().equals(fuehrende)) {
+ ankerIndex = i;
+ break;
+ }
+ }
+ if (ankerIndex < 0) {
+ return Optional.empty();
+ }
+ var komponenten = new ArrayList<>(vorKomp.subList(0, ankerIndex));
+ komponenten.addAll(segment.komponenten());
+ return Optional.of(new Stelle(komponenten));
+ }
+
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 02be593..2c9a33e 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
@@ -5,6 +5,7 @@ import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Anfuegung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Aufhebung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Ersetzung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Neufassung;
+import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Sammelbefehl;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Streichung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturEinfuegung;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturErsetzung;
@@ -86,6 +87,7 @@ public final class BefehlAnwender {
case Anfuegung a -> wendeAnfuegungAn(normen, a);
case Aufhebung a -> wendeAufhebungAn(normen, a);
case Umnummerierung u -> wendeUmnummerierungAn(normen, u);
+ case Sammelbefehl s -> wendeSammelAn(normen, s);
case UnbekannterBefehl u -> manuell(befehl, "Befehl nicht erkannt.");
};
} catch (RuntimeException e) {
@@ -467,6 +469,31 @@ public final class BefehlAnwender {
return angewandt(befehl, "(keine Textänderung nötig)");
}
+ /**
+ * Ein Mehrfachziel-Befehl („In A und B wird jeweils …“): wendet jeden Teilbefehl nacheinander an
+ * (jeder mutiert den fortlaufenden Zwischenstand) und fasst sie zu einem Protokolleintrag
+ * 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) {
+ var betroffene = new LinkedHashSet<String>();
+ var fehler = new ArrayList<String>();
+ int i = 1;
+ for (var teil : befehl.teilbefehle()) {
+ var ergebnis = wendeAn(normen, teil);
+ betroffene.addAll(ergebnis.betroffeneEnbez());
+ if (ergebnis.status() != Status.ANGEWANDT) {
+ fehler.add("Teil " + i + " (" + teil.stelle().anzeigeText() + "): " + ergebnis.begruendung());
+ }
+ i++;
+ }
+ if (fehler.isEmpty()) {
+ return new AngewandteAenderung(befehl, Status.ANGEWANDT, "", betroffene);
+ }
+ return new AngewandteAenderung(
+ befehl, Status.MANUELL_PRUEFEN, String.join(" ", fehler), betroffene);
+ }
+
// --- Gemeinsame Helfer ---------------------------------------------------------------------
private record TextErgebnis(@Nullable String text, @Nullable String fehler) {