/** * Einheiten und Umrechnungen. * * Im Altbestand wurden Geschwindigkeiten mal in km/h und mal in m/s in denselben * Feldern gefuehrt; die Umrechnung /3.6 wurde teils doppelt, teils gar nicht * angewendet. Deshalb sind hier alle Einheiten im Typnamen sichtbar und es gibt * genau eine Stelle fuer jede Umrechnung. * * Rechnerisch gilt projektweit: intern wird ausschliesslich in SI-Einheiten * gerechnet (Meter, Sekunden, Meter je Sekunde). km/h existiert nur an der * Benutzerschnittstelle. */ /** Zeit in Sekunden. */ export type Seconds = number; /** Laenge in Metern. */ export type Meters = number; /** Geschwindigkeit in Metern je Sekunde (interne Rechengroesse). */ export type MetersPerSecond = number; /** Geschwindigkeit in Kilometern je Stunde (nur Ein-/Ausgabe). */ export type KilometersPerHour = number; /** Verkehrsstaerke in Fahrzeugen je Stunde. */ export type VehiclesPerHour = number; /** Dimensionsloser Anteil im Bereich 0..1. */ export type Ratio = number; const SECONDS_PER_HOUR = 3600; const METERS_PER_KILOMETER = 1000; /** Exakter Umrechnungsfaktor km/h -> m/s (= 1000/3600). */ export const KMH_TO_MS = METERS_PER_KILOMETER / SECONDS_PER_HOUR; /** Rechnet km/h in m/s um. */ export function kmhToMs(v: KilometersPerHour): MetersPerSecond { return v * KMH_TO_MS; } /** Rechnet m/s in km/h um. */ export function msToKmh(v: MetersPerSecond): KilometersPerHour { return v / KMH_TO_MS; } /** * Rundet auf ganze Sekunden auf. * * Sicherheitsrelevante Zeiten (Zwischenzeit, Raeumzeit) duerfen nie abgerundet * werden. Der Rundungsfehler-Ausgleich faengt Gleitkomma-Artefakte ab: ohne ihn * wuerde z. B. 3.0000000000000004 auf 4 s aufgerundet. * * KEINE NEGATIVE NULL (Fassung 5.10.0): Math.ceil gibt fuer jeden Wert zwischen * -1 und 0 die negative Null zurueck. Sie rechnet wie 0, druckt sich aber nicht * wie 0 - Intl formatiert sie als "-0", und so stand sie in der * Zwischenzeitenmatrix der Planunterlage, im Rechenweg des Konfliktfensters und * in der CSV-Ausgabe. Abgestreift wird sie hier, an der einen Stelle, die * aufrundet, statt an jeder Ausgabe einzeln. Die Nachbarzeile roundTo faengt * daneben den Vorzeichenfall von Math.round ab, streift die negative Null aber * NICHT ab: roundTo(-0,4; 0) ergibt -0. Wer mit 0 Nachkommastellen rundet, muss * sie dort weiter selbst erwarten. */ export function ceilSeconds(t: Seconds): Seconds { const corrected = roundTo(t, 9); const aufgerundet = Math.ceil(corrected); return aufgerundet === 0 ? 0 : aufgerundet; } /** Kaufmaennisches Runden auf eine feste Anzahl Nachkommastellen. */ export function roundTo(value: number, decimals: number): number { if (!Number.isFinite(value)) return value; const factor = 10 ** decimals; // Math.round(-0.5) ergibt -0; die Vorzeichenbehandlung haelt negative Werte stabil. return Math.sign(value) * (Math.round(Math.abs(value) * factor) / factor); } /** Rundet auf das naechste Vielfache von `step` (kaufmaennisch). */ export function roundToStep(value: number, step: number): number { if (step <= 0) throw new RangeError('roundToStep: step muss groesser als 0 sein'); return Math.round(value / step) * step; } /** Rundet auf das naechste Vielfache von `step` auf. */ export function ceilToStep(value: number, step: number): number { if (step <= 0) throw new RangeError('ceilToStep: step muss groesser als 0 sein'); return Math.ceil(roundTo(value / step, 9)) * step; } /** Begrenzt einen Wert auf das Intervall [min, max]. */ export function clamp(value: number, min: number, max: number): number { if (min > max) throw new RangeError('clamp: min darf nicht groesser als max sein'); return Math.min(max, Math.max(min, value)); } /** * Prueft auf eine endliche Zahl. * * Der Altbestand hat mit parseFloat('') === NaN weitergerechnet; NaN pflanzt * sich durch die gesamte Berechnung fort und erscheint am Ende stumm als leeres * Feld im Plan. Alle Eingaenge der Fachlogik werden deshalb hier geprueft. */ export function isFiniteNumber(value: unknown): value is number { return typeof value === 'number' && Number.isFinite(value); } /** * Liest eine Zahl aus beliebiger Eingabe (Formularfeld, importiertes JSON). * Akzeptiert deutsches Dezimalkomma. Gibt `undefined` zurueck, wenn kein * gueltiger Zahlenwert vorliegt - nie stillschweigend 0. */ export function parseNumber(input: unknown): number | undefined { if (typeof input === 'number') return Number.isFinite(input) ? input : undefined; if (typeof input !== 'string') return undefined; const normalized = input.trim().replace(/\s/g, '').replace(',', '.'); if (normalized === '') return undefined; // Number() ist strenger als parseFloat: '12abc' ergibt NaN statt 12. const value = Number(normalized); return Number.isFinite(value) ? value : undefined; }