aboutsummaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/eu/mulk/aendggner/aenderung/Stelle.java13
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java159
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/StellenAufloeser.java14
-rw-r--r--src/main/java/eu/mulk/aendggner/gesetz/land/LandesRechtTextParser.java33
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;