diff --git a/src/lib/analytics/correlations-fast-path.ts b/src/lib/analytics/correlations-fast-path.ts index e5fc0e07a..cc6b00c1b 100644 --- a/src/lib/analytics/correlations-fast-path.ts +++ b/src/lib/analytics/correlations-fast-path.ts @@ -58,6 +58,7 @@ import { type CorrelationResult, } from "@/lib/insights/correlations"; import { isNearUtc, userDayKey } from "@/lib/tz/resolver"; +import { wallClockInTz } from "@/lib/tz/wall-clock"; /** * v1.4.37 W2 — cold-path correlation window. Trim from 30 to 28 days @@ -368,34 +369,8 @@ function dateFromDayKey(key: string): Date { return new Date(`${key}T00:00:00.000Z`); } -const WEEKDAY_FORMATTER_CACHE = new Map(); -function getWeekdayFormatter(timeZone: string): Intl.DateTimeFormat { - let formatter = WEEKDAY_FORMATTER_CACHE.get(timeZone); - if (!formatter) { - formatter = new Intl.DateTimeFormat("en-CA", { - timeZone, - year: "numeric", - month: "2-digit", - day: "2-digit", - weekday: "short", - }); - WEEKDAY_FORMATTER_CACHE.set(timeZone, formatter); - } - return formatter; -} - -const ISO_WEEKDAY: Record = { - Mon: 1, - Tue: 2, - Wed: 3, - Thu: 4, - Fri: 5, - Sat: 6, - Sun: 7, -}; - function isoWeekdayInTz(d: Date, timeZone: string): number { - const parts = getWeekdayFormatter(timeZone).formatToParts(d); - const weekday = parts.find((p) => p.type === "weekday")?.value ?? "Mon"; - return ISO_WEEKDAY[weekday] ?? 1; + // `wallClockInTz` returns 0=Sun..6=Sat; remap to ISO 1=Mon..7=Sun. + const weekday = wallClockInTz(d, timeZone).weekday; + return weekday === 0 ? 7 : weekday; } diff --git a/src/lib/measurements/consolidation-tz.ts b/src/lib/measurements/consolidation-tz.ts index 35fe4e361..1f58be04c 100644 --- a/src/lib/measurements/consolidation-tz.ts +++ b/src/lib/measurements/consolidation-tz.ts @@ -22,6 +22,9 @@ * every existing import site keeps working unchanged. */ import type { MeasurementType } from "@/generated/prisma/client"; +// `userDayKey` lives in the pure `@/lib/tz/format` leaf (no Prisma / +// node:module pull) so importing it keeps this dependency-free module clean. +import { userDayKey } from "@/lib/tz/format"; /** * Canonical grace window shared by the cumulative + mean drains. Rows @@ -52,12 +55,12 @@ export interface PerSampleRow { /** * Resolve the user's calendar-day key (`YYYY-MM-DD`) for a given - * timestamp + timezone. Reuses the same `sv-SE` Intl formatting choice - * the mood-entries path locked in v1.4.25 W7b so iOS-side and - * server-side day-keys round-trip byte-identically. + * timestamp + timezone. Delegates to the canonical `userDayKey` so the + * server-side and iOS-side day-keys round-trip byte-identically against + * every other day-bucketed surface. */ export function dayKeyForUserTz(date: Date, tz: string): string { - return new Intl.DateTimeFormat("sv-SE", { timeZone: tz }).format(date); + return userDayKey(date, tz); } /** diff --git a/src/lib/medications/scheduling/compliance.ts b/src/lib/medications/scheduling/compliance.ts index ab450ef98..1ee94d230 100644 --- a/src/lib/medications/scheduling/compliance.ts +++ b/src/lib/medications/scheduling/compliance.ts @@ -39,6 +39,7 @@ import { type RecurrenceContext, } from "./recurrence"; import { normaliseDoseWindows } from "./worker-helpers"; +import { userDayKey } from "@/lib/tz/resolver"; export interface ComplianceChips { /** 0-100, taken / (taken + missed). Skipped doses are excluded from @@ -62,9 +63,10 @@ const DAY_MS = 24 * 60 * 60 * 1000; /** * Day-key in the user's timezone (or system-local when `tz` is - * undefined). `Intl.DateTimeFormat` with `en-CA` reliably returns - * `YYYY-MM-DD`, which sorts lexically and matches the chart - * legend's `day` ticks. + * undefined). Routes through the canonical `userDayKey` so the + * `YYYY-MM-DD` bucket stays byte-for-byte aligned with every other + * day-bucketed surface; the host-local branch covers the pre-per-user + * callers that pass no zone. */ function localDayKey(d: Date, tz: string | undefined): string { if (!tz) { @@ -73,12 +75,7 @@ function localDayKey(d: Date, tz: string | undefined): string { const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; } - return new Intl.DateTimeFormat("en-CA", { - timeZone: tz, - year: "numeric", - month: "2-digit", - day: "2-digit", - }).format(d); + return userDayKey(d, tz); } /**