diff options
Diffstat (limited to 'src/main/java/eu')
4 files changed, 204 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 73d067f..121c9bf 100644 --- a/src/main/java/eu/mulk/aendggner/aenderung/Stelle.java +++ b/src/main/java/eu/mulk/aendggner/aenderung/Stelle.java @@ -96,6 +96,19 @@ public record Stelle(List<Komponente> komponenten) { .findFirst(); } + /** Die erste Nummernangabe („Nummer 6“), falls vorhanden. */ + public Optional<NummerNr> nummer() { + return komponenten.stream() + .filter(NummerNr.class::isInstance) + .map(NummerNr.class::cast) + .findFirst(); + } + + /** Dieselbe Stelle ohne die genannte Komponente — für ein Glied, das anderswo aufging. */ + public Stelle ohne(Komponente komponente) { + return new Stelle(komponenten.stream().filter(k -> !k.equals(komponente)).toList()); + } + public Optional<AbsatzNr> absatz() { return komponenten.stream() .filter(AbsatzNr.class::isInstance) diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java index cefb285..486f77b 100644 --- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java +++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java @@ -474,7 +474,19 @@ public final class BefehlAnwender { case BUCHSTABE -> new Stelle.BuchstabeNr(bezeichnung); }; var komponenten = new ArrayList<>(s.stelle().komponenten()); + // Die letzte Komponente weicht, wenn sie gleich fein oder feiner ist als die neue („In § 5 + // Absatz 2 wird folgender Absatz 3 eingefügt“ meint § 5 Absatz 3, nicht § 5 Absatz 2 Absatz 3). + // Die Nummer einer Anlage weicht nicht: Sie gehört zur Bezeichnung der Norm und nicht zu ihrem + // Inneren, und ohne sie träfe der neue Absatz eine andere Norm als der Befehl. + boolean anlagenNummer = + s.stelle().anlagenEnbez().isPresent() + && !komponenten.isEmpty() + && s.stelle() + .nummer() + .filter(komponenten.get(komponenten.size() - 1)::equals) + .isPresent(); if (!komponenten.isEmpty() + && !anlagenNummer && rang(komponenten.get(komponenten.size() - 1)) >= rang(komponente)) { komponenten.remove(komponenten.size() - 1); } @@ -965,7 +977,7 @@ public final class BefehlAnwender { return angewandt(befehl, norm.enbez()); } - if (nurNorm(stelle)) { + if (nurNorm(normen, stelle)) { var aufloesung = loeseNormAuf(normen, stelle); if (aufloesung.fehler() != null) { return manuell(befehl, aufloesung.grund(), aufloesung.fehler()); @@ -1338,8 +1350,95 @@ public final class BefehlAnwender { * der Befehl nennt, ist dabei ohne Belang (in einer Anlage stehen Absätze und Nummern als Zeilen * eines Textes). */ + /** + * „Vor den Wörtern ‚Aus dem Bereich Verkehr:‘ wird folgender Absatz 5 eingefügt“ — ein + * <em>Absatz</em>, der an einem Wortlaut ausgerichtet ist. Er wird ein Absatz und keine Zeile im + * Nachbarabsatz: Der tragende Absatz wird an der Marke geteilt, und was hinter ihr stand, setzt + * den neuen Absatz fort — so steht es hernach auch in der amtlichen Nachfassung, denn ein + * Zwischentitel ohne eigene Absatzbezeichnung schließt sich stets dem vorangehenden an. + * + * <p>Steht die Marke am Anfang des Absatzes, so wird nicht geteilt: Der neue Absatz tritt dann + * schlicht vor ihn. + */ + private static AngewandteAenderung wendeAbsatzWortankerEinfuegungAn( + List<Norm> normen, StrukturEinfuegung befehl, String woerter) { + var aufloesung = loeseNormAuf(normen, befehl.stelle()); + if (aufloesung.fehler() != null) { + return manuell(befehl, aufloesung.grund(), aufloesung.fehler()); + } + var norm = normen.get(aufloesung.normIndex()); + var absaetze = new ArrayList<>(norm.absaetze()); + int treffer = -1; + for (int i = 0; i < absaetze.size(); i++) { + int anzahl = zaehleVorkommen(absaetze.get(i).text(), woerter); + if (anzahl > 1) { + return manuell( + befehl, Grund.MEHRDEUTIG, "„" + woerter + "“ kommt mehrfach vor; mehrdeutig."); + } + if (anzahl == 1) { + if (treffer >= 0) { + return manuell( + befehl, + Grund.MEHRDEUTIG, + "„" + woerter + "“ kommt in mehreren Absätzen vor; mehrdeutig."); + } + treffer = i; + } + } + if (treffer < 0) { + return manuell( + befehl, Grund.ZIELTEXT_FEHLT, "„" + woerter + "“ kommt im Zieltext nicht vor."); + } + var alt = absaetze.get(treffer); + int marke = findeVorkommen(alt.text(), woerter, 0); + int schnitt = + befehl.vorher() + ? alt.text().lastIndexOf('\n', marke) + 1 + : naechsterZeilenAnfang(alt.text(), marke); + var vorn = alt.text().substring(0, schnitt).stripTrailing(); + var hinten = alt.text().substring(schnitt).strip(); + + var neue = parseAbsaetze(befehl.text()); + if (neue.isEmpty()) { + return manuell(befehl, Grund.ZITAT_UNBRAUCHBAR, "Im Zitat wurde kein Absatz erkannt."); + } + var erster = neue.get(0); + var nummer = erster.nummer() != null ? erster.nummer() : befehl.bezeichnung(); + var text = hinten.isEmpty() ? erster.text() : erster.text() + "\n" + hinten; + + if (vorn.isEmpty()) { + // Die Marke eröffnet den Absatz: nichts zu teilen, der neue tritt davor. + absaetze.add(treffer, new Absatz(nummer, erster.text())); + } else { + absaetze.set(treffer, alt.mitText(vorn)); + absaetze.add(treffer + 1, new Absatz(nummer, text)); + } + for (int k = 1; k < neue.size(); k++) { + absaetze.add(treffer + 1 + k, neue.get(k)); + } + normen.set(aufloesung.normIndex(), norm.mitAbsaetzen(absaetze)); + return angewandt(befehl, norm.enbez()); + } + + private static int naechsterZeilenAnfang(String text, int position) { + int umbruch = text.indexOf('\n', position); + return umbruch < 0 ? text.length() : umbruch + 1; + } + private static AngewandteAenderung wendeWortankerEinfuegungAn( List<Norm> normen, StrukturEinfuegung befehl, WortAnker anker) { + // Ein ganzer Absatz wird ein Absatz, keine Zeile in einem anderen. + if (befehl.ebene() == Ebene.ABSATZ) { + var woerter = + switch (anker) { + case WortAnker.NachWoertern nach -> nach.woerter(); + case WortAnker.VorWoertern vor -> vor.woerter(); + default -> null; + }; + if (woerter != null) { + return wendeAbsatzWortankerEinfuegungAn(normen, befehl, woerter); + } + } return bearbeiteText( normen, befehl, @@ -1505,7 +1604,7 @@ public final class BefehlAnwender { befehl, Grund.STELLE_NICHT_AUFLOESBAR, "Absatz (" + nummer + ") nicht gefunden."); } - if (nurNorm(stelle)) { + if (nurNorm(normen, stelle)) { var aufloesung = loeseNormAuf(normen, stelle); if (aufloesung.fehler() != null) { return manuell(befehl, aufloesung.grund(), aufloesung.fehler()); @@ -2114,6 +2213,14 @@ public final class BefehlAnwender { enbez = stelle.paragraph().get().enbez(); } else if (stelle.anlagenEnbez().isPresent()) { enbez = stelle.anlagenEnbez().get(); + // Wie im StellenAufloeser: Die Nummer einer Anlage kann eine eigene Norm sein. + var nummer = stelle.nummer(); + if (nummer.isPresent() + && StellenAufloeser.normIndex( + gesetzAus(normen), enbez + " Nummer " + nummer.get().nummer()) + >= 0) { + enbez = enbez + " Nummer " + nummer.get().nummer(); + } } else { return new NormAufloesung( -1, @@ -2134,20 +2241,37 @@ public final class BefehlAnwender { } /** Wahr, wenn die Stelle als Ganzes eine Norm meint: ein einzelner § oder ein Anhang/Anlage. */ - private static boolean nurNorm(Stelle stelle) { - return stelle.komponenten().size() == 1 + private static boolean nurNorm(List<Norm> normen, Stelle stelle) { + if (stelle.komponenten().size() == 1 && (stelle.komponenten().get(0) instanceof Stelle.Paragraph - || stelle.anlagenEnbez().isPresent()); + || stelle.anlagenEnbez().isPresent())) { + return true; + } + // „Nummer 31 wird wie folgt gefasst“ innerhalb einer Anlage: Trägt das Gesetz die Nummer als + // eigene Norm, so bezeichnet die Stelle diese ganz und nicht einen Teil der Anlage. + return stelle.komponenten().size() == 2 + && stelle.anlagenEnbez().isPresent() + && stelle.nummer().isPresent() + && StellenAufloeser.normIndex( + gesetzAus(normen), + stelle.anlagenEnbez().get() + " Nummer " + stelle.nummer().get().nummer()) + >= 0; } + /** + * Die feinste Komponente der Stelle ist ein Absatz — es geht also um den Absatz selbst und nicht + * um einen Satz, eine Nummer oder einen Buchstaben in ihm. + * + * <p>Maßgeblich ist die <em>letzte</em> Komponente, nicht das bloße Vorkommen einer feineren: Die + * Nummer einer Anlage steht vor der Absatzangabe und gehört zur Bezeichnung der Norm („Anlage + * Nummer 23 Absatz 9“), nicht zu ihrem Inneren. Wer sie mitzählte, hielte die Umnummerierung + * eines solchen Absatzes für die einer Aufzählungseinheit und tauschte eine Marke „23.“ im Text — + * die dort nicht steht. Der Befehl galt dann als angewandt und bewirkte nichts. + */ private static boolean feinsteIstAbsatz(Stelle stelle) { - return stelle.komponenten().stream() - .noneMatch( - k -> - k instanceof Stelle.SatzNr - || k instanceof Stelle.HalbsatzNr - || k instanceof Stelle.NummerNr - || k instanceof Stelle.BuchstabeNr); + var komponenten = stelle.komponenten(); + return !komponenten.isEmpty() + && komponenten.get(komponenten.size() - 1) instanceof Stelle.AbsatzNr; } /** @@ -2335,6 +2459,17 @@ public final class BefehlAnwender { var text = zitat.strip(); String titel = vorlage.titel(); + // Das Zitat einer neugefassten Anlagen-Nummer wiederholt deren Bezeichnung als erste Zeile + // („Nummer 31“ / „Landesamt …“ / „Zu den Ordnungsaufgaben …“). Sie gehört nicht in den + // Wortlaut — ebenso wenig, wie die Bezeichnung eines Paragraphen in seinen Text gehört. + var eigeneBezeichnung = enbez.replaceFirst("^.*?\\b(Nummer \\d+[a-z]?)$", "$1"); + if (!eigeneBezeichnung.equals(enbez)) { + var erste = text.lines().findFirst().orElse("").strip(); + if (erste.equals(eigeneBezeichnung)) { + text = text.substring(text.indexOf('\n') + 1).strip(); + } + } + var absatzStart = ABSATZ_MARKER.matcher(text); int erster = absatzStart.find() ? absatzStart.start() : -1; diff --git a/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java b/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java index fbc3f65..0bb2d91 100644 --- a/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java +++ b/src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java @@ -49,6 +49,20 @@ final class StellenAufloeser { enbez = stelle.paragraph().get().enbez(); } else if (stelle.anlagenEnbez().isPresent()) { enbez = stelle.anlagenEnbez().get(); + // Die Nummern einer Anlage können eigene Einheiten mit eigener Absatzzählung sein — so führt + // sie das Landesrechtsportal, und so sprechen die Befehle sie an („Nach Nummer 6 Absatz 2 + // wird folgender Absatz 2a eingefügt“). Trägt das Gesetz eine solche Norm, ist sie gemeint; + // die Nummer ist damit verbraucht und wird nicht zusätzlich als Marke im Text gesucht. + // Fehlt sie, bleibt es beim bisherigen Weg (im gii-XML stehen die Nummern einer Anlage als + // Aufzählungsmarken in deren Wortlaut, etwa in Anlage 8 des Gebäudeenergiegesetzes). + var nummer = stelle.nummer(); + if (nummer.isPresent()) { + var alsNorm = enbez + " Nummer " + nummer.get().nummer(); + if (normIndex(gesetz, alsNorm) >= 0) { + enbez = alsNorm; + stelle = stelle.ohne(nummer.get()); + } + } } else { return new Ergebnis.NichtGefunden( "Stelle nennt keinen Paragraphen: " + stelle.anzeigeText(), diff --git a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java index d6a1d2e..ae1e861 100644 --- a/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java +++ b/src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java @@ -124,6 +124,17 @@ final class LandesRechtTextParser { // findet entsprechend Anwendung.“) nicht fälschlich als Normkopf „§ 7“ gelesen. + "((?:\\p{Lu}|\\().*[^.]))?\\s*$"); + /** + * Der Kopf einer Nummer innerhalb einer Anlage („Nummer 6“). Die Nummern eines + * Zuständigkeitskatalogs sind keine Aufzählungsglieder, sondern eigene Einheiten mit eigener + * Absatzzählung — das Landesrechtsportal führt jede als eigene Einzelnorm, und die + * Änderungsbefehle sprechen sie so an („Nach Nummer 6 Absatz 2 wird folgender Absatz 2a + * eingefügt“). Sie werden deshalb als eigene Normen geführt, deren Bezeichnung die Anlage + * voranstellt: „Anlage Nummer 6“. + */ + private static final Pattern ANLAGEN_NUMMER_KOPF = + Pattern.compile("^(?:Nummer|Nr\\.)\\s+(\\d+[a-z]?)\\s*$"); + private static final Pattern WEGGEFALLEN_TITEL = Pattern.compile("^\\((?:aufgehoben|weggefallen)\\)$"); @@ -177,6 +188,7 @@ final class LandesRechtTextParser { // Überschriften („Erster Abschnitt“, „Nummer 6“) gliedern nicht das Gesetz, und ein Paragraph, // der in ihnen zitiert wird, eröffnet keine Norm. Nur eine weitere Anlage tut das. boolean imAnlagenteil = false; + String anlagenEnbez = null; String normEnbez = null; String normTitel = null; @@ -214,8 +226,16 @@ final class LandesRechtTextParser { && normKopf.matches() && istNeuerNormKopf(normKopf.group(2), letzteNormNummer); boolean neueAnlage = zeile != null && anlagenKopf.matches(); - - if (zeile == null || uebersicht || gliederungsZeile || neueNorm || neueAnlage) { + var anlagenNummer = + zeile != null && imAnlagenteil ? ANLAGEN_NUMMER_KOPF.matcher(zeile) : null; + boolean neueAnlagenNummer = anlagenNummer != null && anlagenNummer.matches(); + + if (zeile == null + || uebersicht + || gliederungsZeile + || neueNorm + || neueAnlage + || neueAnlagenNummer) { // Laufende Norm abschließen. if (normEnbez != null) { normen.add(baueNorm(normEnbez, normTitel, aktuelleGliederung, normZeilen)); @@ -261,12 +281,19 @@ final class LandesRechtTextParser { gliederungen.add(aktuelleGliederung); } else if (neueAnlage) { imAnlagenteil = true; - normEnbez = + anlagenEnbez = anlagenKopf.group(2) == null ? anlagenKopf.group(1) : anlagenKopf.group(1) + " " + anlagenKopf.group(2); + normEnbez = anlagenEnbez; normTitel = null; aktuelleGliederung = null; + } else if (neueAnlagenNummer) { + // „Anlage Nummer 6“ — die Anlage selbst bleibt als Norm vor ihnen stehen; trägt sie + // keinen eigenen Wortlaut, so ist sie eben leer. + normEnbez = + (anlagenEnbez != null ? anlagenEnbez + " " : "") + "Nummer " + anlagenNummer.group(1); + normTitel = null; } else if (neueNorm) { normEnbez = normKopf.group(1) + " " + normKopf.group(2); normTitel = normKopf.group(3) != null ? normKopf.group(3).strip() : null; |
