日期工具库升级之后,最值得警惕的变化不一定来自接口重命名,而是相同输入被解析成不同的业务含义。上一章已经把日期划分为calendar-day、local-wall-time和instant,并约定对纽约夏令时跳过时刻返回gap、重复时刻返回overlap。现在需要把这些约定固化为回归矩阵:旧驱动与候选驱动各执行同一批输入,再从规范化输出、错误类别和持久化结果三个维度比较,而不是仅看界面上的日期字符是否一致。

示例项目仍采用一个独立的日期规则工作台,用户输入2026-10-28,时区可选Asia/Shanghai或America/New_York。工作台的职责是展示每个用例的旧版结果、候选结果和差异类型;升级判断由领域层决定。这里的版本称为baseline与candidate,它们指业务驱动的两个快照,不预设任何第三方包在鸿蒙工程中的已安装版本。这样处理能区分产品契约回归与工具链依赖兼容问题,也方便未来替换底层解析实现。

日期规则依赖升级与回归矩阵封面

一、比较对象要比函数返回值更完整

初次引入日期库时,容易只关注format()输出是否符合预期。但跨时区系统真正依赖的是输入种类、日期的有效性、时区名称、消歧策略以及UTC毫秒值。若候选驱动把2026-02-29自动挪到3月1日,即使显示形式没有改变,业务校验规则也已经失效;若把纽约秋季重复的一点半默认映射为较早瞬时值,而原规则要求由用户选择,计划任务就可能提前一小时。

建议建立稳定结果结构:成功时使用{ok:true,kind,value};失败时使用{ok:false,code};遇到双重本地时刻时返回候选数量并保留overlap状态。错误文本可以随语言切换,错误代码不可跟着改变。输入中没有地区字段时,应由入口校验返回缺失错误,不要在库升级过程中引入设备默认时区。对同一日期的多次调用也应保持输出不变,避免隐藏的可变对象进入缓存。

不同驱动的结果比较之前先做规范化:键的排列、时区大写小写展示、无意义的格式化空格都可以在比较层处理;但UTC毫秒、日期文本、失败类别和候选数量不可被忽略。错误类别的变化与结果值变化要分开计数,否则报告只会得到“有差异”而无法定位是否影响存量数据。

二、将严格解析写进可执行契约

回归矩阵的入口是一张有名称、输入类型、输入值和预期输出的表。它至少覆盖平年二月末、闰年二月末、缺少前导零、跨月边界与普通工作日。下面使用Node.js原生日期操作实现基线驱动,再通过同一接口包装候选驱动;候选驱动即使内部调用另一套库,也必须满足相同契约。这里的脚本是领域规则的可执行校验样本,与应用UI无绑定关系。

const assert = require('node:assert/strict');
function parseStrict(text) {
  if (!/^\d{4}-\d{2}-\d{2}$/.test(text)) return { ok: false, code: 'FORMAT' };
  const [y, m, d] = text.split('-').map(Number);
  const parsed = new Date(Date.UTC(y, m - 1, d));
  if (parsed.getUTCFullYear() !== y || parsed.getUTCMonth() + 1 !== m ||
      parsed.getUTCDate() !== d) return { ok: false, code: 'INVALID_DATE' };
  return { ok: true, kind: 'calendar-day', value: text };
}
const baseline = { parseDate: parseStrict };
const candidate = { parseDate: input => ({ ...parseStrict(input) }) };
const fixtures = [
  ['2026-10-28', { ok: true, kind: 'calendar-day', value: '2026-10-28' }],
  ['2024-02-29', { ok: true, kind: 'calendar-day', value: '2024-02-29' }],
  ['2026-02-29', { ok: false, code: 'INVALID_DATE' }],
  ['2026-2-09', { ok: false, code: 'FORMAT' }]
];
for (const [input, expected] of fixtures) {
  assert.deepEqual(baseline.parseDate(input), expected);
  assert.deepEqual(candidate.parseDate(input), expected);
}
console.log('calendar contract:', fixtures.length, 'cases');

这种写法让比较标准独立于Day.js或其他库的内部对象。接入Day.js时可以在适配层初始化CustomParseFormat插件,并使用严格格式解析;官方文档明确区分宽松有效性检查与严格检查。回归矩阵应该将“输入不完整”“日历不存在”“时区未知”作为不同记录,且把任何意外抛错转换成受控错误类别,防止异步批量核查在单条数据失败时整体退出。

三、时区回归必须覆盖跳时和重叠

仅测试上海的正常日期不足以保护地区时间业务。纽约2026年3月8日02:30不存在,11月1日01:30有两个可选UTC瞬时值。需要分别核对gap、overlap和unique,对于overlap还要确认候选瞬时值的顺序与较早、较晚选择的含义。若用户选择了later,升级后只比较本地显示的01:30是不够的,还必须比较最终UTC毫秒。

以下脚本通过Intl.DateTimeFormat将UTC候选映射回目标地区,构建一组固定的业务回归用例。有限窗口枚举适合对照这些具体边界,日期库本身的完整时区规则仍应由相应运行环境和依赖实现承担。代码刻意让输出只包含状态与UTC数组,避免把格式化语言差异混入核心比较。

const assert = require('node:assert/strict');
function toWall(ms, zone) {
  const parts = new Intl.DateTimeFormat('en-US', {
    timeZone: zone, hourCycle: 'h23', year: 'numeric', month: '2-digit',
    day: '2-digit', hour: '2-digit', minute: '2-digit'
  }).formatToParts(new Date(ms));
  const p = Object.fromEntries(parts.filter(x => x.type !== 'literal')
    .map(x => [x.type, x.value]));
  return `${p.year}-${p.month}-${p.day} ${p.hour}:${p.minute}`;
}
function classify(wall, zone) {
  const [ymd, hm] = wall.split(' ');
  const [y, m, d] = ymd.split('-').map(Number);
  const [h, minute] = hm.split(':').map(Number);
  const base = Date.UTC(y, m - 1, d, h, minute);
  const epochs = [];
  for (let offset = -14 * 60; offset <= 14 * 60; offset += 15) {
    const ms = base + offset * 60000;
    if (toWall(ms, zone) === wall) epochs.push(ms);
  }
  return { state: epochs.length === 0 ? 'gap' :
    epochs.length === 1 ? 'unique' : 'overlap', epochs };
}
assert.equal(classify('2026-03-08 02:30', 'America/New_York').state, 'gap');
const repeated = classify('2026-11-01 01:30', 'America/New_York');
assert.equal(repeated.state, 'overlap');
assert.equal(repeated.epochs.length, 2);
assert.equal(classify('2026-10-28 09:30', 'Asia/Shanghai').state, 'unique');
console.log(repeated.epochs.map(ms => new Date(ms).toISOString()));

上述循环对候选偏移采用15分钟粒度,适用于本章使用的样本;不适合直接代替完整的历史时区数据库。升级回归表还应涵盖跨年、闰日与本地日期零点边界,并采用[start,end)半开区间查询,以免把下一天零点重复纳入当日记录。涉及国家或地区将来调整时区规则时,需要以业务输入与UTC快照作对照,明确哪些变化是规则更新所致。

日期规则升级矩阵的开发环境

四、从差异清单形成升级门禁

矩阵执行后的原始差异要能够转成决策。格式化颜色、提示语变化可以进入界面回归复核;错误代码变化、UTC值变化、跳时分类变化应视为阻断项。对于新增能力,不必要求旧驱动和候选驱动所有输出完全相同,但必须给每个允许变化的用例明确的迁移条目和责任归属。没有登记允许变化时,默认按不一致处理更安全。

下面的门禁函数接收结构化用例结果。它在基线与候选规范化输出不同、测试样本数量不足或回滚快照缺失时保持旧驱动。字段比较采用JSON序列化的稳定结构,实际工程中应先固定字段排序及空值规则,避免比较器本身造成噪声。

const assert = require('node:assert/strict');
function decideUpgrade(rows, rollbackAvailable, minCases = 4) {
  const issues = [];
  if (rows.length < minCases) issues.push('COVERAGE');
  if (!rollbackAvailable) issues.push('ROLLBACK_MISSING');
  for (const row of rows) {
    if (JSON.stringify(row.baseline) !== JSON.stringify(row.candidate)) {
      issues.push(`DIFF:${row.id}`);
    }
  }
  return { accepted: issues.length === 0, issues };
}
const sample = [
  { id: 'valid', baseline: { ok: true }, candidate: { ok: true } },
  { id: 'leap', baseline: { ok: false, code: 'INVALID_DATE' }, candidate: { ok: false, code: 'INVALID_DATE' } },
  { id: 'spring', baseline: { state: 'gap' }, candidate: { state: 'gap' } },
  { id: 'fall', baseline: { state: 'overlap', count: 2 }, candidate: { state: 'overlap', count: 2 } }
];
assert.equal(decideUpgrade(sample, true).accepted, true);
assert.deepEqual(decideUpgrade(sample.slice(0, 3), true).issues, ['COVERAGE']);
const changed = sample.map(row => ({ ...row, candidate: { ...row.candidate } }));
changed[2].candidate.state = 'unique';
assert.deepEqual(decideUpgrade(changed, true).issues, ['DIFF:spring']);
console.log(decideUpgrade(changed, true));

升级门禁的输出应该能直接关联到原始输入、规则版本、依赖包清单及异常详情。只有聚合结果,没有逐条证据,后续很难说明一次依赖升级为何阻断发布。为避免报告因系统时间或遍历顺序变化而产生大量无意义差异,矩阵用例按稳定ID排序,运行时间作为外层报告信息保留,不参与业务结果比较。异常时优先保存可复现的输入,不需要记录与日期计算无关的个人资料。

五、在ArkUI工作台中呈现升级结果

界面可采用List承载用例行,三列分别展示用例名称、基线与候选状态;窄屏改为上下两层,先呈现用例名称和差异结论,展开时查看两个结果的完整数据。矩阵结果与页面选择应以用例ID连接,避免筛选后使用列表位置作为持久引用。读屏顺序为用例名称、基线状态、候选状态、差异结论,再到详情入口。灰度或语言环境切换时,核心的gap/unique/overlap字符串仍保留为机器可判定字段。

日期规则升级回归矩阵的应用界面

如果应用采用第三方包,依赖信息应从模块级oh-package.json5及锁文件取得,运行前核对来源、许可证与实际解析到的版本。华为开发者的依赖指南说明dependencies、devDependencies在构建处理上存在差异,因此日期解析驱动这类运行期使用的包不应误放进仅用于开发的依赖字段。对使用Day.js的方案,还需要关注UTC、Timezone、CustomParseFormat插件的初始化顺序和调用位置。纯业务脚本中的通过项属于规则回归,工程里的类型、导出入口及打包情况需要单独作为构建检查列。

六、让矩阵成为可维护的工程资产

矩阵不只是一次升级的门票,也是下一次改造的输入。每次新增的地区边界、日期解析缺陷都应沉淀成新的固定用例,并记录它对应哪一种契约。对未来预约时间,保存用户的本地钟面输入、地区与消歧策略,以便时区数据更新后重新评估;对已经确定的历史瞬时值,优先使用持久化的UTC值保持审计一致性。业务规则的版本号和日期库版本号分开记录,避免一次假日表更新被错误归因于库升级。

当矩阵出现差异时,应先检查是测试环境国际化数据变化、驱动适配变化,还是业务规则本身调整,再决定调整候选驱动、补充允许变化清单或保持现有版本。这样的差异处置能减少凭一个“通过率”直接替换依赖的风险。下一篇将围绕锁文件、内容摘要、依赖替换策略和回滚机制,完成从技术比较到版本治理的闭环。

参考资料

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐