refactor(tz): route day-key copies through the canonical userDayKey

Compliance streaks, the measurement-consolidation day key, and the
correlations weekday helper each carried their own Intl formatter
(en-CA / sv-SE / short-weekday). Delegate them to userDayKey and
wallClockInTz so every day-bucket key stays byte-for-byte aligned and the
locale decision lives in one place.
This commit is contained in:
Marc-André Bombeck committed 2026-06-18 18:27:06 +02:00
1 parent 1b354de68b
commit cbfc4a36d2
3 files changed
+17 -42

No files matched your search

+4 -29
View File
@@ -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<string, Intl.DateTimeFormat>();
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<string, number> = {
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;
}
+7 -4
View File
@@ -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);
}
/**
+6 -9
View File
@@ -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);
}
/**