Use Cases (更新: 2026/6/2)

Claude Code 日期与时间处理实战指南

用 Claude Code 安全处理时区、DST、Intl 格式化、测试和数据库时间保存策略的实战指南。

Claude Code 日期与时间处理实战指南

为什么日期时间不能只交给 Claude Code 随手实现

日期与时间的 bug 很容易在代码评审中漏掉,却会在生产环境造成很大影响。页面在中国或日本看起来正常,不代表它能正确处理美国夏令时、欧洲月底结算、印度半小时时差,或服务器与浏览器的时区差异。如果只是对 Claude Code 说“帮我实现日期处理”,它可能会生成漂亮的格式化代码,但不会自动补齐保存策略、API 契约和边界测试。

更稳妥的做法,是把 Claude Code 当成一起梳理日期时间规格的结对工程师,而不只是代码生成器。本文面向预约、账单、通知、客服 SLA、管理后台等跨时区 Web 应用。测试整体设计可参考Claude Code 测试策略,数据库变更可配合数据库迁移指南阅读。

实现前先看官方资料。显示格式以MDN Intl.DateTimeFormat为基准,Temporal 的状态与概念参考TC39 Temporal proposalMDN Temporal,PostgreSQL 的时间保存行为参考PostgreSQL Date/Time Types,需要让 Claude Code 编辑后自动跑检查时则参考Claude Code hooks

先固定术语和保存策略

在让 Claude Code 写代码之前,团队必须先统一语言。否则评审时很容易出现“我们存 UTC,所以没问题”“产品只面向本地用户,所以固定本地时区就好”这类过度简化的判断。

术语直观含义保存原则
instant全球唯一的时间点,例如 2026-06-02T00:00:00Z以 UTC 基准的 timestamp 保存
local date用户日历上的日期,例如生日、截止日、营业日YYYY-MM-DD 保存,不和时刻混在一起
wall clock time用户在钟表上看到的时间,例如 09:00 会议必须和 IANA 时区 ID 一起保存
IANA timezoneAsia/ShanghaiAmerica/New_York 这样的地区名不要用固定 offset 替代
DST夏令时;某些本地日期可能只有 23 或 25 小时必须测试切换日

真实项目中,建议把下面三条写进 AGENTS.mdCLAUDE.md

  1. API 中已经发生的时间点必须是带 Z 或显式 offset 的 ISO 8601 字符串。
  2. 未来预约、通知、计划任务要分开保存 localDatelocalTimetimeZone
  3. UI 显示必须给 Intl.DateTimeFormat 同时传入 localetimeZone,不要依赖运行环境默认值。

边界关系可以这样理解。

flowchart LR
  A["Client: local date/time input"] --> B["API contract: localDate + localTime + timeZone"]
  B --> C["Server: validate and convert when needed"]
  C --> D["Database: instant in timestamptz + original timeZone"]
  D --> E["UI: format with Intl.DateTimeFormat"]
  E --> A

用 Intl.DateTimeFormat 打好安全基础

如果需求只是展示,优先从标准 API Intl.DateTimeFormat 开始。MDN 将它说明为用于语言敏感日期时间格式化的内置 API,并且可以显式指定 timeZone。给 Claude Code 的约束也要写清楚:不要无参数调用 toLocaleString(),不要把代表本地日历日的 YYYY-MM-DD 直接交给 Date 解析。

下面是一个无依赖的基础模块,可以放在 src/lib/date-policy.ts,供 UI、API 和测试共用。

// src/lib/date-policy.ts
export const TIME_POLICY = {
  defaultLocale: 'zh-CN',
  defaultTimeZone: 'Asia/Shanghai',
} as const;

type FormatOptions = {
  locale?: string;
  timeZone?: string;
  includeWeekday?: boolean;
};

function toDate(input: string | Date): Date {
  const date = input instanceof Date ? input : new Date(input);

  if (!Number.isFinite(date.getTime())) {
    throw new Error(`Invalid date value: ${String(input)}`);
  }

  return date;
}

export function toUtcIso(input: string | Date): string {
  if (typeof input === 'string' && !/(Z|[+-]\d{2}:?\d{2})$/i.test(input)) {
    throw new Error('Timestamp must include Z or an explicit UTC offset.');
  }

  return toDate(input).toISOString();
}

function dateParts(date: Date, timeZone: string): Record<string, string> {
  const parts = new Intl.DateTimeFormat('en-US', {
    timeZone,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).formatToParts(date);

  return Object.fromEntries(
    parts
      .filter((part) => part.type !== 'literal')
      .map((part) => [part.type, part.value]),
  );
}

export function dayKeyInTimeZone(
  input: string | Date,
  timeZone = TIME_POLICY.defaultTimeZone,
): string {
  const parts = dateParts(toDate(input), timeZone);
  return `${parts.year}-${parts.month}-${parts.day}`;
}

export function formatInstant(
  input: string | Date,
  {
    locale = TIME_POLICY.defaultLocale,
    timeZone = TIME_POLICY.defaultTimeZone,
    includeWeekday = true,
  }: FormatOptions = {},
): string {
  return new Intl.DateTimeFormat(locale, {
    timeZone,
    weekday: includeWeekday ? 'short' : undefined,
    year: 'numeric',
    month: 'short',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    hourCycle: 'h23',
    timeZoneName: 'short',
  }).format(toDate(input));
}

这段代码的目的不是把所有日期操作都藏进工具函数,而是把三件事分清:全球唯一时间点、本地日期 key、展示标签。以后 Claude Code 想新增工具函数时,先让它说明新需求属于哪一类。

未来的本地时间要显式转换

Intl.DateTimeFormat 擅长输出,但不适合把 2026-11-01 01:30 America/New_York 这种本地时间安全转换成 UTC instant。这个场景应该在项目层选择一个库,并确认当前官方文档。Luxon和它的API docs提供明确的时区 API。date-fns适合函数式日期工具,但涉及时区时要确认最新官方能力。Temporal 在 TC39 已是 Stage 4,但仍要检查目标浏览器、Node 版本和 polyfill 策略。

预约表单中,用户输入的是本地日期和本地时间,服务端需要转换成数据库保存的 instant。

// src/lib/schedule-time.ts
import { DateTime } from 'luxon';

type LocalScheduleInput = {
  localDate: string; // YYYY-MM-DD
  localTime: string; // HH:mm
  timeZone: string; // IANA time zone, for example "America/New_York"
};

export function scheduleToUtcIso(input: LocalScheduleInput): string {
  const rawLocal = `${input.localDate}T${input.localTime}`;
  const local = DateTime.fromISO(rawLocal, { zone: input.timeZone });

  if (!local.isValid) {
    throw new Error(local.invalidExplanation ?? `Invalid local time: ${rawLocal}`);
  }

  const roundTrip = local.setZone(input.timeZone).toFormat("yyyy-MM-dd'T'HH:mm");
  if (roundTrip !== rawLocal) {
    throw new Error(`Nonexistent local time in ${input.timeZone}: ${rawLocal}`);
  }

  const iso = local.toUTC().toISO({ suppressMilliseconds: true });
  if (!iso) {
    throw new Error(`Could not convert local time to UTC: ${rawLocal}`);
  }

  return iso;
}

注意,这个函数并没有自动解决夏令时结束时重复出现的 01:00。产品必须决定:选择较早 offset、选择较晚 offset,还是让用户确认。让 Claude Code 写测试时,要明确要求覆盖不存在的本地时刻、重复时刻、月底和闰日。

分开处理客户端、服务端和数据库边界

最常见的失败,是没有决定一个值到底由浏览器时区还是服务器时区解释,就直接传给 Date。浏览器输入、API payload、数据库列、邮件、CSV 导出都是不同边界。

评审设计时,至少用下面三个用例检查。

  • 预约系统:输入采用场地本地时间,保存采用 UTC instant,展示采用浏览者或场地时区。
  • 账单截止:合同中的“月底 23:59”属于客户合同时区的 local date 规则,不要用加 24 小时拼出下个月。
  • 客服 SLA:营业日、假日、办公时间按地区不同,既要保存截止 instant,也要保存便于人理解的本地上下文。

PostgreSQL 官方文档说明,timestamp with time zone 输入会转换为 UTC,原始时区不会保留。因此,只靠 timestamptz 无法知道用户当时选择的是 America/New_York。产品需要这个信息时要另存一列。

create table scheduled_events (
  id uuid primary key,
  title text not null,
  starts_at timestamptz not null,
  original_time_zone text not null check (original_time_zone <> ''),
  local_date date not null,
  local_time time not null,
  created_at timestamptz not null default now()
);

create index scheduled_events_starts_at_idx
  on scheduled_events (starts_at);

不要以为把 2026-06-02T09:00:00+09:00 插入 timestamp without time zone 就安全。PostgreSQL 先确定类型;如果值已经被判断为 without time zone,时区信息会被忽略。把“日期时间列类型是否匹配 API 契约”加入 Claude Code 的评审清单。

用固定 instant 测试 DST 和边界日

测试不应依赖“今天”、当前时钟或开发者机器时区。把输入固定为 UTC instant,再断言目标时区中的 local date 或显示结果。使用 Vitest 时,可以从下面的测试开始。更完整的策略可参考Claude Code Vitest 进阶

// src/lib/date-policy.test.ts
import { describe, expect, it } from 'vitest';
import { dayKeyInTimeZone, formatInstant, toUtcIso } from './date-policy';

describe('date/time policy', () => {
  it('requires an explicit offset for API timestamps', () => {
    expect(() => toUtcIso('2026-06-02T09:00:00')).toThrow(/offset/);
    expect(toUtcIso('2026-06-02T09:00:00+09:00')).toBe('2026-06-02T00:00:00.000Z');
  });

  it('calculates a local day key across the UTC date boundary', () => {
    expect(dayKeyInTimeZone('2026-03-31T15:01:00Z', 'Asia/Tokyo')).toBe('2026-04-01');
    expect(dayKeyInTimeZone('2026-04-01T00:30:00Z', 'America/Los_Angeles')).toBe('2026-03-31');
  });

  it('formats a DST transition instant in the requested time zone', () => {
    const label = formatInstant('2026-03-08T07:30:00Z', {
      locale: 'en-US',
      timeZone: 'America/New_York',
    });

    expect(label).toMatch(/03:30|3:30/);
    expect(label).toMatch(/EDT|GMT-4/);
  });
});

这里故意允许 EDTGMT-4,因为 ICU 数据在不同 CI 环境可能不同。local date key 和 UTC 转换结果属于业务逻辑,应该严格断言。

用提示词和 hooks 防止回归

给 Claude Code 的约束要在实现前写清楚。提示词可以长一点,重点是列出不能破坏的边界。

Before implementing date/time changes, read date-policy.ts and the database schema.

Constraints:
- API timestamps for completed events must be ISO strings with Z or an explicit offset
- Future bookings must keep localDate, localTime, and timeZone separate
- Intl.DateTimeFormat must always receive locale and timeZone
- Do not use new Date('YYYY-MM-DD') for local calendar dates
- Add Vitest cases for DST start, DST end, month end, and leap day
- Explain the PostgreSQL timestamp/timestamptz difference in the review notes

Done when:
- npm test -- --run date-time passes
- Updated API response examples are added to README

团队使用时,可以用 Claude Code hooks 在编辑后自动运行日期时间测试。官方文档说明,hooks 可以在 Claude Code 生命周期事件中执行 shell 命令。完整流程可参考Claude Code hooks 指南

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "npm test -- --run date-time"
          }
        ]
      }
    ]
  }
}

按时间类型选择库

不要只按流行度选日期库,而要按产品处理的时间类型选择。Intl.DateTimeFormat 适合本地化显示,date-fns 适合轻量日期工具,Luxon 适合包含 IANA 时区的预约和转换,Temporal 适合想在类型层面区分 InstantPlainDateZonedDateTime 的长期设计。即使 Temporal 已是 Stage 4,也要先确认目标运行环境和 polyfill 方针。

选择适合场景注意点
Intl带明确时区的本地化显示不是本地时间转 instant 的工具
date-fns纯日期计算、函数级导入、小工具时区需求要看当前官方文档
LuxonIANA 时区预约、相对时间、ISO 转换DST 重复时刻仍需产品规则
Temporal清晰区分 InstantPlainDateZonedDateTime要确认运行环境与 polyfill

从内容变现角度看,单纯列库没有足够价值。读者真正需要的是:该存什么、在哪里转换、测什么。如果团队想把这套规则放进 CLAUDE.md、hooks、PR review 和 migration 流程,可以查看Claude Code 培训与咨询。个人开发者则可以把本文提示词加入免费速查表作为重复使用的防线。

实际验证记录

Masa 的验证记录:最有用的检查,是固定一个 UTC instant,分别用东京、纽约、洛杉矶显示,再断言跨日期边界的 local date key。最容易复现的 bug,是把 YYYY-MM-DD 当成用户本地日期转成 Date,然后在另一个时区显示成前一天。本文的策略让 local date 保持字符串,只对保存的 instant 和展示标签做显式转换,因此 Claude Code 生成的 diff 更容易评审。

#Claude Code #日期时间 #时区 #Intl #测试
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。