diff options
Diffstat (limited to 'src/main/java/eu')
4 files changed, 281 insertions, 26 deletions
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java b/src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java index 6b7c093..a2db382 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java @@ -89,6 +89,13 @@ public sealed interface Aenderungsbefehl { implements Aenderungsbefehl {} /** + * „Der Wortlaut wird Absatz 1.“ — der bisher unnummerierte Normtext erhält die Absatznummer, wird + * also zum ersten Absatz (Vorbereitung für das Anfügen weiterer Absätze). + */ + record WortlautZuAbsatz(Stelle stelle, String nummer, 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. 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 0ef0973..80f664e 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java @@ -13,6 +13,7 @@ import eu.mulk.aendggner.aenderung.Aenderungsbefehl.StrukturErsetzung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Umnummerierung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WoerterEinfuegung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortAnker; +import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortlautZuAbsatz; import eu.mulk.aendggner.aenderung.Provenienz; import eu.mulk.aendggner.aenderung.Stelle; import java.util.ArrayList; @@ -132,14 +133,61 @@ final class BefehlErkenner { "^(?:In )?(.+?) (?:wird|werden) (?:jeweils )?" + WOERTER + " " + Z + " gestrichen\\.$"); private static final Pattern UMNUMMERIERUNG = - Pattern.compile("^(?:Der bisherige )?(.+?) wird (?:zu )?(Absatz|Satz) (\\d+[a-z]?)\\.$"); + Pattern.compile( + "^(?:Der bisherige |Die bisherige |Das bisherige )?(.+?) wird (?:zu )?" + + "(Absatz|Satz|Nummer|Buchstabe) (\\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. + // „Die bisherigen Absätze 2 bis 4 werden zu den Absätzen 3 bis 5.“ bzw. „Die bisherigen Nummern 4 + // bis 6 werden die Nummern 8 bis 10.“ — Bereichs-Umnummerierung, in Einzelbefehle aufgelöst. 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+)\\.$"); + "^Die bisherigen (?:Absätze|Sätze|Nummern|Buchstaben) (\\d+) bis (\\d+) " + + "werden (?:zu den |die )?(Absätzen|Sätzen|Nummern|Buchstaben|Absätze|Sätze) " + + "(\\d+) bis (\\d+)\\.$"); + + // „Die §§ 52 bis 56 werden wie folgt gefasst: „§ 52 (weggefallen) …““ — Neufassung eines §-Bereichs; + // der Zitatblock wird an „§ N“-Grenzen in Einzel-Neufassungen zerlegt. + private static final Pattern PARAGRAPH_BEREICH_NEUFASSUNG = + Pattern.compile( + "^Die §§ (\\d+[a-z]?) bis (\\d+[a-z]?) (?:wird|werden) wie folgt gefasst: " + Z + "\\.?$"); + + // „Der Wortlaut wird Absatz 1.“ + private static final Pattern WORTLAUT_ZU_ABSATZ = + Pattern.compile("^Der Wortlaut wird Absatz (\\d+[a-z]?)\\.$"); + + // „In Nummer 7 wird das Wort «1» am Ende durch ein Komma ersetzt.“ + private static final Pattern WORT_ZU_SATZZEICHEN = + Pattern.compile( + "^(?:In )?(.+?) (?:wird|werden) " + + WOERTER + + " " + + Z + + "(?: am Ende)? durch (ein Komma|ein Semikolon|einen Punkt) ersetzt\\.$"); + + // „In Satz 2 wird nach dem Wort «1» ein Komma und werden die Wörter «2» eingefügt.“ + private static final Pattern KOMMA_UND_WOERTER_EINFUEGUNG = + Pattern.compile( + "^(?:In )?(.+?) (?:wird|werden) (?:jeweils )?(nach|vor) " + + "(?:dem Wort|den Wörtern|der Angabe|der Zahl) " + + Z + + " ein Komma und (?:wird|werden) " + + WOERTER + + " " + + Z + + " eingefügt\\.$"); + + // „In § 74 werden die Wörter «1» durch ein Komma und die Wörter «2» ersetzt.“ + private static final Pattern ERSETZUNG_DURCH_KOMMA_UND_WOERTER = + Pattern.compile( + "^(?:In )?(.+?) (?:wird|werden) (?:jeweils )?" + + WOERTER + + " " + + Z + + " durch ein Komma und " + + WOERTER + + " " + + Z + + " ersetzt\\.$"); private static final Pattern INHALTSUEBERSICHT_EINFUEGUNG = Pattern.compile( @@ -236,12 +284,20 @@ final class BefehlErkenner { Matcher m; + if ((m = PARAGRAPH_BEREICH_NEUFASSUNG.matcher(text)).matches()) { + return paragraphBereichNeufassung(zitat(zitate, m.group(3)), kontext, provenienz); + } + if ((m = NEUFASSUNG.matcher(text)).matches()) { var neuerText = zitat(zitate, m.group(2)); return StellenParser.parse(m.group(1)) .map(s -> new Neufassung(kontext.plus(s), neuerText, provenienz)); } + if ((m = WORTLAUT_ZU_ABSATZ.matcher(text)).matches()) { + return Optional.of(new WortlautZuAbsatz(kontext, m.group(1), provenienz)); + } + if ((m = STRUKTUR_ERSETZUNG.matcher(text)).matches()) { var neuerText = zitat(zitate, m.group(3)); var ziel = m.group(2).strip(); @@ -262,6 +318,21 @@ final class BefehlErkenner { new StrukturErsetzung(kontext.plus(stelle.get()), ebene, neuerText, provenienz)); } + if ((m = WORT_ZU_SATZZEICHEN.matcher(text)).matches()) { + var alt = wortZitat(zitate, m.group(2)); + var neu = satzzeichen(m.group(3)); + var amEnde = text.contains(" am Ende "); + return ausStellen( + m.group(1), s -> new Ersetzung(kontext.plus(s), alt, neu, false, amEnde, provenienz)); + } + + if ((m = ERSETZUNG_DURCH_KOMMA_UND_WOERTER.matcher(text)).matches()) { + var alt = wortZitat(zitate, m.group(2)); + var neu = ", " + wortZitat(zitate, m.group(3)); + return ausStellen( + m.group(1), s -> new Ersetzung(kontext.plus(s), alt, neu, false, false, provenienz)); + } + if ((m = ERSETZUNG.matcher(text)).matches()) { var jeweils = m.group(2) != null || text.contains(" jeweils durch "); var alt = wortZitat(zitate, m.group(3)); @@ -329,6 +400,17 @@ final class BefehlErkenner { m.group(1), s -> new WoerterEinfuegung(kontext.plus(s), anker, woerter, provenienz)); } + if ((m = KOMMA_UND_WOERTER_EINFUEGUNG.matcher(text)).matches()) { + var ankerWoerter = wortZitat(zitate, m.group(3)); + var anker = + m.group(2).equals("nach") + ? new WortAnker.NachWoertern(ankerWoerter) + : new WortAnker.VorWoertern(ankerWoerter); + var woerter = ", " + wortZitat(zitate, m.group(4)); + return ausStellen( + m.group(1), s -> new WoerterEinfuegung(kontext.plus(s), anker, woerter, provenienz)); + } + if ((m = EINFUEGUNG_ANKER_ZUERST.matcher(text)).matches()) { var ankerWoerter = wortZitat(zitate, m.group(2)); var anker = @@ -412,7 +494,7 @@ final class BefehlErkenner { } if ((m = AUFHEBUNG.matcher(text)).matches()) { - return StellenParser.parse(m.group(1)).map(s -> new Aufhebung(kontext.plus(s), provenienz)); + return ausStellen(m.group(1), s -> new Aufhebung(kontext.plus(s), provenienz)); } if ((m = STREICHUNG.matcher(text)).matches()) { @@ -571,7 +653,10 @@ final class BefehlErkenner { String neuBis, Stelle kontext, Provenienz provenienz) { - var ebene = ebeneWort.equals("Absätzen") ? "Absatz" : "Satz"; + var ebene = ebeneAusWort(ebeneWort); + if (ebene == null) { + return Optional.empty(); + } int av = Integer.parseInt(altVon); int ab = Integer.parseInt(altBis); int nv = Integer.parseInt(neuVon); @@ -654,10 +739,49 @@ final class BefehlErkenner { return switch (ebene) { case "Absatz" -> new Stelle.AbsatzNr(nummer); case "Satz" -> new Stelle.SatzNr(nummer); + case "Nummer" -> new Stelle.NummerNr(nummer); + case "Buchstabe" -> new Stelle.BuchstabeNr(nummer); default -> throw new IllegalArgumentException("Unbekannte Ebene: " + ebene); }; } + /** Normalisiert die Ebenenwörter (auch Dativ-/Pluralformen) auf den Basisnamen. */ + private static @Nullable String ebeneAusWort(String wort) { + return switch (wort) { + case "Absatz", "Absätze", "Absätzen" -> "Absatz"; + case "Satz", "Sätze", "Sätzen" -> "Satz"; + case "Nummer", "Nummern" -> "Nummer"; + case "Buchstabe", "Buchstaben" -> "Buchstabe"; + default -> null; + }; + } + + /** + * Zerlegt den Zitatblock einer §-Bereichs-Neufassung („§ 52 (weggefallen) § 53 (weggefallen) …“) + * an den „§ N“-Grenzen und erzeugt je eine {@link Neufassung} für den betroffenen Paragraphen. + */ + private static Optional<Aenderungsbefehl> paragraphBereichNeufassung( + String block, Stelle kontext, Provenienz provenienz) { + var teile = new ArrayList<Aenderungsbefehl>(); + var stuecke = block.strip().split("(?=§\\s*\\d)"); + for (var stueck : stuecke) { + var s = stueck.strip(); + if (s.isEmpty()) { + continue; + } + var pm = Pattern.compile("^§\\s*(\\d+[a-z]?)\\b").matcher(s); + if (!pm.find()) { + return Optional.empty(); + } + var stelle = kontext.plus(new Stelle(List.of(new Stelle.Paragraph(pm.group(1))))); + teile.add(new Neufassung(stelle, s, provenienz)); + } + if (teile.isEmpty()) { + return Optional.empty(); + } + return Optional.of(teile.size() == 1 ? teile.get(0) : new Sammelbefehl(teile)); + } + /** Zitat für Textblöcke (Neufassung, Einfügung ganzer Einheiten): Zeilenstruktur erhalten. */ private static String zitat(ZitatExtraktor.Ergebnis zitate, String index) { return zitate.zitat(Integer.parseInt(index)); 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 db7633e..2b2b80e 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; /** * Parst Stellenangaben wie „§ 5a Absatz 2 Satz 1 Nummer 4 Buchstabe c“, „der Inhaltsübersicht“ oder @@ -58,7 +59,7 @@ public final class StellenParser { komponenten.add(new Stelle.Paragraph(wert)); i++; } - case "Absatz", "Abs." -> { + case "Absatz", "Abs.", "Absätze" -> { var wert = naechstesWort(woerter, i); if (wert == null || !NUMMER_WERT.matcher(wert).matches()) { return Optional.empty(); @@ -66,7 +67,7 @@ public final class StellenParser { komponenten.add(new Stelle.AbsatzNr(wert)); i++; } - case "Satz" -> { + case "Satz", "Sätze" -> { var wert = naechstesWort(woerter, i); if (wert == null || !NUMMER_WERT.matcher(wert).matches()) { return Optional.empty(); @@ -74,7 +75,7 @@ public final class StellenParser { komponenten.add(new Stelle.SatzNr(wert)); i++; } - case "Nummer", "Nr." -> { + case "Nummer", "Nr.", "Nummern" -> { var wert = naechstesWort(woerter, i); if (wert == null || !NUMMER_WERT.matcher(wert).matches()) { return Optional.empty(); @@ -82,7 +83,7 @@ public final class StellenParser { komponenten.add(new Stelle.NummerNr(wert)); i++; } - case "Buchstabe", "Buchst.", "Doppelbuchstabe" -> { + case "Buchstabe", "Buchst.", "Doppelbuchstabe", "Buchstaben" -> { var wert = naechstesWort(woerter, i); if (wert == null || !BUCHSTABE_WERT.matcher(wert).matches()) { return Optional.empty(); @@ -119,16 +120,21 @@ public final class StellenParser { */ 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 bereich = entfalteBereich(segment, vorige); + if (bereich != null) { + if (bereich.isEmpty()) { + return List.of(); + } + ergebnis.addAll(bereich); + vorige = bereich.get(bereich.size() - 1); + continue; + } Stelle voll; var teil = parse(segment); if (teil.isPresent()) { @@ -162,17 +168,7 @@ public final class StellenParser { /** Ersetzt die letzte Komponente von {@code vorige} durch dieselbe Komponentenart mit neuem Label. */ private static Optional<Stelle> mitGeerbtemLabel(Stelle vorige, String label) { var komponenten = new ArrayList<>(vorige.komponenten()); - var letzte = komponenten.get(komponenten.size() - 1); - Stelle.Komponente neu = - switch (letzte) { - case Stelle.Paragraph p -> new Stelle.Paragraph(label); - case Stelle.AbsatzNr a -> new Stelle.AbsatzNr(label); - 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.Ueberschrift u -> null; - case Stelle.Inhaltsuebersicht i -> null; - }; + var neu = mitLabel(komponenten.get(komponenten.size() - 1), label); if (neu == null) { return Optional.empty(); } @@ -180,6 +176,112 @@ public final class StellenParser { return Optional.of(new Stelle(komponenten)); } + private static final Pattern BEREICH = + Pattern.compile( + "(?:(§§?|Absätze|Absatz|Sätze|Satz|Nummern|Nummer|Buchstaben|Buchstabe)\\s+)?" + + "(\\d+|[a-z])\\s+bis\\s+(\\d+|[a-z])"); + + /** + * Entfaltet einen {@code X bis Y}-Bereich (ganzzahlig oder Einzelbuchstabe) in Einzelstellen. Die + * Komponentenart stammt aus der Bereichsangabe („Nummern 1 bis 3“) oder — bei bloßem „1 bis 3“ — + * aus der letzten Komponente der vorigen Stelle. Liefert {@code null}, wenn {@code segment} kein + * Bereich ist, und eine leere Liste, wenn er unauflösbar ist (z.B. absteigend, gemischt). + */ + private static @Nullable List<Stelle> entfalteBereich(String segment, @Nullable Stelle vorige) { + var ohneArtikel = segment.strip().replaceFirst("^(?:[Dd]ie|[Dd]er|[Dd]as|[Dd]en) ", ""); + var m = BEREICH.matcher(ohneArtikel); + if (!m.matches()) { + return null; + } + var art = m.group(1); + var von = m.group(2); + var bis = m.group(3); + boolean numerisch = von.matches("\\d+") && bis.matches("\\d+"); + boolean alpha = von.matches("[a-z]") && bis.matches("[a-z]"); + if (!numerisch && !alpha) { + return List.of(); + } + var praefix = new ArrayList<Stelle.Komponente>(); + Stelle.Komponente muster; + if (art != null) { + muster = komponenteFuerArt(art, von); + if (muster == null) { + return List.of(); + } + } else { + // Bloßer Bereich „1 bis 3“: Art und Präfix von der vorigen Stelle erben. + if (vorige == null || vorige.komponenten().isEmpty()) { + return List.of(); + } + praefix.addAll(vorige.komponenten().subList(0, vorige.komponenten().size() - 1)); + muster = vorige.komponenten().get(vorige.komponenten().size() - 1); + } + var labels = numerisch ? zahlenBereich(von, bis) : buchstabenBereich(von, bis); + if (labels.isEmpty()) { + return List.of(); + } + var stellen = new ArrayList<Stelle>(); + for (var label : labels) { + var komponenten = new ArrayList<>(praefix); + var neu = mitLabel(muster, label); + if (neu == null) { + return List.of(); + } + komponenten.add(neu); + stellen.add(new Stelle(komponenten)); + } + return stellen; + } + + private static Stelle.@Nullable Komponente komponenteFuerArt(String art, String label) { + return switch (art) { + case "§", "§§" -> new Stelle.Paragraph(label); + case "Absatz", "Absätze" -> new Stelle.AbsatzNr(label); + case "Satz", "Sätze" -> new Stelle.SatzNr(label); + case "Nummer", "Nummern" -> new Stelle.NummerNr(label); + case "Buchstabe", "Buchstaben" -> new Stelle.BuchstabeNr(label); + default -> null; + }; + } + + private static Stelle.@Nullable Komponente mitLabel(Stelle.Komponente muster, String label) { + return switch (muster) { + case Stelle.Paragraph p -> new Stelle.Paragraph(label); + case Stelle.AbsatzNr a -> new Stelle.AbsatzNr(label); + 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.Ueberschrift u -> null; + case Stelle.Inhaltsuebersicht i -> null; + }; + } + + private static List<String> zahlenBereich(String von, String bis) { + int a = Integer.parseInt(von); + int b = Integer.parseInt(bis); + if (b < a) { + return List.of(); + } + var labels = new ArrayList<String>(); + for (int k = a; k <= b; k++) { + labels.add(String.valueOf(k)); + } + return labels; + } + + private static List<String> buchstabenBereich(String von, String bis) { + char a = von.charAt(0); + char b = bis.charAt(0); + if (b < a) { + return List.of(); + } + var labels = new ArrayList<String>(); + for (char c = a; c <= b; c++) { + labels.add(String.valueOf(c)); + } + return labels; + } + /** * 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 diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java index 2c9a33e..94c5db1 100644 --- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java +++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java @@ -13,6 +13,7 @@ import eu.mulk.aendggner.aenderung.Aenderungsbefehl.Umnummerierung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.UnbekannterBefehl; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WoerterEinfuegung; import eu.mulk.aendggner.aenderung.Aenderungsbefehl.WortAnker; +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; @@ -87,6 +88,7 @@ public final class BefehlAnwender { case Anfuegung a -> wendeAnfuegungAn(normen, a); 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 UnbekannterBefehl u -> manuell(befehl, "Befehl nicht erkannt."); }; @@ -469,6 +471,26 @@ public final class BefehlAnwender { return angewandt(befehl, "(keine Textänderung nötig)"); } + private static AngewandteAenderung wendeWortlautZuAbsatzAn( + List<Norm> normen, WortlautZuAbsatz befehl) { + var aufloesung = loeseNormAuf(normen, befehl.stelle()); + if (aufloesung.fehler() != null) { + return manuell(befehl, aufloesung.fehler()); + } + var norm = normen.get(aufloesung.normIndex()); + var sb = new StringBuilder(); + for (var absatz : norm.absaetze()) { + if (sb.length() > 0) { + sb.append("\n\n"); + } + sb.append(absatz.text()); + } + normen.set( + aufloesung.normIndex(), + norm.mitAbsaetzen(List.of(new Absatz(befehl.nummer(), sb.toString())))); + return angewandt(befehl, norm.enbez()); + } + /** * 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 |
