import { describe, expect, it } from 'vitest'; import { computeCycleTime } from '@/domain/rilsa/cycle'; import { RILSA_DEFAULTS } from '@/domain/rilsa/constants'; /** * Die Rasterung entscheidet nicht mehr ueber eine Beanstandung. * * Vor Fassung 5.13.0 prueft `computeCycleTime` den GERASTERTEN Wert * gegen den Hoechstwert. Damit entschied die Rasterung - ein in der * Vorgabenverwaltung von 1 bis 10 s frei einstellbarer Praxiswert ohne * Regelwerksbezug - darueber, ob der Pruefbericht "Die Anlage ist mit dieser * Phaseneinteilung nicht leistungsfaehig" fuehrt. Gemessen mit demselben * rechnerischen Wert 119,48 s: bei den Rasterungen 1 bis 6, 8 und 10 s die * Umlaufzeit 120 s ohne Beanstandung, bei 7 und 9 s dieselben 120 s mit dem * Fehler 'umlaufzeit-ueber-hoechstwert'. * * Beurteilt wird jetzt der ERFORDERLICHE Umlauf vor der Rasterung: das Maximum * aus dem rechnerischen Wert, der Mindestumlaufzeit aus Mindestfreigabe- und * Zwischenzeiten und der kleinsten zulaessigen Umlaufzeit. * * GEGEN DEN ALTSTAND (gemessen: `cycle.ts` und `signalPlan.ts` auf den Stand * vor Fassung 5.13.0 zurueckgespielt) sind fuenf der elf Faelle rot: die * Messreihe ueber alle zehn Rasterungen, der Regelbereichshinweis, die * Meldung mit dem erforderlichen statt dem gerasterten Wert und die beiden * Faelle zum dichten Zahlenpaar. Die uebrigen sechs sind Gegenproben und * vorher wie nachher gruen - darunter ein erforderlicher Umlauf ueber dem * Hoechstwert, eine Mindestumlaufzeit ueber dem Hoechstwert und der * uebersaettigte Knoten; sie halten fest, was sich NICHT aendern darf. */ const HOECHSTWERT = RILSA_DEFAULTS.cycleTime.max; const RASTERUNGEN = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as const; /** * Eingang, dessen rechnerischer Wert nach Webster genau `raw` betraegt: * tU = (1,5 * L + 5) / (1 - Y) nach L aufgeloest, mit Y = 0,5. */ function eingang(raw: number, step: number, minimumCycle = 30) { const Y = 0.5; return { lostTime: (raw * (1 - Y) - 5) / 1.5, criticalFlowRatios: [Y], minimumCycle, step, }; } function codes(raw: number, step: number, minimumCycle?: number): string[] { return computeCycleTime('webster', eingang(raw, step, minimumCycle)).notes.map((n) => n.code); } describe('Die Rasterung der Umlaufzeit entscheidet nicht ueber die Beanstandung', () => { it('haelt der rechnerische Wert 119,48 s den Hoechstwert bei jeder Rasterung ein', () => { for (const step of RASTERUNGEN) { const ergebnis = computeCycleTime('webster', eingang(119.48, step)); expect(ergebnis.raw, `Rasterung ${step} s: rechnerischer Wert`).toBeCloseTo(119.48, 2); expect(ergebnis.cycleTime, `Rasterung ${step} s: Umlaufzeit`).toBe(HOECHSTWERT); expect(ergebnis.bounded, `Rasterung ${step} s: Zustand`).toBe('keine'); expect( ergebnis.notes.map((n) => n.code), `Rasterung ${step} s: Meldungen`, ).not.toContain('umlaufzeit-ueber-hoechstwert'); } }); it('steht bei jeder Rasterung derselbe Hinweis zum Regelbereich', () => { // Bis zur Aenderung gab es zwei Ausgaenge: 'keine' mit dem // Regelbereichshinweis (Rasterung 5 s) und 'maximum' mit dem Fehler // (Rasterung 7 s) - beide bei derselben Umlaufzeit von 120 s. for (const step of RASTERUNGEN) { expect(codes(119.48, step), `Rasterung ${step} s`).toEqual([ 'umlaufzeit-ausserhalb-regelbereich', ]); } }); it('meldet ein erforderlicher Umlauf ueber dem Hoechstwert bei jeder Rasterung', () => { // Gegenprobe: 121 s sind wirklich mehr als 120 s. Hier darf nichts // verschwinden - und die Umlaufzeit bleibt der gekappte Hoechstwert. for (const step of RASTERUNGEN) { const ergebnis = computeCycleTime('webster', eingang(121, step)); expect( ergebnis.notes.map((n) => n.code), `Rasterung ${step} s`, ).toContain('umlaufzeit-ueber-hoechstwert'); expect(ergebnis.bounded, `Rasterung ${step} s`).toBe('maximum'); expect(ergebnis.cycleTime, `Rasterung ${step} s`).toBe(HOECHSTWERT); } }); it('bleibt die Meldung, wenn die Mindestumlaufzeit ueber dem Hoechstwert liegt', () => { /* * Die wichtigste Gegenprobe: `minimumCycle` folgt zwingend aus * Mindestfreigabe- und Zwischenzeiten. Liegt SIE ueber dem Hoechstwert, * ist die Anlage nicht schaltbar - der rechnerische Wert allein (hier * 60 s) sagt davon nichts. Waere nur `raw` beurteilt worden, fiele diese * Meldung fort. */ for (const step of RASTERUNGEN) { const ergebnis = computeCycleTime('webster', eingang(60, step, 125)); expect( ergebnis.notes.map((n) => n.code), `Rasterung ${step} s`, ).toContain('umlaufzeit-ueber-hoechstwert'); expect(ergebnis.bounded, `Rasterung ${step} s`).toBe('maximum'); } }); it('bleibt eine Mindestumlaufzeit dicht unter dem Hoechstwert unbeanstandet', () => { // 119 s sind schaltbar. Bei Rasterung 7 s trifft die Aufrundung sie genau, // bei 5 s hebt sie auf 120 s - beides ohne Beanstandung. expect(codes(60, 7, 119)).toEqual(['mindestumlauf-massgebend']); expect(codes(60, 5, 119)).toEqual(['mindestumlauf-massgebend']); expect(computeCycleTime('webster', eingang(60, 7, 119)).cycleTime).toBe(119); expect(computeCycleTime('webster', eingang(60, 5, 119)).cycleTime).toBe(120); }); it('nennt die Meldung den erforderlichen und nicht den gerasterten Wert', () => { /* * "Die erforderliche Umlaufzeit beträgt {value} s" stand bis zur Aenderung * mit dem GERASTERTEN Wert im Satz: bei 121 s Bedarf und Rasterung 7 s * las der Bearbeiter "126,0 s" - eine Zahl, die in keiner Rechnung * erforderlich ist. */ const meldung = computeCycleTime('webster', eingang(121, 7)).notes.find( (n) => n.code === 'umlaufzeit-ueber-hoechstwert', ); expect(meldung, 'Meldung zum Hoechstwert').toBeDefined(); expect(meldung!.message).toContain('Die erforderliche Umlaufzeit beträgt 121,0 s'); expect(meldung!.message, 'der gerasterte Wert gehoert nicht in diesen Satz').not.toContain( '126', ); }); it('nennt die Meldung die Mindestumlaufzeit, wo diese massgebend ist', () => { const meldung = computeCycleTime('webster', eingang(60, 5, 125)).notes.find( (n) => n.code === 'umlaufzeit-ueber-hoechstwert', ); expect(meldung!.message).toContain('Die erforderliche Umlaufzeit beträgt 125,0 s'); }); it('schreibt die Meldung die beiden Zahlen unterscheidbar, wenn sie dicht beieinanderliegen', () => { /* * Mit dem gerasterten Wert konnte das nicht vorkommen - er lag stets * mindestens eine Rasterstufe ueber dem Hoechstwert. Der erforderliche * Umlauf kann ihn um Hundertstelsekunden ueberschreiten; mit einer * Nachkommastelle stuende dann "beträgt 120,0 s und überschreitet den * Höchstwert von 120,0 s". */ const meldung = computeCycleTime('webster', eingang(120.01, 7)).notes.find( (n) => n.code === 'umlaufzeit-ueber-hoechstwert', ); expect(meldung, 'auch 0,01 s ueber dem Hoechstwert werden gemeldet').toBeDefined(); expect(meldung!.message).toContain('beträgt 120,01 s'); expect(meldung!.message).toContain('Höchstwert von 120,00 s'); }); it('sagt sie "mehr als", wo auch drei Stellen den Unterschied nicht mehr zeigen', () => { /* * Der Rueckfall des Helfers `unterscheidbar`. Er ist erreichbar, weil der * erforderliche Umlauf aus dem rechnerischen Wert stammt und stetig ist: * 120,0001 s runden auf drei Stellen wie der Hoechstwert selbst. Statt * eine Zahl zu erfinden, die dann beide Male gleich lautete, sagt der Satz * "mehr als". Die Zusage steht im Kopfkommentar des Helfers; ohne diesen * Fall bewacht sie nichts. */ const meldung = computeCycleTime('webster', eingang(120.0001, 7)).notes.find( (n) => n.code === 'umlaufzeit-ueber-hoechstwert', ); expect(meldung, 'auch ein Zehntausendstel ueber dem Hoechstwert wird gemeldet').toBeDefined(); expect(meldung!.message).toContain('beträgt mehr als 120,0 s'); expect(meldung!.message).toContain('Höchstwert von 120,0 s'); expect(meldung!.message, 'keine erfundene Zahl').not.toContain('beträgt 120,0 s'); }); it('geht der Hoechstwert der Rasterung vor - die ausgegebene Umlaufzeit steht dann nicht im Raster', () => { /* * Die Rasterung ist keine harte Nebenbedingung des Ergebnisses: Die * Kappung bricht sie, und zwar auch schon vor dieser Aenderung (120 ist * kein Vielfaches von 7). Genau deshalb darf sie der Beurteilung nicht * vorangehen. */ const ergebnis = computeCycleTime('webster', eingang(119.48, 7)); expect(ergebnis.cycleTime).toBe(120); expect(ergebnis.cycleTime % 7, '120 ist kein Vielfaches von 7').not.toBe(0); }); it('bleibt der Zustand "uebersaettigt" unberuehrt', () => { // Gegenprobe an der anderen Ausnahme: Y >= 1 kehrt vor der Rasterung // zurueck und traegt weiterhin den eigenen Zustand. const ergebnis = computeCycleTime('webster', { lostTime: 20, criticalFlowRatios: [0.6, 0.5], step: 7, }); expect(ergebnis.bounded).toBe('uebersaettigt'); expect(ergebnis.notes.map((n) => n.code)).toContain('uebersaettigt'); }); });