const BERLIN_TZ = "Europe/Berlin"; /** * Converts a JS Date (a UTC instant) into its Europe/Berlin wall-clock * representation for iCalendar's local `TZID` date-time form * (`DTSTART;TZID=Europe/Berlin:YYYYMMDDTHHMMSS`). Uses the ICU timezone * database via Intl rather than manual UTC+1/+2 arithmetic, so daylight * saving transitions are always handled correctly — no naive UTC offset * math that would drift by an hour half the year. */ export function toBerlinICSDateTime(date: Date): string { const parts = new Intl.DateTimeFormat("en-US", { timeZone: BERLIN_TZ, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23", }).formatToParts(date); const get = (type: string) => parts.find((p) => p.type === type)?.value ?? "00"; return `${get("year")}${get("month")}${get("day")}T${get("hour")}${get("minute")}${get("second")}`; } /** UTC timestamp form (`YYYYMMDDTHHMMSSZ`) — used only for DTSTAMP, which marks generation time, not a local event time. */ export function toUTCICSStamp(date: Date): string { return date.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z"; } /** * Standard IANA Europe/Berlin VTIMEZONE block (the same one commonly * embedded by calendar exporters): CEST from the last Sunday in March, * CET from the last Sunday in October — the actual EU DST rule, not an * approximation. */ export const BERLIN_VTIMEZONE_LINES = [ "BEGIN:VTIMEZONE", "TZID:Europe/Berlin", "X-LIC-LOCATION:Europe/Berlin", "BEGIN:DAYLIGHT", "TZOFFSETFROM:+0100", "TZOFFSETTO:+0200", "TZNAME:CEST", "DTSTART:19700329T020000", "RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU", "END:DAYLIGHT", "BEGIN:STANDARD", "TZOFFSETFROM:+0200", "TZOFFSETTO:+0100", "TZNAME:CET", "DTSTART:19701025T030000", "RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU", "END:STANDARD", "END:VTIMEZONE", ];