function extractDateParts(date: Date, timeZone: string) { const formatter = new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit", }); const parts = formatter.formatToParts(date); const year = Number(parts.find((part) => part.type === "year")?.value); const month = Number(parts.find((part) => part.type === "month")?.value); const day = Number(parts.find((part) => part.type === "day")?.value); return { year, month, day }; } export function getDateStringInTimeZone( date: Date, timeZone: string, ): string { const { year, month, day } = extractDateParts(date, timeZone); return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`; } export function getShiftedDateStringInTimeZone( daysOffset: number, timeZone: string, baseDate: Date = new Date(), ): string { const { year, month, day } = extractDateParts(baseDate, timeZone); const shifted = new Date(Date.UTC(year, month - 1, day)); shifted.setUTCDate(shifted.getUTCDate() + daysOffset); return shifted.toISOString().split("T")[0]; } function getTimeZoneOffsetMs(date: Date, timeZone: string): number { const formatter = new Intl.DateTimeFormat("en-US", { timeZone, timeZoneName: "shortOffset", }); const offsetLabel = formatter.formatToParts(date).find((part) => part.type === "timeZoneName") ?.value || "GMT+0"; const match = offsetLabel.match(/GMT([+-])(\d{1,2})(?::?(\d{2}))?/); if (!match) return 0; const sign = match[1] === "-" ? -1 : 1; const hours = Number(match[2] || "0"); const minutes = Number(match[3] || "0"); return sign * (hours * 60 + minutes) * 60 * 1000; } export function getDayBoundsForTimeZone( dateString: string, timeZone: string, ): { startMs: number; endMs: number } { const [year, month, day] = dateString.split("-").map(Number); const startGuess = new Date(Date.UTC(year, month - 1, day, 0, 0, 0)); const nextDayGuess = new Date(Date.UTC(year, month - 1, day + 1, 0, 0, 0)); const startOffsetMs = getTimeZoneOffsetMs(startGuess, timeZone); const nextDayOffsetMs = getTimeZoneOffsetMs(nextDayGuess, timeZone); const startMs = Date.UTC(year, month - 1, day, 0, 0, 0) - startOffsetMs; const nextDayStartMs = Date.UTC(year, month - 1, day + 1, 0, 0, 0) - nextDayOffsetMs; return { startMs, endMs: nextDayStartMs - 1, }; } export function getDateOnlyValueForTimeZone( timeZone: string, date: Date = new Date(), ): Date { return new Date(`${getDateStringInTimeZone(date, timeZone)}T00:00:00.000Z`); }