lsa-planer

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

/ tests domain schwerverkehrsaufteilung.test.ts

21,5 KB Rohdatei
tests/domain/schwerverkehrsaufteilung.test.ts — 465 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import {
3 aufteilungAusLastzuganteil,
4 istLastzuganteil,
5 lastzuganteilSpanne,
6 schwerverkehrsfaktor,
7 } from '@/domain/rilsa/capacity';
8 import {
9 HBS_LASTZUGANTEIL_BEI_UNBEKANNTER_AUFTEILUNG,
10 HBS_PKW_GLEICHWERTE,
11 RILSA_DEFAULTS,
12 } from '@/domain/rilsa/constants';
13 import { QUELLEN } from '@/domain/rilsa/quellen';
14 import { settingFields } from '@/domain/rilsa/settings';
15 import { buildSignalPlan } from '@/domain/plan/signalPlan';
16 import { SCHWERVERKEHR_AUFTEILUNG_MELDESCHWELLE } from '@/domain/validation/rules';
17 import { validateProject } from '@/domain/validation/engine';
18 import { loadProject } from '@/domain/model/migrate';
19 import { createStandardIntersectionProject } from '@/domain/model/factory';
20 import { CURRENT_SCHEMA_VERSION, type Project, type TrafficDemand } from '@/domain/model/project';
21 import { setDemand, setLastzugAnteil } from '@/app/actions';
22
23 /**
24 * Fassung 5.6.0 - die Aufteilung des Schwerverkehrs nach HBS 2015 Gl. 2-5.
25 *
26 * WORUM ES GEHT. Bis Fassung 5.5.0 kannte das Datenmodell nur einen
27 * Gesamt-Schwerverkehrsanteil. Das Programm rechnete deshalb jeden Strom nach
28 * Gl. 2-6 mit dem Pauschalwert 1,90 je Schwerfahrzeug - auch einen aus lauter
29 * Lastzuegen, der nach Gl. 2-5 mit 2,50 zu rechnen ist. Bei 20 % Schwerverkehr
30 * aus lauter Lastzuegen fiel die Saettigungsverkehrsstaerke damit 10,2 % zu
31 * gross aus, bei reinem Lastzugverkehr 31,6 %. Der Posten war im Programm
32 * ehrlich ausgewiesen - aber ausgewiesen ist nicht gerechnet.
33 *
34 * DIE BELEGE (BASt-Bericht V 400, 2025, S. 10), woertlich:
35 * "fSV = (qLV + 1,75 * qLkw+Bus + 2,5 * qLkwK) / qKfz (2-5)"
36 * "fSV = (qLV + 1,9 * qSV) / qKfz (2-6)"
37 * und zur Wahl zwischen beiden: "in Abhaengigkeit von der Datenverfuegbarkeit
38 * zur Aufteilung des Schwerverkehrs".
39 *
40 * DER KONSISTENZBELEG, an dem die Aufteilung haengt - V 400 zu Bild 2-1 d):
41 * "Bei einem Anteil der Lkw mit Anhaenger und Sattel-Kfz (Fahrzeugklasse LkwK)
42 * am Schwerverkehr von 20 % ergibt sich der Funktionsverlauf der Gleichung
43 * (2-6)." Nachgerechnet: 1,75 * 0,8 + 2,50 * 0,2 = 1,90 exakt. Der Pauschalwert
44 * IST Gl. 2-5 mit 20 % Lastzuganteil; die Erfassung ist eine Verfeinerung
45 * derselben Formel und kein anderes Verfahren. Genau deshalb darf eine Altdatei
46 * ihr Ergebnis nicht aendern.
47 */
48
49 const DATUM = new Date('2026-01-01T00:00:00Z');
50
51 /** Grundwert je Fahrstreifen unter Standardbedingungen: 2000 Kfz/h. */
52 const QS_GRUND = RILSA_DEFAULTS.capacity.saturationFlow;
53
54 /**
55 * Der Knotenpunkt der Vorlage mit EINER erfassten Verkehrsstaerke an K1
56 * (geradeaus, ein Fahrstreifen - also Grundwert ohne Abbiegerfaktor).
57 */
58 function knotenMitStrom(demand: Omit<TrafficDemand, 'signalGroupId'>): {
59 project: Project;
60 groupId: string;
61 } {
62 const roh = createStandardIntersectionProject('Schwerverkehr', DATUM);
63 const k1 = roh.signalGroups.find((g) => g.name === 'K1');
64 if (!k1) throw new Error('K1 fehlt in der Vorlage');
65 return {
66 project: { ...roh, demands: [{ signalGroupId: k1.id, ...demand }] },
67 groupId: k1.id,
68 };
69 }
70
71 /** Die gerechnete Saettigungsverkehrsstaerke dieser Gruppe im Signalzeitenplan. */
72 function qSimPlan(project: Project, groupId: string): number {
73 const gruppe = buildSignalPlan(project).groups.find((g) => g.groupId === groupId);
74 if (!gruppe?.capacity) throw new Error('keine Kapazitaet gerechnet');
75 return gruppe.capacity.saturationFlow;
76 }
77
78 // --- Die beiden Gleichungen -------------------------------------------------
79
80 describe('Gl. 2-5 und Gl. 2-6 sind dieselbe Gleichung', () => {
81 it('faellt bei 20 % Lastzuganteil exakt mit dem Pauschalwert zusammen', () => {
82 /*
83 * Die Kernaussage dieser Tests, und zugleich der schaerfste
84 * MUTATIONSNACHWEIS: Jede Verwechslung der Bezugsgroesse in
85 * aufteilungAusLastzuganteil (Lastzuganteil an qKfz statt am
86 * Schwerverkehr), jeder vertauschte Pkw-Gleichwert und jedes verrutschte
87 * Vorzeichen laesst die beiden Wege auseinanderfallen. Geprueft wird auf
88 * zwoelf Stellen, nicht "ungefaehr gleich" - 1,75 * 0,8 + 2,50 * 0,2 = 1,90
89 * ist exakt und nicht gerundet.
90 */
91 for (const pSV of [0, 0.03, 0.1, 0.2, 0.35, 0.5, 0.8, 1]) {
92 const nachAufteilung = schwerverkehrsfaktor(
93 pSV,
94 aufteilungAusLastzuganteil(pSV, HBS_LASTZUGANTEIL_BEI_UNBEKANNTER_AUFTEILUNG),
95 );
96 expect(nachAufteilung, `pSV ${pSV}`).toBeCloseTo(schwerverkehrsfaktor(pSV), 12);
97 }
98 });
99
100 it('haelt den belegten Pauschalanteil und seine Nachrechnung fest', () => {
101 expect(HBS_LASTZUGANTEIL_BEI_UNBEKANNTER_AUFTEILUNG).toBe(0.2);
102 const nachgerechnet =
103 HBS_PKW_GLEICHWERTE.lkwUndBus * (1 - HBS_LASTZUGANTEIL_BEI_UNBEKANNTER_AUFTEILUNG) +
104 HBS_PKW_GLEICHWERTE.lkwMitAnhaenger * HBS_LASTZUGANTEIL_BEI_UNBEKANNTER_AUFTEILUNG;
105 expect(nachgerechnet).toBeCloseTo(HBS_PKW_GLEICHWERTE.schwerverkehrPauschal, 12);
106 });
107
108 it('rechnet den Lastzuganteil auf die Bezugsgroesse von Gl. 2-5 um', () => {
109 // Anteile AN qKfz, wie Gl. 2-5 sie braucht: pSV * anteil und
110 // pSV * (1 - anteil). Ihre Summe ist genau pSV - mehr Schwerverkehr als
111 // Schwerverkehr entsteht nicht.
112 const aufteilung = aufteilungAusLastzuganteil(0.3, 0.4);
113 expect(aufteilung).toEqual({ lkwUndBus: 0.3 * 0.6, lkwMitAnhaenger: 0.3 * 0.4 });
114 expect(aufteilung!.lkwUndBus + aufteilung!.lkwMitAnhaenger).toBeCloseTo(0.3, 12);
115 });
116
117 it('fuehrt die Aufteilung nicht als Kennwert in der Vorgabenverwaltung', () => {
118 // Befund C20 gilt weiter: Waehlbar ist die Datenlage je Strom, nicht der
119 // Pkw-Gleichwert des Regelwerks.
120 const schluessel = settingFields().map((f) => String(f.key));
121 for (const verboten of ['lastzugAnteil', 'pkwGleichwertLastzug', 'schwerverkehrPauschal']) {
122 expect(schluessel).not.toContain(verboten);
123 }
124 });
125 });
126
127 // --- Der Lastzugstrom -------------------------------------------------------
128
129 describe('Ein Strom aus lauter Lastzuegen', () => {
130 it('kommt auf fSV 1,30 und 1538 statt 1695 Kfz/h', () => {
131 // Das Beispiel, das seit der Fassung 5.5.0 in Unterlage, Hilfe und
132 // Fundstellenverzeichnis steht - jetzt rechenbar statt nur benannt.
133 const nurLastzuege = schwerverkehrsfaktor(0.2, aufteilungAusLastzuganteil(0.2, 1));
134 expect(nurLastzuege).toBeCloseTo(1.3, 12);
135 expect(schwerverkehrsfaktor(0.2)).toBeCloseTo(1.18, 12);
136
137 const { project, groupId } = knotenMitStrom({
138 volume: 600,
139 heavyVehicleShare: 0.2,
140 lastzugAnteil: 1,
141 });
142 expect(Math.round(qSimPlan(project, groupId))).toBe(1538);
143
144 // MUTATIONSNACHWEIS: Wer die Aufteilung im Signalzeitenplan nicht
145 // durchreicht, landet wieder bei 1695 - der Zahl bis Fassung 5.5.0.
146 const { project: ohne, groupId: id2 } = knotenMitStrom({
147 volume: 600,
148 heavyVehicleShare: 0.2,
149 });
150 expect(Math.round(qSimPlan(ohne, id2))).toBe(1695);
151 });
152
153 it('beziffert beide Fehlerrichtungen des Pauschalwerts fuer einen Anteil', () => {
154 // Bezugsgroesse ist die WAHRE Saettigungsverkehrsstaerke: qS = qS0 / fSV,
155 // also (qS_Programm - qS_wahr) / qS_wahr = fSV_wahr / fSV_pauschal - 1.
156 const spanne = lastzuganteilSpanne(0.2);
157 expect(spanne.pauschal).toBeCloseTo(1.18, 12);
158 expect(spanne.nurLastzuege).toBeCloseTo(1.3, 12);
159 expect(spanne.ohneLastzuege).toBeCloseTo(1.15, 12);
160 expect(spanne.hoechstensZuGross * 100).toBeCloseTo(10.169, 3);
161 expect(spanne.hoechstensZuKlein * 100).toBeCloseTo(2.542, 3);
162
163 // Bei reinem Lastzugverkehr die 31,6 %, die im Fundstellenverzeichnis
164 // stehen - und die konservative Gegenrichtung dazu.
165 const voll = lastzuganteilSpanne(1);
166 expect(voll.hoechstensZuGross * 100).toBeCloseTo(31.579, 3);
167 expect(voll.hoechstensZuKlein * 100).toBeCloseTo(7.895, 3);
168
169 // Ohne Schwerverkehr gibt es keine Spanne - und keinen Grund zu melden.
170 const leer = lastzuganteilSpanne(0);
171 expect(leer.hoechstensZuGross).toBe(0);
172 expect(leer.hoechstensZuKlein).toBe(0);
173 });
174 });
175
176 // --- Randfaelle -------------------------------------------------------------
177
178 describe('Randfaelle des Lastzuganteils', () => {
179 it('nimmt 0 und 1 als Angaben an', () => {
180 expect(istLastzuganteil(0)).toBe(true);
181 expect(istLastzuganteil(1)).toBe(true);
182 // 0 heisst "gezaehlt, kein Lastzug dabei" - Gl. 2-5 mit 1,75 je
183 // Schwerfahrzeug, also GUENSTIGER als der Pauschalwert.
184 expect(schwerverkehrsfaktor(0.2, aufteilungAusLastzuganteil(0.2, 0))).toBeCloseTo(1.15, 12);
185 expect(schwerverkehrsfaktor(0.2, aufteilungAusLastzuganteil(0.2, 1))).toBeCloseTo(1.3, 12);
186 });
187
188 it('verwirft alles andere und faellt auf Gl. 2-6 zurueck', () => {
189 /*
190 * MUTATIONSNACHWEIS gegen ein Begrenzen auf 0..1: Wer -0,3 auf 0 kappt,
191 * macht aus einer Fehleingabe die GUENSTIGSTE aller Aufteilungen (fSV
192 * 1,150 statt 1,180) und weist den Strom danach als "Aufteilung erfasst"
193 * aus. Beide Erwartungen unten schlagen dann fehl.
194 */
195 for (const unbrauchbar of [-0.3, 1.5, Number.NaN, Number.POSITIVE_INFINITY, undefined]) {
196 expect(istLastzuganteil(unbrauchbar), `${unbrauchbar}`).toBe(false);
197 expect(aufteilungAusLastzuganteil(0.2, unbrauchbar), `${unbrauchbar}`).toBeUndefined();
198 expect(
199 schwerverkehrsfaktor(0.2, aufteilungAusLastzuganteil(0.2, unbrauchbar)),
200 `${unbrauchbar}`,
201 ).toBeCloseTo(schwerverkehrsfaktor(0.2), 12);
202 }
203 });
204
205 it('meldet einen eingetragenen, aber unbrauchbaren Anteil im Signalzeitenplan', () => {
206 // Ueber die Datei ist der Fall nicht erreichbar (das Schema verwirft ihn
207 // beim Lesen), ueber Oberflaeche und Fremdwerkzeuge schon. Ohne diese
208 // Notiz waere es eine stille Ersetzung: eingetragene Aufteilung,
209 // gerechnet ohne sie.
210 const { project, groupId } = knotenMitStrom({
211 volume: 600,
212 heavyVehicleShare: 0.2,
213 lastzugAnteil: 1.5,
214 });
215 const plan = buildSignalPlan(project);
216 const notiz = plan.notes.find((n) => n.code === 'lastzuganteil-ausserhalb');
217 expect(notiz?.severity).toBe('warnung');
218 expect(notiz?.signalGroupId).toBe(groupId);
219 expect(notiz?.message).toContain('150 %');
220 expect(notiz?.message).toContain('1,90');
221 // Gerechnet wird der Pauschalwert - nicht etwa mit 1,5 als Anteil.
222 expect(Math.round(qSimPlan(project, groupId))).toBe(1695);
223
224 // Und im Pruefbericht steht sie mit Titel, Fundstelle und Rat.
225 const bericht = validateProject(project);
226 const befund = bericht.findings.find((f) => f.title.includes('Lastzuganteil außerhalb'));
227 expect(befund?.severity).toBe('warnung');
228 expect(befund?.category).toBe('leistungsfaehigkeit');
229 expect(befund?.reference).toContain('HBS 2015');
230 expect(befund?.suggestion).toContain('entfernen');
231 });
232 });
233
234 // --- Schema und Bestandsdaten ----------------------------------------------
235
236 describe('Schemastand 13 und Bestandsdaten', () => {
237 it('fuehrt den Lastzuganteil ueber Speichern und Laden', () => {
238 expect(CURRENT_SCHEMA_VERSION).toBe(19);
239
240 const { project, groupId } = knotenMitStrom({
241 volume: 600,
242 heavyVehicleShare: 0.25,
243 lastzugAnteil: 0.6,
244 });
245 const geladen = loadProject(JSON.parse(JSON.stringify(project)));
246 expect(geladen.issues.filter((i) => i.path.includes('lastzugAnteil'))).toEqual([]);
247 expect(geladen.project.schemaVersion).toBe(19);
248 expect(geladen.project.demands[0]?.lastzugAnteil).toBe(0.6);
249 // Und die Rechnung ueberlebt die Rundreise.
250 expect(qSimPlan(geladen.project, groupId)).toBeCloseTo(qSimPlan(project, groupId), 9);
251 });
252
253 it('laesst eine Altdatei ohne das Feld unveraendert rechnen', () => {
254 /*
255 * Die Zusage dabei: Schema 12 kannte den Lastzuganteil nicht, also
256 * darf keine Zahl einer alten Datei sich bewegen. 2000/1,180 = 1694,92 -
257 * derselbe Wert wie in Fassung 5.5.0.
258 *
259 * MUTATIONSNACHWEIS: Wuerde die Migration den belegten Pauschalanteil von
260 * 20 % vorbelegen, waere die ZAHL gleich (das ist der Konsistenzbeleg),
261 * aber `lastzugAnteil` stuende in der Datei - und der Strom gaelte als
262 * gezaehlt. Genau das prueft die zweite Erwartung.
263 */
264 const { project, groupId } = knotenMitStrom({ volume: 600, heavyVehicleShare: 0.2 });
265 const alt = { ...JSON.parse(JSON.stringify(project)), schemaVersion: 12 };
266 const geladen = loadProject(alt);
267 expect(geladen.project.demands[0]?.lastzugAnteil).toBeUndefined();
268 expect(qSimPlan(geladen.project, groupId)).toBeCloseTo(QS_GRUND / 1.18, 9);
269 });
270
271 it('verwirft einen unlesbaren Eintrag beim Lesen - mit Meldung', () => {
272 const { project } = knotenMitStrom({ volume: 600, heavyVehicleShare: 0.2 });
273 for (const eingetragen of ['viele', -0.2, 2]) {
274 const roh = JSON.parse(JSON.stringify(project));
275 roh.demands[0].lastzugAnteil = eingetragen;
276 const geladen = loadProject(roh);
277 const meldung = geladen.issues.find((i) => i.path === 'demands[0].lastzugAnteil');
278 expect(meldung?.severity, `${eingetragen}`).toBe('warnung');
279 expect(meldung?.message, `${eingetragen}`).toContain('1,90');
280 expect(geladen.project.demands[0]?.lastzugAnteil, `${eingetragen}`).toBeUndefined();
281 }
282 });
283
284 it('schreibt kein Feld, wo keines eingetragen wurde', () => {
285 // Ein leeres Feld ist keine Angabe: Weder das Schema noch die Aktion
286 // duerfen aus "nichts eingetragen" eine 0 machen - das waere ein
287 // Schwerverkehr ohne Lastzuege und damit die unsichere Richtung.
288 const { project, groupId } = knotenMitStrom({ volume: 600, heavyVehicleShare: 0.2 });
289 const mitAnteil = setLastzugAnteil(project, groupId, 0.5);
290 expect(mitAnteil.demands[0]?.lastzugAnteil).toBe(0.5);
291 const wiederOhne = setLastzugAnteil(mitAnteil, groupId, null);
292 expect('lastzugAnteil' in (wiederOhne.demands[0] ?? {})).toBe(false);
293 expect(JSON.stringify(wiederOhne.demands[0])).not.toContain('lastzugAnteil');
294 // Die Verkehrsstaerke selbst bleibt dabei stehen.
295 expect(wiederOhne.demands[0]?.volume).toBe(600);
296 // Und die Zahl ist wieder die des Pauschalwerts.
297 expect(qSimPlan(wiederOhne, groupId)).toBeCloseTo(QS_GRUND / 1.18, 9);
298 });
299
300 it('ruehrt die uebrigen Angaben der Verkehrsstaerke nicht an', () => {
301 const { project, groupId } = knotenMitStrom({ volume: 600, heavyVehicleShare: 0.2 });
302 const geaendert = setDemand(setLastzugAnteil(project, groupId, 0.4), groupId, { volume: 900 });
303 expect(geaendert.demands[0]).toEqual({
304 signalGroupId: groupId,
305 volume: 900,
306 heavyVehicleShare: 0.2,
307 lastzugAnteil: 0.4,
308 });
309 });
310 });
311
312 // --- Der Pruefbericht -------------------------------------------------------
313
314 describe('Der Pruefbericht zur fehlenden Aufteilung', () => {
315 const REGEL = 'leistungsfaehigkeit.schwerverkehr-ohne-aufteilung';
316
317 it('schweigt, wo die Aufteilung erfasst ist', () => {
318 // Zweig (a): Es gibt keine Annahme zu benennen - der Strom rechnet nach
319 // Gl. 2-5. Auch dann, wenn der eingetragene Anteil genau der
320 // Pauschalannahme entspricht: Gezaehlt ist gezaehlt.
321 for (const anteil of [0, 0.2, 1]) {
322 const { project } = knotenMitStrom({
323 volume: 600,
324 heavyVehicleShare: 0.4,
325 lastzugAnteil: anteil,
326 });
327 const bericht = validateProject(project);
328 expect(
329 bericht.findings.filter((f) => f.rule === REGEL),
330 `${anteil}`,
331 ).toEqual([]);
332 }
333 });
334
335 it('meldet oberhalb der Schwelle mit Annahme und Fehlerrichtung', () => {
336 // Zweig (b). 25 % Schwerverkehr: 2000/1,225 = 1633 Kfz/h gerechnet,
337 // 2000/1,375 = 1455 Kfz/h bei lauter Lastzuegen (12,2 % zu gross),
338 // 2000/1,1875 = 1684 Kfz/h ohne Lastzuege (3,1 % zu klein).
339 const { project, groupId } = knotenMitStrom({ volume: 600, heavyVehicleShare: 0.25 });
340 const befund = validateProject(project).findings.find((f) => f.rule === REGEL);
341 expect(befund?.severity).toBe('hinweis');
342 expect(befund?.category).toBe('leistungsfaehigkeit');
343 expect(befund?.target).toEqual({ kind: 'signalgruppe', id: groupId, label: 'K1' });
344 // Die Annahme steht da, mit ihrer Nachrechnung.
345 expect(befund?.message).toContain('20 %');
346 expect(befund?.message).toContain('1,75 · 0,80 + 2,50 · 0,20 = 1,90');
347 // Und die Zahlen sind die DIESES Stroms, nicht die des Handbuchbeispiels.
348 expect(befund?.message).toContain('1633 Kfz/h');
349 expect(befund?.message).toContain('1455 Kfz/h');
350 expect(befund?.message).toContain('12,2 % ZU GROSS');
351 expect(befund?.message).toContain('1684 Kfz/h');
352 expect(befund?.message).toContain('3,1 % zu klein');
353 expect(befund?.message).not.toContain('10,2 %');
354 expect(befund?.reference).toContain('2-5');
355 expect(befund?.suggestion).toContain('Lastzüge');
356 });
357
358 it('schweigt unterhalb der Schwelle', () => {
359 /*
360 * Die Schwelle ist ein Programmwert und begruendet: Bei 10 %
361 * Schwerverkehr betraegt die moegliche Ueberschaetzung 5,5 %, bei 5 % nur
362 * 2,9 % - darunter liegt sie unter der Genauigkeit einer Zaehlung. Die
363 * Vorlage mit ihren 5 % soll keinen Hinweis erzeugen.
364 */
365 expect(SCHWERVERKEHR_AUFTEILUNG_MELDESCHWELLE).toBe(0.1);
366 expect(lastzuganteilSpanne(0.1).hoechstensZuGross * 100).toBeCloseTo(5.505, 3);
367 expect(lastzuganteilSpanne(0.05).hoechstensZuGross * 100).toBeCloseTo(2.871, 3);
368 // Die uebrigen Zahlen der Begruendung an der Meldeschwelle - sie stehen im
369 // Kommentar und muessen dort stimmen.
370 expect(lastzuganteilSpanne(0.25).hoechstensZuGross * 100).toBeCloseTo(12.245, 3);
371 expect(lastzuganteilSpanne(0.5).hoechstensZuGross * 100).toBeCloseTo(20.69, 2);
372
373 for (const pSV of [0, 0.05, 0.1]) {
374 const { project } = knotenMitStrom({ volume: 600, heavyVehicleShare: pSV });
375 expect(
376 validateProject(project).findings.filter((f) => f.rule === REGEL),
377 `${pSV}`,
378 ).toEqual([]);
379 }
380 // Knapp darueber kommt sie.
381 const { project } = knotenMitStrom({ volume: 600, heavyVehicleShare: 0.11 });
382 expect(validateProject(project).findings.filter((f) => f.rule === REGEL)).toHaveLength(1);
383 });
384
385 it('nennt den Anteil, mit dem gerechnet wird - nicht den eingetragenen', () => {
386 /*
387 * BEFUND (Fassung 5.6.0): Der Meldungstext nahm `demand.heavyVehicleShare`
388 * roh, waehrend `lastzuganteilSpanne` intern auf 0 bis 1 begrenzt. Eine
389 * Datei mit 150 % Schwerverkehr (ueber das Schema nicht erreichbar, ueber
390 * Fremdwerkzeuge sehr wohl) ergab damit eine Ueberschrift "... (150 %)"
391 * ueber Kfz/h-Zahlen, die fuer 100 % gelten - und derselbe Bericht meldet
392 * den Wert daneben als begrenzt.
393 *
394 * Gerechnet wird mit 100 %: fSV = 1,90, also 2000/1,90 = 1053 Kfz/h; bei
395 * lauter Lastzuegen 2000/2,50 = 800 Kfz/h (31,6 % zu gross), ohne
396 * Lastzuege 2000/1,75 = 1143 Kfz/h (7,9 % zu klein).
397 */
398 const { project } = knotenMitStrom({ volume: 600, heavyVehicleShare: 1.5 });
399 const befund = validateProject(project).findings.find((f) => f.rule === REGEL);
400 expect(befund?.title).toContain('(100 %)');
401 expect(befund?.title).not.toContain('150');
402 expect(befund?.message).toContain('100 % Schwerverkehr');
403 expect(befund?.message).not.toContain('150 %');
404 expect(befund?.message).toContain('1053 Kfz/h');
405 expect(befund?.message).toContain('800 Kfz/h');
406 expect(befund?.message).toContain('31,6 % ZU GROSS');
407 expect(befund?.message).toContain('1143 Kfz/h');
408 expect(befund?.message).toContain('7,9 % zu klein');
409 });
410
411 it('schweigt ohne Verkehrsstaerke', () => {
412 // Ohne sie gibt es keinen Auslastungsgrad, und die Unterlage druckt an
413 // dieser Stelle einen Strich statt eines Faktors.
414 const { project } = knotenMitStrom({ volume: 0, heavyVehicleShare: 0.4 });
415 expect(validateProject(project).findings.filter((f) => f.rule === REGEL)).toEqual([]);
416 });
417
418 it('haelt sich an die Kennwerte des Plans, nicht an 2000 Kfz/h', () => {
419 /*
420 * MUTATIONSNACHWEIS gegen eine fest verdrahtete 2000 im Meldungstext: Mit
421 * einem geaenderten Grundwert muss die Meldung mitwandern, sonst nennt
422 * der Bericht eine Zahl, die im selben Bericht anders steht.
423 */
424 const { project } = knotenMitStrom({ volume: 600, heavyVehicleShare: 0.25 });
425 const mitVorgabe: Project = {
426 ...project,
427 settings: { ...project.settings, rilsa: { ...project.settings.rilsa, saturationFlow: 1800 } },
428 };
429 const befund = validateProject(mitVorgabe).findings.find((f) => f.rule === REGEL);
430 // 1800/1,225 = 1469 Kfz/h - die Prozentzahlen bleiben, sie haengen allein
431 // am Schwerverkehrsanteil.
432 expect(befund?.message).toContain('1469 Kfz/h');
433 expect(befund?.message).toContain('12,2 % ZU GROSS');
434 });
435 });
436
437 // --- Fundstellenverzeichnis -------------------------------------------------
438
439 describe('Das Fundstellenverzeichnis nach P14', () => {
440 const hinweis = QUELLEN['hbs-kapazitaet'].hinweis ?? '';
441
442 it('sagt nicht mehr, das Programm rechne immer nach Gl. 2-6', () => {
443 // Der Satz war bis Fassung 5.5.0 richtig und ist es jetzt nicht mehr.
444 expect(hinweis).not.toContain('erfasst keine Aufteilung des Schwerverkehrs');
445 expect(hinweis).not.toContain('IMMER mit der zweiten Gleichung');
446 expect(hinweis).toContain('Datenverfügbarkeit');
447 });
448
449 it('fuehrt den Konsistenzbeleg mit seiner Nachrechnung', () => {
450 expect(hinweis).toContain('Funktionsverlauf der Gleichung (2-6)');
451 expect(hinweis).toContain('1,75 · 0,80 + 2,5 · 0,20 = 1,9');
452 });
453
454 it('haelt den unsicheren Posten fest - jetzt begrenzt auf Ströme ohne Aufteilung', () => {
455 expect(hinweis).toContain('10,2 %');
456 expect(hinweis).toContain('31,6 %');
457 expect(hinweis).toContain('ohne erfasste Aufteilung');
458 // Die Gegenrichtung war bisher nirgends beziffert.
459 expect(hinweis).toContain('2,5 % zu klein');
460 // Und die Grenzen der Rechnung stehen weiter da.
461 expect(hinweis).toContain('Standardbedingungen');
462 expect(hinweis).toContain('Mischfahrstreifen');
463 expect(hinweis).toContain('HCM 2010');
464 });
465 });