日历翻了 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时,逐位一致。
截断只在临界值上看得见
把 diff 喂进那 5 行,临界点如下(四个时区一致):
| diff 毫秒 | 页面显示 |
|---|---|
| 86399000 | 00天 23时 59分 59秒 |
| 86400000 | 01天 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 天。
模板对外那 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 时的尾巴。提醒这里给不了——本页既不存清单也不出声,到点只有画面上一句绿字。