aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java123
-rw-r--r--src/main/resources/sampledata/Landesrecht-Beispiele.adoc14
-rw-r--r--src/test/java/eu/mulk/aendggner/EndToEndTest.java33
-rw-r--r--src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java96
4 files changed, 241 insertions, 25 deletions
diff --git a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
index 32350ff..73745a6 100644
--- a/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
+++ b/src/main/java/eu/mulk/aendggner/anwendung/BefehlAnwender.java
@@ -716,10 +716,11 @@ public final class BefehlAnwender {
return TextErgebnis.fehler(
"„" + befehl.woerter() + "“ kommt " + anzahl + "-mal vor.");
}
+ int naht = text.indexOf(befehl.woerter());
return TextErgebnis.ok(
- text.replace(befehl.woerter(), "")
- .replaceAll(" +", " ")
- .replaceAll(" ([,;.])", "$1"));
+ heileNaht(
+ text.substring(0, naht) + text.substring(naht + befehl.woerter().length()),
+ naht));
}));
}
@@ -833,7 +834,27 @@ public final class BefehlAnwender {
return angewandt(befehl, norm.enbez());
}
- // Neufassung eines Satzes / einer Nummer / eines Buchstabens: Bereich ersetzen.
+ // Neufassung einer Nummer / eines Buchstabens: Der neue Wortlaut tritt an die Stelle der
+ // alten Einheit — und zwar auf deren Einrückung. Ohne sie stünde die neugefasste Zeile in
+ // Spalte 0, und der Zeilenblock ihrer Marke reichte fortan bis ans Absatzende, weil er so
+ // weit reicht, wie tiefer eingerückt wird (StellenAufloeser.zeilenBlock): ein späterer
+ // Befehl „Nach Nr. 4 werden die folgenden Nrn. 5 bis 7 eingefügt“ träte dann hinter die
+ // letzte Nummer des Absatzes statt hinter die Nr. 4.
+ if (feinsteIstAufzaehlung(befehl.stelle())) {
+ return bearbeiteBereich(
+ normen,
+ befehl,
+ (text, bereich) ->
+ TextErgebnis.ok(
+ text.substring(0, bereich.von())
+ + rueckeZitatEin(
+ normalisiereZitatText(befehl.neuerText()),
+ einrueckungVon(text, bereich.von()))
+ + text.substring(bereich.bis())));
+ }
+
+ // Neufassung eines Satzes / Halbsatzes: Bereich ersetzen. Eine Einrückung gibt es hier
+ // nicht — der Bereich beginnt mitten in der Zeile.
return bearbeiteText(
normen, befehl, text -> TextErgebnis.ok(normalisiereZitatText(befehl.neuerText())));
}
@@ -1105,10 +1126,19 @@ public final class BefehlAnwender {
// Der Einfügeblock darf mehrere Einheiten enthalten („die Nummern 4a bis 4c“);
// jede Aufzählungszeile des Zitats bleibt eine eigene Zeile.
var block = rueckeZitatEin(normalisiereZitatText(befehl.text()), einrueckung);
- return TextErgebnis.ok(
+ var neuerText =
befehl.vorher()
? text.substring(0, position) + block + "\n" + text.substring(position)
- : text.substring(0, position) + "\n" + block + text.substring(position));
+ : text.substring(0, position) + "\n" + block + text.substring(position);
+ // Vergibt der Block eine Bezeichnung, die ein leerer Platzhalter der Altfassung
+ // noch hält („7. (aufgehoben)“), so weicht dieser: Zwei Einheiten gleicher
+ // Bezeichnung kann es nicht geben, und Inhalt geht dabei nicht verloren. Es ist
+ // dieselbe Regel, die schon bei der Umnummerierung auf eine weggefallene
+ // Bezeichnung gilt, und so zeigt es die amtliche Nachfassung.
+ for (var marke : platzhalterMarken(block, befehl.ebene())) {
+ neuerText = entferneWeggefallenenPlatzhalter(neuerText, marke);
+ }
+ return TextErgebnis.ok(neuerText);
});
};
}
@@ -1421,6 +1451,36 @@ public final class BefehlAnwender {
};
}
+ private static final Pattern LEERER_PLATZHALTER =
+ Pattern.compile("\\((?:weggefallen|gegenstandslos|aufgehoben)\\)");
+
+ /**
+ * Die Aufzählungsmarken, die ein eingefügter Block vergibt („5.“, „6.“, „7.“) — ohne solche, die
+ * er selbst als leeren Platzhalter führt, denn ein Platzhalter verdrängt keinen Platzhalter.
+ */
+ private static List<String> platzhalterMarken(String block, Ebene ebene) {
+ var muster =
+ switch (ebene) {
+ case NUMMER -> NUMMER_MARKER;
+ case BUCHSTABE -> BUCHSTABE_MARKER;
+ default -> null;
+ };
+ if (muster == null) {
+ return List.of();
+ }
+ var marken = new ArrayList<String>();
+ var marker = muster.matcher(block);
+ while (marker.find()) {
+ int zeilenEnde = block.indexOf('\n', marker.end());
+ var rest = block.substring(marker.end(), zeilenEnde < 0 ? block.length() : zeilenEnde);
+ if (LEERER_PLATZHALTER.matcher(rest.strip()).matches()) {
+ continue;
+ }
+ marken.add(marker.group(1) + (ebene == Ebene.BUCHSTABE ? ")" : "."));
+ }
+ return marken;
+ }
+
/**
* Entfernt die weggefallene Aufzählungszeile mit der gegebenen Marke — die soeben umbenannte
* Zeile bleibt stehen, weil nur ein leerer Platzhalter getroffen wird.
@@ -1812,6 +1872,50 @@ public final class BefehlAnwender {
|| k instanceof Stelle.BuchstabeNr);
}
+ /**
+ * Wahr, wenn die feinste Komponente der Stelle eine Aufzählungseinheit ist (Nummer/Buchstabe).
+ */
+ private static boolean feinsteIstAufzaehlung(Stelle stelle) {
+ var komponenten = stelle.komponenten();
+ if (komponenten.isEmpty()) {
+ return false;
+ }
+ var feinste = komponenten.get(komponenten.size() - 1);
+ return feinste instanceof Stelle.NummerNr || feinste instanceof Stelle.BuchstabeNr;
+ }
+
+ /**
+ * Heilt den Weißraum an der Stelle, an der eine Streichung Text herausgenommen hat: Aus zwei
+ * Leerzeichen wird eines, vor einem Satzzeichen bleibt keines. Geheilt wird ausdrücklich nur die
+ * Naht und nicht der ganze Zieltext — dieser ist bei einer Stelle ohne Fein-Komponente der
+ * gesamte Absatz, und eine Heilung über ihn hinweg fräße die Einrückung der Aufzählungszeilen:
+ * Aus den zwei Leerzeichen vor jeder Marke würde eines. Traf die Streichung den Zeilenanfang, so
+ * ist der Weißraum vor der Naht Einrückung und bleibt unangetastet.
+ */
+ private static String heileNaht(String text, int naht) {
+ int von = naht;
+ while (von > 0 && text.charAt(von - 1) == ' ') {
+ von--;
+ }
+ int bis = naht;
+ while (bis < text.length() && text.charAt(bis) == ' ') {
+ bis++;
+ }
+ if (bis == von) {
+ return text;
+ }
+ if (von == 0 || text.charAt(von - 1) == '\n') {
+ return text.substring(0, naht) + text.substring(bis);
+ }
+ if (bis < text.length() && ",;.".indexOf(text.charAt(bis)) >= 0) {
+ return text.substring(0, von) + text.substring(bis);
+ }
+ if (bis - von == 1) {
+ return text;
+ }
+ return text.substring(0, von) + " " + text.substring(bis);
+ }
+
private static @Nullable String labelVon(Stelle stelle) {
for (var komponente : stelle.komponenten().reversed()) {
if (komponente instanceof Stelle.NummerNr n) {
@@ -1969,6 +2073,11 @@ public final class BefehlAnwender {
private static final Pattern AUFZAEHLUNGSZEILE =
Pattern.compile("^(\\d+[a-z]?\\.|[a-z]{1,3}\\))\\s.*");
+ // Der Zwischenraum zwischen der Aufzählungsmarke und ihrem Text: im Gesetzblattsatz steht dort
+ // die Marken-Tabulatorspalte, im kanonischen Klartext ein einzelnes Leerzeichen.
+ private static final Pattern MARKEN_ZWISCHENRAUM =
+ Pattern.compile("^(\\d+[a-z]?\\.|[a-z]{1,3}\\))[ \\t]+");
+
private static String normalisiereZitatText(String text) {
var zeilen = text.split("\n");
var sb = new StringBuilder();
@@ -1978,7 +2087,7 @@ public final class BefehlAnwender {
// Stellenauflösung sie als Kindzeilen der Einheit erkennt.
var fortsetzungsEinzug = "";
for (var zeile : zeilen) {
- var gestutzt = zeile.strip();
+ var gestutzt = MARKEN_ZWISCHENRAUM.matcher(zeile.strip()).replaceFirst("$1 ");
if (gestutzt.isEmpty()) {
continue;
}
diff --git a/src/main/resources/sampledata/Landesrecht-Beispiele.adoc b/src/main/resources/sampledata/Landesrecht-Beispiele.adoc
index 4de907f..d0242fc 100644
--- a/src/main/resources/sampledata/Landesrecht-Beispiele.adoc
+++ b/src/main/resources/sampledata/Landesrecht-Beispiele.adoc
@@ -232,16 +232,18 @@ Nach absteigendem Nutzen:
angefasst — ein Sammelheft mit mehreren Verkündungen, also zugleich ein Test der Artikel-/
Verkündungsauswahl.
. *Berlin, ganzseitenbreite Rahmen* (XY-Cut) — die letzte offene Layout-Frage, siehe „Noch offen“.
-. *Stellung eines eingefügten Blocks* — seit die Umnummerierungs-Kaskade des BayJG aufgeht, ist
- dies der sichtbare Rest an Art. 56: „Nach Nr. 4 werden die folgenden Nrn. 5 bis 7 eingefügt“ gilt
- als angewandt, der Block tritt aber ans Ende des Absatzes statt hinter die Nr. 4, und der leere
- Platzhalter „7. (aufgehoben)“ bleibt zwischen den Nrn. 9 und 10 stehen. Beides ist im
- Akzeptanztest festgehalten, damit es nicht unbemerkt bleibt.
+
+*Erledigt:* Die _Stellung eines eingefügten Blocks_ war der letzte sichtbare Rest an Art. 56 des
+BayJG. Ursache war nicht die Einfügung selbst, sondern die Neufassung der Nr. 4 einen Punkt zuvor:
+Sie setzte ihren Wortlaut ohne Einrückung, worauf der Zeilenblock dieser Marke bis ans Absatzende
+reichte — er reicht so weit, wie tiefer eingerückt wird. Art. 56 Abs. 1 gleicht seither in Aufbau
+und Wortlaut der amtlichen Nachfassung; der Akzeptanztest prüft das.
Regressionsschutz: `./mvnw verify` prüft die bestehenden Belegfälle mit. Gepinnt ist dabei nicht
überall dieselbe Art von Zahl — für UWG, AGG und ProdHaftG, dass *kein* Befehl manuell bleibt; für
das BayJG die 154 erkannten Befehle, die vollständige Anwendung und den Wortlaut des
-Bußgeldkatalogs an drei Stellen; für Sachsen,
+Bußgeldkatalogs — dort auch Aufbau und Einrückung, weil an ihnen die Stellung eingefügter Blöcke
+hängt; für Sachsen,
Niedersachsen und die vier NRW-Artikel die vollständige Anwendung bis auf das eine benannte
Residuum im WDR-Gesetz; für Schleswig-Holstein, Berlin und Hessen die vollständige *Erkennung*
(dort 21 Befehle). Wer eine dieser Zahlen
diff --git a/src/test/java/eu/mulk/aendggner/EndToEndTest.java b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
index 865afe6..fe0ab21 100644
--- a/src/test/java/eu/mulk/aendggner/EndToEndTest.java
+++ b/src/test/java/eu/mulk/aendggner/EndToEndTest.java
@@ -271,8 +271,9 @@ class EndToEndTest {
// vorrücken, ihre Begleitänderungen aber an ihrer Dokumentstelle bleiben — die Wortfolge
// „schriftliche“ etwa steht erst nach dem vorangehenden Punkt nur noch einmal im
// Artikel und ist damit eindeutig.
- // „Kein Rest“ heißt: jeder Befehl hat gegriffen. Es heißt nicht, dass die Norm in allem der
- // amtlichen Nachfassung gleicht — was daran noch fehlt, steht weiter unten an Art. 56.
+ // „Kein Rest“ heißt zunächst nur: jeder Befehl hat gegriffen. Dass Art. 56 Abs. 1 darüber
+ // hinaus in Aufbau und Wortlaut der amtlichen Nachfassung gleicht, prüfen die
+ // Zusicherungen weiter unten.
var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
assertThat(anwendung.protokoll()).hasSameSizeAs(parseErgebnis.befehle());
var manuellPfade =
@@ -298,16 +299,24 @@ class EndToEndTest {
// Abs. 2 Nr. 12 Buchst. b führt ihre Marke allein auf der Zeile — sie ist trotzdem auflösbar.
assertThat(art56.absaetze().get(1).text()).contains("(§ 2 Abs. 3 BJagdG)");
- // Offen bleibt an dieser Norm eine Frage, die mit der Reihenfolge nichts zu tun hat und schon
- // vorher so stand: Der eingefügte Block „Nach Nr. 4 werden die folgenden Nrn. 5 bis 7
- // eingefügt“ landet am Ende des Absatzes statt hinter der Nr. 4, und der leere Platzhalter
- // „7. (aufgehoben)“ der Altfassung bleibt zwischen den Nrn. 9 und 10 stehen. Beides ist hier
- // festgehalten, damit es nicht unbemerkt bleibt — der Befehl gilt als angewandt, die Stellung
- // seines Blocks ist aber falsch.
- assertThat(katalog).contains("7. (aufgehoben)");
- assertThat(katalog.indexOf("5.\t den Verboten des Art. 29 Abs. 2"))
- .as("die eingefügten Nrn. 5 bis 7 stehen (noch) hinter der Nr. 16")
- .isGreaterThan(katalog.indexOf("16."));
+ // Der Block aus „Nach Nr. 4 werden die folgenden Nrn. 5 bis 7 eingefügt“ steht an seinem Platz
+ // — zwischen der Nr. 4 und der Nr. 8 — und nicht mehr am Ende des Absatzes. Dass er dort
+ // landete, lag an der Neufassung der Nr. 4 einen Punkt zuvor: Sie setzte ihren Wortlaut ohne
+ // Einrückung, worauf der Zeilenblock der Nr. 4 bis ans Absatzende reichte (er reicht so weit,
+ // wie tiefer eingerückt wird).
+ assertThat(katalog.indexOf(" 5. den Verboten des Art. 29 Abs. 2"))
+ .as("die eingefügten Nrn. 5 bis 7 stehen zwischen der Nr. 4 und der Nr. 8")
+ .isGreaterThan(katalog.indexOf(" 4. entgegen Art. 29 Abs. 1"))
+ .isLessThan(katalog.indexOf(" 8. entgegen Art. 31 Abs. 2"));
+ // Der leere Platzhalter „7. (aufgehoben)“ der Altfassung weicht dem eingefügten Block, weil
+ // dieser dieselbe Bezeichnung vergibt — dieselbe Regel wie bei der Umnummerierung auf eine
+ // weggefallene Bezeichnung. Verdrängt wird dabei nur ein Platzhalter, nie ein Wortlaut: Der
+ // Platzhalter „1. (aufgehoben)“ des Abs. 2, den kein Befehl anrührt, bleibt unberührt stehen.
+ assertThat(katalog).doesNotContain("(aufgehoben)");
+ assertThat(art56.absaetze().get(1).text()).contains("1. (aufgehoben)");
+ // Die Einrückung der Aufzählungszeilen übersteht die Streichungen des Hefts: Sie wird nicht
+ // mehr von der Leerzeichen-Heilung mitgenommen, die nur noch die Naht der Streichung glättet.
+ assertThat(katalog).contains("\n 12. ohne Begleitung").doesNotContain("\n 12. ");
// Art. 29a Abs. 5 trägt nach der Kaskade die neue Behördenbezeichnung und den eingefügten Satz.
var art29a = anwendung.neu().norm("Art. 29a").orElseThrow();
diff --git a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
index 7e7da85..212a037 100644
--- a/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
+++ b/src/test/java/eu/mulk/aendggner/anwendung/BefehlAnwenderTest.java
@@ -1192,6 +1192,102 @@ class BefehlAnwenderTest {
.isEqualTo("Zweck dieses Gesetzes ist die Erprobung, auch in Teilen,.");
}
+ /**
+ * Ein Katalog, wie ihn das Stammgesetz führt: Aufzählungszeilen auf zwei Leerzeichen, darunter
+ * ein weggefallener Platzhalter.
+ */
+ private static Gesetz katalog() {
+ return new Gesetz(
+ "TestG",
+ "Gesetz zur Erprobung",
+ "Testgesetz",
+ List.of(
+ new Norm(
+ "§ 1",
+ "Zweck",
+ null,
+ List.of(
+ new Absatz(
+ "1",
+ "Erprobt wird, wer\n"
+ + " 1. Befehle erkennt,\n"
+ + " 2. Befehle schriftlich anwendet,\n"
+ + " 3. (weggefallen)\n"
+ + " 4. Ergebnisse prüft.")),
+ false)),
+ List.of());
+ }
+
+ /**
+ * Die Neufassung einer Nummer setzt ihren Wortlaut auf die Einrückung der alten Einheit. Ohne sie
+ * stünde die Zeile in Spalte 0 und der Zeilenblock ihrer Marke reichte bis ans Absatzende — ein
+ * nachfolgender Einfügebefehl träte dann an die falsche Stelle.
+ */
+ @Test
+ void neufassungEinerNummerBehaeltDieEinrueckung() {
+ var befehl =
+ new Neufassung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("1"), new Stelle.NummerNr("2")),
+ "2. Befehle anwendet,",
+ PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(katalog(), List.of(befehl));
+
+ assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT);
+ assertThat(absatzText(ergebnis.neu(), "§ 1", 0))
+ .contains("\n 2. Befehle anwendet,\n")
+ .doesNotContain("\n2. ");
+ }
+
+ /**
+ * Die Heilung des Weißraums nach einer Streichung glättet die Naht und sonst nichts: Die
+ * Einrückung der Aufzählungszeilen bleibt, wie sie war.
+ */
+ @Test
+ void streichungLaesstDieEinrueckungUnberuehrt() {
+ var befehl =
+ new Streichung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("1")), "schriftlich ", PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(katalog(), List.of(befehl));
+
+ assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT);
+ assertThat(absatzText(ergebnis.neu(), "§ 1", 0))
+ .isEqualTo(
+ "Erprobt wird, wer\n"
+ + " 1. Befehle erkennt,\n"
+ + " 2. Befehle anwendet,\n"
+ + " 3. (weggefallen)\n"
+ + " 4. Ergebnisse prüft.");
+ }
+
+ /**
+ * Vergibt ein eingefügter Block eine Bezeichnung, die ein leerer Platzhalter noch hält, so weicht
+ * dieser — dieselbe Regel wie bei der Umnummerierung auf eine weggefallene Bezeichnung.
+ */
+ @Test
+ void eingefuegterBlockVerdraengtDenWeggefallenenPlatzhalter() {
+ var befehl =
+ new StrukturEinfuegung(
+ stelle(new Stelle.Paragraph("1"), new Stelle.AbsatzNr("1"), new Stelle.NummerNr("2")),
+ false,
+ Ebene.NUMMER,
+ "3",
+ "3. Befehle ordnet,",
+ PROV);
+
+ var ergebnis = BefehlAnwender.anwenden(katalog(), List.of(befehl));
+
+ assertThat(ergebnis.protokoll().get(0).status()).isEqualTo(Status.ANGEWANDT);
+ assertThat(absatzText(ergebnis.neu(), "§ 1", 0))
+ .isEqualTo(
+ "Erprobt wird, wer\n"
+ + " 1. Befehle erkennt,\n"
+ + " 2. Befehle schriftlich anwendet,\n"
+ + " 3. Befehle ordnet,\n"
+ + " 4. Ergebnisse prüft.");
+ }
+
private static String absatzText(Gesetz gesetz, String enbez, int index) {
return gesetz.norm(enbez).orElseThrow().absaetze().get(index).text();
}