diff options
Diffstat (limited to 'src/main')
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) { |
