This commit is contained in:
jarek committed 2026-07-11 15:31:52 +02:00
1 parent 685132ff89
commit c718ac9a84
101 files changed
+5097 -642

No files matched your search

+72
View File
@@ -82,3 +82,75 @@ export function formatTimePartWith(
: `${hour24}:${minutes}`;
}
}
/**
* Offset of a timezone from UTC (in ms) at a given instant.
* Positive for zones ahead of UTC (e.g. UTC+2 -> 7_200_000).
*/
function tzOffsetMs(timeZone: string, at: Date): number {
const dtf = new Intl.DateTimeFormat('en-US', {
timeZone,
hourCycle: 'h23',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
const parts = dtf.formatToParts(at);
const get = (type: Intl.DateTimeFormatPartTypes) =>
parseInt(parts.find((p) => p.type === type)?.value ?? '0', 10);
const asUtc = Date.UTC(get('year'), get('month') - 1, get('day'), get('hour'), get('minute'), get('second'));
// Drop sub-second precision of `at` - formatToParts only has seconds
return asUtc - Math.floor(at.getTime() / 1000) * 1000;
}
/**
* Current calendar date in a timezone as 'YYYY-MM-DD'.
* Falsy timeZone means the browser's local timezone.
*/
export function currentDateInTimezone(timeZone?: string): string {
const now = new Date();
if (!timeZone) {
const y = now.getFullYear();
const m = String(now.getMonth() + 1).padStart(2, '0');
const d = String(now.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
const parts = new Intl.DateTimeFormat('en-CA', {
timeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).formatToParts(now);
const get = (type: Intl.DateTimeFormatPartTypes) => parts.find((p) => p.type === type)?.value ?? '';
return `${get('year')}-${get('month')}-${get('day')}`;
}
/**
* UTC instant (ISO string) of a day boundary in a timezone: midnight for the
* start of the day, 23:59:59.999 for the end. Used to turn date-only filter
* values ('YYYY-MM-DD', meaning a day in the user's configured timezone) into
* timestamps comparable against UTC-stored event times (#1269).
* Falsy timeZone means the browser's local timezone.
*/
export function dayBoundaryToUtcISO(dateStr: string, timeZone: string | undefined, endOfDay: boolean): string {
const [y, m, d] = dateStr.split('-').map(Number);
const h = endOfDay ? 23 : 0;
const min = endOfDay ? 59 : 0;
const s = endOfDay ? 59 : 0;
const ms = endOfDay ? 999 : 0;
if (!timeZone) {
return new Date(y, m - 1, d, h, min, s, ms).toISOString();
}
const wallClockUtc = Date.UTC(y, m - 1, d, h, min, s, ms);
// Two passes so DST transitions near the boundary converge on the right offset
let ts = wallClockUtc;
for (let i = 0; i < 2; i++) {
ts = wallClockUtc - tzOffsetMs(timeZone, new Date(ts));
}
return new Date(ts).toISOString();
}