【寻迹校园 HarmonyOS NEXT 实战 19】“今天/近3天/近7天”为什么容易出错:自然日边界测试实战
【寻迹校园 HarmonyOS NEXT 实战 19】“今天/近3天/近7天”为什么容易出错:自然日边界测试实战
这是“寻迹校园 HarmonyOS NEXT 实战”系列第 19 篇。本文结合
matchesHomeTimeRange()与固定时钟测试,说明为什么不能直接用两个时间戳相减判断“近 3 天”,以及如何严格解析YYYY-MM-DD、按本机自然日计算边界、拒绝未来日期和非法日期。

上图为原创生成的技术插画,不是项目截图。时间筛选比较的是日历格,而不是“当前时刻向前滚动 72 小时”的连续窗口;这两个定义在每天的大部分时间里并不相同。
一、“近 3 天”首先是产品定义问题
假设现在是 2026-08-12 23:30,一条记录发生在 2026-08-10 00:10。
按自然日理解,它属于今天、昨天、前天,应进入“近 3 天”。但两个时间戳相差超过 71 小时,若边界稍有偏差就可能被误排除。
反过来,一条发生在 8 月 9 日 23:40 的记录,距当前不足 72 小时,却已经跨过 8 月 9、10、11、12 四个日期,不应进入“近 3 天自然日”。
因此项目明确采用:
- 今天:日期差为 0;
- 近 3 天:今天 + 前 2 个自然日;
- 近 7 天:今天 + 前 6 个自然日;
- 未来日期:不命中;
- 非法日期:不命中;
- 全部:不施加时间过滤。
先写清业务语义,代码才有可验证目标。
二、为什么直接除以 24 小时不可靠
常见错误写法是:
const days = Math.floor((Date.now() - new Date(eventDate).getTime()) / 86400000);
return days <= 2;
它有四类风险:
- 比较的是时刻差,不是自然日差;
new Date('YYYY-MM-DD')的解析语义容易受平台和时区理解影响;- 夏令时地区某些自然日不是严格 24 小时;
- 非法输入和未来日期可能被静默转换后继续计算。
项目不依赖字符串构造 Date 的隐式规则,而是显式拆年、月、日,再验证 Date 没有自动溢出。
三、严格解析 YYYY-MM-DD
calendarDayNumber() 先执行 trim(),再用正则检查固定的四位年、两位月、两位日,并显式拆出 year、month、day。
这会拒绝 2026/08/12、2026-8-12、空字符串和带额外内容的值。
格式正确仍不代表日期存在。JavaScript 的 Date 会把 2026-02-30 自动滚动到 3 月,因此还要回读年、月、日;只要和输入不一致,就返回 -1。
只要发生溢出,原输入就被判为非法。
四、为什么转成 UTC day number
解析时使用本地 Date 验证用户选择的本地年月日;比较时则用 Date.UTC(year, month - 1, day) / MILLISECONDS_PER_DAY 把年月日映射为统一的日序号。
这里的 UTC 不是把事件强行解释成“UTC 时刻”,而是用 Date.UTC(year, month, day) 为一个日历日期生成稳定编号。
只比较编号差,就不会把本地时分秒或夏令时某天的 23/25 小时带入自然日计算。
五、今天也要先取本地年月日
当前时间来自毫秒值,但“今天”必须按本机日历理解:先用 getFullYear/getMonth/getDate 取出本机年月日,再映射到同一种 day number。
先用本地 getFullYear/getMonth/getDate 取出今天,再映射到 day number。若直接用 Math.floor(nowMs / 86400000),得到的是 UTC 日期边界,北京时间凌晨的数小时会被归到前一个 UTC 日。
这一前一后的组合很重要:业务日历使用本机日期,差值计算使用稳定的 UTC 编号。
六、三个范围的边界写法
下面把严格解析、日序号和三个边界放在一个连续实现中展示:
const MILLISECONDS_PER_DAY: number = 24 * 60 * 60 * 1000;
function calendarDayNumber(value: string): number {
const normalized = value.trim();
if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized)) return -1;
const year = Number(normalized.substring(0, 4));
const month = Number(normalized.substring(5, 7));
const day = Number(normalized.substring(8, 10));
const selected = new Date(year, month - 1, day);
if (Number.isNaN(selected.getTime()) || selected.getFullYear() !== year ||
selected.getMonth() !== month - 1 || selected.getDate() !== day) {
return -1;
}
return Math.floor(Date.UTC(year, month - 1, day) / MILLISECONDS_PER_DAY);
}
export function matchesHomeTimeRange(
eventDate: string,
timeRange: HomeTimeRange,
nowMs: number = Date.now()
): boolean {
if (timeRange === HomeTimeRange.ALL) return true;
const selectedDay = calendarDayNumber(eventDate);
if (selectedDay < 0) return false;
const now = new Date(nowMs);
const today = Math.floor(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()) /
MILLISECONDS_PER_DAY);
const difference = today - selectedDay;
if (difference < 0) return false;
if (timeRange === HomeTimeRange.TODAY) return difference === 0;
if (timeRange === HomeTimeRange.THREE_DAYS) return difference <= 2;
if (timeRange === HomeTimeRange.SEVEN_DAYS) return difference <= 6;
return true;
}
为什么近 3 天是 <= 2 而不是 <= 3?因为今天已经占一天:差值 0、1、2 一共三个日期。
同理,近 7 天对应 0 到 6。把 UI 文案、代码边界和测试用例放在同一张表里最不容易误解:
| 筛选 | 允许的 difference | 以 2026-08-12 为今天时最早日期 |
|---|---|---|
| 今天 | 0 | 2026-08-12 |
| 近 3 天 | 0~2 | 2026-08-10 |
| 近 7 天 | 0~6 | 2026-08-06 |
七、未来日期必须单独拒绝
difference < 0 表示事件日在今天之后。即使未来日期绝对差很小,也不应进入任何活动时间范围。
发布表单已经限制事件日期不晚于今天,但数据可能来自旧版本、迁移、手工测试或未来远端同步。筛选 Policy 仍要自我保护,不能假设所有调用方永远正确。
这体现了两层校验:Service 在写入前保护数据质量,Policy 在读取时保护结果语义。
八、ALL 为什么允许空日期
函数一开始对 HomeTimeRange.ALL 返回 true。
因此旧记录即使没有 eventDate,在“全部时间”下仍可展示;只有用户启用活动时间筛选时,非法或缺失日期才被排除。
这是兼容策略,而不是认为空日期有效。若产品要求所有公开记录必须有合法事件日期,应在迁移和写入层完成修复,再决定是否收紧 ALL 的行为。
过早在列表层隐藏旧记录,会让用户误以为数据丢失。

上图以固定日期展示三个包含区间,并把未来日期和非法日期放在拒绝区域。测试边界时,最有价值的不是区间中间值,而是最早允许日的前后两天。
九、测试必须注入固定时钟
如果测试直接调用 Date.now(),今天一变,固定样例就会失效。项目把当前时间作为可选参数:生产环境默认使用 Date.now(),测试则传入 new Date(2026, 7, 12, 12, 0, 0, 0).getTime() 作为固定 NOW。
选择中午而不是临近午夜,也能减少测试运行环境在日期边界附近产生的偶然干扰。
十、边界用例比普通用例更重要
项目测试包含:
| 输入 | 范围 | 预期 |
|---|---|---|
| 2026-08-12 | 今天 | true |
| 2026-08-11 | 今天 | false |
| 2026-08-10 | 近 3 天 | true |
| 2026-08-09 | 近 3 天 | false |
| 2026-08-06 | 近 7 天 | true |
| 2026-08-05 | 近 7 天 | false |
| 2026-08-13 | 近 7 天 | false |
| 2026-02-30 | 近 7 天 | false |
| 空字符串 | 近 7 天 | false |
| 空字符串 | 全部 | true |
这组用例同时固定了包含边界、排除边界、未来日期、非法日期和兼容语义。仅测“昨天在近 3 天内”无法发现 off-by-one。
十一、跨时区同步会改变问题
当前项目是单机本地应用,eventDate 表示用户在本设备选择的校园自然日,算法以本机时区为准。
未来接入远端同步时,需要先回答:权威时区是什么?
- 事件发生地所在校园时区;
- 发布者设备时区;
- 服务器统一时区;
- 查看者设备时区。
对于校园失物,通常应以校园所在地自然日作为业务日期,并把 eventDate 当成日期值而非 UTC 时间戳。若跨校跨时区,就需要在数据合约中保存校园时区或业务时区,不能只依赖查看设备。
十二、日期值与时间戳不要混用
项目同时有 eventDate 和 createdAt:
eventDate:用户选择的事件自然日,格式YYYY-MM-DD;createdAt:记录创建的时间点,用于排序和审计。
筛选“事件发生在近 3 天”应该使用 eventDate,而不是 createdAt。用户今天补发三天前丢失的物品,两者可能相差很大。
迁移旧记录时可以临时从 createdAt 推导 eventDate,但那只是兼容回填,不应改变两个字段长期承担的不同语义。
十三、页面层还要处理哪些状态
纯函数正确不代表交互已经正确。ArkUI 页面还应检查:
- 筛选标签显示“近 3 天”时传入的是
THREE_DAYS; - 清空筛选恢复
ALL; - 应用跨过午夜后重新查询,而不是继续使用旧结果;
- 从后台返回时是否需要刷新“今天”;
- 日期为空的历史记录是否在 ALL 下正常显示;
- 小屏下三个时间选项是否可点击且不挤压。
如果页面长期缓存 nowMs,自然日切换后会出现“昨天仍被当作今天”的问题。当前纯函数默认每次调用读取 Date.now(),页面刷新策略仍需要设备验证。
十四、运行真实规则测试
本地测试命令:
powershell -ExecutionPolicy Bypass -File .\scripts\test-report-filter.ps1
该测试能证明固定时钟下的自然日边界与非法输入处理。它不能证明设备时区切换、跨午夜生命周期、系统日期异常、远端同步和 ArkUI 交互。
更完整的设备测试应至少覆盖本地 23:59 到次日 00:01、修改系统时区、后台恢复、旧记录空日期和日期选择器最大值。
十五、维护日期代码的原则
- 先写清“自然日”还是“滚动小时窗口”;
- 日期值使用稳定格式,时间点使用时间戳;
- 不依赖字符串 Date 的隐式解析;
- 验证自动溢出的非法日期;
- 用本地年月日确定业务今天;
- 用稳定日序号计算差值;
- 测试固定时钟和边界前后值;
- 跨时区前先定义权威业务时区。
这些原则比记住某个 Date API 更能避免线上边界错误。
十六、日期规则变更前的影响分析
把“近 3 天”改成“过去 72 小时”,看起来只是替换一个表达式,实际上会改变边界记录、产品文案、测试数据和用户预期。改动前应先确认业务采用自然日还是滚动窗口、权威时区在哪里、未来日期是否允许,以及旧客户端产生的日期字符串如何解释。
日期规则还会影响首页筛选、匹配候选、统计报表与消息提醒。若多个入口各自计算边界,同一条记录可能在首页可见却不进入匹配。集中到纯策略并注入时钟,才能让所有消费者共享同一合同。
十七、异常时间与回滚边界
系统时间被手动修改、设备跨时区、夏令时切换或远端数据缺少时区时,程序都不应猜测成“今天”。非法输入应返回明确的不匹配结果并记录来源;若业务必须兼容旧格式,应通过版本化解析器处理,而不是放宽正则到任何字符串都能通过。
上线新规则前要保留旧算法和固定样本,比较两版命中差异。若差异超出产品确认范围,应回滚策略实现而不是修改历史日期。远端同步场景还应保存事件发生地时区或统一业务时区,否则客户端本地时间会让同一记录在不同设备得到不同答案。
十八、验收证据如何覆盖午夜边界
自动化至少需要固定在某一天的中午、23:59、次日 00:00 和时区切换前后执行,分别记录 TODAY、LAST_3_DAYS、LAST_7_DAYS 的命中集合。设备层则应验证应用进入后台跨过午夜再恢复时是否重新计算,而不是继续使用页面创建时缓存的“今天”。
证据记录要包含系统时区、固定时钟、输入日期、预期日差与实际结果。普通 Node 用例可以证明纯算法,但不能证明系统日期选择器、生命周期刷新或设备时区通知已经接通;这些未运行项必须单独保留。
十九、本文小结
“今天/近 3 天/近 7 天”看似只是毫秒比较,实际同时包含产品语义、日期解析、时区边界、非法输入和测试时钟。
“寻迹校园”严格解析 YYYY-MM-DD,用本地年月日确定业务日期,再映射为 UTC day number 计算自然日差;近 3 天使用差值 0~2,近 7 天使用 0~6,并明确拒绝未来和非法日期。固定时钟测试把这些边界变成可重复契约。
系列导航:第 19 篇 / 共 50 篇。上一篇:《六维 AND 组合筛选》;下一篇:《标准值与历史文本共存》。
更多推荐


所有评论(0)