/** * Datums- und Zeitformate der Oberfläche (#002): TT.MM.JJJJ, 24-h-Zeit, deutsch. * * Die Zeitzone ist fest Europe/Berlin und nicht die des Prozesses: Server (UTC im * Container) und Browser sollen dieselbe Zeit anzeigen, sonst springen Zeitstempel * zwischen Server- und Client-Rendering um Stunden. */ export const ZEITZONE = "Europe/Berlin"; type Eingabe = Date | string | number; const DATUM = new Intl.DateTimeFormat("de-DE", { timeZone: ZEITZONE, day: "2-digit", month: "2-digit", year: "numeric", }); const UHRZEIT = new Intl.DateTimeFormat("de-DE", { timeZone: ZEITZONE, hour: "2-digit", minute: "2-digit", hourCycle: "h23", }); function alsDatum(wert: Eingabe): Date { const d = wert instanceof Date ? wert : new Date(wert); if (Number.isNaN(d.getTime())) throw new RangeError(`Ungültiges Datum: ${String(wert)}`); return d; } /** „12.10.2026“ */ export function datum(wert: Eingabe): string { return DATUM.format(alsDatum(wert)); } /** „08:42“ */ export function uhrzeit(wert: Eingabe): string { return UHRZEIT.format(alsDatum(wert)); } /** „12.10.2026, 08:42“ */ export function datumZeit(wert: Eingabe): string { const d = alsDatum(wert); return `${DATUM.format(d)}, ${UHRZEIT.format(d)}`; } const MINUTE = 60_000; const STUNDE = 60 * MINUTE; const TAG = 24 * STUNDE; /** * Grobe Relativangabe für Listen: „gerade eben“, „vor 5 Min.“, „vor 3 Std.“, * „vor 2 Tagen“, „in 12 Tagen“. Ab 30 Tagen Abstand wird das Datum gezeigt, weil * „vor 47 Tagen“ schlechter einzuordnen ist als ein Datum. * * `jetzt` ist ein Parameter, damit Tests und statisch gerenderte Seiten einen * festen Bezugspunkt haben. */ export function relativ(wert: Eingabe, jetzt: Eingabe = new Date()): string { const abstand = alsDatum(wert).getTime() - alsDatum(jetzt).getTime(); const betrag = Math.abs(abstand); const zukunft = abstand > 0; if (betrag < MINUTE) return "gerade eben"; if (betrag >= 30 * TAG) return `am ${datum(wert)}`; let text: string; if (betrag < STUNDE) { text = `${Math.floor(betrag / MINUTE)} Min.`; } else if (betrag < TAG) { text = `${Math.floor(betrag / STUNDE)} Std.`; } else { const tage = Math.floor(betrag / TAG); // „vor“ und „in“ verlangen beide den Dativ: „vor 1 Tag“, „in 2 Tagen“. text = tage === 1 ? "1 Tag" : `${tage} Tagen`; } return zukunft ? `in ${text}` : `vor ${text}`; } /** Ganze Kalendertage bis `wert` (negativ, wenn vorbei) – Grundlage für Ablaufhinweise. */ export function tageBis(wert: Eingabe, jetzt: Eingabe = new Date()): number { const tagesnummer = (d: Date) => { const [t, m, j] = DATUM.format(d).split(".").map(Number); return Date.UTC(j ?? 0, (m ?? 1) - 1, t ?? 1) / TAG; }; return Math.round(tagesnummer(alsDatum(wert)) - tagesnummer(alsDatum(jetzt))); }