diff options
Diffstat (limited to 'src')
9 files changed, 749 insertions, 40 deletions
diff --git a/src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java b/src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java index a2db382..f445811 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/Aenderungsbefehl.java @@ -47,9 +47,20 @@ public sealed interface Aenderungsbefehl { /** * „§ 2 Absatz 2 wird durch die folgenden Absätze 2 und 3 ersetzt: „…““ — ein Ziel wird durch * einen Block ersetzt, der auch mehrere neue Einheiten enthalten darf. + * + * @param stelle das (erste) zu ersetzende Ziel. + * @param bisStelle bei einem zusammenhängenden Bereich („Die Absätze 8 und 9 …“, „Die Sätze 4 bis + * 5 …“) das letzte Ziel; {@code null} bei einem Einzelziel. */ - record StrukturErsetzung(Stelle stelle, Ebene ebene, String text, Provenienz provenienz) - implements Aenderungsbefehl {} + record StrukturErsetzung( + Stelle stelle, @Nullable Stelle bisStelle, Ebene ebene, String text, Provenienz provenienz) + implements Aenderungsbefehl { + + /** Einzelziel-Konstruktor (kein Bereich). */ + public StrukturErsetzung(Stelle stelle, Ebene ebene, String text, Provenienz provenienz) { + this(stelle, null, ebene, text, provenienz); + } + } /** „… werden nach dem Wort „A“ die Wörter „B“ eingefügt.“ */ record WoerterEinfuegung(Stelle stelle, WortAnker anker, String woerter, Provenienz provenienz) 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 7283d90..2cc5108 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/BefehlErkenner.java @@ -45,9 +45,23 @@ final class BefehlErkenner { // „§ 2 Absatz 2 wird durch die folgenden Absätze 2 und 3 ersetzt: „…““ (neues BGBl-Format); // auch „Die Überschrift wird durch die folgende Überschrift ersetzt: „…““ (Entwürfe). + // Der optionale Enumerator-Präfix („3. “, „a) “) fängt die Entwurfs-/Drucksachenform ab, bei der + // die Aufzählungsbezeichnung außerhalb des Zitats steht („… ersetzt: 3. „…““); er wird dem + // Ersatztext wieder vorangestellt, da das Label sonst verloren ginge. private static final Pattern STRUKTUR_ERSETZUNG = Pattern.compile( "^(?:In )?(.+?) (?:wird|werden) durch (?:den |die |das )?folgende[nrs]? (.+?) ersetzt: " + + "((?:\\d+[a-z]?\\.|[a-z]{1,3}\\))\\s*)?" + + Z + + "\\.?$"); + + // „In Anlage 7 wird die Überschrift durch die folgende Überschrift ersetzt: „…““ — Neufassung der + // Überschrift einer benannten Einheit (Anlage/Gliederung/Paragraph); das „die Überschrift“-Objekt + // steht hier zwischen Stelle und „durch“, weshalb STRUKTUR_ERSETZUNG nicht greift. + private static final Pattern UEBERSCHRIFT_ERSETZUNG = + Pattern.compile( + "^(?:In )?(.+?) (?:wird|werden) die Überschrift durch " + + "(?:die |den |das )?folgende[nrs]? Überschrift ersetzt: " + Z + "\\.?$"); @@ -152,19 +166,31 @@ final class BefehlErkenner { Pattern.compile( "^(?:In )?(.+?) (?:wird|werden) (?:jeweils )?" + WOERTER + " " + Z + " gestrichen\\.$"); + // „§ 9 wird gestrichen.“, „Absatz 3 wird gestrichen.“, „Die §§ 34 bis 39 werden gestrichen.“, + // „Der bisherige Teil 3 wird gestrichen.“ — Streichung ganzer Struktureinheiten (semantisch eine + // Aufhebung). Greift erst, wenn die Wörter-Streichung STREICHUNG (die ein Zitat verlangt) und die + // Überschrift-/Absatzbezeichnungs-Streichungen nicht passen. + private static final Pattern STRUKTUR_STREICHUNG = + Pattern.compile("^(.+?) (?:wird|werden) gestrichen\\.$"); + private static final Pattern UMNUMMERIERUNG = Pattern.compile( "^(?:Der bisherige |Die bisherige |Das bisherige )?(.+?) wird (?:zu )?" - + "(Absatz|Satz|Nummer|Buchstabe) (\\d+[a-z]?)\\.$"); + + "(§|Absatz|Satz|Nummer|Buchstabe|Teil|Abschnitt|Unterabschnitt|Buch|Kapitel|Anlage) " + + "(\\d+[a-z]?)\\.$"); // „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|Nummern|Buchstaben) (\\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 §§ 46 und 47 werden zu den §§ 34 und 35.“ — paarweise §-Umnummerierung (auch Bereiche). + private static final Pattern UMNUMMERIERUNG_PARAGRAPHEN = + Pattern.compile("^Die §§ (.+?) werden (?:zu den §§ |zu §§ |die §§ )(.+?)\\.$"); + // „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 = @@ -323,32 +349,77 @@ final class BefehlErkenner { 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)); + var stellen = StellenParser.parseMehrfach(m.group(1)); + if (stellen.size() == 1) { + return Optional.of(new Neufassung(kontext.plus(stellen.get(0)), neuerText, provenienz)); + } + if (stellen.size() > 1) { + // „Die bisherigen Sätze 4 und 5 werden wie folgt gefasst: „…““ — ein zusammenhängender + // Bereich wird durch einen Block ersetzt (Ebene aus dem Ziel abgeleitet). + return koordinierteErsetzung(stellen, null, kontext, neuerText, provenienz); + } + return Optional.empty(); } if ((m = WORTLAUT_ZU_ABSATZ.matcher(text)).matches()) { return Optional.of(new WortlautZuAbsatz(kontext, m.group(1), provenienz)); } + if ((m = UEBERSCHRIFT_ERSETZUNG.matcher(text)).matches()) { + var neuerText = zitat(zitate, m.group(2)); + return StellenParser.parse(m.group(1)) + .map( + s -> + new Neufassung( + kontext.plus(s).plus(new Stelle(List.of(new Stelle.Ueberschrift()))), + neuerText, + provenienz)); + } + if ((m = STRUKTUR_ERSETZUNG.matcher(text)).matches()) { - var neuerText = zitat(zitate, m.group(3)); + var enumerator = m.group(3); + var rohText = zitat(zitate, m.group(4)); + // Entwurfsform „… ersetzt: 3. „…““: das außerhalb des Zitats stehende Label wieder anfügen. + var neuerText = enumerator != null ? enumerator.strip() + " " + rohText.strip() : rohText; var ziel = m.group(2).strip(); - var stelle = StellenParser.parse(m.group(1)); - if (stelle.isEmpty()) { + var stellen = StellenParser.parseMehrfach(m.group(1)); + if (stellen.isEmpty()) { return Optional.empty(); } // „durch die folgende Überschrift ersetzt“ ist eine Neufassung der Überschrift, - // „§ 19 wird durch den folgenden § 19 ersetzt“ eine Neufassung des Paragraphen. - if (ziel.equals("Überschrift") || ziel.matches("§\\s*\\d+[a-z]?")) { - return Optional.of(new Neufassung(kontext.plus(stelle.get()), neuerText, provenienz)); + // „§ 19 wird durch den folgenden § 19 ersetzt“ eine Neufassung des Paragraphen, + // „Die Inhaltsübersicht wird durch die folgende Inhaltsübersicht ersetzt“ eine Neufassung der + // Inhaltsübersicht (die der Applier stets zur manuellen Prüfung markiert). + if (stellen.size() == 1 + && (ziel.equals("Überschrift") + || ziel.equals("Inhaltsübersicht") + || ziel.matches("§\\s*\\d+[a-z]?"))) { + return Optional.of(new Neufassung(kontext.plus(stellen.get(0)), neuerText, provenienz)); } var ebene = strukturEbene(ziel); if (ebene == null) { return Optional.empty(); } - return Optional.of( - new StrukturErsetzung(kontext.plus(stelle.get()), ebene, neuerText, provenienz)); + if (ebene == Ebene.PARAGRAPH) { + // „§ 71 wird durch die folgenden §§ 71 bis 71p ersetzt: „…““ bzw. „Die §§ 42 bis 45 werden + // durch die folgenden §§ 42 bis 45 ersetzt: „…““ — ein §-Bereich wird durch einen §-Block + // ersetzt. + var first = stellen.get(0); + var last = stellen.get(stellen.size() - 1); + return Optional.of( + new StrukturErsetzung( + kontext.plus(first), + stellen.size() > 1 ? kontext.plus(last) : null, + Ebene.PARAGRAPH, + neuerText, + provenienz)); + } + if (stellen.size() == 1) { + return Optional.of( + new StrukturErsetzung(kontext.plus(stellen.get(0)), ebene, neuerText, provenienz)); + } + // „Die Absätze 8 und 9 werden durch die folgenden Absätze 8 bis 10 ersetzt: „…““ + return koordinierteErsetzung(stellen, ebene, kontext, neuerText, provenienz); } if ((m = WORT_ZU_SATZZEICHEN.matcher(text)).matches()) { @@ -471,9 +542,25 @@ final class BefehlErkenner { if ((m = STRUKTUR_EINFUEGUNG.matcher(text)).matches()) { var vorher = m.group(1).equals("Vor"); var stelle = StellenParser.parse(m.group(2)); - var ebeneBez = ebeneUndBezeichnung(m.group(3)); var textInhalt = zitat(zitate, m.group(4)); - if (stelle.isEmpty() || ebeneBez.isEmpty()) { + if (stelle.isEmpty()) { + return Optional.empty(); + } + var ebeneBez = ebeneUndBezeichnung(m.group(3)); + if (ebeneBez.isEmpty()) { + // „Nach § 60a werden die folgenden §§ 60b und 60c eingefügt: „…““ — Block mehrerer + // Paragraphen (Aufteilung an den §-Überschriften erfolgt beim Anwenden). Signal: + // Ebene PARAGRAPH mit bezeichnung == null. + if (m.group(3).strip().matches("§§\\s*\\d.*")) { + return Optional.of( + new StrukturEinfuegung( + kontext.plus(stelle.get()), + vorher, + Ebene.PARAGRAPH, + null, + textInhalt, + provenienz)); + } return Optional.empty(); } return Optional.of( @@ -546,6 +633,14 @@ final class BefehlErkenner { return ausStellen(m.group(1), s -> new Streichung(kontext.plus(s), woerter, provenienz)); } + if ((m = STRUKTUR_STREICHUNG.matcher(text)).matches()) { + return ausStellen(m.group(1), s -> new Aufhebung(kontext.plus(s), provenienz)); + } + + if ((m = UMNUMMERIERUNG_PARAGRAPHEN.matcher(text)).matches()) { + return paragraphenUmnummerierung(m.group(1), m.group(2), kontext, 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); @@ -685,6 +780,24 @@ final class BefehlErkenner { } /** + * Löst „Die §§ 46 und 47 werden zu den §§ 34 und 35.“ (auch Bereiche) in paarweise + * §-Umnummerierungen auf. Beide Seiten werden zu Paragraphenlisten expandiert und zusammengeführt. + */ + private static Optional<Aenderungsbefehl> paragraphenUmnummerierung( + String altPhrase, String neuPhrase, Stelle kontext, Provenienz provenienz) { + var alt = StellenParser.parseMehrfach("§ " + altPhrase); + var neu = StellenParser.parseMehrfach("§ " + neuPhrase); + if (alt.isEmpty() || alt.size() != neu.size()) { + return Optional.empty(); + } + var teile = new ArrayList<Aenderungsbefehl>(); + for (int i = 0; i < alt.size(); i++) { + teile.add(new Umnummerierung(kontext.plus(alt.get(i)), kontext.plus(neu.get(i)), provenienz)); + } + return Optional.of(teile.size() == 1 ? teile.get(0) : new Sammelbefehl(teile)); + } + + /** * 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. @@ -730,6 +843,11 @@ final class BefehlErkenner { String phrase, Function<Stelle, Aenderungsbefehl> bauer) { var stellen = StellenParser.parseMehrfach(phrase); if (stellen.isEmpty()) { + // „Im Satzteil vor Nummer 1 …“, „In der Angabe vor Nummer 1 …“ — reiner Chapeau-Qualifier + // ohne eigene Stelle: die Operation bezieht sich auf die Kontextstelle. + if (StellenParser.istNurChapeau(phrase)) { + return Optional.of(bauer.apply(Stelle.LEER)); + } return Optional.empty(); } if (stellen.size() == 1) { @@ -738,6 +856,46 @@ final class BefehlErkenner { return Optional.of(new Sammelbefehl(stellen.stream().map(bauer).toList())); } + /** + * Baut aus einem zusammenhängenden, koordinierten Ziel-Bereich („Die Absätze 8 und 9 …“, „Die + * bisherigen Sätze 4 und 5 …“) eine bereichsbezogene {@link StrukturErsetzung}: erstes und letztes + * Ziel spannen den zu ersetzenden Bereich auf; der zitierte Block ersetzt ihn. Nur für Absatz- und + * Satz-Bereiche (die der Applier auflösen kann); andere Ebenen bleiben unbekannt. + */ + private static Optional<Aenderungsbefehl> koordinierteErsetzung( + List<Stelle> stellen, + @Nullable Ebene ebeneHint, + Stelle kontext, + String block, + Provenienz provenienz) { + var first = stellen.get(0); + var last = stellen.get(stellen.size() - 1); + var ebene = ebeneHint != null ? ebeneHint : ebeneAusStelle(first); + if (ebene != Ebene.ABSATZ && ebene != Ebene.SATZ) { + return Optional.empty(); + } + return Optional.of( + new StrukturErsetzung( + kontext.plus(first), kontext.plus(last), ebene, block, provenienz)); + } + + /** Die Ebene der feinsten Komponente einer Stelle (Buchstabe < Nummer < Satz < Absatz < §). */ + private static @Nullable Ebene ebeneAusStelle(Stelle stelle) { + Ebene ebene = null; + for (var komponente : stelle.komponenten()) { + ebene = + switch (komponente) { + case Stelle.Paragraph p -> Ebene.PARAGRAPH; + case Stelle.AbsatzNr a -> Ebene.ABSATZ; + case Stelle.SatzNr s -> Ebene.SATZ; + case Stelle.NummerNr n -> Ebene.NUMMER; + case Stelle.BuchstabeNr b -> Ebene.BUCHSTABE; + default -> ebene; + }; + } + return ebene; + } + private record EbeneBezeichnung(Ebene ebene, String bezeichnung) {} private static Optional<EbeneBezeichnung> ebeneUndBezeichnung(String phrase) { @@ -771,6 +929,7 @@ final class BefehlErkenner { private static @Nullable Ebene strukturEbene(String ziel) { var erstesWort = ziel.split("\\s+", 2)[0]; return switch (erstesWort) { + case "§", "§§" -> Ebene.PARAGRAPH; case "Absatz", "Absätze" -> Ebene.ABSATZ; case "Satz", "Sätze" -> Ebene.SATZ; case "Nummer", "Nummern" -> Ebene.NUMMER; @@ -781,10 +940,13 @@ final class BefehlErkenner { private static Stelle.Komponente komponenteFuer(String ebene, String nummer) { return switch (ebene) { + case "§" -> new Stelle.Paragraph(nummer); case "Absatz" -> new Stelle.AbsatzNr(nummer); case "Satz" -> new Stelle.SatzNr(nummer); case "Nummer" -> new Stelle.NummerNr(nummer); case "Buchstabe" -> new Stelle.BuchstabeNr(nummer); + case "Teil", "Abschnitt", "Unterabschnitt", "Buch", "Kapitel", "Anlage" -> + new Stelle.Gliederungseinheit(ebene, nummer); default -> throw new IllegalArgumentException("Unbekannte Ebene: " + ebene); }; } 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 4660343..ef45714 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/parse/StellenParser.java @@ -48,9 +48,19 @@ public final class StellenParser { // 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 " + "(?i)(?:im |in dem |in der |dem |der )?(?:Satzteil|Angabe) vor " + "(?:Nummer|Buchstabe|Satz|Absatz) \\S+"); + /** + * Wahr, wenn die Phrase ausschließlich aus einem Chapeau-Qualifier besteht (z.B. „im Satzteil + * vor Nummer 1“, „in der Angabe vor Nummer 1“) und daher keine eigene Stelle-Komponente trägt. + * Die Operation bezieht sich dann auf die Kontextstelle (den umgebenden Änderungsrahmen). + */ + public static boolean istNurChapeau(String phrase) { + var rest = CHAPEAU_QUALIFIER.matcher(phrase.strip()).replaceAll(" ").strip(); + return rest.isEmpty() && !phrase.isBlank(); + } + public static Optional<Stelle> parse(String phrase) { phrase = CHAPEAU_QUALIFIER.matcher(phrase).replaceAll(" ").strip(); var woerter = phrase.strip().split("\\s+"); @@ -62,7 +72,7 @@ public final class StellenParser { continue; } switch (wort) { - case "§" -> { + case "§", "§§" -> { var wert = naechstesWort(woerter, i); if (wert == null || !NUMMER_WERT.matcher(wert).matches()) { return Optional.empty(); diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java index 86eccaf..ee56b2b 100644 --- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java +++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java @@ -87,6 +87,7 @@ public final class BefehlAnwender { return switch (befehl) { case Neufassung n -> wendeGliederungNeufassungAn(gliederungen, n); case Aufhebung a -> wendeGliederungStreichungAn(gliederungen, a); + case Umnummerierung u -> wendeGliederungUmnummerierungAn(gliederungen, u); default -> manuell(befehl, "Strukturänderung wird nicht automatisch angewandt."); }; } @@ -137,6 +138,23 @@ public final class BefehlAnwender { return angewandt(befehl, alt.bezeichnung()); } + /** „Der bisherige Abschnitt 2 wird zu Abschnitt 3.“ — Bezeichnung der Gliederungseinheit umsetzen. */ + private static AngewandteAenderung wendeGliederungUmnummerierungAn( + List<Gliederung> gliederungen, Umnummerierung befehl) { + int idx = findeGliederung(gliederungen, befehl.stelle().gliederungsPfad()); + if (idx < 0) { + return manuell(befehl, "Gliederungseinheit nicht gefunden: " + befehl.stelle().anzeigeText()); + } + var neuPfad = befehl.neu().gliederungsPfad(); + if (neuPfad.isEmpty()) { + return manuell(befehl, "Neue Gliederungsbezeichnung fehlt."); + } + var neueBezeichnung = neuPfad.get(neuPfad.size() - 1).bezeichnung(); + var alt = gliederungen.get(idx); + gliederungen.set(idx, alt.mitBezeichnung(neueBezeichnung)); + return angewandt(befehl, neueBezeichnung); + } + /** * Findet die Gliederungseinheit zum Pfad („Teil 3 Abschnitt 2“): jede Ebene wird per Bezeichnung * innerhalb des Kennzahl-Präfixes der übergeordneten Ebene aufgelöst. @@ -335,14 +353,34 @@ public final class BefehlAnwender { } var fundstelle = ((StellenAufloeser.Ergebnis.Gefunden) ergebnis).fundstelle(); var norm = normen.get(fundstelle.normIndex()); + int vonIndex = fundstelle.absatzIndex(); + int bisIndex = vonIndex; + // Bereich („Die Absätze 8 und 9 werden … ersetzt“): das letzte Ziel bestimmt das Ende. + if (befehl.bisStelle() != null) { + var e2 = StellenAufloeser.aufloese(gesetzAus(normen), befehl.bisStelle()); + if (e2 instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) { + yield manuell(befehl, nicht.begruendung()); + } + var f2 = ((StellenAufloeser.Ergebnis.Gefunden) e2).fundstelle(); + if (f2.normIndex() != fundstelle.normIndex() || f2.absatzIndex() == null) { + yield manuell(befehl, "Ersetzungsbereich liegt nicht in einer einzigen Norm."); + } + bisIndex = f2.absatzIndex(); + } + if (bisIndex < vonIndex) { + yield manuell(befehl, "Ersetzungsbereich ist leer oder absteigend."); + } var absaetze = new ArrayList<>(norm.absaetze()); - absaetze.remove((int) fundstelle.absatzIndex()); - absaetze.addAll(fundstelle.absatzIndex(), parseAbsaetze(befehl.text())); + for (int k = bisIndex; k >= vonIndex; k--) { + absaetze.remove(k); + } + absaetze.addAll(vonIndex, parseAbsaetze(befehl.text())); normen.set(fundstelle.normIndex(), norm.mitAbsaetzen(absaetze)); yield angewandt(befehl, norm.enbez()); } - case SATZ -> - bearbeiteBereich( + case SATZ -> { + if (befehl.bisStelle() == null) { + yield bearbeiteBereich( normen, befehl, (text, bereich) -> @@ -350,6 +388,9 @@ public final class BefehlAnwender { text.substring(0, bereich.von()) + befehl.text().strip().replaceAll("\\s+", " ") + text.substring(bereich.bis()))); + } + yield wendeSatzBereichsErsetzungAn(normen, befehl); + } case NUMMER, BUCHSTABE -> bearbeiteBereich( normen, @@ -365,30 +406,116 @@ public final class BefehlAnwender { return TextErgebnis.ok( text.substring(0, bereich.von()) + ersatz + text.substring(bereich.bis())); }); - case PARAGRAPH -> - manuell(befehl, "Struktur-Ersetzung ganzer Paragraphen wird nicht unterstützt."); + case PARAGRAPH -> { + // „§ 71 wird durch die folgenden §§ 71 bis 71p ersetzt: „…““ — der adressierte §-Bereich + // wird entfernt und durch die Paragraphen des Blocks ersetzt. + var aufloesung = loeseNormAuf(normen, befehl.stelle()); + if (aufloesung.fehler() != null) { + yield manuell(befehl, aufloesung.fehler()); + } + int vonIndex = aufloesung.normIndex(); + int bisIndex = vonIndex; + if (befehl.bisStelle() != null) { + var a2 = loeseNormAuf(normen, befehl.bisStelle()); + if (a2.fehler() != null) { + yield manuell(befehl, a2.fehler()); + } + bisIndex = a2.normIndex(); + } + if (bisIndex < vonIndex) { + yield manuell(befehl, "Ersetzungsbereich ist leer oder absteigend."); + } + var neue = parseNormenBlock(befehl.text(), normen.get(vonIndex).gliederung()); + if (neue.isEmpty()) { + yield manuell(befehl, "Im Ersetzungsblock wurde kein Paragraph erkannt."); + } + for (int k = bisIndex; k >= vonIndex; k--) { + normen.remove(k); + } + normen.addAll(vonIndex, neue); + yield angewandt(befehl, neue.stream().map(Norm::enbez).toList()); + } }; } + /** + * Ersetzt einen zusammenhängenden Satz-Bereich („Die Sätze 4 und 5 werden … gefasst“) durch einen + * Block: vom Anfang des ersten bis zum Ende des letzten adressierten Satzes (beide müssen im + * selben Absatz derselben Norm liegen). + */ + private static AngewandteAenderung wendeSatzBereichsErsetzungAn( + List<Norm> normen, StrukturErsetzung befehl) { + var e1 = StellenAufloeser.aufloese(gesetzAus(normen), befehl.stelle()); + if (e1 instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) { + return manuell(befehl, nicht.begruendung()); + } + var e2 = StellenAufloeser.aufloese(gesetzAus(normen), befehl.bisStelle()); + if (e2 instanceof StellenAufloeser.Ergebnis.NichtGefunden nicht) { + return manuell(befehl, nicht.begruendung()); + } + var f1 = ((StellenAufloeser.Ergebnis.Gefunden) e1).fundstelle(); + var f2 = ((StellenAufloeser.Ergebnis.Gefunden) e2).fundstelle(); + if (f1.normIndex() != f2.normIndex() + || f1.absatzIndex() == null + || !f1.absatzIndex().equals(f2.absatzIndex()) + || f1.bereich() == null + || f2.bereich() == null) { + return manuell(befehl, "Satz-Bereich liegt nicht in einem einzigen Absatz."); + } + int von = f1.bereich().von(); + int bis = f2.bereich().bis(); + if (bis < von) { + return manuell(befehl, "Satz-Bereich ist leer oder absteigend."); + } + var norm = normen.get(f1.normIndex()); + var absaetze = new ArrayList<>(norm.absaetze()); + var absatz = absaetze.get(f1.absatzIndex()); + var text = absatz.text(); + var neu = + text.substring(0, von) + + befehl.text().strip().replaceAll("\\s+", " ") + + text.substring(bis); + absaetze.set(f1.absatzIndex(), absatz.mitText(neu)); + normen.set(f1.normIndex(), norm.mitAbsaetzen(absaetze)); + return angewandt(befehl, norm.enbez()); + } + private static AngewandteAenderung wendeStrukturEinfuegungAn( List<Norm> normen, StrukturEinfuegung befehl) { return switch (befehl.ebene()) { case PARAGRAPH -> { - var enbezNeu = "§ " + befehl.bezeichnung(); - if (StellenAufloeser.normIndex(gesetzAus(normen), enbezNeu) >= 0) { - yield manuell(befehl, enbezNeu + " existiert bereits im Stammgesetz."); - } var aufloesung = loeseNormAuf(normen, befehl.stelle()); if (aufloesung.fehler() != null) { yield manuell(befehl, aufloesung.fehler()); } var anker = normen.get(aufloesung.normIndex()); + int position = aufloesung.normIndex() + (befehl.vorher() ? 0 : 1); + + // „Nach § 60a werden die folgenden §§ 60b und 60c eingefügt: „…““ — Block mehrerer §§. + if (befehl.bezeichnung() == null) { + var neue = parseNormenBlock(befehl.text(), anker.gliederung()); + if (neue.isEmpty()) { + yield manuell(befehl, "Im Einfügeblock wurde kein Paragraph erkannt."); + } + for (var n : neue) { + if (StellenAufloeser.normIndex(gesetzAus(normen), n.enbez()) >= 0) { + yield manuell(befehl, n.enbez() + " existiert bereits im Stammgesetz."); + } + } + normen.addAll(position, neue); + yield angewandt(befehl, neue.stream().map(Norm::enbez).toList()); + } + + var enbezNeu = "§ " + befehl.bezeichnung(); + if (StellenAufloeser.normIndex(gesetzAus(normen), enbezNeu) >= 0) { + yield manuell(befehl, enbezNeu + " existiert bereits im Stammgesetz."); + } var neueNorm = parseNorm( befehl.text(), enbezNeu, new Norm(enbezNeu, null, anker.gliederung(), List.of(), false)); - normen.add(aufloesung.normIndex() + (befehl.vorher() ? 0 : 1), neueNorm); + normen.add(position, neueNorm); yield angewandt(befehl, enbezNeu); } case ABSATZ -> { @@ -542,6 +669,31 @@ public final class BefehlAnwender { private static AngewandteAenderung wendeUmnummerierungAn( List<Norm> normen, Umnummerierung befehl) { + // „§ 9a wird zu § 9.“ — Umbenennung einer ganzen Norm. + if (nurParagraph(befehl.stelle()) && nurParagraph(befehl.neu())) { + var enbezAlt = "§ " + befehl.stelle().paragraph().get().nummer(); + var enbezNeu = "§ " + befehl.neu().paragraph().get().nummer(); + int idx = StellenAufloeser.normIndex(gesetzAus(normen), enbezAlt); + if (idx < 0) { + return manuell(befehl, enbezAlt + " existiert nicht im Gesetz."); + } + if (!enbezNeu.equals(enbezAlt)) { + int zielIdx = StellenAufloeser.normIndex(gesetzAus(normen), enbezNeu); + if (zielIdx >= 0 && !normen.get(zielIdx).weggefallen()) { + return manuell(befehl, enbezNeu + " existiert bereits im Gesetz."); + } + // Eine bereits weggefallene Zielnorm wird durch die Umnummerierung überschrieben. + if (zielIdx >= 0) { + normen.remove(zielIdx); + if (zielIdx < idx) { + idx--; + } + } + } + normen.set(idx, normen.get(idx).mitEnbez(enbezNeu)); + return angewandt(befehl, enbezNeu); + } + var altAbsatz = befehl.stelle().absatz(); var neuAbsatz = befehl.neu().absatz(); if (altAbsatz.isPresent() && neuAbsatz.isPresent()) { @@ -794,6 +946,34 @@ public final class BefehlAnwender { return anzahl; } + // Eine §-Überschrift beginnt mit „§ N“, gefolgt von einem großgeschriebenen Titelwort — im + // Gegensatz zu Querverweisen wie „§ 71 Absatz 1“ oder „§§ 42 bis 45“. Die Negativliste schließt + // die Untergliederungs- und Verbindungswörter aus, sodass an solchen Stellen nicht getrennt wird. + private static final Pattern PARAGRAPH_UEBERSCHRIFT = + Pattern.compile( + "(?=§\\s*\\d+[a-z]?\\s+" + + "(?!Absatz|Absätze|Abs|Satz|Sätze|Nummer|Nummern|Nr|Buchstabe|Buchstaben|Buchst" + + "|und|bis|oder|sowie|des|der|dieses|genannten)" + + "\\p{Lu})"); + + /** + * Zerlegt einen Zitatblock mehrerer Paragraphen an den §-Überschriften (nicht an Querverweisen) + * und parst jeden Abschnitt zu einer {@link Norm}. Die Gliederung wird von der Vorlage übernommen. + */ + private static List<Norm> parseNormenBlock(String block, @Nullable Gliederung gliederung) { + var normen = new ArrayList<Norm>(); + for (var stueck : PARAGRAPH_UEBERSCHRIFT.split(block.strip())) { + var s = stueck.strip(); + var m = Pattern.compile("^§\\s*(\\d+[a-z]?)\\b").matcher(s); + if (s.isEmpty() || !m.find()) { + continue; + } + var enbez = "§ " + m.group(1); + normen.add(parseNorm(s, enbez, new Norm(enbez, null, gliederung, List.of(), false))); + } + return normen; + } + /** Zerlegt einen zitierten Normtext („§ 28a Titel (1) … (2) …“) in Titel und Absätze. */ private static Norm parseNorm(String zitat, String enbez, Norm vorlage) { var text = zitat.strip(); @@ -889,6 +1069,10 @@ public final class BefehlAnwender { befehl, Status.ANGEWANDT, "", new LinkedHashSet<>(List.of(enbez))); } + private static AngewandteAenderung angewandt(Aenderungsbefehl befehl, List<String> enbezliste) { + return new AngewandteAenderung(befehl, Status.ANGEWANDT, "", new LinkedHashSet<>(enbezliste)); + } + private static AngewandteAenderung manuell(Aenderungsbefehl befehl, String begruendung) { return new AngewandteAenderung(befehl, Status.MANUELL_PRUEFEN, begruendung, Set.of()); } diff --git a/src/main/java/eu/mulk/aendggner/gesetz/Gliederung.java b/src/main/java/eu/mulk/aendggner/gesetz/Gliederung.java index 53eb6b9..7a09093 100644 --- a/src/main/java/eu/mulk/aendggner/gesetz/Gliederung.java +++ b/src/main/java/eu/mulk/aendggner/gesetz/Gliederung.java @@ -24,4 +24,8 @@ public record Gliederung(@Nullable String kennzahl, String bezeichnung, @Nullabl public Gliederung mitTitel(@Nullable String neuerTitel) { return new Gliederung(kennzahl, bezeichnung, neuerTitel); } + + public Gliederung mitBezeichnung(String neueBezeichnung) { + return new Gliederung(kennzahl, neueBezeichnung, titel); + } } diff --git a/src/main/java/eu/mulk/aendggner/gesetz/Norm.java b/src/main/java/eu/mulk/aendggner/gesetz/Norm.java index 10c3fa6..a5703ec 100644 --- a/src/main/java/eu/mulk/aendggner/gesetz/Norm.java +++ b/src/main/java/eu/mulk/aendggner/gesetz/Norm.java @@ -31,6 +31,10 @@ public record Norm( return new Norm(enbez, neuerTitel, gliederung, absaetze, weggefallen); } + public Norm mitEnbez(String neuerEnbez) { + return new Norm(neuerEnbez, titel, gliederung, absaetze, weggefallen); + } + public Norm alsWeggefallen() { return new Norm(enbez, titel, gliederung, List.of(new Absatz(null, "(weggefallen)")), true); } diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java index 44903e7..483f06a 100644 --- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java +++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java @@ -110,11 +110,11 @@ class EndToEndTest { assertThat(parseErgebnis.artikel()).contains("1"); assertThat(parseErgebnis.befehle().size()).isGreaterThanOrEqualTo(100); - // Der Großteil der Befehle wird erkannt; nur einige Spezialfälle (Mehrquell-Ersetzungen, - // §§-Bereiche, Chapeau-Angaben) bleiben unbekannt. + // Im BGBl-Format werden inzwischen alle Befehle typisiert (Bereichs-/Struktur-Ersetzungen, + // §-Blöcke, Chapeau-Lokatoren, strukturelle Streichungen usw.). var unbekannt = parseErgebnis.befehle().stream().filter(b -> b instanceof UnbekannterBefehl).count(); - assertThat(unbekannt).isLessThanOrEqualTo(8); + assertThat(unbekannt).isZero(); // Das XML ist bereits konsolidiert; entscheidend ist, dass die Anwendung sauber terminiert // und jeden Befehl protokolliert. diff --git a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java index 230bfca..bbee916 100644 --- a/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java +++ b/src/test/java/eu/mulk/aendggner/aenderung/parse/BefehlErkennerTest.java @@ -441,14 +441,47 @@ class BefehlErkennerTest { } @Test - void faelltBeiBisBereichenAufUnbekanntZurueck() { - // „bis“-Bereiche über Struktureinheiten sind hier (Phase 1) noch nicht unterstützt. - assertThat( - erkenne( - "Die Absätze 2 bis 4 werden durch die folgenden Absätze 2 bis 6 ersetzt:" - + " „(2) Text.“", - Stelle.LEER)) - .isEmpty(); + void bereichsErsetzungAbsaetzeWirdStrukturErsetzung() { + // „bis“-Bereich über Absätze: das erste und letzte Ziel spannen den zu ersetzenden Bereich auf. + var befehl = + erkenne( + "Die Absätze 2 bis 4 werden durch die folgenden Absätze 2 bis 6 ersetzt:" + + " „(2) Text.“", + new Stelle(List.of(new Stelle.Paragraph("5")))); + assertThat(befehl).containsInstanceOf(Aenderungsbefehl.StrukturErsetzung.class); + var e = (Aenderungsbefehl.StrukturErsetzung) befehl.orElseThrow(); + assertThat(e.ebene()).isEqualTo(Ebene.ABSATZ); + assertThat(e.stelle().anzeigeText()).isEqualTo("§ 5 Absatz 2"); + assertThat(e.bisStelle().anzeigeText()).isEqualTo("§ 5 Absatz 4"); + } + + @Test + void koordinierteAbsatzErsetzungWirdBereich() { + // IfSG: „Die Absätze 8 und 9 werden durch die folgenden Absätze 8 bis 10 ersetzt: „…““ + var befehl = + erkenne( + "Die Absätze 8 und 9 werden durch die folgenden Absätze 8 bis 10 ersetzt:" + + " „(8) Erstes. (9) Zweites. (10) Drittes.“", + new Stelle(List.of(new Stelle.Paragraph("14")))); + assertThat(befehl).containsInstanceOf(Aenderungsbefehl.StrukturErsetzung.class); + var e = (Aenderungsbefehl.StrukturErsetzung) befehl.orElseThrow(); + assertThat(e.stelle().anzeigeText()).isEqualTo("§ 14 Absatz 8"); + assertThat(e.bisStelle().anzeigeText()).isEqualTo("§ 14 Absatz 9"); + } + + @Test + void mehrSatzNeufassungWirdStrukturErsetzung() { + // IfSG: „Die bisherigen Sätze 4 und 5 werden wie folgt gefasst: „…““ + var befehl = + erkenne( + "Die bisherigen Sätze 4 und 5 werden wie folgt gefasst: „Erster neuer Satz." + + " Zweiter neuer Satz.“", + new Stelle(List.of(new Stelle.Paragraph("14"), new Stelle.AbsatzNr("2")))); + assertThat(befehl).containsInstanceOf(Aenderungsbefehl.StrukturErsetzung.class); + var e = (Aenderungsbefehl.StrukturErsetzung) befehl.orElseThrow(); + assertThat(e.ebene()).isEqualTo(Ebene.SATZ); + assertThat(e.stelle().anzeigeText()).isEqualTo("§ 14 Absatz 2 Satz 4"); + assertThat(e.bisStelle().anzeigeText()).isEqualTo("§ 14 Absatz 2 Satz 5"); } @Test @@ -556,6 +589,162 @@ class BefehlErkennerTest { } @Test + void strukturStreichungGanzerEinheit() { + // „§ 9 wird gestrichen.“ — Streichung einer ganzen Einheit ist semantisch eine Aufhebung. + var befehl = erkenne("§ 9 wird gestrichen.", Stelle.LEER); + assertThat(befehl).get().isInstanceOf(Aufhebung.class); + assertThat(befehl.orElseThrow().stelle().anzeigeText()).isEqualTo("§ 9"); + } + + @Test + void strukturStreichungMitKontext() { + var befehl = + erkenne("Absatz 3 wird gestrichen.", new Stelle(List.of(new Stelle.Paragraph("102")))); + assertThat(befehl).get().isInstanceOf(Aufhebung.class); + assertThat(befehl.orElseThrow().stelle().anzeigeText()).isEqualTo("§ 102 Absatz 3"); + } + + @Test + void strukturStreichungBereichWirdSammelbefehl() { + var teile = + ((Sammelbefehl) erkenne("Die §§ 34 bis 39 werden gestrichen.", Stelle.LEER).orElseThrow()) + .teilbefehle(); + assertThat(teile).hasSize(6).allMatch(t -> t instanceof Aufhebung); + assertThat(teile).extracting(t -> t.stelle().anzeigeText()) + .containsExactly("§ 34", "§ 35", "§ 36", "§ 37", "§ 38", "§ 39"); + } + + @Test + void strukturStreichungGliederung() { + var befehl = erkenne("Der bisherige Teil 3 wird gestrichen.", Stelle.LEER); + assertThat(befehl).get().isInstanceOf(Aufhebung.class); + assertThat(befehl.orElseThrow().stelle().betrifftGliederung()).isTrue(); + } + + @Test + void chapeauLokatorBeziehtSichAufKontext() { + // „In der Angabe vor Nummer 1 …“ / „Im Satzteil vor Nummer 1 …“ tragen keine eigene Stelle; + // die Operation bezieht sich auf die Kontextstelle. + var kontext = new Stelle(List.of(new Stelle.Paragraph("48"), new Stelle.AbsatzNr("1"))); + var befehl = + erkenne( + "In der Angabe vor Nummer 1 wird die Angabe „2025“ durch die Angabe „2030“ ersetzt.", + kontext); + assertThat(befehl).get().isInstanceOf(Ersetzung.class); + var e = (Ersetzung) befehl.orElseThrow(); + assertThat(e.stelle().anzeigeText()).isEqualTo("§ 48 Absatz 1"); + assertThat(e.alt()).isEqualTo("2025"); + assertThat(e.neu()).isEqualTo("2030"); + } + + @Test + void bereichsUmnummerierungOhneBisherigen() { + var teile = + ((Sammelbefehl) + erkenne( + "Die Absätze 4 bis 7 werden zu den Absätzen 3 bis 6.", + new Stelle(List.of(new Stelle.Paragraph("108")))) + .orElseThrow()) + .teilbefehle(); + assertThat(teile).hasSize(4).allMatch(t -> t instanceof Umnummerierung); + assertThat(teile).extracting(t -> t.stelle().anzeigeText()) + .containsExactly("§ 108 Absatz 7", "§ 108 Absatz 6", "§ 108 Absatz 5", "§ 108 Absatz 4"); + } + + @Test + void strukturErsetzungMitEnumeratorPraefix() { + // Entwurfs-/Drucksachenform: das Aufzählungslabel steht außerhalb des Zitats. + var befehl = + erkenne( + "Nummer 3 wird durch die folgende Nummer 3 ersetzt: 3. „ die Maßgaben der §§ 42 bis 45" + + " entsprechend eingehalten werden.“", + new Stelle(List.of(new Stelle.Paragraph("10"), new Stelle.AbsatzNr("2")))); + assertThat(befehl).containsInstanceOf(Aenderungsbefehl.StrukturErsetzung.class); + var e = (Aenderungsbefehl.StrukturErsetzung) befehl.orElseThrow(); + assertThat(e.ebene()).isEqualTo(Ebene.NUMMER); + assertThat(e.stelle().anzeigeText()).isEqualTo("§ 10 Absatz 2 Nummer 3"); + // Das Label „3.“ wird dem Ersatztext wieder vorangestellt. + assertThat(e.text()).startsWith("3. die Maßgaben"); + } + + @Test + void paragraphUmnummerierung() { + var befehl = erkenne("§ 9a wird zu § 9.", Stelle.LEER); + assertThat(befehl).get().isInstanceOf(Umnummerierung.class); + var u = (Umnummerierung) befehl.orElseThrow(); + assertThat(u.stelle().anzeigeText()).isEqualTo("§ 9a"); + assertThat(u.neu().anzeigeText()).isEqualTo("§ 9"); + } + + @Test + void koordinierteParagraphUmnummerierung() { + var teile = + ((Sammelbefehl) erkenne("Die §§ 46 und 47 werden zu den §§ 34 und 35.", Stelle.LEER).orElseThrow()) + .teilbefehle(); + assertThat(teile).hasSize(2).allMatch(t -> t instanceof Umnummerierung); + assertThat(teile).extracting(t -> t.stelle().anzeigeText()).containsExactly("§ 46", "§ 47"); + assertThat(teile).extracting(t -> ((Umnummerierung) t).neu().anzeigeText()) + .containsExactly("§ 34", "§ 35"); + } + + @Test + void gliederungsUmnummerierung() { + var befehl = erkenne("Der bisherige Abschnitt 2 wird zu Abschnitt 3.", Stelle.LEER); + assertThat(befehl).get().isInstanceOf(Umnummerierung.class); + assertThat(befehl.orElseThrow().stelle().betrifftGliederung()).isTrue(); + } + + @Test + void paragraphenBlockEinfuegung() { + var befehl = + erkenne( + "Nach § 60a werden die folgenden §§ 60b und 60c eingefügt: „§ 60b Prüfung (1) Text." + + " § 60c Optimierung (1) Mehr.“", + Stelle.LEER); + assertThat(befehl).containsInstanceOf(StrukturEinfuegung.class); + var e = (StrukturEinfuegung) befehl.orElseThrow(); + assertThat(e.ebene()).isEqualTo(Ebene.PARAGRAPH); + assertThat(e.bezeichnung()).isNull(); + assertThat(e.stelle().anzeigeText()).isEqualTo("§ 60a"); + } + + @Test + void paragraphBlockErsetzung() { + var befehl = + erkenne( + "Die §§ 42 bis 45 werden durch die folgenden §§ 42 bis 45 ersetzt: „§ 42 Grundsatz" + + " (1) Text.“", + Stelle.LEER); + assertThat(befehl).containsInstanceOf(Aenderungsbefehl.StrukturErsetzung.class); + var e = (Aenderungsbefehl.StrukturErsetzung) befehl.orElseThrow(); + assertThat(e.ebene()).isEqualTo(Ebene.PARAGRAPH); + assertThat(e.stelle().anzeigeText()).isEqualTo("§ 42"); + assertThat(e.bisStelle().anzeigeText()).isEqualTo("§ 45"); + } + + @Test + void ueberschriftErsetzungMitStelle() { + var befehl = + erkenne( + "In Anlage 7 wird die Überschrift durch die folgende Überschrift ersetzt: „Anlage 7" + + " (zu § 36) Höchstwerte“.", + Stelle.LEER); + assertThat(befehl).get().isInstanceOf(Neufassung.class); + assertThat(befehl.orElseThrow().stelle().betrifftGliederung()).isTrue(); + } + + @Test + void inhaltsuebersichtErsetzung() { + var befehl = + erkenne( + "Die Inhaltsübersicht wird durch die folgende Inhaltsübersicht ersetzt: „Inhaltsübersicht" + + " § 1 Zweck § 2 Begriffe“.", + Stelle.LEER); + assertThat(befehl).get().isInstanceOf(Neufassung.class); + assertThat(befehl.orElseThrow().stelle().betrifftInhaltsuebersicht()).isTrue(); + } + + @Test void mehrfachErsetzungWirdSammelbefehl() { // Mehrere Ersetzungspaare unter einem gemeinsamen „ersetzt“. var befehl = diff --git a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java index 8a1b1e0..9b9c18d 100644 --- a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java +++ b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java @@ -20,6 +20,7 @@ import eu.mulk.aendggner.aenderung.Stelle; import eu.mulk.aendggner.anwendung.BefehlAnwender.Status; 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.List; import org.junit.jupiter.api.Test; @@ -349,6 +350,44 @@ class BefehlAnwenderTest { } @Test + void ersetztAbsatzBereichDurchBlock() { + // „Die Absätze 1 und 2 werden durch die folgenden Absätze 1 bis 3 ersetzt: „…““ + var befehl = + new StrukturErsetzung( + stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("1")), + stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("2")), + Ebene.ABSATZ, + "(1) Neu eins. (2) Neu zwei. (3) Neu drei.", + PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl)); + + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT); + var norm = ergebnis.neu().norm("§ 1").orElseThrow(); + assertThat(norm.absaetze()).hasSize(3); + assertThat(norm.absaetze()).extracting(Absatz::nummer).containsExactly("1", "2", "3"); + assertThat(norm.absaetze().get(0).text()).isEqualTo("Neu eins."); + } + + @Test + void ersetztSatzBereichDurchBlock() { + // „Die Sätze 1 und 2 werden wie folgt gefasst: „…““ in § 2 (3 Sätze). + var befehl = + new StrukturErsetzung( + stelle(new Stelle.Paragraph("2"), new Stelle.SatzNr("1")), + stelle(new Stelle.Paragraph("2"), new Stelle.SatzNr("2")), + Ebene.SATZ, + "Neuer Satz eins. Neuer Satz zwei.", + PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl)); + + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT); + assertThat(absatzText(ergebnis.neu(), "§ 2", 0)) + .isEqualTo("Neuer Satz eins. Neuer Satz zwei. Sie endet mit einem Bericht."); + } + + @Test void fuegtSatzNachSatzEin() { var befehl = new StrukturEinfuegung( @@ -422,6 +461,112 @@ class BefehlAnwenderTest { .isEqualTo("Zweck dieses Gesetzes ist die Prüfung."); } + @Test + void nummeriertParagraphUm() { + var befehl = + new Umnummerierung(stelle(new Stelle.Paragraph("3")), stelle(new Stelle.Paragraph("4")), PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl)); + + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT); + var enbezListe = ergebnis.neu().normen().stream().map(Norm::enbez).toList(); + assertThat(enbezListe).containsExactly("§ 1", "§ 2", "§ 4"); + assertThat(ergebnis.neu().norm("§ 4").orElseThrow().titel()).isEqualTo("Schlussvorschriften"); + } + + @Test + void paragraphUmnummerierungUeberschreibtWeggefalleneZielnorm() { + // § 2 aufheben, dann § 3 → § 2: die weggefallene Zielnorm wird ersetzt. + var befehle = + List.<eu.mulk.aendggner.aenderung.Aenderungsbefehl>of( + new Aufhebung(stelle(new Stelle.Paragraph("2")), PROV), + new Umnummerierung( + stelle(new Stelle.Paragraph("3")), stelle(new Stelle.Paragraph("2")), PROV)); + + var ergebnis = BefehlAnwender.anwenden(gesetz(), befehle); + + assertThat(ergebnis.protokoll()).allMatch(a -> a.status() == Status.ANGEWANDT); + var enbezListe = ergebnis.neu().normen().stream().map(Norm::enbez).toList(); + assertThat(enbezListe).containsExactly("§ 1", "§ 2"); + assertThat(ergebnis.neu().norm("§ 2").orElseThrow().titel()).isEqualTo("Schlussvorschriften"); + } + + @Test + void meldetKonfliktBeiParagraphUmnummerierung() { + var befehl = + new Umnummerierung(stelle(new Stelle.Paragraph("3")), stelle(new Stelle.Paragraph("1")), PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl)); + + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.MANUELL_PRUEFEN); + assertThat(ergebnis.protokoll().get(0).begruendung()).contains("existiert bereits"); + } + + @Test + void fuegtParagraphenBlockEin() { + // „Nach § 1 werden die folgenden §§ 1a und 1b eingefügt: „…““ (bezeichnung == null). + var befehl = + new StrukturEinfuegung( + stelle(new Stelle.Paragraph("1")), + false, + Ebene.PARAGRAPH, + null, + "§ 1a Erstes Neu (1) Inhalt eins. § 1b Zweites Neu (1) Inhalt zwei.", + PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl)); + + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT); + var enbezListe = ergebnis.neu().normen().stream().map(Norm::enbez).toList(); + assertThat(enbezListe).containsExactly("§ 1", "§ 1a", "§ 1b", "§ 2", "§ 3"); + assertThat(ergebnis.neu().norm("§ 1a").orElseThrow().titel()).isEqualTo("Erstes Neu"); + assertThat(ergebnis.neu().norm("§ 1b").orElseThrow().titel()).isEqualTo("Zweites Neu"); + } + + @Test + void ersetztParagraphBlock() { + // „§ 2 wird durch die folgenden §§ 2 und 2a ersetzt: „…““ — § 2 wird durch zwei §§ ersetzt. + // Der Querverweis „§ 1 Absatz 1“ im Text darf NICHT als Grenze zerteilt werden. + var befehl = + new StrukturErsetzung( + stelle(new Stelle.Paragraph("2")), + null, + Ebene.PARAGRAPH, + "§ 2 Begriffe (1) Erprobung nach § 1 Absatz 1 ist die Prüfung. § 2a Weiteres (1) Mehr.", + PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz(), List.of(befehl)); + + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT); + var enbezListe = ergebnis.neu().normen().stream().map(Norm::enbez).toList(); + assertThat(enbezListe).containsExactly("§ 1", "§ 2", "§ 2a", "§ 3"); + assertThat(ergebnis.neu().norm("§ 2").orElseThrow().absaetze().get(0).text()) + .isEqualTo("Erprobung nach § 1 Absatz 1 ist die Prüfung."); + assertThat(ergebnis.neu().norm("§ 2a").orElseThrow().titel()).isEqualTo("Weiteres"); + } + + @Test + void nummeriertGliederungUm() { + var gesetz = + new Gesetz( + "TestG", + "Gesetz", + "Test", + List.of(new Norm("§ 1", "Zweck", null, List.of(new Absatz("1", "Text.")), false)), + List.of(new Gliederung("010020", "Abschnitt 2", "Früherkennung"))); + var befehl = + new Umnummerierung( + stelle(new Stelle.Gliederungseinheit("Abschnitt", "2")), + stelle(new Stelle.Gliederungseinheit("Abschnitt", "3")), + PROV); + + var ergebnis = BefehlAnwender.anwenden(gesetz, List.of(befehl)); + + assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT); + assertThat(ergebnis.neu().gliederungen().get(0).bezeichnung()).isEqualTo("Abschnitt 3"); + assertThat(ergebnis.neu().gliederungen().get(0).titel()).isEqualTo("Früherkennung"); + } + private static Gesetz gesetz() { return new Gesetz( "TestG", |
