Notes › Life Essays › A Countdown Day Is 86400 Seconds, So DST Can Steal a Day

A Countdown Day Is 86400 Seconds, So DST Can Steal a Day

The calendar turned 31 pages but the first box says 30

One entry counts down from 2026-03-01 00:00 to 2026-04-01 00:00 on a machine whose zone is America/New_York. The calendar sits 31 days apart, yet the first box on the page reads 30 days 23:00:00, and that 23 in the second box is hard to look at. The first guess was bad arithmetic; reading the code shows two definitions of a day booked into one ledger.

The front end is only those 181 template lines and 7,374 bytes, with five lines of arithmetic: line 121 takes the milliseconds of c.target - now, line 128 floors the millisecond place, line 129 runs days = Math.floor(totalSec / 86400) where the divisor is a hard-coded literal 86400, and lines 130-132 spread the remainder into hours, minutes and seconds. There is no round, ceil or toFixed, every step truncates toward zero, and this conversion reruns once per second on the countdown timer page.

Spread out, 2,674,800,000 milliseconds is 743 real hours, and cutting it 86400 seconds at a time yields only 30 whole blocks with 82800 seconds left over, read as 23 hours. The same pair of dates in the DST-free Asia/Shanghai zone is 2,678,400,000 milliseconds and 31 days 00:00. What is lost is a single hour, and the floor turns that into a missing day.

Only zones that observe DST drift apart

The groups below are measured through the script harness (Node 24 with TZ injected, FakeDate freezing the base stamp 1793070000000): the template has no time zone or DST branch anywhere, so the difference comes from the IANA rules of the machine that runs it, and every region with DST is hit.

  • New York 3-01 to 4-01: 31 calendar days become 30 days 23:00; London behaves the same across the same month span.
  • New York 11-01 to 11-03: the clock going back hands out one extra hour, 176400000 milliseconds, 49 hours, 2 calendar days become 02 days 01:00.
  • Sydney 3-31 to 4-30: 2595600000 milliseconds, 721 hours, 30 calendar days become 30 days 01:00.
  • Control pair 3-07 to 3-09: New York 169200000 milliseconds, 47 hours become 01 days 23:00; the same dates in Shanghai are 172800000 milliseconds and 02 days 00:00, matching digit for digit.
Fig. 1 Two ledgers, one span 03-01 31 calendar days 04-01 Page 30 23 00 00 days hrs min sec

Truncation is only visible at the boundary

Feed a diff into those five lines and the boundary looks like this, the same in all four zones:

Input diff in milliseconds versus page output
diff millisecondsPage output
86,399,00000 days 23:59:59
86,400,00001 days 00:00:00

When the diff is 1000 milliseconds short of a day the first box still shows zero, and round would push it to 01 days. The four boxes can never contradict one another, because lines 130-132 are a remainder split, and the harness proved days*86400 + hours*3600 + mins*60 + secs === totalSec over eight diffs. The only disagreement is between the day box and the number of pages the calendar turned.

A passed target leaves you one sentence

Line 124 reads if (diff <= 0), zero included; line 125 zeroes the four numbers, but lines 143-144 print only the green 已到达!, and those four zeros never reach the screen. A diffMs of 0, the -3155760000000 of 100 years ago, even the -1793070000000 of 1970-01-01T08:00 all give the same sentence: no minus sign, no error, and no reverse counter reading 已过期 N 天. The card keeps the 目标: line and the delete button.

Do not read the other row of zeros as an expiry signal: while the diff is between 1 and 999 milliseconds the page shows 00 days 00:00:00 with reached=NO, and a real Chrome run reproduced exactly that at diff=268 milliseconds on the moment the entry was added; only past 1000 milliseconds does it switch to 01 seconds. There is no third branch between the two, so the last second is by design one second short.

The default leaves you 1 to 3600 seconds

The target field is the <input type="datetime-local"> on line 20, not a date field, and it has no min, max or step: date and clock are forced to arrive as a pair, in 60 second steps. Typing only 2026-12-31 is sanitized into an empty string and raises the message on line 67, 请选择目标日期时间; the message on line 70, 日期格式无效, takes something like 275760-09-13T00:00 before a real browser will print it.

The first screen prefills neither 00:00 nor the current time but the next whole hour (lines 51-52 call setHours(getHours() + 1) and then zero minutes, seconds and milliseconds): on a base of 11:00:00 it prefills 2026-10-27T12:00 with 3600 seconds left, and at 23:59:59 it prefills next day 00:00 with one second left. Press Add without touching anything and the worst case on screen is 已到达!.

One timer drives 200 entries and remembers nothing

State lives only in that array on line 44: grepping the whole file for localStorage and sessionStorage returns zero hits, so a reload drops you back on line 34, 暂无倒计时,请在上方添加. Lines 98-101 run if (timer) return; before attaching setInterval(render, 1000), so one entry and 200 entries share a single handle, and the latter build is 295582 bytes; even after all three entries expire, intervals still reads 1 and clearInterval was called zero times.

Neither does error accumulate: every tick recomputes c.target - new Date() (lines 120-121) instead of counting itself down, so if the clock is pushed 86437400 milliseconds late the reading catches up in the next repaint. Nothing sounds when a target lands, though: Audio, Notification and document.title each score zero hits, so the promise of an expiry notice, 过期提醒:倒计时结束后自动提示, has no carrier.

The same 10 years folds two ways

On the calendar, 10 years is 315,619,200,000 milliseconds and prints 3653 days 00:00; 10 years folded at 365.25 days each is 315,576,000,000 milliseconds and prints 3652 days 12:00, a gap of 43,200,000 milliseconds, which is 12 hours. A 100 year span is blunter still: 36524 days against 36525 days differ by exactly 86,400,000 milliseconds, one whole day.

Fig. 2 Blocks lost to folding Across DST 3,600,000 ms 10 years, two folds 43,200,000 ms 100 years, two folds 86,400,000 ms

Of the 13 claims the template publishes about itself, measurement agrees with eight and contradicts five, and the three hardest are second-level accuracy, the expiry notice and days online: a day here is stock counted 86400 seconds to the block, not the pages the calendar tore off.

Quick answers

Q: Is that 30 days 23 hours a miscalculation?

It is a difference of definition. Line 129 spends 86400 seconds per day and counts whole blocks, while a calendar counts how many pages flipped. The two only separate where DST is observed: springs forward costs one hour, falls back hands one hour back, and the floor renders either as a one day gap.

Q: Can a passed target show a negative number or days overdue?

It cannot. Once diff <= 0 matches, the sentence on line 144, 已到达!, is what you get, so one day ago and 100 years ago look identical. The row 00 days 00:00:00 appears only inside the final 999 milliseconds before a target, and it is not an expiry flag.

Q: I only want the gap in days, or an alarm when time is up?

For a day gap, change the definition: the on-site date difference calculator counts calendar days and never carries a 23 hour tail. For a reminder, this page cannot help: it stores no list and makes no sound, so when time is up there is only that green line on screen.

📎 Related articles

How many characters fit on a copybook page: the default A4 gives 20, not 100 ÷ 4 = 25 2026-10-27 A day on both the weekend list and the make-up list: which rule the workday tool obeys 2026-10-27 Where the world clock offset comes from: one regex, and a hardcoded hour count that ignores daylight saving 2026-10-27 One calculation prints two body fat percentages: 6.1 points apart by default, 0.7 per 1 cm of waist 2026-10-26 A photo rotated 90° lies flat again on another device: how the EXIF flag and a real rotation split the work 2026-10-22