Daylight saving in a time zone planner: the bugs that hide in a 25-hour day
How Timezone Buddy keeps meeting times right when clocks change, with the code for converting local time to a moment, and the bug we fixed where a day was always 24 hours long.
A time zone planner looks simple: take everyone's local time, line the clocks up, find a time that is not the middle of anyone's night. The hard part is that clocks change, and not at the same time everywhere. This post covers what we learned building Timezone Buddy, with the code that matters.
Do not write down offsets#
The first rule is to never store "India is UTC+5:30" or "New York is UTC-5". New York is UTC-5 in winter and UTC-4 in summer, and the switch dates move around by country and by year. A table of offsets goes wrong the day a government changes its rules.
Browsers already carry the full time zone database, and Intl.DateTimeFormat can tell you the wall-clock time in any zone at any moment. From that you can work out the offset for that exact moment:
/** Minutes that `zone` is ahead of UTC at the moment `ms`. */
function offsetMinutes(zone, ms) {
const p = wallClock(zone, ms) // year, month, day, hour, minute, second in that zone
const asUtc = Date.UTC(p.y, p.m - 1, p.d, p.h, p.min, p.sec)
return Math.round((asUtc - Math.floor(ms / 1000) * 1000) / 60000)
}
wallClock is a small helper over Intl.DateTimeFormat(...).formatToParts(ms). The browser does all the daylight saving maths, and the code never mentions a date when clocks change.
From "9:00 on 4 November in Kolkata" to a moment#
The reverse direction is trickier. A person types a local time, and you need the exact moment it means. The offset depends on the moment, which is what you are trying to find. The fix is to guess, then correct:
function zonedToInstant(zone, y, m, d, h, min) {
const guess = Date.UTC(y, m - 1, d, h, min)
const first = guess - offsetMinutes(zone, guess) * 60000
return guess - offsetMinutes(zone, first) * 60000
}
The first pass uses the offset at the wrong moment, and the second pass corrects it using the offset at the first answer. Two passes are enough for real zones. We test it by round trip: convert a local time to a moment and back, in zones like Kolkata, Kathmandu (UTC+5:45), New York, London, Sydney and Auckland, and check we get the same time.
Two clock times are special. When clocks go forward, an hour does not exist (2:30 on that morning). When they go back, an hour happens twice. The code above gives a real moment in both cases: for the repeated hour it returns the first of the two, and for the missing time it returns the moment that reads an hour earlier on the clock (so 2:30 on a spring-forward morning in New York becomes 1:30). That is consistent and never crashes, but it is a choice, and a more careful app would warn the person when they pick one of these times.
The bug: a day is not always 24 hours#
Our first version of the timeline made 24 hourly slots starting at midnight in the first person's zone. That is wrong on two days a year in most places:
- When clocks go forward, the day has 23 hours, so 24 slots spill into the next day.
- When clocks go back, the day has 25 hours, so 24 slots miss the last hour.
We noticed because 1 November 2026 was a day when US clocks went back. The fix is to build the day from one local midnight to the next, and count the slots in between:
const start = zonedToInstant(zone, y, m, d, 0, 0)
const end = zonedToInstant(zone, nextY, nextM, nextD, 0, 0)
const count = Math.round((end - start) / SLOT) // SLOT is 30 minutes
With half-hour slots this gives 48 on a normal day, 46 on a 23-hour day and 50 on a 25-hour day. Lord Howe Island in Australia changes clocks by only 30 minutes, so its day has 47 slots. Our tests check all of these, and also that each day starts and ends at local midnight.
Two countries, two change dates#
The other trap is that neighbours do not switch on the same day. In 2026, the UK moved its clocks back on 25 October and the US on 1 November. For that week, London and New York are 4 hours apart instead of the usual 5. The same happens in March, when the US changes before the UK.
So a team in London and New York has the same shared working window most of the year, but a different one for a week:
| Dates | London and New York apart | Shared window (both 9:00 to 18:00), London time |
|---|---|---|
| Before 25 Oct | 5 hours | 14:00 to 18:00 |
| 25 Oct to 1 Nov | 4 hours | 13:00 to 18:00 |
| After 1 Nov | 5 hours | 14:00 to 18:00 |
A planner that checks only "today" will give the wrong answer for a meeting booked on the other side of a change. That is why Timezone Buddy works on a chosen date, and warns when anyone's clocks change within a few days of it.
Share a moment, not a clock time#
When you send someone the meeting time, send the moment and let their device show it locally. In the calendar invite we write the start as a UTC time (DTSTART:20261104T093000Z), so Google Calendar, Apple Calendar and Outlook each show it correctly for the person who opens it, including after clocks change.
What we did not solve#
A few places change their clocks at midnight, so the first midnight of the day can be skipped. We pick the first valid time, but we have not tested those zones in a browser. And the planner trusts the browser's time zone database, so a device with an out-of-date system will be wrong about a recent rule change.