diff options
Diffstat (limited to 'src/main/java')
| -rw-r--r-- | src/main/java/eu/mulk/aendggner/AendGgner.java | 90 | ||||
| -rw-r--r-- | src/main/java/eu/mulk/aendggner/Pipeline.java | 30 | ||||
| -rw-r--r-- | src/main/java/eu/mulk/aendggner/bericht/Korpusbericht.java | 476 |
3 files changed, 587 insertions, 9 deletions
diff --git a/src/main/java/eu/mulk/aendggner/AendGgner.java b/src/main/java/eu/mulk/aendggner/AendGgner.java index d089f21..3067a13 100644 --- a/src/main/java/eu/mulk/aendggner/AendGgner.java +++ b/src/main/java/eu/mulk/aendggner/AendGgner.java @@ -8,6 +8,7 @@ import eu.mulk.aendggner.aenderung.parse.PatchTextExtraktor; import eu.mulk.aendggner.aenderung.parse.TextBereiniger; import eu.mulk.aendggner.anwendung.BefehlAnwender; import eu.mulk.aendggner.anwendung.Grund; +import eu.mulk.aendggner.bericht.Korpusbericht; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -15,7 +16,6 @@ import java.nio.file.Path; import java.time.LocalDate; import java.time.format.DateTimeParseException; import java.util.ArrayList; -import java.util.EnumMap; import java.util.List; import java.util.concurrent.Callable; import java.util.logging.LogManager; @@ -36,6 +36,7 @@ public class AendGgner implements Callable<Integer> { @Parameters( index = "0", + arity = "0..1", paramLabel = "<base law>", description = "The base law: gii-norm XML from gesetze-im-internet.de, or — for state law — the" @@ -134,6 +135,28 @@ public class AendGgner implements Callable<Integer> { + "exit code 3.") private String nachfassung; + @Option( + names = "--korpus", + paramLabel = "<file>", + description = + "Run every job of the given tab-separated job list and write a report of key figures " + + "instead of a synopsis. Paths in the list are relative to the list's directory.") + private String korpus; + + @Option( + names = "--grundlinie", + paramLabel = "<file>", + description = + "With --korpus: hold the run against this earlier report and complain about every figure " + + "that has fallen. A regression yields exit code 3.") + private String grundlinie; + + @Option( + names = "--synopsen", + paramLabel = "<dir>", + description = "With --korpus: also write each job's synopsis into this directory.") + private String synopsen; + /** Die angegebenen Eingaben, jede über {@link Bezug} beschafft. */ private static List<Quelle> hole(List<String> angaben) throws IOException, InterruptedException { var quellen = new ArrayList<Quelle>(); @@ -154,6 +177,15 @@ public class AendGgner implements Callable<Integer> { log.debugf("Logging configured."); + if (korpus != null) { + return fuehreKorpusAus(); + } + + if (baseFile == null) { + System.err.println("Fehler: das Stammgesetz muss angegeben werden."); + return 1; + } + if (dumpGesetz) { var gesetz = Pipeline.ladeStammgesetz(Bezug.hole(baseFile)); System.out.printf( @@ -305,12 +337,7 @@ public class AendGgner implements Callable<Integer> { * jedem Befehl darüber. */ private static void haeufigkeitDerGruende(BefehlAnwender.AnwendungsErgebnis anwendung) { - var haeufigkeit = new EnumMap<Grund, Integer>(Grund.class); - for (var eintrag : anwendung.protokoll()) { - if (eintrag.status() == BefehlAnwender.Status.MANUELL_PRUEFEN && eintrag.grund() != null) { - haeufigkeit.merge(eintrag.grund(), 1, Integer::sum); - } - } + var haeufigkeit = Pipeline.zaehleGruende(anwendung.protokoll()); if (haeufigkeit.isEmpty()) { return; } @@ -320,6 +347,55 @@ public class AendGgner implements Callable<Integer> { .forEach(e -> System.out.printf(" %4d %s%n", e.getValue(), e.getKey().bezeichnung())); } + /** + * Der Massenlauf: die Aufträge der Liste nacheinander, eine Zeile Kennzahlen je Auftrag. + * + * <p>Der Bericht geht dahin, wohin sonst die Synopse ginge ({@code -o}). Ist eine Grundlinie + * angegeben, so wird der Lauf gegen sie gehalten; jede gefallene Kennzahl wird gerügt und der + * Lauf endet mit 3 — dieselbe Zahl, mit der ein nicht aufgehender Abgleich endet. + */ + private Integer fuehreKorpusAus() throws IOException { + var liste = Path.of(korpus); + var wurzel = liste.toAbsolutePath().getParent(); + var auftraege = Korpusbericht.liesListe(liste); + log.infof("Korpuslauf: %d Aufträge aus %s.", auftraege.size(), liste); + + var zeilen = + Korpusbericht.fuehreAus(auftraege, wurzel, synopsen == null ? null : Path.of(synopsen)); + var tsv = Korpusbericht.alsTsv(zeilen); + if (output == null || output.equals("-") || output.equals("synopse.html")) { + // Die Voreinstellung des Schalters meint die Synopse; ein Bericht gehört dann auf die + // Standardausgabe und nicht in eine Datei namens „synopse.html“. + System.out.print(tsv); + } else { + Files.writeString(Path.of(output), tsv, StandardCharsets.UTF_8); + System.out.println("Bericht geschrieben: " + output); + } + if (synopsen != null) { + var uebersicht = Path.of(synopsen).resolveSibling("uebersicht.html"); + Files.writeString( + uebersicht, + Korpusbericht.alsHtml(zeilen, Path.of(synopsen).getFileName().toString()), + StandardCharsets.UTF_8); + System.out.println("Übersicht geschrieben: " + uebersicht); + } + System.out.println(Korpusbericht.summe(zeilen)); + + if (grundlinie == null) { + return zeilen.stream().anyMatch(z -> z.fehler() != null) ? 3 : 0; + } + var ruegen = + Korpusbericht.gegenGrundlinie( + zeilen, Files.readString(Path.of(grundlinie), StandardCharsets.UTF_8)); + if (ruegen.isEmpty()) { + System.out.println("Die Grundlinie ist gehalten."); + return 0; + } + System.out.println("Rückschritte gegenüber der Grundlinie:"); + ruegen.forEach(r -> System.out.println(" " + r)); + return 3; + } + private static String kuerze(String text) { var einzeilig = text.replaceAll("\\s+", " "); return einzeilig.length() <= 160 ? einzeilig : einzeilig.substring(0, 157) + "…"; diff --git a/src/main/java/eu/mulk/aendggner/Pipeline.java b/src/main/java/eu/mulk/aendggner/Pipeline.java index e4ce9fb..483d456 100644 --- a/src/main/java/eu/mulk/aendggner/Pipeline.java +++ b/src/main/java/eu/mulk/aendggner/Pipeline.java @@ -30,7 +30,9 @@ import java.io.IOException; import java.nio.file.Path; import java.time.LocalDate; import java.util.ArrayList; +import java.util.EnumMap; import java.util.List; +import java.util.Map; import java.util.Set; import org.jspecify.annotations.Nullable; @@ -116,6 +118,11 @@ public final class Pipeline { * @param abgleich der normweise Vergleich mit der amtlichen Nachfassung; {@code null}, wenn der * Auftrag keine nannte. */ + /** + * @param gruende die Auszählung der Gründe, aus denen Befehle liegengeblieben sind. Sie steht + * schon in der Synopse; als Zahl herausgegeben trägt sie überdies den Korpusbericht (§ 6d), + * der über viele Hefte hinweg zeigt, woran die Reste liegen. + */ public record Ergebnis( String html, long anzahlAngewandt, @@ -123,7 +130,25 @@ public final class Pipeline { int anzahlGeaenderteNormen, int anzahlProtokollEintraege, String neufassung, - @Nullable Nachfassungsabgleich abgleich) {} + @Nullable Nachfassungsabgleich abgleich, + Map<Grund, Integer> gruende) { + + public Ergebnis { + gruende = Map.copyOf(gruende); + } + } + + /** Die Gründe der liegengebliebenen Befehle, nach Häufigkeit auszählbar. */ + public static Map<Grund, Integer> zaehleGruende( + List<BefehlAnwender.AngewandteAenderung> protokoll) { + var haeufigkeit = new EnumMap<Grund, Integer>(Grund.class); + for (var eintrag : protokoll) { + if (eintrag.status() == BefehlAnwender.Status.MANUELL_PRUEFEN && eintrag.grund() != null) { + haeufigkeit.merge(eintrag.grund(), 1, Integer::sum); + } + } + return haeufigkeit; + } /** * Ein eingespeistes Änderungsdokument samt erkannter Art und aufbereitetem Text. @@ -257,7 +282,8 @@ public final class Pipeline { synopse.eintraege().size(), protokoll.size(), LandesRechtTextAusgeber.ausgeben(gesetz), - abgleich); + abgleich, + zaehleGruende(protokoll)); } /** diff --git a/src/main/java/eu/mulk/aendggner/bericht/Korpusbericht.java b/src/main/java/eu/mulk/aendggner/bericht/Korpusbericht.java new file mode 100644 index 0000000..1daba42 --- /dev/null +++ b/src/main/java/eu/mulk/aendggner/bericht/Korpusbericht.java @@ -0,0 +1,476 @@ +// SPDX-FileCopyrightText: 2026 Matthias Andreas Benkard <code@mail.matthias.benkard.de> +// SPDX-License-Identifier: AGPL-3.0-or-later +package eu.mulk.aendggner.bericht; + +import eu.mulk.aendggner.Pipeline; +import eu.mulk.aendggner.Quelle; +import eu.mulk.aendggner.anwendung.Grund; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.jspecify.annotations.Nullable; + +/** + * Der Massenlauf: viele Hefte in einem Durchgang, eine Zeile Kennzahlen je Heft. + * + * <p>Wozu: Ein Belegfall sagt, dass <em>dieses</em> Heft aufgeht. Er sagt nicht, woran die Reste im + * Ganzen liegen — ob an der Befehlssprache eines Landes, an der Aufbereitung des Druckwerks oder am + * Alter der Stammfassung. Die Auszählung der Gründe (§ 1 Absatz 5 des Handbuchs) beantwortet das + * für ein Heft; über einen Korpus geführt beantwortet sie es für das Erzeugnis. + * + * <p>Der Bericht ist zugleich eine Grundlinie: Wird er eingecheckt, so schlägt jeder Lauf an, bei + * dem eine Kennzahl <em>fällt</em> — weniger erschlossene Befehle, weniger angewandte, weniger + * gleiche Normen. Steigt eine, ist die Grundlinie fortzuschreiben; das ist der Vermerk der Welle. + * + * <p>Verworfen wird auch hier nichts: Ein Auftrag, der mit einem Fehler abbricht, hinterlässt seine + * Zeile samt Fehlertext und hält den Lauf nicht an. + */ +public final class Korpusbericht { + + private Korpusbericht() {} + + /** Kein Wert — in der Liste wie im Bericht. */ + private static final String LEER = "-"; + + /** + * Ein Auftrag der Liste. + * + * @param bezeichnung der Name, unter dem der Auftrag im Bericht und in der Grundlinie steht. + * @param stamm die Stammfassung, relativ zum Verzeichnis der Liste. + * @param hefte die Änderungsdokumente, in der Reihenfolge der Liste. + * @param artikel nur diesen Artikel anwenden; {@code null} = alle betroffenen. + * @param nachfassung die amtliche Nachfassung; {@code null} = kein Abgleich, dann bleiben dessen + * Spalten leer. + * @param stichtag die Fassung dieses Tages; {@code null} = alle Befehle anwenden. + */ + public record Auftrag( + String bezeichnung, + String stamm, + List<String> hefte, + @Nullable String artikel, + @Nullable String nachfassung, + @Nullable LocalDate stichtag) { + + public Auftrag { + hefte = List.copyOf(hefte); + } + } + + /** + * Die Kennzahlen eines Laufes. + * + * @param fehler der Grund des Abbruchs; {@code null}, wenn der Auftrag durchgelaufen ist. + */ + public record Zeile( + String bezeichnung, + int befehle, + long angewandt, + long manuell, + long zurueckgestellt, + @Nullable Integer gleich, + @Nullable Integer geprueft, + @Nullable Integer fehlend, + @Nullable Integer ueberzaehlig, + @Nullable Integer abweichend, + Map<Grund, Integer> gruende, + @Nullable String fehler) { + + public Zeile { + gruende = Map.copyOf(gruende); + } + } + + /** Die Spaltenüberschriften des Berichts; sie stehen als Kommentarzeile obenan. */ + private static final List<String> SPALTEN = + List.of( + "Bezeichnung", + "Befehle", + "angewandt", + "manuell", + "zurückgestellt", + "gleich", + "geprüft", + "fehlend", + "überzählig", + "abweichend", + "Gründe"); + + // ---------------------------------------------------------------- Liste lesen + + /** + * Liest die Auftragsliste. Sie ist eine Tabulatortabelle mit den Spalten Bezeichnung, + * Stammfassung, Hefte (durch Komma getrennt), Artikel, Nachfassung, Stichtag; „-“ steht für + * „nicht angegeben“. Zeilen, die mit „#“ beginnen, und Leerzeilen werden übergangen. + */ + public static List<Auftrag> liesListe(Path liste) throws IOException { + var auftraege = new ArrayList<Auftrag>(); + int nummer = 0; + for (var zeile : Files.readAllLines(liste, StandardCharsets.UTF_8)) { + nummer++; + if (zeile.isBlank() || zeile.startsWith("#")) { + continue; + } + var felder = zeile.split("\t"); + if (felder.length < 3) { + throw new IOException( + "%s, Zeile %d: Ein Auftrag braucht wenigstens Bezeichnung, Stammfassung und Heft." + .formatted(liste, nummer)); + } + auftraege.add( + new Auftrag( + felder[0].strip(), + felder[1].strip(), + List.of(felder[2].strip().split("\\s*,\\s*")), + feld(felder, 3), + feld(felder, 4), + feld(felder, 5) == null ? null : LocalDate.parse(feld(felder, 5)))); + } + return auftraege; + } + + private static @Nullable String feld(String[] felder, int index) { + if (index >= felder.length) { + return null; + } + var wert = felder[index].strip(); + return wert.isEmpty() || wert.equals(LEER) ? null : wert; + } + + // ---------------------------------------------------------------- Lauf + + /** + * Führt alle Aufträge aus. + * + * @param wurzel das Verzeichnis, auf das sich die Pfade der Liste beziehen. + * @param synopsen wohin die Einzelsynopsen zu legen sind; {@code null} = keine ablegen. + */ + public static List<Zeile> fuehreAus( + List<Auftrag> auftraege, Path wurzel, @Nullable Path synopsen) { + var zeilen = new ArrayList<Zeile>(auftraege.size()); + for (var auftrag : auftraege) { + zeilen.add(fuehreAus(auftrag, wurzel, synopsen)); + } + return zeilen; + } + + public static Zeile fuehreAus(Auftrag auftrag, Path wurzel, @Nullable Path synopsen) { + try { + var hefte = new ArrayList<Quelle>(auftrag.hefte().size()); + for (var heft : auftrag.hefte()) { + hefte.add(Quelle.lies(wurzel.resolve(heft))); + } + var ergebnis = + Pipeline.erzeugeSynopse( + new Pipeline.Auftrag( + Quelle.lies(wurzel.resolve(auftrag.stamm())), + hefte, + auftrag.artikel(), + false, + auftrag.stichtag(), + auftrag.nachfassung() == null + ? null + : Quelle.lies(wurzel.resolve(auftrag.nachfassung())))); + if (synopsen != null) { + Files.createDirectories(synopsen); + Files.writeString( + synopsen.resolve(dateiname(auftrag.bezeichnung()) + ".html"), + ergebnis.html(), + StandardCharsets.UTF_8); + } + var abgleich = ergebnis.abgleich(); + return new Zeile( + auftrag.bezeichnung(), + ergebnis.anzahlProtokollEintraege(), + ergebnis.anzahlAngewandt(), + ergebnis.anzahlManuell(), + // Was am Stichtag noch nicht galt, ist weder angewandt noch liegengeblieben. + ergebnis.anzahlProtokollEintraege() + - ergebnis.anzahlAngewandt() + - ergebnis.anzahlManuell(), + abgleich == null ? null : abgleich.gleich(), + abgleich == null ? null : abgleich.geprueft(), + abgleich == null ? null : abgleich.fehlende().size(), + abgleich == null ? null : abgleich.ueberzaehlige().size(), + abgleich == null ? null : abgleich.abweichungen().size(), + ergebnis.gruende(), + null); + } catch (Exception e) { + // Ein gescheiterter Auftrag hält den Lauf nicht an; sein Fehler steht in seiner Zeile. + return new Zeile( + auftrag.bezeichnung(), + 0, + 0, + 0, + 0, + null, + null, + null, + null, + null, + Map.of(), + e.getClass().getSimpleName() + ": " + e.getMessage()); + } + } + + private static String dateiname(String bezeichnung) { + return bezeichnung.replaceAll("[^\\p{L}\\p{N}]+", "-"); + } + + // ---------------------------------------------------------------- Ausgabe + + /** Der Bericht als Tabulatortabelle — die Form, in der er auch als Grundlinie liegt. */ + public static String alsTsv(List<Zeile> zeilen) { + var sb = new StringBuilder("# ").append(String.join("\t", SPALTEN)).append('\n'); + for (var zeile : zeilen) { + sb.append(zeile.bezeichnung()) + .append('\t') + .append(zeile.befehle()) + .append('\t') + .append(zeile.angewandt()) + .append('\t') + .append(zeile.manuell()) + .append('\t') + .append(zeile.zurueckgestellt()) + .append('\t') + .append(zahl(zeile.gleich())) + .append('\t') + .append(zahl(zeile.geprueft())) + .append('\t') + .append(zahl(zeile.fehlend())) + .append('\t') + .append(zahl(zeile.ueberzaehlig())) + .append('\t') + .append(zahl(zeile.abweichend())) + .append('\t') + .append(gruendeText(zeile.gruende())); + if (zeile.fehler() != null) { + sb.append('\t').append(zeile.fehler().replace('\t', ' ').replace('\n', ' ')); + } + sb.append('\n'); + } + return sb.toString(); + } + + private static String zahl(@Nullable Integer wert) { + return wert == null ? LEER : String.valueOf(wert); + } + + /** „Zieltext nicht vorhanden:3; Befehl nicht erkannt:1“ — in der Reihenfolge der Gründe. */ + static String gruendeText(Map<Grund, Integer> gruende) { + if (gruende.isEmpty()) { + return LEER; + } + var geordnet = new EnumMap<>(gruende); + var sb = new StringBuilder(); + for (var eintrag : geordnet.entrySet()) { + if (sb.length() > 0) { + sb.append("; "); + } + sb.append(eintrag.getKey().bezeichnung()).append(':').append(eintrag.getValue()); + } + return sb.toString(); + } + + /** + * Der Bericht als Übersichtsseite. Sie ist kein Ersatz für die Tabulatortabelle, sondern deren + * Lesefassung: Jede Zeile verweist auf die Synopse ihres Auftrags, sodass sich ein Rest an seinem + * eigenen Dokument nachschlagen lässt. + * + * @param verzeichnis das Verzeichnis der Einzelsynopsen, relativ zur Übersicht; {@code null} = + * ohne Verweise. + */ + public static String alsHtml(List<Zeile> zeilen, @Nullable String verzeichnis) { + var sb = new StringBuilder(); + sb.append("<!DOCTYPE html>\n<html lang=\"de\"><head><meta charset=\"utf-8\">\n") + .append("<title>Korpusbericht — ÄndGgner</title>\n") + .append("<style>\n") + .append("body { font-family: system-ui, sans-serif; margin: 2rem; max-width: 70rem; }\n") + .append("table { border-collapse: collapse; width: 100%; font-size: 0.85rem; }\n") + .append("th, td { border: 1px solid #999; padding: 0.25rem 0.5rem; text-align: right; }\n") + .append("th:first-child, td:first-child { text-align: left; }\n") + .append("td.gruende { text-align: left; font-size: 0.8rem; }\n") + .append("tr.rest td { background: #fff6f2; }\n") + .append("caption { text-align: left; padding-bottom: 0.5rem; }\n") + .append("</style></head><body>\n") + .append("<h1>Korpusbericht</h1>\n<p>") + .append(esc(summe(zeilen))) + .append("</p>\n<table><thead><tr>"); + for (var spalte : SPALTEN) { + sb.append("<th>").append(esc(spalte)).append("</th>"); + } + sb.append("</tr></thead><tbody>\n"); + for (var zeile : zeilen) { + sb.append(zeile.manuell() > 0 || zeile.fehler() != null ? "<tr class=\"rest\">" : "<tr>"); + sb.append("<td>"); + if (verzeichnis == null) { + sb.append(esc(zeile.bezeichnung())); + } else { + sb.append("<a href=\"") + .append(esc(verzeichnis)) + .append('/') + .append(esc(dateiname(zeile.bezeichnung()))) + .append(".html\">") + .append(esc(zeile.bezeichnung())) + .append("</a>"); + } + sb.append("</td>"); + for (var wert : + List.of( + String.valueOf(zeile.befehle()), + String.valueOf(zeile.angewandt()), + String.valueOf(zeile.manuell()), + String.valueOf(zeile.zurueckgestellt()), + zahl(zeile.gleich()), + zahl(zeile.geprueft()), + zahl(zeile.fehlend()), + zahl(zeile.ueberzaehlig()), + zahl(zeile.abweichend()))) { + sb.append("<td>").append(esc(wert)).append("</td>"); + } + sb.append("<td class=\"gruende\">") + .append(esc(zeile.fehler() != null ? zeile.fehler() : gruendeText(zeile.gruende()))) + .append("</td></tr>\n"); + } + return sb.append("</tbody></table>\n</body></html>\n").toString(); + } + + private static String esc(String text) { + return text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """); + } + + /** Die Summe über den Korpus — die Zahl, die eine Welle vor und nach sich nennt. */ + public static String summe(List<Zeile> zeilen) { + long befehle = zeilen.stream().mapToLong(Zeile::befehle).sum(); + long angewandt = zeilen.stream().mapToLong(Zeile::angewandt).sum(); + long manuell = zeilen.stream().mapToLong(Zeile::manuell).sum(); + long gleich = zeilen.stream().filter(z -> z.gleich() != null).mapToLong(Zeile::gleich).sum(); + long geprueft = + zeilen.stream().filter(z -> z.geprueft() != null).mapToLong(Zeile::geprueft).sum(); + long gescheitert = zeilen.stream().filter(z -> z.fehler() != null).count(); + var sb = + new StringBuilder( + "%d Aufträge, %d Befehle, %d angewandt, %d manuell; %d von %d Normen gleich" + .formatted(zeilen.size(), befehle, angewandt, manuell, gleich, geprueft)); + if (gescheitert > 0) { + sb.append("; %d Aufträge gescheitert".formatted(gescheitert)); + } + return sb.toString(); + } + + // ---------------------------------------------------------------- Grundlinie + + /** + * Hält den Lauf gegen die Grundlinie und meldet jede Kennzahl, die <em>gefallen</em> ist. + * + * <p>Nur der Rückschritt wird gerügt. Ein Fortschritt ist kein Fehler, sondern der Zweck der + * Arbeit; er verlangt allein, die Grundlinie fortzuschreiben. + * + * @return die Rügen; leer, wenn nichts gefallen ist. + */ + public static List<String> gegenGrundlinie(List<Zeile> lauf, String grundlinie) { + var alt = liesGrundlinie(grundlinie); + var ruegen = new ArrayList<String>(); + for (var zeile : lauf) { + if (zeile.fehler() != null) { + ruegen.add("%s: gescheitert — %s".formatted(zeile.bezeichnung(), zeile.fehler())); + continue; + } + var frueher = alt.get(zeile.bezeichnung()); + if (frueher == null) { + // Ein neuer Auftrag ist kein Rückschritt; die Grundlinie ist bloß noch nicht + // fortgeschrieben. + continue; + } + pruefe( + ruegen, zeile.bezeichnung(), "erschlossene Befehle", zeile.befehle(), frueher[1], true); + pruefe( + ruegen, zeile.bezeichnung(), "angewandte Befehle", zeile.angewandt(), frueher[2], true); + pruefe( + ruegen, + zeile.bezeichnung(), + "liegengebliebene Befehle", + zeile.manuell(), + frueher[3], + false); + pruefe(ruegen, zeile.bezeichnung(), "gleiche Normen", wert(zeile.gleich()), frueher[5], true); + pruefe( + ruegen, zeile.bezeichnung(), "fehlende Normen", wert(zeile.fehlend()), frueher[7], false); + pruefe( + ruegen, + zeile.bezeichnung(), + "überzählige Normen", + wert(zeile.ueberzaehlig()), + frueher[8], + false); + pruefe( + ruegen, + zeile.bezeichnung(), + "abweichende Normen", + wert(zeile.abweichend()), + frueher[9], + false); + } + for (var bezeichnung : alt.keySet()) { + if (lauf.stream().noneMatch(z -> z.bezeichnung().equals(bezeichnung))) { + ruegen.add( + "%s: steht in der Grundlinie, ist im Lauf aber nicht vorgekommen." + .formatted(bezeichnung)); + } + } + return ruegen; + } + + private static long wert(@Nullable Integer zahl) { + return zahl == null ? Long.MIN_VALUE : zahl; + } + + /** + * @param hoeherIstBesser ob ein Fallen der Zahl den Rückschritt bedeutet (angewandte Befehle, + * gleiche Normen) oder ihr Steigen (liegengebliebene, abweichende). + */ + private static void pruefe( + List<String> ruegen, + String bezeichnung, + String kennzahl, + long jetzt, + String frueherText, + boolean hoeherIstBesser) { + if (frueherText.equals(LEER) || jetzt == Long.MIN_VALUE) { + return; + } + long frueher; + try { + frueher = Long.parseLong(frueherText); + } catch (NumberFormatException e) { + return; + } + boolean rueckschritt = hoeherIstBesser ? jetzt < frueher : jetzt > frueher; + if (rueckschritt) { + ruegen.add("%s: %s %d statt %d.".formatted(bezeichnung, kennzahl, jetzt, frueher)); + } + } + + private static Map<String, String[]> liesGrundlinie(String tsv) { + var zeilen = new LinkedHashMap<String, String[]>(); + for (var zeile : tsv.split("\n")) { + if (zeile.isBlank() || zeile.startsWith("#")) { + continue; + } + var felder = zeile.split("\t"); + if (felder.length >= 10) { + zeilen.put(felder[0].strip(), felder); + } + } + return zeilen; + } +} |
