import { inflateSync } from 'node:zlib'; import { describe, expect, it } from 'vitest'; import { buildProjectPdf, DEFAULT_PDF_OPTIONS } from '@/services/export/pdf'; import { signalGroupsCsv } from '@/services/export/csv'; import { aufteilungslage, schwerverkehrSatz, schwerverkehrsangabe, } from '@/services/export/bewertung'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { validateProject } from '@/domain/validation'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { saturationFlow, schwerverkehrsfaktor } from '@/domain/rilsa/capacity'; import { HBS_LASTZUGANTEIL_BEI_UNBEKANNTER_AUFTEILUNG, HBS_PKW_GLEICHWERTE, RILSA_DEFAULTS, } from '@/domain/rilsa/constants'; import { toWinAnsi } from '@/render/pdfSurface'; import type { Project, TrafficDemand } from '@/domain/model/project'; import * as fmt from '@/ui/format'; /* * Was AUSDRUCK und TABELLENAUSGABE von der Aufteilung des Schwerverkehrs sagen * muessen. * * Jede Verkehrsstaerke kennt einen optionalen Lastzuganteil am * Schwerverkehr (`TrafficDemand.lastzugAnteil`), und der Plan rechnet danach * nach HBS 2015 Gl. 2-5 statt Gl. 2-6. Fuer die Ausgabe folgt daraus dreierlei: * * 1. Der gedruckte Faktor fSV muss MIT der Aufteilung gebildet werden. Zuvor * rief jede der beiden Ausgaben `schwerverkehrsfaktor(anteil)` selbst auf - * also ohne sie. Bei 20 % Schwerverkehr aus lauter Lastzuegen stand dann * fSV = 1,18 neben einer Saettigungsverkehrsstaerke von 1.538 Fz/h, die zu * 1,30 gehoert: zwei Spalten derselben Zeile, die einander widersprechen. * 2. Die Zeile muss sagen, WAS erfasst ist und nach welcher Gleichung * gerechnet wurde. Zwei Stroeme mit denselben 20 % tragen jetzt * verschiedene Faktoren, und ohne diese Angabe liesse sich die Unterlage * nicht nachrechnen. * 3. Der Erlaeuterungsabsatz darf nicht mehr behaupten, dieses Programm * erfasse keine Aufteilung. Die Fehlerrichtung des Pauschalwerts gilt nur * noch fuer Stroeme ohne erfasste Aufteilung. * * Die Zahlen dieses Tests sind die Belegzahlen der Umstellung (BASt V 400, * S. 10, Gl. 2-5/2-6) und bei einem Grundwert von 2000 Kfz/h je Fahrstreifen * und 20 % Schwerverkehr nachgerechnet: * * lauter Lastzuege fSV = 1 + 1,50 · 0,20 = 1,30 -> 1.538 Fz/h * keine Aufteilung fSV = 1 + 0,90 · 0,20 = 1,18 -> 1.695 Fz/h * keine Lastzuege fSV = 1 + 0,75 · 0,20 = 1,15 -> 1.739 Fz/h * 40 % Lastzuege fSV = 1 + 0,75·0,12 + 1,50·0,08 = 1,21 -> 1.653 Fz/h */ const STICHTAG = new Date('2026-01-01T12:00:00Z'); /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */ function pdfText(bytes: Uint8Array): string { const roh = new TextDecoder('latin1').decode(bytes); const teile: string[] = []; const suche = /stream\r?\n/g; let treffer: RegExpExecArray | null; while ((treffer = suche.exec(roh)) !== null) { const start = treffer.index + treffer[0].length; const ende = roh.indexOf('endstream', start); if (ende < 0) continue; let inhalt: string; try { inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1'); } catch { continue; } for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) { teile.push(t[1]!.replace(/\\([()\\])/g, '$1')); } } return teile.join(' ').replace(/\s+/g, ' '); } const gedruckt = (text: string): string => toWinAnsi(text).replace(/\s+/g, ' '); /** * Vier Kfz-Stroeme mit demselben Schwerverkehrsanteil von 20 % und den vier * Datenlagen, die es gibt: lauter Lastzuege, keine Angabe, gezaehlte 0 % und * ein Zwischenwert. Derselbe Anteil bei jedem - so ist jeder Unterschied in * der Ausgabe ein Unterschied der AUFTEILUNG und nichts anderes. */ function vierDatenlagen(): Project { const basis = createStandardIntersectionProject('Aufteilung', STICHTAG); const anteile: readonly (number | undefined)[] = [1, undefined, 0, 0.4]; return { ...basis, demands: basis.demands.map((d, index): TrafficDemand => { const lastzugAnteil = anteile[index]; const gemeinsam = { ...d, heavyVehicleShare: 0.2 }; // Kein `lastzugAnteil: undefined`: Das Feld fehlt, wo nichts erfasst ist // - genau das ist der Fall, fuer den Gl. 2-6 gilt. return lastzugAnteil === undefined ? gemeinsam : { ...gemeinsam, lastzugAnteil }; }), }; } /** Dasselbe Projekt, aber an JEDEM Strom eine erfasste Aufteilung. */ function ueberallErfasst(): Project { const basis = createStandardIntersectionProject('Aufteilung', STICHTAG); return { ...basis, demands: basis.demands.map((d) => ({ ...d, heavyVehicleShare: 0.2, lastzugAnteil: 0.4 })), }; } /** Dasselbe Projekt mit einem Lastzuganteil, den es nicht geben kann. */ function unbrauchbarerEintrag(): Project { const basis = createStandardIntersectionProject('Aufteilung', STICHTAG); return { ...basis, demands: basis.demands.map((d, index) => index === 0 ? { ...d, heavyVehicleShare: 0.2, lastzugAnteil: 1.5 } : d, ), }; } function ausdruck(project: Project): string { const plan = buildSignalPlan(project); const bericht = validateProject(project, plan, STICHTAG); return pdfText(buildProjectPdf(project, plan, bericht, DEFAULT_PDF_OPTIONS, STICHTAG)); } /** * Die Tabellenausgabe als Felder je Zeile. * * Mit Anfuehrungszeichen gelesen und nicht mit `split(';')`: Die Spalte * "Gleichung für fSV" nennt die Annahme hinter Gl. 2-6 und traegt dafuer ein * Semikolon, das die Ausgabe ordnungsgemaess in Anfuehrungszeichen setzt. Ein * naives Trennen zerschnitte genau die Zelle, um die es hier geht. */ function tabelle(project: Project): string[][] { const text = signalGroupsCsv(project, buildSignalPlan(project)).replace('', ''); const zeilen: string[][] = []; let zeile: string[] = []; let feld = ''; let inAnfuehrung = false; for (let i = 0; i < text.length; i += 1) { const z = text[i]!; if (inAnfuehrung) { if (z === '"') { if (text[i + 1] === '"') { feld += '"'; i += 1; } else inAnfuehrung = false; } else feld += z; continue; } if (z === '"') inAnfuehrung = true; else if (z === ';') { zeile.push(feld); feld = ''; } else if (z === '\r') continue; else if (z === '\n') { zeile.push(feld); zeilen.push(zeile); zeile = []; feld = ''; } else feld += z; } if (feld !== '' || zeile.length > 0) { zeile.push(feld); zeilen.push(zeile); } return zeilen; } const spalte = (zeilen: string[][], name: string): string[] => { const index = zeilen[0]!.indexOf(name); expect(index, `Spalte "${name}" muss es geben`).toBeGreaterThanOrEqual(0); return zeilen.slice(1).map((z) => z[index] ?? ''); }; // --- Die Rechnung selbst, als Grundlage der Erwartungen --------------------- describe('Belegzahlen der vier Datenlagen', () => { it('rechnet die vier Faktoren, auf die sich Ausdruck und Tabelle berufen', () => { const pSV = 0.2; // Das fehlende Feld und ein Feld mit dem Wert `undefined` sind hier nicht // dasselbe (exactOptionalPropertyTypes): "nicht erfasst" heisst, dass es // den Schluessel nicht gibt. const fsv = (lastzugAnteil?: number): number => schwerverkehrsangabe( lastzugAnteil === undefined ? { heavyVehicleShare: pSV } : { heavyVehicleShare: pSV, lastzugAnteil }, ).fsv; expect(fsv(1)).toBeCloseTo(1.3, 12); expect(fsv(undefined)).toBeCloseTo(1.18, 12); expect(fsv(0)).toBeCloseTo(1.15, 12); expect(fsv(0.4)).toBeCloseTo(1.21, 12); // Der Pauschalwert IST Gl. 2-5 mit 20 % Lastzuegen (V 400, S. 10) - die // Erfassung ist eine Verfeinerung derselben Gleichung, kein anderes // Verfahren. expect(fsv(HBS_LASTZUGANTEIL_BEI_UNBEKANNTER_AUFTEILUNG)).toBeCloseTo(fsv(undefined), 12); // Mutationsnachweis: Ohne die Aufteilung waeren alle vier gleich - dann // pruefte dieser Test nichts. expect(schwerverkehrsfaktor(pSV)).toBeCloseTo(1.18, 12); expect(fsv(1)).not.toBeCloseTo(schwerverkehrsfaktor(pSV), 6); expect(fsv(0)).not.toBeCloseTo(schwerverkehrsfaktor(pSV), 6); }); it('unterscheidet "0 % gezaehlt" von "nicht erfasst"', () => { // Die eine Angabe fuehrt auf Gl. 2-5, die andere auf Gl. 2-6, und beide // ergeben verschiedene Saettigungsverkehrsstaerken. Eine Ausgabe, die die // leere Zelle als 0 schreibt, macht aus einer fehlenden Zaehlung eine // guenstige Behauptung. const gezaehlt = schwerverkehrsangabe({ heavyVehicleShare: 0.2, lastzugAnteil: 0 }); const ohne = schwerverkehrsangabe({ heavyVehicleShare: 0.2 }); expect(gezaehlt.mitAufteilung).toBe(true); expect(gezaehlt.lastzuganteilProzent).toBe('0'); expect(ohne.mitAufteilung).toBe(false); expect(ohne.lastzuganteilProzent).toBe(''); expect(gezaehlt.fsv).not.toBeCloseTo(ohne.fsv, 6); }); it('verwirft einen unbrauchbaren Eintrag und sagt es - kein stiller Rueckfall', () => { const verworfen = schwerverkehrsangabe({ heavyVehicleShare: 0.2, lastzugAnteil: 1.5 }); expect(verworfen.mitAufteilung).toBe(false); expect(verworfen.gleichungKurz).toBe('Gl. 2-6'); expect(verworfen.fsv).toBeCloseTo(schwerverkehrsfaktor(0.2), 12); // Der Unterschied zum Fall "nichts eingetragen" steht in den Worten - der // Bearbeiter hat etwas eingetragen, und das darf die Unterlage nicht // verschweigen. expect(verworfen.datenlage).toContain('verworfen'); expect(verworfen.gleichungSpalte).toContain('unbrauchbar'); expect(schwerverkehrsangabe({ heavyVehicleShare: 0.2 }).datenlage).not.toContain('verworfen'); }); }); // --- Ausdruck: die Leistungstabelle ----------------------------------------- describe('Leistungstabelle nennt Anteil, Aufteilung und die Gleichung', () => { it('druckt je Strom den Anteil und - wo erfasst - den Lastzuganteil', () => { const text = ausdruck(vierDatenlagen()); expect(text).toContain(gedruckt('20 % Lastzüge 100 % des Schwerverkehrs fSV = 1,3 (Gl. 2-5)')); expect(text).toContain(gedruckt('20 % Aufteilung nicht erfasst fSV = 1,18 (Gl. 2-6)')); expect(text).toContain(gedruckt('20 % Lastzüge 0 % des Schwerverkehrs fSV = 1,15 (Gl. 2-5)')); expect(text).toContain(gedruckt('20 % Lastzüge 40 % des Schwerverkehrs fSV = 1,21 (Gl. 2-5)')); // Die Spaltenueberschrift sagt, was die drei Zeilen der Zelle sind. expect(text).toContain(gedruckt('Schwerverkehr (Anteil, Aufteilung, fSV)')); }); it('druckt einen Faktor, der zur Saettigungsverkehrsstaerke derselben Zeile gehoert', () => { /* * Der Befund, um den es geht: Der Plan rechnete mit Gl. 2-5, der Ausdruck * druckte den Pauschalwert. Die Gegenprobe ist qS · fSV = Grundwert je * Fahrstreifen - sie geht nur auf, wenn beide Spalten aus derselben * Datenlage stammen. */ const projekt = vierDatenlagen(); const plan = buildSignalPlan(projekt); const text = ausdruck(projekt); let geprueft = 0; for (const gruppe of plan.groups) { const nachfrage = projekt.demands.find((d) => d.signalGroupId === gruppe.groupId); const stammdaten = projekt.signalGroups.find((g) => g.id === gruppe.groupId); if (!nachfrage || !stammdaten || gruppe.capacity === null) continue; const angabe = schwerverkehrsangabe(nachfrage); const grundwert = saturationFlow(stammdaten.movement, stammdaten.lanes, plan.defaults); expect(gruppe.capacity.saturationFlow * angabe.fsv, gruppe.name).toBeCloseTo(grundwert, 6); // Beides steht gedruckt da: der Faktor und die Zahl, die zu ihm gehoert. expect(text, gruppe.name).toContain(gedruckt(`fSV = ${fmt.numShort(angabe.fsv, 3)}`)); expect(text, gruppe.name).toContain( gedruckt(fmt.vehiclesPerHour(gruppe.capacity.saturationFlow)), ); geprueft += 1; } expect(geprueft, 'Vorbedingung: es gibt Gruppen mit Kapazität').toBe(4); // Die vier Zahlen ausgeschrieben - und der Altstand daneben: Er haette // ueberall 1,18 gedruckt, auch neben 1.538 und 1.739 Fz/h. for (const zahl of ['1.538 Fz/h', '1.695 Fz/h', '1.739 Fz/h', '1.653 Fz/h']) { expect(text).toContain(gedruckt(zahl)); } expect(text).not.toContain(gedruckt('fSV = 1,18 (Gl. 2-5)')); }); it('nennt den verworfenen Eintrag als solchen', () => { const text = ausdruck(unbrauchbarerEintrag()); expect(text).toContain(gedruckt('20 % Aufteilung unbrauchbar, verworfen fSV = 1,18 (Gl. 2-6)')); // Ein Verwerfen, das die Unterlage als "nicht erfasst" ausgibt, waere // gegenueber dem Bearbeiter schlicht falsch. expect(text).not.toContain(gedruckt('20 % Aufteilung nicht erfasst fSV = 1,18')); }); }); // --- Ausdruck: der Rechenweg unter der Tabelle ------------------------------ describe('Rechenweg nennt beide Gleichungen und die Annahme dahinter', () => { it('nennt Gl. 2-5 und Gl. 2-6 und sagt, was ueber sie entscheidet', () => { const text = ausdruck(vierDatenlagen()); expect(text).toContain( gedruckt('fSV = (qLV + 1,75 · qLkw+Bus + 2,5 · qLkwK) / qKfz (Gl. 2-5)'), ); expect(text).toContain( gedruckt('fSV = (qLV + 1,9 · qSV) / qKfz, also fSV = 1 + 0,9 · pSV (Gl. 2-6)'), ); expect(text).toContain(gedruckt('entscheidet die Datenlage DES EINZELNEN STROMS')); // Die Behauptung des Altstands - sie ist seit P14 falsch. expect(text).not.toContain(gedruckt('erfasst keine Aufteilung des Schwerverkehrs')); }); it('rechnet die Annahme hinter dem Pauschalwert vor', () => { // V 400, S. 10: "Bei einem Anteil der Lkw mit Anhänger und Sattel-Kfz // (Fahrzeugklasse LkwK) am Schwerverkehr von 20 % ergibt sich der // Funktionsverlauf der Gleichung (2-6)." Nachgerechnet ohne Rundung. const text = ausdruck(vierDatenlagen()); expect(text).toContain(gedruckt('1,75 · 0,80 + 2,50 · 0,20 = 1,90')); expect(text).toContain(gedruckt('Gl. 2-6 ist damit Gl. 2-5 mit 20 % Lastzügen')); expect( HBS_PKW_GLEICHWERTE.lkwUndBus * (1 - HBS_LASTZUGANTEIL_BEI_UNBEKANNTER_AUFTEILUNG) + HBS_PKW_GLEICHWERTE.lkwMitAnhaenger * HBS_LASTZUGANTEIL_BEI_UNBEKANNTER_AUFTEILUNG, ).toBeCloseTo(HBS_PKW_GLEICHWERTE.schwerverkehrPauschal, 12); }); it('zaehlt, welcher Weg in DIESEM Plan gegriffen hat', () => { const text = ausdruck(vierDatenlagen()); expect(aufteilungslage(vierDatenlagen())).toEqual({ mitAufteilung: 3, ohneAufteilung: 1 }); expect(text).toContain( gedruckt('In diesem Plan rechnen 3 Ströme nach Gl. 2-5 und 1 Strom nach Gl. 2-6.'), ); }); it('nennt die Fehlerrichtung, solange ein Strom ohne Aufteilung dabei ist', () => { const text = ausdruck(vierDatenlagen()); /* * Anker ist der SCHLUSSSATZ des Absatzes und nicht die Ueberschrift * "FEHLERRICHTUNG, WO DIE AUFTEILUNG FEHLT": Dieselbe Ueberschrift steht * auch im Fundstellenverzeichnis (quellen.ts, 'hbs-kapazitaet') und wuerde * die Probe unten unbrauchbar machen. */ expect(text).toContain(gedruckt('Für Ströme MIT erfasster Aufteilung entfällt dieser Posten.')); expect(text).toContain(gedruckt('ZU GROSS')); // Beide Richtungen, beziffert aus derselben Funktion, mit der gerechnet // wird: 2000/1,180 = 1.695 statt 2000/1,300 = 1.538 Fz/h. expect(text).toContain(gedruckt('um 10,2 % (1.695 Fz/h statt 1.538 Fz/h)')); expect(text).toContain(gedruckt('bis zu 31,6 %')); expect(text).toContain( gedruckt('rechnet das Programm 2,5 % zu klein, also auf der sicheren Seite'), ); }); it('laesst den Vorbehalt weg, wo jeder Strom eine erfasste Aufteilung hat', () => { const text = ausdruck(ueberallErfasst()); expect(aufteilungslage(ueberallErfasst())).toEqual({ mitAufteilung: 4, ohneAufteilung: 0 }); expect(text).toContain(gedruckt('gerechnet wurde durchgehend nach Gl. 2-5')); expect(text).toContain(gedruckt('geht damit in keine Zahl dieses Plans ein')); // Mutationsnachweis: Genau diese Saetze stehen im Plan mit einem Strom // ohne Aufteilung. expect(text).not.toContain( gedruckt('Für Ströme MIT erfasster Aufteilung entfällt dieser Posten.'), ); expect(text).not.toContain(gedruckt('um 10,2 % (1.695 Fz/h statt 1.538 Fz/h)')); }); it('behaelt den Vorbehalt, wo die Datenlage nicht durchgereicht wird', () => { // Der Vorbehalt darf nur entfallen, wenn NACHGEWIESEN ist, dass jeder // Strom gezaehlt ist - nicht schon dann, wenn der Aufrufer nichts weiss. const ohneLage = schwerverkehrSatz(RILSA_DEFAULTS); expect(ohneLage).toContain('FEHLERRICHTUNG, WO DIE AUFTEILUNG FEHLT'); expect(ohneLage).not.toContain('In diesem Plan'); expect( schwerverkehrSatz(RILSA_DEFAULTS, { mitAufteilung: 4, ohneAufteilung: 0 }), ).not.toContain('FEHLERRICHTUNG, WO DIE AUFTEILUNG FEHLT'); }); }); // --- Tabellenausgabe -------------------------------------------------------- describe('Tabellenausgabe fuehrt Lastzuganteil, fSV und die Gleichung', () => { it('fuehrt die drei Spalten in der Reihenfolge des Rechenwegs', () => { const zeilen = tabelle(vierDatenlagen()); const kopf = zeilen[0]!; const anteil = kopf.indexOf('Schwerverkehrsanteil [%]'); expect(anteil).toBeGreaterThanOrEqual(0); expect(kopf[anteil + 1]).toBe('Lastzuganteil am Schwerverkehr [%] (leer = nicht erfasst)'); expect(kopf[anteil + 2]).toBe('Schwerverkehrsfaktor fSV'); expect(kopf[anteil + 3]).toBe('Gleichung für fSV (HBS 2015)'); }); it('schreibt den Anteil, mit dem gerechnet wurde - nicht den eingetragenen', () => { /* * Derselbe Befund wie im Pruefbericht, eine Ausgabe weiter: Tabelle und * Ausdruck bildeten die Prozentzahl je selbst aus * `demand.heavyVehicleShare`, waehrend fSV daneben aus dem auf 0 bis 1 * begrenzten Anteil entsteht. Bei 150 % Schwerverkehr - ueber Datei und * Oberflaeche nicht erreichbar, ueber Fremdwerkzeuge sehr wohl - stand * "150,0 %" neben "1,900", also neben dem Faktor fuer 100 %. */ const basis = vierDatenlagen(); const projekt: Project = { ...basis, demands: basis.demands.map((d, index) => index === 1 ? { ...d, heavyVehicleShare: 1.5 } : d, ), }; const zeilen = tabelle(projekt); const namen = spalte(zeilen, 'Signalgruppe'); const anteil = spalte(zeilen, 'Schwerverkehrsanteil [%]'); const faktor = spalte(zeilen, 'Schwerverkehrsfaktor fSV'); const i = namen.indexOf(projekt.signalGroups[1]!.name); expect(anteil[i]).toBe('100'); expect(faktor[i]).toBe('1,9'); // Und der Ausdruck nimmt dieselbe Angabe. const zelle = schwerverkehrsangabe({ heavyVehicleShare: 1.5 }); expect(zelle.anteilProzent).toBe('100'); expect(zelle.fsv).toBeCloseTo(1.9, 9); }); it('schreibt je Zeile Aufteilung, Faktor und Gleichung', () => { const projekt = vierDatenlagen(); const zeilen = tabelle(projekt); const namen = spalte(zeilen, 'Signalgruppe'); const lastzug = spalte(zeilen, 'Lastzuganteil am Schwerverkehr [%] (leer = nicht erfasst)'); const faktor = spalte(zeilen, 'Schwerverkehrsfaktor fSV'); const gleichung = spalte(zeilen, 'Gleichung für fSV (HBS 2015)'); const zeile = (index: number): number => namen.indexOf(projekt.signalGroups[index]!.name); // Lauter Lastzuege. expect(lastzug[zeile(0)]).toBe('100'); expect(faktor[zeile(0)]).toBe('1,3'); expect(gleichung[zeile(0)]).toBe('2-5 (Aufteilung erfasst)'); // Keine Angabe: leere Zelle, Pauschalwert, und die Annahme steht dabei. expect(lastzug[zeile(1)]).toBe(''); expect(faktor[zeile(1)]).toBe('1,18'); expect(gleichung[zeile(1)]).toBe( '2-6 (keine Aufteilung erfasst; Annahme 20 % Lastzüge am Schwerverkehr)', ); // Gezaehlte 0 % - andere Zelle, andere Gleichung, anderer Faktor als oben. expect(lastzug[zeile(2)]).toBe('0'); expect(faktor[zeile(2)]).toBe('1,15'); expect(gleichung[zeile(2)]).toBe('2-5 (Aufteilung erfasst)'); expect(faktor[zeile(2)]).not.toBe(faktor[zeile(1)]); expect(lastzug[zeile(3)]).toBe('40'); expect(faktor[zeile(3)]).toBe('1,21'); }); it('haelt fSV und Saettigungsverkehrsstaerke derselben Zeile zusammen', () => { /* * Derselbe Befund wie im Ausdruck: Die Tabelle bildete den Faktor ohne die * Aufteilung und schrieb ihn neben eine Saettigungsverkehrsstaerke, die * aus einem anderen Faktor entstanden ist. Gegenprobe qS · fSV = Grundwert. */ const projekt = vierDatenlagen(); const plan = buildSignalPlan(projekt); const zeilen = tabelle(projekt); const namen = spalte(zeilen, 'Signalgruppe'); const faktor = spalte(zeilen, 'Schwerverkehrsfaktor fSV'); const qS = spalte(zeilen, 'Sättigungsverkehrsstärke [Fz/h]'); let geprueft = 0; for (const gruppe of projekt.signalGroups) { const i = namen.indexOf(gruppe.name); if (i < 0 || faktor[i] === '') continue; const zahl = (text: string): number => Number(text.replace(/\./g, '').replace(',', '.')); const grundwert = saturationFlow(gruppe.movement, gruppe.lanes, plan.defaults); // Die Tabelle rundet die Saettigungsverkehrsstaerke auf ganze Fz/h; der // Faktor traegt drei Stellen. Die Toleranz prueft die Zuordnung, nicht // die Rundung. // // WAS DIESER FALL FAENGT (Fassung 5.6.0): die HALBE Umstellung - Plan // nach Gl. 2-5, Ausfuhr weiter nach Gl. 2-6 (oder umgekehrt). Dann // stuende neben einer Saettigungsverkehrsstaerke aus 1,300 ein Faktor // 1,180, und das Produkt verfehlte den Grundwert um 10 %. Hier stand "der // Altstand lag hier um bis zu 10 % daneben" - das trifft nicht zu: Auf // dem Altstand rechnen Plan UND Tabelle nach Gl. 2-6, das Produkt ergibt // weiterhin den Grundwert, und der Fall besteht. Er ist eine // Zusammenhangs- und keine Altstandspruefung. expect(zahl(qS[i]!) * zahl(faktor[i]!), gruppe.name).toBeCloseTo(grundwert, -1); geprueft += 1; } expect(geprueft, 'Vorbedingung: es gibt Zeilen mit Faktor').toBe(4); }); it('laesst die drei Spalten fuer Fussgaenger und Radverkehr leer', () => { // Fuer sie wird keine Saettigungsverkehrsstaerke gebildet; ein Faktor // daneben laese sich als Eingangsgroesse (Befund B4). const projekt = vierDatenlagen(); const zeilen = tabelle(projekt); const namen = spalte(zeilen, 'Signalgruppe'); const lastzug = spalte(zeilen, 'Lastzuganteil am Schwerverkehr [%] (leer = nicht erfasst)'); const faktor = spalte(zeilen, 'Schwerverkehrsfaktor fSV'); const gleichung = spalte(zeilen, 'Gleichung für fSV (HBS 2015)'); const fussRad = projekt.signalGroups.filter((g) => g.mode === 'fuss' || g.mode === 'rad'); expect(fussRad.length).toBeGreaterThan(0); for (const gruppe of fussRad) { const i = namen.indexOf(gruppe.name); expect(lastzug[i], gruppe.name).toBe(''); expect(faktor[i], gruppe.name).toBe(''); expect(gleichung[i], gruppe.name).toBe(''); } }); it('nennt den verworfenen Eintrag in der Gleichungsspalte', () => { const projekt = unbrauchbarerEintrag(); const zeilen = tabelle(projekt); const namen = spalte(zeilen, 'Signalgruppe'); const lastzug = spalte(zeilen, 'Lastzuganteil am Schwerverkehr [%] (leer = nicht erfasst)'); const gleichung = spalte(zeilen, 'Gleichung für fSV (HBS 2015)'); const i = namen.indexOf(projekt.signalGroups[0]!.name); // Die Zahlenspalte bleibt leer - 150 neben "Gl. 2-6" waere eine Angabe, // die in keine Rechnung eingegangen ist. Der Grund steht in der Spalte // daneben und als Warnung im Pruefbericht. expect(lastzug[i]).toBe(''); expect(gleichung[i]).toContain('unbrauchbar und verworfen'); expect(gleichung[i]).toContain('Annahme 20 % Lastzüge am Schwerverkehr'); }); });