The Duty Day Is Not Midnight
The Duty Day Is Not Midnight
An operator counts hours in days that start at 04:00 New York time. A driver logs work at 23:30 on 31 December, 03:30 on 1 January and 04:30 on 1 January. Which of these fall in the same day?
The naive approach
// The naive approach: group by the local calendar date
const day = (iso) =>
new Date(iso).toLocaleDateString("en-CA", { timeZone: "America/New_York" });
day("2024-01-01T04:30:00Z"); // "2023-12-31" — 23:30 on the 31st
day("2024-01-01T08:30:00Z"); // "2024-01-01" — 03:30 on the 1st
day("2024-01-01T09:30:00Z"); // "2024-01-01" — 04:30 on the 1st
// The 03:30 entry is counted with 04:30, in a day the operator has not started yet.Watch it break
The naive approach silently produces wrong results — there is no GMT equivalent to demonstrate because the failure is not using GMT.
Why
The day a rule counts is the operator's: a 24-hour period that starts at a time the operator designated, in a reference zone, wherever the person is. Local midnight is never that day unless the operator says so. With a 04:00 start, 23:30 on the 31st and 03:30 on the 1st are one day, and 04:30 opens the next. Grouping by calendar date splits the first pair and joins the second. The date is also read in the reference zone, not where the entry was logged: an entry from Tokyo still falls in a New York day. `dutyDayFor` takes the zone and the start time, with no default for either, and returns the real period. When a clock change falls inside it, the period is shorter or longer by the change: 23 or 25 hours across a one-hour change.
The gmt approach
import { dutyDayFor } from "@northguild/gmt";
const day = { timeZone: "America/New_York", dayStartTime: "04:00" };
dutyDayFor("2024-01-01T04:30:00Z", day); // { start: "2023-12-31T09:00:00Z", end: "2024-01-01T09:00:00Z" } — 23:30 on the 31st
dutyDayFor("2024-01-01T08:30:00Z", day); // { start: "2023-12-31T09:00:00Z", end: "2024-01-01T09:00:00Z" } — 03:30, the same day
dutyDayFor("2024-01-01T09:30:00Z", day); // { start: "2024-01-01T09:00:00Z", end: "2024-01-02T09:00:00Z" } — 04:30, the next
// Logged in Tokyo, read in New York: 22:00 on the 31st
dutyDayFor("2024-01-01T12:00:00+09:00[Asia/Tokyo]", day); // { start: "2023-12-31T09:00:00Z", end: "2024-01-01T09:00:00Z" }Same widget, working
In the playground, change the instant to "2024-01-01T09:00:00Z" and the day moves on: 04:00
local opens it. Change dayStartTime to "00:00" and the day is the calendar date, because
the operator said so. Try "2024-03-10T12:00:00Z" with dayStartTime: "02:30": 02:30 is
skipped that morning, so the day starts at 03:30 and is 23 hours long. Leave dayStartTime out
and the result is null: there is no default. The rules are in Hours of
Work.