Midnight Comes at One O'Clock
I keep a small page called A Small Light. It shows one short thought a day, chosen by the day of the year. No cookies, no accounts, nothing to sign. Just the thought, and tomorrow a different one.
Except it doesn’t change at midnight. In summer, where I live, it changes at one o’clock.
If you have code that looks like this, yours does too:
var now = new Date();
var start = new Date(now.getFullYear(), 0, 0);
var diff = now - start;
var oneDay = 86400000;
var day = Math.floor(diff / oneDay);
It reads well. It’s short. It passes every test you’ll think to write at noon.
What goes wrong
start is local midnight on December 31st, in winter time. diff counts real milliseconds since then. Dividing by 86,400,000 assumes every day since has been exactly 24 hours long.
In spring, one day was 23 hours. So all summer, local midnight comes an hour before another full 24-hour block has passed. From 00:00 to 01:00, Math.floor still gives yesterday.
You can see it with one line:
TZ=Europe/Prague node -e 'var n=new Date(2026,9,3,0,30),s=new Date(2026,0,0);console.log(Math.floor((n-s)/864e5))'
That prints 275. At half past midnight on October 3rd, 2026, it is day 276.
How often
I didn’t want to guess, so I checked every day of 2026 at three local times: 00:30, 12:00 and 23:30.
| Time zone | Days wrong | When |
|---|---|---|
| Europe/Prague | 210 | at 00:30, from 30 March to 25 October |
| America/New_York | 238 | at 00:30, from 9 March to 1 November |
| Australia/Sydney | 182 | at 23:30, from 5 April to 3 October |
| UTC, Asia/Tokyo | 0 | never (no summer time) |
At noon, the answer was right every day in every zone.
Sydney runs the other way. There, December 31st falls in summer time, and the day the clocks went back in April was 25 hours long. So all through their winter, an hour more has passed than the count expects, and it rolls over at 23:00. The day turns early instead of late. Same bug, other side of the planet, other end of the night.
Check yours
Save this as check-day-of-year.js and paste your own function at the top. It has to take the date as an argument. If yours calls new Date() inside, pass now in instead, just for the check.
// Paste your function here. It must take the date as an argument.
function dayOfYear(now) {
var start = new Date(now.getFullYear(), 0, 0);
var diff = now - start;
var oneDay = 86400000;
var day = Math.floor(diff / oneDay);
return day;
}
// Check it: every day of the year, every 15 minutes, local time.
// The calendar gives the right answer: new Date(year, 0, i) is day i.
var year = +process.argv[2] || new Date().getFullYear();
var wrong = [];
for (var i = 1; new Date(year, 0, i).getFullYear() === year; i++) {
for (var m = 0; m < 24 * 60; m += 15) {
var t = new Date(year, 0, i, 0, m);
if (t.getDate() !== new Date(year, 0, i).getDate()) continue; // clock skipped past midnight
var got = dayOfYear(t);
if (got !== i) { wrong.push(t.toString() + " -> " + got + ", expected " + i); break; }
}
}
var zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
console.log(zone + " " + year + ": " + wrong.length + " days wrong");
if (wrong.length) console.log(" first: " + wrong[0] + "\n last: " + wrong[wrong.length - 1]);
Then run it in a few zones that change their clocks:
TZ=Europe/Prague node check-day-of-year.js 2026
TZ=America/New_York node check-day-of-year.js 2026
TZ=Australia/Sydney node check-day-of-year.js 2026
It reports the first wrong moment it finds on each day, so Prague shows 00:00 where the table says 00:30. The whole hour after midnight is wrong, and the table only sampled half past.
This needs Node. A browser can’t switch time zones, so in a browser it only ever tests your own zone. The right answer comes from the calendar (new Date(year, 0, i) is day i), not from any formula, so the check doesn’t grade the fix with the fix. With the code from the top of this post pasted in, it prints the table above: 210, 238, 182.
The fix
Don’t count days with a stopwatch. Count them with a calendar:
function dayOfYear(now) {
return (Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()) -
Date.UTC(now.getFullYear(), 0, 0)) / 86400000;
}
It takes the local date, year, month and day, and lays it out in UTC, where every day is exactly 86,400,000 milliseconds long. The subtraction is exact. No floor needed, no hour lost. It reads only the date, so it gives the same answer at every hour of that date. In the same sweep it matched the old code at noon on every day, in every zone, where the old code is right. January 1st is 1, December 31st is 365 (366 in 2028). Paste it into the checker and it prints 0 days wrong. I tried Prague, New York, Sydney, Lord Howe Island (where the clocks move by half an hour), Santiago (where they change at midnight itself), UTC and Tokyo, for 2026 and for the leap year 2028.
The lesson I’d keep
A day is a calendar thing. Milliseconds are a clock thing. Summer time is exactly where the two disagree, and they disagree at the edges of the night, not in the middle of it.
So if you test anything that turns over at midnight, test it at 00:30 and 23:30, in a zone that changes its clocks, on a date in the other half of the year. Noon will tell you everything is fine.
In honesty
My own page still has this bug as I write. I found it yesterday, measured it today, and the fix is written. It waits behind one more change to the same file, a new thought for the page. I don’t want to stack two edits and get one of them wrong. If you live where the clocks change, and you open A Small Light in the hour after midnight in summer time, you’re reading yesterday’s light. It was a good one too.