blob: 8c19c78c4f24388f78525286fb71e01007785d7c [file] [log] [blame]
package eu.mulk.aendggner.anwendung;
import eu.mulk.aendggner.aenderung.Stelle;
import eu.mulk.aendggner.gesetz.Gesetz;
import eu.mulk.aendggner.gesetz.Norm;
import java.util.List;
import java.util.regex.Pattern;
import org.jspecify.annotations.Nullable;
/**
* Löst eine {@link Stelle} gegen ein {@link Gesetz} zu einer konkreten Fundstelle auf: Norm, Absatz
* und — bei Satz-/Nummer-/Buchstaben-Angaben — Zeichenbereich im Absatztext.
*/
final class StellenAufloeser {
/**
* @param normIndex Index der Norm in {@code gesetz.normen()}.
* @param absatzIndex Index des Absatzes; {@code null}, wenn die Stelle die ganze Norm meint.
* @param bereich Zeichenbereich im Absatztext; {@code null}, wenn der ganze Absatz gemeint ist.
*/
record Fundstelle(
int normIndex, @Nullable Integer absatzIndex, SatzTeiler.@Nullable SatzBereich bereich) {}
sealed interface Ergebnis {
record Gefunden(Fundstelle fundstelle) implements Ergebnis {}
record NichtGefunden(String begruendung) implements Ergebnis {}
}
private StellenAufloeser() {}
static Ergebnis aufloese(Gesetz gesetz, Stelle stelle) {
if (stelle.istLeer()) {
return new Ergebnis.NichtGefunden("Stelle nennt keine Fundstelle im Gesetz.");
}
// 1. Norm bestimmen.
String enbez;
if (stelle.betrifftInhaltsuebersicht()) {
enbez = "Inhaltsübersicht";
} else if (stelle.paragraph().isPresent()) {
enbez = "§ " + stelle.paragraph().get().nummer();
} else {
return new Ergebnis.NichtGefunden("Stelle nennt keinen Paragraphen: " + stelle.anzeigeText());
}
int normIndex = normIndex(gesetz, enbez);
if (normIndex < 0) {
return new Ergebnis.NichtGefunden(enbez + " existiert nicht im Gesetz.");
}
var norm = gesetz.normen().get(normIndex);
// 2. Absatz bestimmen.
Integer absatzIndex = null;
if (stelle.absatz().isPresent()) {
var nummer = stelle.absatz().get().nummer();
absatzIndex = absatzIndex(norm, nummer);
if (absatzIndex < 0) {
return new Ergebnis.NichtGefunden(enbez + " hat keinen Absatz " + nummer + ".");
}
}
// 3. Feinste Komponente (Buchstabe > Nummer > Satz) als Textbereich auflösen.
var feinste = feinsteKomponente(stelle);
if (feinste == null) {
return new Ergebnis.Gefunden(new Fundstelle(normIndex, absatzIndex, null));
}
if (absatzIndex == null) {
if (norm.absaetze().size() == 1) {
absatzIndex = 0;
} else {
return new Ergebnis.NichtGefunden(
enbez
+ " hat "
+ norm.absaetze().size()
+ " Absätze; „"
+ stelle.anzeigeText()
+ "“ ist ohne Absatzangabe nicht eindeutig.");
}
}
var text = norm.absaetze().get(absatzIndex).text();
var bereich = loeseKomponenteAuf(feinste, text);
if (bereich == null) {
return new Ergebnis.NichtGefunden(
"„" + stelle.anzeigeText() + "“ ist im Text von " + enbez + " nicht auffindbar.");
}
return new Ergebnis.Gefunden(new Fundstelle(normIndex, absatzIndex, bereich));
}
static int normIndex(Gesetz gesetz, String enbez) {
var normen = gesetz.normen();
for (int i = 0; i < normen.size(); i++) {
if (normen.get(i).enbez().equals(enbez)) {
return i;
}
}
return -1;
}
static int absatzIndex(Norm norm, String nummer) {
var absaetze = norm.absaetze();
for (int i = 0; i < absaetze.size(); i++) {
if (nummer.equals(absaetze.get(i).nummer())) {
return i;
}
}
// Unnummerierter Einzelabsatz gilt als „Absatz 1“.
if (absaetze.size() == 1 && absaetze.get(0).nummer() == null && nummer.equals("1")) {
return 0;
}
return -1;
}
private static Stelle.@Nullable Komponente feinsteKomponente(Stelle stelle) {
Stelle.Komponente feinste = null;
for (var komponente : stelle.komponenten()) {
switch (komponente) {
case Stelle.SatzNr s -> feinste = besser(feinste, s, 1);
case Stelle.NummerNr n -> feinste = besser(feinste, n, 2);
case Stelle.BuchstabeNr b -> feinste = besser(feinste, b, 3);
default -> {}
}
}
return feinste;
}
private static Stelle.Komponente besser(
Stelle.@Nullable Komponente bisher, Stelle.Komponente neu, int rang) {
if (bisher == null) {
return neu;
}
return rang(bisher) >= rang ? bisher : neu;
}
private static int rang(Stelle.Komponente komponente) {
return switch (komponente) {
case Stelle.SatzNr s -> 1;
case Stelle.NummerNr n -> 2;
case Stelle.BuchstabeNr b -> 3;
default -> 0;
};
}
private static SatzTeiler.@Nullable SatzBereich loeseKomponenteAuf(
Stelle.Komponente komponente, String text) {
return switch (komponente) {
case Stelle.SatzNr satz -> {
int index = Integer.parseInt(satz.nummer().replaceAll("[a-z]$", "")) - 1;
var saetze = SatzTeiler.teile(text);
yield index >= 0 && index < saetze.size() ? saetze.get(index) : null;
}
case Stelle.NummerNr nummer -> zeilenBereich(text, Pattern.quote(nummer.nummer()) + "\\.");
case Stelle.BuchstabeNr buchstabe ->
zeilenBereich(text, Pattern.quote(buchstabe.kennung()) + "\\)");
default -> null;
};
}
/** Findet die (eindeutige) Aufzählungszeile, die mit dem gegebenen Label beginnt. */
private static SatzTeiler.@Nullable SatzBereich zeilenBereich(String text, String labelRegex) {
var muster = Pattern.compile("(?m)^[ \\t]*" + labelRegex + "[ \\t].*$");
var matcher = muster.matcher(text);
SatzTeiler.SatzBereich gefunden = null;
while (matcher.find()) {
if (gefunden != null) {
return null; // mehrdeutig (z.B. gleiche Buchstaben in mehreren Nummern)
}
gefunden = new SatzTeiler.SatzBereich(matcher.start(), matcher.end());
}
return gefunden;
}
static List<Stelle.Komponente> komponenten(Stelle stelle) {
return stelle.komponenten();
}
}