Files
HealthLog/src/lib/timezone.ts
T
Marc-André Bombeck 38309932f3 Initial release v1.0.0
HealthLog — personal health-tracking PWA with weight, blood pressure,
pulse, mood tracking, medication compliance, Withings integration,
doctor report PDF export, and Dracula-themed UI.
2026-03-10 20:14:33 +01:00

107 lines
3.0 KiB
TypeScript

/**
* Robust timezone utilities using Intl.DateTimeFormat.
* Replaces the fragile `new Date(date.toLocaleString("en-US", { timeZone }))` pattern
* which can produce incorrect results due to locale-dependent date string parsing.
*/
interface LocalDateParts {
year: number;
month: number;
day: number;
hour: number;
minute: number;
second: number;
dayOfWeek: number;
}
/**
* Extract date/time parts for a given instant in a specific timezone.
* Uses Intl.DateTimeFormat.formatToParts for reliable, locale-independent parsing.
*/
export function getLocalDateParts(date: Date, tz: string): LocalDateParts {
const fmt = new Intl.DateTimeFormat("en-US", {
timeZone: tz,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
weekday: "short",
});
const parts = fmt.formatToParts(date);
const get = (type: Intl.DateTimeFormatPartTypes): string =>
parts.find((p) => p.type === type)?.value ?? "0";
// Intl hour12:false can return "24" for midnight in some engines
const rawHour = parseInt(get("hour"), 10);
const weekdayStr = get("weekday");
const weekdayMap: Record<string, number> = {
Sun: 0,
Mon: 1,
Tue: 2,
Wed: 3,
Thu: 4,
Fri: 5,
Sat: 6,
};
return {
year: parseInt(get("year"), 10),
month: parseInt(get("month"), 10),
day: parseInt(get("day"), 10),
hour: rawHour === 24 ? 0 : rawHour,
minute: parseInt(get("minute"), 10),
second: parseInt(get("second"), 10),
dayOfWeek: weekdayMap[weekdayStr] ?? 0,
};
}
/**
* Get the start (midnight) and end (23:59:59.999) of "today" in the user's timezone,
* returned as UTC Date objects suitable for database queries.
*/
export function getUserTodayBounds(
now: Date,
tz: string,
): { start: Date; end: Date } {
const parts = getLocalDateParts(now, tz);
// Build a UTC date that represents the user's local midnight
// by computing the offset between the real UTC time and the local representation
const localMidnightAsUtc = new Date(
Date.UTC(parts.year, parts.month - 1, parts.day, 0, 0, 0, 0),
);
// Compute offset: how far ahead (positive) or behind (negative) the tz is from UTC
// localMidnightAsUtc represents "midnight in tz as if it were UTC"
// We need to find what UTC instant corresponds to midnight in the given tz
const localNowAsUtc = new Date(
Date.UTC(
parts.year,
parts.month - 1,
parts.day,
parts.hour,
parts.minute,
parts.second,
),
);
const offsetMs =
Math.round((localNowAsUtc.getTime() - now.getTime()) / 60000) * 60000;
const start = new Date(localMidnightAsUtc.getTime() - offsetMs);
const end = new Date(start.getTime() + 24 * 60 * 60 * 1000 - 1);
return { start, end };
}
/**
* Get the day of the week (0=Sun..6=Sat) for a given instant in a specific timezone.
*/
export function getDayOfWeekInTz(now: Date, tz: string): number {
return getLocalDateParts(now, tz).dayOfWeek;
}