import { describe, expect, it } from 'vitest'; import { intergreenCsv } from '@/services/export/csv'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { intergreenKey } from '@/domain/plan/signalPlan'; import type { Project } from '@/domain/model/project'; /** * Die Zwischenzeitenmatrix als Tabellenausgabe fuehrt denselben Nachweis wie * der Ausdruck. * * BEFUND (Fassung 5.8.0): Die Kreuztabelle verlor zwei Unterscheidungen, die * `drawIntergreenMatrix` macht. * * 1. Eine von Hand vorgegebene Zwischenzeit traegt im Ausdruck einen Stern. * Hier stand dieselbe Zahl ohne Kennzeichnung - wer die Datei auswertete, * sah nicht, welche Werte die Berechnung ersetzt hatten. * 2. Die leere Zelle stand fuer ZWEI Sachverhalte: die Diagonale und ein * vertraegliches Paar. Im Ausdruck ist das erste leer, das zweite ein * Punkt. * * Das wiegt schwer, weil diese Ausgabe die maschinell verwertbare Fassung * derselben Tabelle ist, und weil die Oberflaeche Matrix und Rechenweg als * getrennte Schaltflaechen anbietet: Wer nur die Matrix laedt, haette den * Nachweis sonst nirgends. Sie ist zugleich die einzige maschinell lesbare * Quelle dafuer, WELCHE Paare vertraeglich sind - der Rechenweg fuehrt nur die * erfassten Beziehungen. * * GEPRUEFT WIRD BEIDES: dass der Nachweis da ist UND dass die Wertetabelle * darueber rein numerisch bleibt. Ein "7*" waere in einer Tabellenkalkulation * keine Zahl mehr, und in einer Spalte mit gemischten Zellen schluegen Summe, * Hoechstwert und Vergleich fehl. */ const STICHTAG = new Date('2026-01-01T00:00:00Z'); /** Die Ausgabe ohne Steuerzeichen, in Zeilen und Felder zerlegt. */ function tabelle(csv: string): string[][] { return csv .replace(/^/, '') .split('\r\n') .map((zeile) => zeile.split(';')); } /** Der Block ab der Zeile, die mit `titel` beginnt - bis zur naechsten Leerzeile. */ function block(zeilen: string[][], kopfIndex: number): string[][] { const raus: string[][] = []; for (let i = kopfIndex; i < zeilen.length; i += 1) { const z = zeilen[i]!; if (z.length <= 1 && (z[0] ?? '') === '') break; if (z.length <= 1) break; raus.push(z); } return raus; } function bloecke(csv: string): { werte: string[][]; herkunft: string[][]; alle: string[][] } { const zeilen = tabelle(csv); const titel = zeilen.findIndex((z) => (z[0] ?? '').startsWith('Herkunft der Zwischenzeit')); expect(titel, 'Titelzeile des Herkunftsblocks').toBeGreaterThan(0); return { werte: block(zeilen, 0), herkunft: block(zeilen, titel + 1), alle: zeilen }; } /** Standardknotenpunkt; `vorgabe` setzt an der ersten Konfliktbeziehung eine feste Zwischenzeit. */ function projekt(vorgabe: number | null = null): Project { const p = createStandardIntersectionProject('Prüfstück', STICHTAG); if (vorgabe === null) return p; const [erster, ...rest] = p.conflicts; expect(erster, 'Vorbedingung: die Vorlage hat eine Konfliktbeziehung').toBeDefined(); return { ...p, conflicts: [{ ...erster!, manualIntergreen: vorgabe }, ...rest] }; } describe('Zwischenzeitenmatrix als Tabellenausgabe', () => { it('weist eine von Hand vorgegebene Zwischenzeit als solche aus', () => { const p = projekt(9); const plan = buildSignalPlan(p); const konflikt = p.conflicts[0]!; const gruppen = p.signalGroups; const zeile = gruppen.findIndex((g) => g.id === konflikt.fromId); const spalte = gruppen.findIndex((g) => g.id === konflikt.toId); const { werte, herkunft } = bloecke(intergreenCsv(p, plan)); // Der Wert steht in der Wertetabelle - als blosse Zahl, ohne Stern. expect(werte[zeile + 1]![spalte + 1]).toBe('9'); // Und die Herkunft steht an derselben Stelle des zweiten Blocks. expect(herkunft[zeile + 1]![spalte + 1]).toBe('vorgegeben'); /* * Gegenprobe: ohne Vorgabe ist dieselbe Zelle berechnet. Das Projekt wird * EINMAL erzeugt und fuer Plan und Ausgabe wiederverwendet - die Vorlage * vergibt je Aufruf neue Kennungen, und ein Plan aus einem zweiten Aufruf * fuende keine einzige Zwischenzeit wieder. */ const ohneVorgabe = projekt(); const ohne = bloecke(intergreenCsv(ohneVorgabe, buildSignalPlan(ohneVorgabe))); expect(ohne.herkunft[zeile + 1]![spalte + 1]).toBe('berechnet'); }); it('unterscheidet die Diagonale von einem vertraeglichen Paar', () => { const p = projekt(); const plan = buildSignalPlan(p); const gruppen = p.signalGroups; const { werte, herkunft } = bloecke(intergreenCsv(p, plan)); // Ein vertraegliches Paar suchen: keine Zwischenzeit hinterlegt, i != j. let paar: [number, number] | null = null; for (let i = 0; i < gruppen.length && !paar; i += 1) { for (let j = 0; j < gruppen.length; j += 1) { if (i === j) continue; if (!plan.intergreens.get(intergreenKey(gruppen[i]!.id, gruppen[j]!.id))) { paar = [i, j]; break; } } } expect(paar, 'Vorbedingung: die Vorlage hat ein vertraegliches Paar').not.toBeNull(); const [vi, vj] = paar!; // In der Wertetabelle sehen beide gleich aus - dort steht keine Zahl. expect(werte[vi + 1]![vj + 1]).toBe(''); expect(werte[vi + 1]![vi + 1]).toBe(''); // Die Herkunft trennt sie: benannt gegen leer. expect(herkunft[vi + 1]![vj + 1]).toBe('verträglich'); expect(herkunft[vi + 1]![vi + 1]).toBe(''); }); it('haelt die Wertetabelle rein numerisch', () => { const p = projekt(9); const { werte } = bloecke(intergreenCsv(p, buildSignalPlan(p))); for (let z = 1; z < werte.length; z += 1) { for (let s = 1; s < werte[z]!.length; s += 1) { const zelle = werte[z]![s]!; if (zelle === '') continue; expect(zelle, `Zelle ${z}/${s} ist keine Zahl`).toMatch(/^\d+(,\d+)?$/); } } }); it('legt beide Bloecke Zelle auf Zelle - gleicher Kopf, gleiche Zeilenfolge', () => { const p = projekt(); const { werte, herkunft } = bloecke(intergreenCsv(p, buildSignalPlan(p))); expect(herkunft[0]).toEqual(werte[0]); expect(herkunft.length).toBe(werte.length); for (let z = 1; z < werte.length; z += 1) { expect(herkunft[z]![0], `Zeilenschluessel ${z}`).toBe(werte[z]![0]); expect(herkunft[z]!.length).toBe(werte[z]!.length); } }); it('nennt fuer jedes Paar genau einen der drei Sachverhalte', () => { const p = projekt(9); const { herkunft } = bloecke(intergreenCsv(p, buildSignalPlan(p))); const zulaessig = new Set(['berechnet', 'vorgegeben', 'verträglich']); let gezaehlt = 0; for (let z = 1; z < herkunft.length; z += 1) { for (let s = 1; s < herkunft[z]!.length; s += 1) { const zelle = herkunft[z]![s]!; if (z === s) { expect(zelle, 'Diagonale').toBe(''); continue; } expect(zulaessig.has(zelle), `Zelle ${z}/${s}: "${zelle}"`).toBe(true); gezaehlt += 1; } } const n = p.signalGroups.length; expect(gezaehlt, 'jedes geordnete Paar genau einmal').toBe(n * (n - 1)); }); it('laesst die Vermerke die letzten Zeilen bleiben (Befund C7)', () => { const p = projekt(); const plan = buildSignalPlan(p); const zeilen = tabelle( intergreenCsv(p, plan, { errorCount: 0, warningCount: 2, hintCount: 0 } as never), ); const letzte = zeilen[zeilen.length - 1]!.join(';'); expect(letzte).toContain('Hinweis:'); // Und der Herkunftsblock steht davor, nicht dahinter. const titel = zeilen.findIndex((z) => (z[0] ?? '').startsWith('Herkunft der Zwischenzeit')); expect(titel).toBeLessThan(zeilen.length - 1); }); });