mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
The card and the table carried byte-identical copies of the day-label arithmetic. Two copies is how they drift, and neither was reachable from a test, which is why the double conversion in both survived review. Lift it into resolveNextDueDayLabel, which returns a discriminated label the callers map onto their own strings, and cover the case the old code could not get right: a browser zone that differs from the profile zone. Those tests pin the host zone explicitly — the defect was invisible whenever host and profile agree, which is the only configuration a UTC-only suite runs in. Also pin that the window-status verdict is identical from every host zone, including one whose spring-forward gap swallows the profile wall clock.
56 lines
2.2 KiB
TypeScript
56 lines
2.2 KiB
TypeScript
/**
|
|
* Which day label the next-intake line carries — "today", "tomorrow", a
|
|
* weekday name, or a full date.
|
|
*
|
|
* Lifted out of `<MedicationCard>` and `<MedicationTable>`, which carried
|
|
* byte-identical copies of this arithmetic. Both surfaces must agree; two
|
|
* copies is how they drift.
|
|
*
|
|
* The caller maps the result onto its own i18n strings, so this module
|
|
* stays free of translation plumbing and is directly testable.
|
|
*/
|
|
import { localDayIndex, wallClockInTz } from "@/lib/tz/wall-clock";
|
|
|
|
export type NextDueDayLabel =
|
|
| { kind: "today" }
|
|
| { kind: "tomorrow" }
|
|
/** 0 = Sunday … 6 = Saturday, read in the PROFILE zone. */
|
|
| { kind: "weekday"; weekday: number }
|
|
/**
|
|
* Far enough out to want a full date. Carries the RAW instant — the
|
|
* formatter applies the profile zone itself, so handing it a
|
|
* zone-shifted `Date` would convert twice.
|
|
*/
|
|
| { kind: "date"; instant: Date };
|
|
|
|
/**
|
|
* Resolve the label for a next-due instant relative to `now`, both read in
|
|
* `tz` (the user's PROFILE timezone — never the browser's).
|
|
*
|
|
* Two properties this function exists to guarantee:
|
|
*
|
|
* - The distance is a CALENDAR-day difference, not an hour count. A
|
|
* `(next - now) / 86_400_000` round admits the 23 h / 25 h length of a
|
|
* DST-transition day and can round "tomorrow" into "today".
|
|
* - Every field is read through `wallClockInTz` in the profile zone, and
|
|
* the `date` case hands back the untouched instant. The previous
|
|
* implementation pre-shifted the instant into the profile zone and then
|
|
* passed that shifted value to a formatter that applies the zone AGAIN,
|
|
* so the rendered day was wrong for any user whose browser zone differs
|
|
* from their profile zone — invisible whenever the two agree.
|
|
*/
|
|
export function resolveNextDueDayLabel(
|
|
nextAt: Date,
|
|
now: Date,
|
|
tz: string,
|
|
): NextDueDayLabel {
|
|
const nextParts = wallClockInTz(nextAt, tz);
|
|
const dayDelta =
|
|
localDayIndex(nextParts) - localDayIndex(wallClockInTz(now, tz));
|
|
|
|
if (dayDelta === 0) return { kind: "today" };
|
|
if (dayDelta === 1) return { kind: "tomorrow" };
|
|
if (dayDelta <= 5) return { kind: "weekday", weekday: nextParts.weekday };
|
|
return { kind: "date", instant: nextAt };
|
|
}
|