/** * Was ginge beim Ersetzen verloren? – reine Rechnerei, ohne Electron. * * ## Warum es diese Auszählung gibt * * Das Ersetzen legt eine Sicherheitskopie an und lässt sich zurückrollen – * aber nur, solange jemand die Kopie findet und weiss, dass es sie gibt. Die * bessere Sicherung ist, vorher zu wissen, was man aufgibt. * * Bisher stellte die Rückfrage nur zwei Gesamtzahlen nebeneinander. Das * genügt nicht: Wer hier zwei Profile mit zusammen 1.000 Antworten hat und * eine Datei mit einem Profil und 1.200 Antworten einliest, liest „mehr“ – * und verliert trotzdem eines seiner beiden Profile vollständig. * * ## Was hier NICHT behauptet wird * * Dass ein Befund ohne Fund gleichbedeutend mit „verlustfrei“ wäre. Verglichen * werden Profile über ihre **Namen** und je Profil vier Zahlen. Nicht * verglichen werden – und das steht auch so in der Anzeige: * * - **Prüfungstermin und Kapitelabwahl.** Beide werden mitersetzt. Die * Kapitelabwahl verschiebt den Nenner des Reifegrads: Nachgerechnet fällt * derselbe Bestand von 85,0 auf 71,9 Prozent, sobald Kapitel IV wieder * mitzählt – über zwei Ampelschwellen, ohne dass eine Antwort verloren ging. * - **Welche** Antworten es sind. Zwei Dateien können dieselbe Zahl tragen * und verschiedene Fragen meinen. Eine Zeile im Protokoll lässt sich über * Geräte hinweg nicht zuverlässig wiedererkennen; das ist derselbe Grund, * aus dem das Verschmelzen zweier Lernstände abgelehnt wurde * (`docs/entscheidung-lernstand-verschmelzen.md`). * - Der unterbrochene Prüfungsbogen. Er wird gesondert genannt, weil er sich * nicht in einer Zahl ausdrücken lässt. */ import type { Kennzahlen, Profilkennzahlen } from './sicherung'; /** Ein Profil, das es hier gibt und in der Datei nicht. */ export interface FehlendesProfil { readonly name: string; readonly antworten: number; } /** Ein Profil, das es in beiden gibt, hier aber mit mehr Inhalt. */ export interface GeschrumpftesProfil { readonly name: string; /** Antworten hier minus Antworten in der Datei; immer grösser als 0. */ readonly antwortenWeniger: number; readonly gemerktWeniger: number; readonly laeufeWeniger: number; } export interface Verlustbefund { /** Profile, die die Datei überhaupt nicht kennt. */ readonly fehlendeProfile: readonly FehlendesProfil[]; /** Profile, die in der Datei weniger enthalten als hier. */ readonly geschrumpfteProfile: readonly GeschrumpftesProfil[]; /** Ein unterbrochener Bogen läge hier und ginge verloren. */ readonly offenerBogenGehtVerloren: boolean; /** Nichts von dem, was gezählt wurde, fehlt in der Datei. */ readonly ohneBefund: boolean; } function nachNamen(zahlen: readonly Profilkennzahlen[]): ReadonlyMap { return new Map(zahlen.map((p) => [p.name, p])); } /** * Vergleicht den jetzigen Stand mit dem aus der Datei. * * Der Name ist der einzige Anker. Die Nummern werden auf jedem Gerät * unabhängig vergeben und sagen über die Zugehörigkeit nichts – genau * deshalb wechselt die Oberfläche nach dem Ersetzen zwingend ins erste * Profil der eingespielten Datei (siehe `Einspielergebnis.ersteProfilId`). */ export function verlustbefund(jetzt: Kennzahlen, ausDatei: Kennzahlen): Verlustbefund { const inDatei = nachNamen(ausDatei.jeProfil); const fehlendeProfile: FehlendesProfil[] = []; const geschrumpfteProfile: GeschrumpftesProfil[] = []; for (const hier of jetzt.jeProfil) { const dort = inDatei.get(hier.name); if (dort === undefined) { fehlendeProfile.push({ name: hier.name, antworten: hier.antworten }); continue; } const antwortenWeniger = Math.max(0, hier.antworten - dort.antworten); const gemerktWeniger = Math.max(0, hier.gemerkt - dort.gemerkt); const laeufeWeniger = Math.max(0, hier.pruefungslaeufe - dort.pruefungslaeufe); if (antwortenWeniger > 0 || gemerktWeniger > 0 || laeufeWeniger > 0) { geschrumpfteProfile.push({ name: hier.name, antwortenWeniger, gemerktWeniger, laeufeWeniger, }); } } return { fehlendeProfile, geschrumpfteProfile, offenerBogenGehtVerloren: jetzt.offenerBogen, ohneBefund: fehlendeProfile.length === 0 && geschrumpfteProfile.length === 0 && !jetzt.offenerBogen, }; } /** Ein Wort für „n Antworten“, im richtigen Numerus. */ function antwortwort(n: number): string { return n === 1 ? '1 Antwort' : `${String(n)} Antworten`; } /** * Der Befund als Sätze. * * Bewusst keine Gesamtzahl und keine Note: Was zählt, ist welches Profil * betroffen ist. Eine Summe über verschiedene Profile hinweg sagte nichts. */ export function verlustsaetze(befund: Verlustbefund): readonly string[] { const saetze: string[] = []; for (const p of befund.fehlendeProfile) { saetze.push( `Das Profil „${p.name}“ kommt in der Datei nicht vor. Es geht mit ${antwortwort(p.antworten)} verloren.`, ); } for (const p of befund.geschrumpfteProfile) { const teile: string[] = []; if (p.antwortenWeniger > 0) { teile.push(antwortwort(p.antwortenWeniger)); } if (p.gemerktWeniger > 0) { teile.push( `${String(p.gemerktWeniger)} gemerkte ${p.gemerktWeniger === 1 ? 'Frage' : 'Fragen'}`, ); } if (p.laeufeWeniger > 0) { teile.push( `${String(p.laeufeWeniger)} ${p.laeufeWeniger === 1 ? 'Prüfungslauf' : 'Prüfungsläufe'}`, ); } saetze.push( `Im Profil „${p.name}“ enthält die Datei ${teile.join(', ')} weniger als dieser Rechner.`, ); } if (befund.offenerBogenGehtVerloren) { saetze.push( 'Ein unterbrochener Prüfungsbogen liegt hier und lässt sich danach nicht fortsetzen.', ); } return saetze; } /** * Der Satz für den Fall ohne Befund. * * Er sagt ausdrücklich, **was** verglichen wurde – nicht „alles stimmt“. * Ein unbedingter Satz über einen Inhalt, der nicht vollständig geprüft * wurde, ist genau der Fehler, den `docs/stand.md` 7.6 zuletzt behoben hat. */ export const OHNE_BEFUND_SATZ = 'Verglichen wurden Profile, Antworten, gemerkte Fragen und Prüfungsläufe. ' + 'Davon fehlt in der Datei nichts. Nicht verglichen wurden Prüfungstermin und ' + 'Kapitelabwahl – beide werden mitersetzt, und die Kapitelabwahl verändert Ihren ' + 'angezeigten Lernstand, ohne dass eine Antwort verloren geht. ' + 'Ihre Einstellungen werden ebenfalls ersetzt, wenn die Datei welche mitbringt – Sicherungen aus Fassung 0.26.7 und davor tun das nicht.';