杂记 › 生活随笔 › 倒计时里的 86400 秒一天,碰上夏令时会少给你 1 天

倒计时里的 86400 秒一天,碰上夏令时会少给你 1 天

日历翻了 31 页,第一格只写着 30

一条从 2026-03-01 00:00 倒数到 2026-04-01 00:00 的条目,机器时区 America/New_York。日历上明明隔着 31 天,页面第一格写的却是 30天 23时 00分 00秒,第二格那个 23 格外刺眼。第一反应是算错了,读完代码才知道是两套「天」在一起记账。

前端只有模板那 181 行、7374 字节,算术拢共 5 行:第 121 行取 c.target - now 的毫秒,第 128 行 floor 掉毫秒位,第 129 行 days = Math.floor(totalSec / 86400)——除数是硬编码字面量 86400,第 130—132 行再取余摊成时、分、秒。没有 round、ceil、toFixed,每步向零截断,这条折算在倒计时页每秒重跑一次。

2674800000 毫秒摊开是 743 个真实小时,按 86400 秒一块只数得出 30 块,余下 82800 秒读成 23 时;同组日期在无夏令时的 Asia/Shanghai 是 2678400000 毫秒、31天 00时。亏掉的只是 1 小时,被 floor 一挡就成了少 1 天。

只有带夏令时的地区会错开

下列几组是脚本口径实测(Node 24 注入 TZ、FakeDate 冻结基准 1793070000000):模板里没有任何时区或夏令时分支,出入来自运行机器的 IANA 规则,凡是有夏令时的地区都会撞上。

  • 纽约 3-01 到 4-01:日历 31 天 → 30天 23时;伦敦同月段一致。
  • 纽约 11-01 到 11-03:回拨多给 1 小时,176400000 毫秒、49 小时、日历 2 天 → 02天 01时。
  • 悉尼 3-31 到 4-30:2595600000 毫秒、721 小时、日历 30 天 → 30天 01时。
  • 对照 3-07 到 3-09:纽约 169200000 毫秒、47 小时 → 01天 23时;同组在上海是 172800000 毫秒 → 02天 00时,逐位一致。
图 1 同一段区间的两套账 03-01 日历 31 天 04-01 页面 30 23 00 00 天 时 分 秒

截断只在临界值上看得见

把 diff 喂进那 5 行,临界点如下(四个时区一致):

输入 diff 毫秒与页面显示
diff 毫秒页面显示
8639900000天 23时 59分 59秒
8640000001天 00时 00分 00秒

离 1 天只差 1000 毫秒时第一格仍是 0,换成 round 就会跳成 01天。四格之间不可能自相矛盾,第 130—132 行是取余分解,harness 用 8 组 diff 验过 days*86400 + hours*3600 + mins*60 + secs === totalSec 恒成立,矛盾的只有天格与日历天数。

目标过了只给你一句「已到达!」

第 124 行写的是 if (diff <= 0),含等于 0;第 125 行把四个数归零,可第 143—144 行只输出绿色的「已到达!」,那 4 个 0 从不上屏。diffMs=0、100 年前的 -3155760000000、连 1970-01-01T08:00 的 -1793070000000,都是同一句:没有负号、没有报错,也没有「已过期 N 天」的反向计时,卡片只留着「目标:」一行和删除按钮。

别把另一排 0 当成过期信号:diff 在 1 到 999 毫秒之间是 00天 00时 00分 00秒 且 reached=NO,真实 Chrome 添加瞬间 diff=268 毫秒复现的正是它,跨过 1000 毫秒才换 01秒。两分支之间没有第三条路,最后一秒的读数天生少 1 秒。

默认值只留给你 1 到 3600 秒

目标框是第 20 行的 <input type="datetime-local">,不是 date,也没有 min、max、step:日期加时刻强制成对,步进 60 秒。只写 2026-12-31 会被净化成空串,弹第 67 行「请选择目标日期时间」;第 70 行那句「日期格式无效」在真浏览器里只有 275760-09-13T00:00 打得出。

首屏预填不是 00:00 也不是当前时刻,而是下一个整点(第 51—52 行 setHours(getHours() + 1) 后把分秒毫秒归零):基准 11:00:00 预填 2026-10-27T12:00,剩 3600 秒;23:59:59 预填次日 00:00,只剩 1 秒。不改动直接点添加,最坏当场就是「已到达!」。

一个计时器管 200 条,却什么都不记得

状态只活在第 44 行那个数组里:全文 grep localStorage、sessionStorage 命中 0 处,刷新一次就回到第 34 行「暂无倒计时,请在上方添加」。第 98—101 行 if (timer) return; 之后才挂 setInterval(render, 1000),1 条和 200 条都只有 1 个句柄,后者产物 295582 字节;3 条全部过期后仍是 intervals=1、clearInterval 0 次。

误差也不累积:每次都是 c.target - new Date() 重算(第 120—121 行)而非自减,时钟推后 86437400 毫秒,读数当场追上。但到点什么都不会响——Audio、Notification、document.title 全部 0 命中,「过期提醒:倒计时结束后自动提示」没有载体。

同一个 10 年有两种折法

日历上的 10 年是 315619200000 毫秒 → 3653天 00时;按 365.25 天折的 10 年是 315576000000 毫秒 → 3652天 12时,差 43200000 毫秒即 12 小时。100 年更干脆,36524天 与 36525天 正好差 86400000 毫秒、1 天。

图 2 被折没的时间块 跨夏令时那组 3600000 毫秒 10 年两种折法 43200000 毫秒 100 年两种折法 86400000 毫秒

模板对外那 13 条断言实测一致 8 条、冲突 5 条,最硬的三条正是精确到秒、过期提醒、在线天数:这里的天是 86400 秒一块的存货,不是日历撕掉的页数。

顺手答疑

Q:30天 23时 是页面算错了吗?

是口径不同:第 129 行拿 86400 秒当一天,数的是整块时间,日历数的是日期换了几页。所在地区实行夏令时才会错开,进夏令时少 1 小时、回拨多 1 小时,被 floor 一挡就体现成 1 天的出入。

Q:目标过了能显示负数或已过几天吗?

不能。diff <= 0 一命中就走第 144 行那句「已到达!」,1 天前和 100 年前一模一样;那排 00天 00时 00分 00秒 只在目标前最后 999 毫秒里出现,不是过期标志。

Q:只想数相差几天,或者要一个到点提醒?

数天数请换口径:站内日期天数计算按日历日数,不会挂着 23 时的尾巴。提醒这里给不了——本页既不存清单也不出声,到点只有画面上一句绿字。

📎 相关文章

字帖每页放几个字:默认 A4 是 20 个,不是 100÷4=25 2026-10-27 同一天既是周末又被登记成补班,工作日计算器听谁的 2026-10-27 世界时间的偏移是谁给的:一条正则,和一个没有夏令时的写死小时数 2026-10-27 同一次计算给出两个体脂率:默认一组差 6.1 个百分点,腰围 1 cm 值 0.7 2026-10-26 照片转了 90°,换台设备方向又回来了:EXIF 与真实旋转的分工 2026-10-22