lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ tests export csvZwischenzeitenmatrix.test.ts

7,6 KB Rohdatei
tests/export/csvZwischenzeitenmatrix.test.ts — 184 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { intergreenCsv } from '@/services/export/csv';
3 import { buildSignalPlan } from '@/domain/plan/signalPlan';
4 import { createStandardIntersectionProject } from '@/domain/model/factory';
5 import { intergreenKey } from '@/domain/plan/signalPlan';
6 import type { Project } from '@/domain/model/project';
7
8 /**
9 * Die Zwischenzeitenmatrix als Tabellenausgabe fuehrt denselben Nachweis wie
10 * der Ausdruck.
11 *
12 * BEFUND (Fassung 5.8.0): Die Kreuztabelle verlor zwei Unterscheidungen, die
13 * `drawIntergreenMatrix` macht.
14 *
15 * 1. Eine von Hand vorgegebene Zwischenzeit traegt im Ausdruck einen Stern.
16 * Hier stand dieselbe Zahl ohne Kennzeichnung - wer die Datei auswertete,
17 * sah nicht, welche Werte die Berechnung ersetzt hatten.
18 * 2. Die leere Zelle stand fuer ZWEI Sachverhalte: die Diagonale und ein
19 * vertraegliches Paar. Im Ausdruck ist das erste leer, das zweite ein
20 * Punkt.
21 *
22 * Das wiegt schwer, weil diese Ausgabe die maschinell verwertbare Fassung
23 * derselben Tabelle ist, und weil die Oberflaeche Matrix und Rechenweg als
24 * getrennte Schaltflaechen anbietet: Wer nur die Matrix laedt, haette den
25 * Nachweis sonst nirgends. Sie ist zugleich die einzige maschinell lesbare
26 * Quelle dafuer, WELCHE Paare vertraeglich sind - der Rechenweg fuehrt nur die
27 * erfassten Beziehungen.
28 *
29 * GEPRUEFT WIRD BEIDES: dass der Nachweis da ist UND dass die Wertetabelle
30 * darueber rein numerisch bleibt. Ein "7*" waere in einer Tabellenkalkulation
31 * keine Zahl mehr, und in einer Spalte mit gemischten Zellen schluegen Summe,
32 * Hoechstwert und Vergleich fehl.
33 */
34
35 const STICHTAG = new Date('2026-01-01T00:00:00Z');
36
37 /** Die Ausgabe ohne Steuerzeichen, in Zeilen und Felder zerlegt. */
38 function tabelle(csv: string): string[][] {
39 return csv
40 .replace(/^/, '')
41 .split('\r\n')
42 .map((zeile) => zeile.split(';'));
43 }
44
45 /** Der Block ab der Zeile, die mit `titel` beginnt - bis zur naechsten Leerzeile. */
46 function block(zeilen: string[][], kopfIndex: number): string[][] {
47 const raus: string[][] = [];
48 for (let i = kopfIndex; i < zeilen.length; i += 1) {
49 const z = zeilen[i]!;
50 if (z.length <= 1 && (z[0] ?? '') === '') break;
51 if (z.length <= 1) break;
52 raus.push(z);
53 }
54 return raus;
55 }
56
57 function bloecke(csv: string): { werte: string[][]; herkunft: string[][]; alle: string[][] } {
58 const zeilen = tabelle(csv);
59 const titel = zeilen.findIndex((z) => (z[0] ?? '').startsWith('Herkunft der Zwischenzeit'));
60 expect(titel, 'Titelzeile des Herkunftsblocks').toBeGreaterThan(0);
61 return { werte: block(zeilen, 0), herkunft: block(zeilen, titel + 1), alle: zeilen };
62 }
63
64 /** Standardknotenpunkt; `vorgabe` setzt an der ersten Konfliktbeziehung eine feste Zwischenzeit. */
65 function projekt(vorgabe: number | null = null): Project {
66 const p = createStandardIntersectionProject('Prüfstück', STICHTAG);
67 if (vorgabe === null) return p;
68 const [erster, ...rest] = p.conflicts;
69 expect(erster, 'Vorbedingung: die Vorlage hat eine Konfliktbeziehung').toBeDefined();
70 return { ...p, conflicts: [{ ...erster!, manualIntergreen: vorgabe }, ...rest] };
71 }
72
73 describe('Zwischenzeitenmatrix als Tabellenausgabe', () => {
74 it('weist eine von Hand vorgegebene Zwischenzeit als solche aus', () => {
75 const p = projekt(9);
76 const plan = buildSignalPlan(p);
77 const konflikt = p.conflicts[0]!;
78 const gruppen = p.signalGroups;
79 const zeile = gruppen.findIndex((g) => g.id === konflikt.fromId);
80 const spalte = gruppen.findIndex((g) => g.id === konflikt.toId);
81
82 const { werte, herkunft } = bloecke(intergreenCsv(p, plan));
83
84 // Der Wert steht in der Wertetabelle - als blosse Zahl, ohne Stern.
85 expect(werte[zeile + 1]![spalte + 1]).toBe('9');
86 // Und die Herkunft steht an derselben Stelle des zweiten Blocks.
87 expect(herkunft[zeile + 1]![spalte + 1]).toBe('vorgegeben');
88
89 /*
90 * Gegenprobe: ohne Vorgabe ist dieselbe Zelle berechnet. Das Projekt wird
91 * EINMAL erzeugt und fuer Plan und Ausgabe wiederverwendet - die Vorlage
92 * vergibt je Aufruf neue Kennungen, und ein Plan aus einem zweiten Aufruf
93 * fuende keine einzige Zwischenzeit wieder.
94 */
95 const ohneVorgabe = projekt();
96 const ohne = bloecke(intergreenCsv(ohneVorgabe, buildSignalPlan(ohneVorgabe)));
97 expect(ohne.herkunft[zeile + 1]![spalte + 1]).toBe('berechnet');
98 });
99
100 it('unterscheidet die Diagonale von einem vertraeglichen Paar', () => {
101 const p = projekt();
102 const plan = buildSignalPlan(p);
103 const gruppen = p.signalGroups;
104 const { werte, herkunft } = bloecke(intergreenCsv(p, plan));
105
106 // Ein vertraegliches Paar suchen: keine Zwischenzeit hinterlegt, i != j.
107 let paar: [number, number] | null = null;
108 for (let i = 0; i < gruppen.length && !paar; i += 1) {
109 for (let j = 0; j < gruppen.length; j += 1) {
110 if (i === j) continue;
111 if (!plan.intergreens.get(intergreenKey(gruppen[i]!.id, gruppen[j]!.id))) {
112 paar = [i, j];
113 break;
114 }
115 }
116 }
117 expect(paar, 'Vorbedingung: die Vorlage hat ein vertraegliches Paar').not.toBeNull();
118 const [vi, vj] = paar!;
119
120 // In der Wertetabelle sehen beide gleich aus - dort steht keine Zahl.
121 expect(werte[vi + 1]![vj + 1]).toBe('');
122 expect(werte[vi + 1]![vi + 1]).toBe('');
123
124 // Die Herkunft trennt sie: benannt gegen leer.
125 expect(herkunft[vi + 1]![vj + 1]).toBe('verträglich');
126 expect(herkunft[vi + 1]![vi + 1]).toBe('');
127 });
128
129 it('haelt die Wertetabelle rein numerisch', () => {
130 const p = projekt(9);
131 const { werte } = bloecke(intergreenCsv(p, buildSignalPlan(p)));
132 for (let z = 1; z < werte.length; z += 1) {
133 for (let s = 1; s < werte[z]!.length; s += 1) {
134 const zelle = werte[z]![s]!;
135 if (zelle === '') continue;
136 expect(zelle, `Zelle ${z}/${s} ist keine Zahl`).toMatch(/^\d+(,\d+)?$/);
137 }
138 }
139 });
140
141 it('legt beide Bloecke Zelle auf Zelle - gleicher Kopf, gleiche Zeilenfolge', () => {
142 const p = projekt();
143 const { werte, herkunft } = bloecke(intergreenCsv(p, buildSignalPlan(p)));
144 expect(herkunft[0]).toEqual(werte[0]);
145 expect(herkunft.length).toBe(werte.length);
146 for (let z = 1; z < werte.length; z += 1) {
147 expect(herkunft[z]![0], `Zeilenschluessel ${z}`).toBe(werte[z]![0]);
148 expect(herkunft[z]!.length).toBe(werte[z]!.length);
149 }
150 });
151
152 it('nennt fuer jedes Paar genau einen der drei Sachverhalte', () => {
153 const p = projekt(9);
154 const { herkunft } = bloecke(intergreenCsv(p, buildSignalPlan(p)));
155 const zulaessig = new Set(['berechnet', 'vorgegeben', 'verträglich']);
156 let gezaehlt = 0;
157 for (let z = 1; z < herkunft.length; z += 1) {
158 for (let s = 1; s < herkunft[z]!.length; s += 1) {
159 const zelle = herkunft[z]![s]!;
160 if (z === s) {
161 expect(zelle, 'Diagonale').toBe('');
162 continue;
163 }
164 expect(zulaessig.has(zelle), `Zelle ${z}/${s}: "${zelle}"`).toBe(true);
165 gezaehlt += 1;
166 }
167 }
168 const n = p.signalGroups.length;
169 expect(gezaehlt, 'jedes geordnete Paar genau einmal').toBe(n * (n - 1));
170 });
171
172 it('laesst die Vermerke die letzten Zeilen bleiben (Befund C7)', () => {
173 const p = projekt();
174 const plan = buildSignalPlan(p);
175 const zeilen = tabelle(
176 intergreenCsv(p, plan, { errorCount: 0, warningCount: 2, hintCount: 0 } as never),
177 );
178 const letzte = zeilen[zeilen.length - 1]!.join(';');
179 expect(letzte).toContain('Hinweis:');
180 // Und der Herkunftsblock steht davor, nicht dahinter.
181 const titel = zeilen.findIndex((z) => (z[0] ?? '').startsWith('Herkunft der Zwischenzeit'));
182 expect(titel).toBeLessThan(zeilen.length - 1);
183 });
184 });