import { describe, expect, it } from 'vitest'; import { schwerverkehrSatz } from '@/services/export/bewertung'; import { signalGroupsCsv } from '@/services/export/csv'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { saturationFlow } from '@/domain/rilsa/capacity'; import { RILSA_DEFAULTS } from '@/domain/rilsa/constants'; import { resolveRilsaSettings } from '@/domain/rilsa/settings'; import type { Project } from '@/domain/model/project'; import * as fmt from '@/ui/format'; /* * Befund 34 zur Fassung 5.9.0. * * Der Erlaeuterungsabsatz zum Schwerverkehr beschrieb die Kette als * tB = fSV * tB0 und qS = 3600 / tB und behauptete daneben, bei reinem * Pkw-Verkehr stuenden in der Spalte "Saettigungsverkehrsstaerke" die 2000 Fz/h * je Fahrstreifen. Die Spalte fuehrt aber saturationFlow(movement, lanes, * defaults) / fSV, also zusaetzlich die Abminderung fuer abbiegende Stroeme * (capacity.ts: qS0 * n, bei 'links' und 'rechts' mal turningFactor). Bei einer * einstreifigen abbiegenden Kfz-Gruppe ohne Schwerverkehr stehen dort 1800 Fz/h * - der Absatz sagte eine Zahl zu, die die Tabelle daneben widerlegt, und * schloss zugleich mit dem Anspruch, an dem er scheiterte ("ohne ihn liesse * sich die Saettigungsverkehrsstaerke aus dem Regelwert je Fahrstreifen nicht * nachvollziehen"). * * Keine Zahl der Rechnung ist betroffen; die Abminderung liegt auf der sicheren * Seite. Falsch war der Nachweis in der Unterlage, die zur Behoerde geht. * * NACHGEBESSERT: Die berichtigte Kette fuehrte das Zeichen qS zweimal - einmal * als 3600/tB (je Fahrstreifen, einschliesslich fSV), einmal als Wert des * ganzen Stroms. Der Grundwert je Fahrstreifen heisst jetzt qS0. * * MUTATIONSNACHWEIS (gemessen an einer Pruefkopie mit den HEAD-Fassungen von * bewertung.ts und csv.ts): Gegen den Altstand fallen die vier Satzfaelle; die * beiden Belegzahlfaelle halten den Ausloeser fest - die 1.800 Fz/h in Rechnung * und Tabellenzelle - und sind in beiden Staenden gruen. 4 von 6. */ const STICHTAG = new Date('2026-01-01T12:00:00Z'); const QS0 = RILSA_DEFAULTS.capacity.saturationFlow; const FA = RILSA_DEFAULTS.capacity.turningFactor; /** Knotenpunkt mit einstreifiger, links abbiegender erster Kfz-Gruppe ohne Schwerverkehr. */ function abbiegerOhneSchwerverkehr(): Project { const basis = createStandardIntersectionProject('Sättigungskette', STICHTAG); return { ...basis, signalGroups: basis.signalGroups.map((g, index) => index === 0 ? { ...g, movement: 'links' as const, lanes: 1 } : g, ), demands: basis.demands.map((d, index) => (index === 0 ? { ...d, heavyVehicleShare: 0 } : d)), }; } describe('Befund 34 - der Leistungsabsatz nennt die vollständige Kette der Spalte', () => { it('rechnet die Belegzahl vor: ein einstreifiger Abbieger steht mit 1800 Fz/h in der Spalte', () => { const projekt = abbiegerOhneSchwerverkehr(); const plan = buildSignalPlan(projekt); const gruppe = plan.groups.find((g) => g.groupId === projekt.signalGroups[0]!.id)!; expect(saturationFlow('links', 1, plan.defaults)).toBeCloseTo(QS0 * FA, 9); expect(gruppe.capacity?.saturationFlow).toBeCloseTo(QS0 * FA, 6); expect(fmt.vehiclesPerHour(gruppe.capacity!.saturationFlow)).toBe('1.800 Fz/h'); }); it('behauptet nicht mehr, in der Spalte stünde der Regelwert je Fahrstreifen', () => { const satz = schwerverkehrSatz(); expect(satz).not.toContain('je Fahrstreifen der Spalte'); expect(satz).not.toContain('aus dem Regelwert je Fahrstreifen nicht nachvollziehen'); }); it('nennt Fahrstreifenzahl und Abminderung in derselben Kette', () => { const satz = schwerverkehrSatz(); expect(satz).toContain(`qS0 = ${fmt.vehiclesPerHour(QS0)} je Fahrstreifen`); expect(satz).toContain('qS0 · n · fA / fSV'); expect(satz).toContain('Abminderung für abbiegende Ströme'); expect(satz).toContain(`fA = ${fmt.numShort(FA, 2)}`); expect(satz).toContain('fA = 1'); // Und die Zahl, die daneben in der Tabelle steht. expect(satz).toContain(fmt.vehiclesPerHour(QS0 * FA)); }); it('führt qS nicht zugleich für den Wert des ganzen Stroms', () => { /* * Der Absatz nennt zwei Groessen: qS = 3600 / tB ist der Wert JE * FAHRSTREIFEN einschliesslich fSV, die Spalte der ganze Strom. Stuenden * beide unter dem Zeichen qS, waere die Kette nur fuer n = 1 und fA = 1 * richtig, und wer die erste Gleichung in die zweite einsetzt, teilt ein * zweites Mal durch fSV - in einem Absatz, dessen Zweck die * Nachrechenbarkeit ist. */ const satz = schwerverkehrSatz(); // Der Wert je Fahrstreifen - aus dem Zeitbedarfswert. expect(satz).toContain('tB = fSV · 1,8 s und qS = 3600 / tB'); // Der Wert des ganzen Stroms - eigene Größe, eigenes Zeichen. expect(satz).toContain('nennt den Wert des ganzen Stroms'); expect(satz).not.toContain(`qS = ${fmt.vehiclesPerHour(QS0)}`); expect(satz).not.toContain('qS = qS0'); }); it('nimmt die Abminderung aus den wirksamen Vorgaben, nicht aus dem Satz', () => { const { defaults } = resolveRilsaSettings({ turningFactor: 0.8, saturationFlow: 1900 }); const satz = schwerverkehrSatz(defaults); expect(satz).toContain('fA = 0,8'); expect(satz).toContain(`qS0 = ${fmt.vehiclesPerHour(1900)} je Fahrstreifen`); expect(satz).toContain('qS0 · n · fA / fSV'); expect(satz).toContain(fmt.vehiclesPerHour(1900 * 0.8)); expect(satz).not.toContain('fA = 0,9'); }); it('Belegzahl der Tabellenausgabe: dieselbe Spalte trägt 1.800 Fz/h', () => { const projekt = abbiegerOhneSchwerverkehr(); const csv = signalGroupsCsv(projekt, buildSignalPlan(projekt)); const zerlege = (zeile: string): string[] => (zeile.match(/("([^"]|"")*"|[^;]*)(;|$)/g) ?? []).map((feld) => feld.replace(/;$/, '').replace(/^"|"$/g, '').replace(/""/g, '"'), ); const alle = csv.split('\r\n'); const kopf = zerlege((alle[0] ?? '').replace(/^/, '')); const spalte = kopf.indexOf('Sättigungsverkehrsstärke [Fz/h]'); const zeile = zerlege(alle.find((z) => z.startsWith(`${projekt.signalGroups[0]!.name};`))!); expect(spalte).toBeGreaterThanOrEqual(0); // Die Zelle traegt die Abminderung; der Kommentar ueber der Spalte fSV // sagte, mit fSV allein liesse sie sich nachrechnen. expect(zeile[spalte]).toBe('1.800'); }); });