/** * Terminplaner-Zeit: Wanduhr Europe/Berlin <-> UTC, nur ueber Intl (kein Browser-Datum, keine Zusatzbibliothek). * Tage sind 'YYYY-MM-DD'-Strings, Uhrzeiten 'HH:MM'; Kalenderrechnung (plusTage, wochentag) fasst nie einen Zeitpunkt an. * Nicht existierende Wanduhrzeiten (Maerz-Umstellung) liefert zuUtc als null, doppelte (Oktober) als erstes Vorkommen. * Vorlage fuer offsetMs: /root/reports/terminplaner-2026-10-02/spike-code/tz.mjs */ export const TZ = 'Europe/Berlin' const TAG_RE = /^\d{4}-\d{2}-\d{2}$/ const ZEIT_RE = /^\d{2}:\d{2}$/ const MONAT_RE = /^\d{4}-\d{2}$/ const WOCHENTAGE = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'] const MONATE = [ 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember', ] const pad = (n) => String(n).padStart(2, '0') const formatierer = new Intl.DateTimeFormat('en-US', { timeZone: TZ, hourCycle: 'h23', year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', }) /** Versatz (Wanduhr minus UTC) in ms zum UTC-Zeitpunkt utcMs. */ function offsetMs(utcMs) { const p = {} for (const { type, value } of formatierer.formatToParts(utcMs)) p[type] = Number(value) const alsUtc = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second) return alsUtc - Math.floor(utcMs / 1000) * 1000 } /** Ist tag ein echter Kalendertag (kein 30.02.)? */ function gueltigerTag(tag) { if (typeof tag !== 'string' || !TAG_RE.test(tag)) return false const [y, m, d] = tag.split('-').map(Number) return new Date(Date.UTC(y, m - 1, d)).toISOString().slice(0, 10) === tag } /** * Berliner Wanduhr -> UTC-Zeitpunkt (Date). '24:00' = Folgetag 00:00. * null bei ungueltiger Eingabe und bei nicht existierenden Zeiten (Luecke der Maerz-Umstellung); * doppelte Zeiten (Oktober) ergeben das erste Vorkommen (Sommerzeit). */ export function zuUtc(tag, hhmm) { if (!gueltigerTag(tag) || typeof hhmm !== 'string' || !ZEIT_RE.test(hhmm)) return null const [h, mi] = hhmm.split(':').map(Number) if (h > 24 || mi > 59 || (h === 24 && mi !== 0)) return null const [y, mo, d] = tag.split('-').map(Number) const wand = Date.UTC(y, mo - 1, d, h, mi) // die Wanduhr als ob UTC; 24:00 rollt auf den Folgetag const treffer = [...new Set([wand - offsetMs(wand - 86400000), wand - offsetMs(wand + 86400000)])] .filter((t) => t + offsetMs(t) === wand) .sort((a, b) => a - b) return treffer.length ? new Date(treffer[0]) : null } /** UTC-Zeitpunkt -> Berliner Wanduhr { tag: 'YYYY-MM-DD', uhrzeit: 'HH:MM' } (Sekunden werden abgeschnitten). */ export function ausUtc(date) { const t = new Date(date.getTime() + offsetMs(date.getTime())) return { tag: `${t.getUTCFullYear()}-${pad(t.getUTCMonth() + 1)}-${pad(t.getUTCDate())}`, uhrzeit: `${pad(t.getUTCHours())}:${pad(t.getUTCMinutes())}`, } } /** ISO-Wochentag 1..7 (Montag = 1) eines 'YYYY-MM-DD'. */ export function wochentag(tag) { const [y, m, d] = tag.split('-').map(Number) const w = new Date(Date.UTC(y, m - 1, d)).getUTCDay() return w === 0 ? 7 : w } /** Kalendertag + n Tage (n darf negativ sein), unabhaengig von der Zeitumstellung. */ export function plusTage(tag, n) { const [y, m, d] = tag.split('-').map(Number) return new Date(Date.UTC(y, m - 1, d + n)).toISOString().slice(0, 10) } /** Berliner Kalendertag des Zeitpunkts now. */ export function heute(now = new Date()) { return ausUtc(now).tag } /** * Kalenderdaten eines Monats 'YYYY-MM': { label: 'Oktober 2026', ersterWochentag (ISO 1..7), anzahlTage, * vorher, nachher ('YYYY-MM') }. Ungueltiger Monat => null. */ export function monatInfo(monat) { if (typeof monat !== 'string' || !MONAT_RE.test(monat)) return null const [y, m] = monat.split('-').map(Number) if (m < 1 || m > 12) return null const monatString = (jahr, nr) => { const t = new Date(Date.UTC(jahr, nr - 1, 1)) return `${t.getUTCFullYear()}-${pad(t.getUTCMonth() + 1)}` } return { label: `${MONATE[m - 1]} ${y}`, ersterWochentag: wochentag(`${monat}-01`), anzahlTage: new Date(Date.UTC(y, m, 0)).getUTCDate(), vorher: monatString(y, m - 1), nachher: monatString(y, m + 1), } } /** 'Montag, 12.10.2026' (Berliner Wanduhr). */ export function datumLang(date) { const { tag } = ausUtc(date) return `${WOCHENTAGE[wochentag(tag) - 1]}, ${datumKurzVonTag(tag)}` } /** '12.10.2026' (Berliner Wanduhr). */ export function datumKurz(date) { return datumKurzVonTag(ausUtc(date).tag) } /** '10:30' (Berliner Wanduhr). */ export function uhrzeit(date) { return ausUtc(date).uhrzeit } function datumKurzVonTag(tag) { const [y, m, d] = tag.split('-') return `${d}.${m}.${y}` }