Temporal API:重构 JavaScript 的日期时间引擎,解决 IANA 时区与夏令时计算复杂性

各位开发者、架构师,以及所有对JavaScript日期时间处理深感“痛楚”的朋友们,大家好。

今天,我们将深入探讨一个长期困扰JavaScript生态系统的核心问题:日期和时间的处理。具体来说,我们将聚焦于JavaScript中现有的Date对象所带来的复杂性,尤其是它在处理IANA时区和夏令时(DST)计算时的诸多弊端,并隆重介绍即将彻底改变这一现状的解决方案——Temporal API

可以说,在JavaScript中,日期时间操作的“坑”深不见底。从看似简单的日期加减,到跨时区的事件调度,再到夏令时切换时的诡异行为,Date对象常常让开发者们如履薄冰。它缺乏直观的API、不明确的时区处理,以及最令人头疼的可变性,都使得开发者不得不求助于庞大的第三方库,如Moment.js、date-fns或Luxon,即便如此,这些库也只是在Date对象的基础上进行了封装和优化,并未从根本上解决问题。Temporal API的出现,正是为了从语言层面彻底重构JavaScript的日期时间引擎,为我们带来一个更健壮、更精确、更易用的未来。

I. Date 对象:历史的包袱与现实的困境

要理解Temporal API的价值,我们首先需要深刻认识到当前Date对象的局限性。JavaScript的Date对象,从设计之初就带着一些固有的缺陷,这些缺陷在现代全球化、分布式系统中变得尤为突出。

1. 可变性(Mutability):无声的陷阱

Date对象最臭名昭著的特性之一就是它的可变性。一个Date实例可以被它的方法修改,这意味着如果你不小心,一个日期对象在程序的某个地方被修改后,所有引用它的地方都会受到影响,这会引入难以追踪的bug。

const initialDate = new Date('2023-10-27T10:00:00Z');
console.log('原始日期:', initialDate.toISOString()); // 原始日期: 2023-10-27T10:00:00.000Z

// 复制日期对象(常见错误:直接赋值只复制引用)
let modifiedDate = initialDate;
modifiedDate.setHours(12); // 修改了 initialDate

console.log('修改后的日期:', modifiedDate.toISOString()); // 修改后的日期: 2023-10-27T12:00:00.000Z
console.log('原始日期(被意外修改):', initialDate.toISOString()); // 原始日期(被意外修改): 2023-10-27T12:00:00.000Z

// 正确的复制方式(繁琐)
const correctCopy = new Date(initialDate.getTime());
correctCopy.setHours(14);
console.log('正确复制后的日期:', correctCopy.toISOString()); // 正确复制后的日期: 2023-10-27T14:00:00.000Z
console.log('原始日期(未被修改):', initialDate.toISOString()); // 原始日期(未被修改): 2023-10-27T12:00:00.000Z

这种隐式的副作用是复杂系统中错误的主要来源。

2. 有限且不直观的API

Date对象提供的API非常基础,缺乏进行复杂日期时间操作的能力。例如,它没有直接的方法来:

  • 安全地添加或减去一个时间段(如“3天”或“2小时”),这常常导致手动计算毫秒,易出错。
  • 比较两个日期对象时,需要手动提取时间戳。
  • 处理日期时间的组件(年、月、日等)时,常常需要区分UTC和本地时间的方法,容易混淆。
const d1 = new Date('2023-10-27T10:00:00Z');
const d2 = new Date('2023-10-30T10:00:00Z');

// 比较日期(需要手动)
if (d1.getTime() < d2.getTime()) {
    console.log('d1 在 d2 之前');
}

// 添加3天(需要手动计算毫秒)
const threeDaysLater = new Date(d1.getTime() + (3 * 24 * 60 * 60 * 1000));
console.log('3天后:', threeDaysLater.toISOString());

3. 时区处理的模糊与混乱

Date对象内部存储的是自Unix纪元(1970年1月1日00:00:00 UTC)以来的毫秒数,这本身是UTC时间。然而,它的许多方法(如getFullYear(), getHours())默认返回的是本地时区的值,而另一些方法(如getUTCFullYear(), getUTCHours())则返回UTC的值。这种混合导致了巨大的混乱。

更糟糕的是,Date对象本身并不“知道”它所代表的特定时区。它只是在创建或转换为字符串时,依赖于运行代码的宿主系统(浏览器或Node.js进程)的本地时区。这意味着:

  • 在不同时区的机器上运行相同的代码,可能会得到不同的本地时间结果。
  • 你无法直接通过Date对象指定一个具体的IANA时区(例如America/New_YorkEurope/Berlin)进行操作。
const utcDate = new Date('2023-10-27T10:00:00Z'); // 10:00 UTC

// 在我的本地时区(假设是 CST, UTC-6)
console.log('本地时间:', utcDate.toString()); // Fri Oct 27 2023 04:00:00 GMT-0600 (Central Standard Time)
console.log('本地小时:', utcDate.getHours()); // 4

// UTC时间
console.log('UTC时间:', utcDate.toUTCString()); // Fri, 27 Oct 2023 10:00:00 GMT
console.log('UTC小时:', utcDate.getUTCHours()); // 10

// 没有任何方法可以直接告诉我们这个Date对象当前“认为”自己处于哪个IANA时区,
// 也没有方法可以把它“移到”另一个IANA时区并进行操作。

这对于需要处理全球用户、跨时区事件或数据中心操作的应用程序来说,是灾难性的。

4. 夏令时(DST)的噩梦

夏令时是时区处理中最为复杂和棘手的部分。在许多国家和地区,每年会有两次时钟调整:一次“春季向前拨快一小时”(Spring Forward),一次“秋季向后拨慢一小时”(Fall Back)。这导致了两个主要问题:

  • 非存在时间(Non-existent times): 在“春季向前拨快”的那一刻,某些时间段会直接消失。例如,在UTC-5时区,如果3月12日2:00 AM时钟跳到3:00 AM,那么2:00 AM到2:59 AM这段时间就不存在了。
  • 模糊时间(Ambiguous times): 在“秋季向后拨慢”的那一刻,时钟会回拨一小时,导致某一个小时重复出现。例如,在UTC-5时区,如果11月5日2:00 AM时钟跳回1:00 AM,那么1:00 AM到1:59 AM这段时间就会出现两次,一次是DST时段,一次是非DST时段。

Date对象在处理这些情况时,行为往往不明确,或者需要开发者进行复杂的猜测和补偿。

// 假设 'America/New_York' 时区
// 2023年3月12日,2:00 AM 跳到 3:00 AM (非存在时间)
const nonExistentTime = new Date('2023-03-12T02:30:00-05:00'); // 这是DST前的EST
// 实际上,这个构造函数会尝试“解决”这个不存在的时间,其行为可能因环境而异,
// 并且通常会将其调整到3:30 AM,或者在某些实现中,会将其视为无效日期。
console.log('非存在时间:', nonExistentTime.toLocaleString('en-US', { timeZone: 'America/New_York' }));
// 结果可能是 3/12/2023, 3:30:00 AM ET,而不是2:30 AM。这已经是一个不精确的推断。

// 2023年11月5日,2:00 AM 跳回 1:00 AM (模糊时间)
// 如果你创建一个 Date 对象表示 11月5日 1:30 AM
const ambiguousTime = new Date('2023-11-05T01:30:00-05:00'); // 这是一个模糊的时间,可能是DST,也可能是标准时间
console.log('模糊时间:', ambiguousTime.toLocaleString('en-US', { timeZone: 'America/New_York' }));
// Date 对象无法区分这是 DST 还是非 DST 的 1:30 AM,它只会根据内部算法和系统时区选择一个。
// 这导致了同一个本地时间字符串可能对应两个不同的UTC瞬间。

// 更复杂的是日期算术:
const d = new Date('2023-03-11T10:00:00-05:00'); // EST (UTC-5)
d.setDate(d.getDate() + 1); // 加一天
console.log('加一天后:', d.toLocaleString('en-US', { timeZone: 'America/New_York' }));
// 结果是 2023-03-12 11:00:00 AM ET (因为 DST 切换,UTC偏移量从-5变为-4,导致本地时间“跳”了一小时)
// 这与我们直观认为的“加一天,时间应该不变”相悖。

这些问题共同构成了Date对象在现代Web开发中的主要障碍。它们迫切需要一个更强大、更明确、更可靠的替代方案。

II. Temporal API:重塑JavaScript日期时间处理的基石

Temporal API正是为了解决上述所有痛点而生。它不是对现有Date对象的修补,而是一次彻底的、从头开始的设计。其核心目标是提供一个健壮、精确、易于理解和使用的日期时间API,能够优雅地处理时区、夏令时以及各种复杂的日期时间计算。

1. Temporal 的设计哲学

Temporal API遵循了几个关键的设计原则:

  • 不可变性(Immutability): 所有Temporal对象都是不可变的。任何修改操作都会返回一个新的Temporal实例,而不会修改原始实例。这消除了意外副作用的风险。
  • 显式时区(Explicit Time Zones): Temporal强制开发者明确指定或处理时区。没有模糊的“本地时区”默认行为,所有时区操作都基于IANA时区数据库。
  • 分离关注点(Separation of Concerns): Temporal引入了多种类型来表示不同的日期时间概念,例如纯日期、纯时间、带时区的日期时间等。这使得开发者可以根据需求选择最合适的类型。
  • 丰富的API(Rich API Surface): 提供全面的方法来执行日期时间算术、比较、格式化和转换,无需手动计算或求助于外部库。
  • ISO 8601优先(ISO 8601 First): 默认使用ISO 8601标准字符串进行解析和序列化,确保了数据交换的一致性。
  • 国际化友好(Internationalization Friendly):Intl API深度集成,支持各种日历系统和本地化格式。

2. Temporal 的核心类型概览

Temporal API引入了一系列新的对象类型,每种类型都旨在解决特定的日期时间问题。我们可以将它们大致分为“纯”类型(不带时区信息)、“带时区”类型和“辅助”类型。

| 类型名称 | 描述 | 关键特性 Temporal API is a new version of the JavaScript Date object that handles time zones more reliably. It is a new global object that developers can use to parse, format, and calculate with dates and times.

Here is a simple example:

// Create a PlainDateTime object for a specific date and time
const plainDateTime = Temporal.PlainDateTime.from({ year: 2023, month: 10, day: 27, hour: 10, minute: 30 });
console.log('PlainDateTime:', plainDateTime.toString()); // 2023-10-27T10:30:00

// Create a ZonedDateTime object by combining PlainDateTime with a time zone
const zonedDateTime = plainDateTime.toZonedDateTime('America/New_York');
console.log('ZonedDateTime (New York):', zonedDateTime.toString()); // 2023-10-27T10:30:00-04:00[America/New_York]

// Convert to another time zone
const parisDateTime = zonedDateTime.withTimeZone('Europe/Paris');
console.log('ZonedDateTime (Paris):', parisDateTime.toString()); // 2023-10-27T16:30:00+02:00[Europe/Paris]

// Add a duration
const nextDay = zonedDateTime.add({ days: 1 });
console.log('Next day (New York):', nextDay.toString()); // 2023-10-28T10:30:00-04:00[America/New_York]

Notice how nextDay correctly adds 24 hours while preserving the wall-clock time in the America/New_York timezone, even if there was a DST transition. The Date object would have potentially shifted the hour.

III. Temporal.Instant: 绝对时间点

Temporal.Instant代表一个固定的、明确的、与任何日历或时区无关的时间点。它被存储为自Unix纪元(1970-01-01T00:00Z)以来的纳秒数。这是Temporal中最低级别的、最精确的时间表示。

  • 创建Instant:

    • 通过当前时间:Temporal.Instant.now()
    • 通过ISO 8601字符串(必须包含Z或UTC偏移量):Temporal.Instant.from('2023-10-27T10:30:00Z')
    • 通过Unix纪元毫秒:Temporal.Instant.fromEpochMilliseconds(Date.now())
  • 特点:

    • 没有年、月、日、时、分等概念。它只表示一个时间轴上的精确点。
    • 主要用于计算时间差,或作为与ZonedDateTimePlainDateTime之间转换的桥梁。
// 获取当前瞬间
const now = Temporal.Instant.now();
console.log('当前瞬间:', now.toString()); // 2023-10-27T12:34:56.789123456Z

// 从ISO字符串创建
const specificInstant = Temporal.Instant.from('2023-01-01T12:00:00Z');
console.log('特定瞬间:', specificInstant.toString()); // 2023-01-01T12:00:00Z

// 计算两个Instant之间的持续时间
const durationBetween = specificInstant.until(now);
console.log('时间间隔:', durationBetween.toString()); // P299DT22H34M56.789123456S (持续299天22小时...)

// Instant 无法直接获取年、月、日等,因为它不带时区上下文
// console.log(specificInstant.year); // undefined

IV. “纯”日期时间类型:独立于时区进行操作

Temporal提供了一系列“纯”类型,它们不包含任何时区信息。这对于需要在不考虑时区的情况下进行日期或时间操作的场景非常有用,例如表示生日、会议室预订时间(在预订系统内部,不关心用户时区)或每日提醒。

1. Temporal.PlainDate: 纯粹的日期

Temporal.PlainDate表示一个日期,没有时间也没有时区。

  • 创建:

    • 从字符串:Temporal.PlainDate.from('2023-10-27')
    • 从对象:Temporal.PlainDate.from({ year: 2023, month: 10, day: 27 })
    • 从现有对象:Temporal.PlainDate.from(someTemporalObject)
  • 特点:

    • 不可变。
    • 拥有year, month, day, dayOfWeek, dayOfYear等属性。
    • 支持日期算术(加减天、周、月、年)。
// 创建一个 PlainDate
const today = Temporal.PlainDate.from({ year: 2023, month: 10, day: 27 });
console.log('今日:', today.toString()); // 2023-10-27
console.log('年份:', today.year);     // 2023
console.log('星期几 (1=Mon, 7=Sun):', today.dayOfWeek); // 5 (Friday)

// 添加10天
const tenDaysLater = today.add({ days: 10 });
console.log('10天后:', tenDaysLater.toString()); // 2023-11-06

// 减去一个月
const oneMonthAgo = today.subtract({ months: 1 });
console.log('一个月前:', oneMonthAgo.toString()); // 2023-09-27

// 比较日期
console.log('今天在10天后之前吗:', today.before(tenDaysLater)); // true

2. Temporal.PlainTime: 纯粹的时间

Temporal.PlainTime表示一个时间,没有日期也没有时区。

  • 创建:

    • 从字符串:Temporal.PlainTime.from('14:30:00')
    • 从对象:Temporal.PlainTime.from({ hour: 14, minute: 30, second: 0 })
  • 特点:

    • 不可变。
    • 拥有hour, minute, second, millisecond, microsecond, nanosecond等属性。
    • 支持时间算术(加减小时、分钟等),并能正确处理午夜跨越。
// 创建一个 PlainTime
const appointmentTime = Temporal.PlainTime.from({ hour: 9, minute: 30 });
console.log('预约时间:', appointmentTime.toString()); // 09:30:00

// 添加2小时15分钟
const laterTime = appointmentTime.add({ hours: 2, minutes: 15 });
console.log('稍晚时间:', laterTime.toString()); // 11:45:00

// 跨越午夜
const eveningTime = Temporal.PlainTime.from({ hour: 22, minute: 0 });
const nextMorning = eveningTime.add({ hours: 4 });
console.log('第二天早上:', nextMorning.toString()); // 02:00:00 (时间会回卷,但不会有日期概念)

// 计算时间差
const diff = appointmentTime.until(laterTime);
console.log('时间差:', diff.toString()); // PT2H15M

3. Temporal.PlainDateTime: 纯粹的日期和时间

Temporal.PlainDateTime结合了PlainDatePlainTime,表示一个日期和时间,但仍然没有时区。这是最常用于本地化日期时间操作的类型。

  • 创建:

    • 从字符串:Temporal.PlainDateTime.from('2023-10-27T10:30:00')
    • 从对象:Temporal.PlainDateTime.from({ year: 2023, month: 10, day: 27, hour: 10, minute: 30 })
    • 组合PlainDatePlainTimetoday.toPlainDateTime(appointmentTime)
  • 特点:

    • 不可变。
    • 拥有所有PlainDatePlainTime的属性。
    • 支持日期时间算术,但不会考虑DST,因为没有时区上下文。
// 创建一个 PlainDateTime
const meetingStart = Temporal.PlainDateTime.from('2023-10-27T10:30:00');
console.log('会议开始:', meetingStart.toString()); // 2023-10-27T10:30:00

// 添加3小时15分钟
const meetingEnd = meetingStart.add({ hours: 3, minutes: 15 });
console.log('会议结束:', meetingEnd.toString()); // 2023-10-27T13:45:00

// 减去5天
const fiveDaysAgo = meetingStart.subtract({ days: 5 });
console.log('5天前:', fiveDaysAgo.toString()); // 2023-10-22T10:30:00

// 计算两个 PlainDateTime 之间的持续时间
const diffDateTime = meetingStart.until(meetingEnd);
console.log('会议持续:', diffDateTime.toString()); // PT3H15M

4. Temporal.PlainYearMonthTemporal.PlainMonthDay

  • Temporal.PlainYearMonth: 表示一个特定的年份和月份,不带日期或时间。常用于周期性事件,如“每年二月”。
  • Temporal.PlainMonthDay: 表示一个特定的月份和日期,不带年份或时间。常用于生日、周年纪念日,如“每年的三月十四日”。

这些类型进一步细化了日期时间的粒度,使开发者能够精确表达意图。

V. Temporal.ZonedDateTime: 时区感知的日期时间处理核心

Temporal.ZonedDateTime是Temporal API的“主力军”,它将PlainDateTime与一个具体的IANA时区(Temporal.TimeZone对象)结合起来,从而能够准确地处理时区转换、夏令时调整和跨时区日期时间算术。

1. Temporal.TimeZone: 时区表示

Temporal.TimeZone对象代表一个IANA时区,例如'America/New_York''Europe/London''Asia/Tokyo'

  • 创建: Temporal.TimeZone.from('America/New_York')

  • 特点:

    • 提供获取时区偏移量的方法:getOffsetFor(instant)
    • 提供获取DST转换信息的方法。
// 创建一个时区对象
const nyTimeZone = Temporal.TimeZone.from('America/New_York');
console.log('纽约时区:', nyTimeZone.toString()); // America/New_York

// 获取特定 Instant 在该时区的偏移量
const someInstant = Temporal.Instant.from('2023-10-27T10:00:00Z');
console.log('纽约时区对2023-10-27T10:00:00Z的偏移量:', nyTimeZone.getOffsetFor(someInstant)); // -04:00 (EDT)

const winterInstant = Temporal.Instant.from('2023-12-27T10:00:00Z'); // 冬季,DST结束
console.log('纽约时区对2023-12-27T10:00:00Z的偏移量:', nyTimeZone.getOffsetFor(winterInstant)); // -05:00 (EST)

2. Temporal.ZonedDateTime: 时区感知日期时间

Temporal.ZonedDateTimeInstantPlainDateTimeTimeZone的结合体,它能完全且准确地表示一个特定时区中的特定日期和时间。

  • 创建:

    • InstantTimeZonesomeInstant.toZonedDateTime(nyTimeZone)
    • PlainDateTimeTimeZonemeetingStart.toZonedDateTime(nyTimeZone)
    • 从ISO 8601字符串(必须包含时区信息):Temporal.ZonedDateTime.from('2023-10-27T10:30:00-04:00[America/New_York]')
    • 获取当前时刻:Temporal.ZonedDateTime.now(nyTimeZone)
  • 特点:

    • 不可变。
    • 拥有所有PlainDateTime的属性,以及timeZoneoffset属性。
    • 支持日期时间算术,并正确处理DST转换
    • 可以在不同时区之间进行转换。
// 从 PlainDateTime 和 TimeZone 创建 ZonedDateTime
const meetingInNY = Temporal.PlainDateTime.from('2023-10-27T10:30:00').toZonedDateTime('America/New_York');
console.log('纽约会议开始:', meetingInNY.toString()); // 2023-10-27T10:30:00-04:00[America/New_York]

// 从 Instant 和 TimeZone 创建 ZonedDateTime
const instantInNY = Temporal.Instant.from('2023-10-27T10:00:00Z').toZonedDateTime('America/New_York');
console.log('Instant在纽约:', instantInNY.toString()); // 2023-10-27T06:00:00-04:00[America/New_York]

// 时区转换
const meetingInParis = meetingInNY.withTimeZone('Europe/Paris');
console.log('巴黎会议开始:', meetingInParis.toString()); // 2023-10-27T16:30:00+02:00[Europe/Paris]
// 注意:巴黎时间自动调整为比纽约早6小时,因为纽约是UTC-4,巴黎是UTC+2。

VI. Temporal.Duration: 时间长度的精确表示

Temporal.Duration表示一个时间长度或时间间隔,例如“3小时15分钟”或“2天”。它不与任何特定的开始或结束时间关联。

  • 创建:

    • 从对象:Temporal.Duration.from({ hours: 3, minutes: 15 })
    • 从字符串:Temporal.Duration.from('PT3H15M') (ISO 8601持续时间格式)
  • 特点:

    • 不可变。
    • 拥有years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds等属性。
    • 支持持续时间算术(加减持续时间)。
    • 可以“平衡”持续时间(例如,将90分钟平衡为1小时30分钟)。
// 创建一个持续时间
const travelTime = Temporal.Duration.from({ hours: 7, minutes: 45 });
console.log('旅行时间:', travelTime.toString()); // PT7H45M

// 添加另一个持续时间
const totalDuration = travelTime.add({ hours: 1, minutes: 30 });
console.log('总持续时间:', totalDuration.toString()); // PT8H75M

// 平衡持续时间
const balancedDuration = totalDuration.normalize();
console.log('平衡后的总持续时间:', balancedDuration.toString()); // PT9H15M

// 从两个 ZonedDateTime 之间计算持续时间
const eventStart = Temporal.ZonedDateTime.from('2023-10-27T10:00:00-04:00[America/New_York]');
const eventEnd = Temporal.ZonedDateTime.from('2023-10-27T14:30:00-04:00[America/New_York]');
const eventDuration = eventStart.until(eventEnd);
console.log('事件持续时间:', eventDuration.toString()); // PT4H30M

VII. Temporal.Calendar: 日历系统

Temporal.Calendar代表一个日历系统,默认是ISO 8601日历。它允许Temporal API支持非公历的日期,如日本日历、伊斯兰日历等。

  • 创建: Temporal.Calendar.from('iso8601')

  • 特点:

    • 所有Temporal对象都带有一个calendar属性。
    • 允许在不同日历系统之间进行转换(尽管这超出了本次讲座的深度)。

VIII. 实际场景:Temporal API如何解决DST和时区复杂性

现在,让我们通过几个具体的例子,看看Temporal API是如何优雅地处理Date对象曾面临的难题的。

1. 跨DST边界的日期算术

还记得Date对象在DST切换时,add(days: 1)可能会改变时间的问题吗?Temporal的ZonedDateTime会正确处理。

// 假设 'America/New_York' 时区
// 2023年3月12日,2:00 AM 跳到 3:00 AM (Spring Forward)
const nyTimeZone = Temporal.TimeZone.from('America/New_York');

// 2023年3月11日 10:00 AM EDT (UTC-5)
const beforeDst = Temporal.ZonedDateTime.from('2023-03-11T10:00:00-05:00[America/New_York]');
console.log('DST前一天:', beforeDst.toString());

// 添加1天
const afterDstAddOneDay = beforeDst.add({ days: 1 });
console.log('添加1天后:', afterDstAddOneDay.toString());
// 预期结果: 2023-03-12T10:00:00-04:00[America/New_York]
// ZonedDateTime 确保了“挂钟时间”保持不变 (10:00 AM),而UTC偏移量和内部Instant会相应调整。
// 这是正确的行为:你期望“明天上午10点”仍然是上午10点,而不是上午11点。

// 如果使用 Instant 进行算术,则会添加严格的24小时
const instantAfterOneDay = beforeDst.toInstant().add({ hours: 24 });
console.log('Instant添加24小时:', instantAfterOneDay.toString()); // 2023-03-12T15:00:00Z (如果beforeDst是2023-03-11T10:00:00-05:00[America/New_York], 那么其Instant是2023-03-11T15:00:00Z)
// 转换为ZonedDateTime后,本地时间会是 2023-03-12T11:00:00-04:00[America/New_York]
console.log('Instant添加24小时后转换为ZonedDateTime:', instantAfterOneDay.toZonedDateTime(nyTimeZone).toString());
// 这突出了 ZonedDateTime (按挂钟时间算术) 和 Instant (按绝对时间算术) 的区别。

2. 处理DST切换时的模糊和不存在时间

Temporal API在从PlainDateTime转换为ZonedDateTime时,提供了显式的disambiguation(消歧)和overflow(溢出)选项,来处理DST切换时的特殊情况。

a. 非存在时间(Non-existent times): 当时钟“春季向前拨快”时,某些本地时间是不存在的。
例如,在America/New_York时区,2023年3月12日2:00 AM跳到3:00 AM。那么2:30 AM就不存在了。

const nonExistentPlainDateTime = Temporal.PlainDateTime.from('2023-03-12T02:30:00'); // 这是一个不存在的本地时间
const nyTimeZone = Temporal.TimeZone.from('America/New_York');

// 默认行为:'compatible',会将其调整到最近的有效时间 (通常是跳过的那一小时的结束)
const zdtCompatible = nonExistentPlainDateTime.toZonedDateTime(nyTimeZone, { disambiguation: 'compatible' });
console.log('非存在时间 (compatible):', zdtCompatible.toString()); // 2023-03-12T03:30:00-04:00[America/New_York]

// 'reject':抛出错误
try {
    nonExistentPlainDateTime.toZonedDateTime(nyTimeZone, { disambiguation: 'reject' });
} catch (e) {
    console.log('非存在时间 (reject):', e.message); // RangeError: 2023-03-12T02:30:00 is not a valid wall-clock time in America/New_York
}

// 'balance':调整持续时间以保持平衡,通常是向前调整
const zdtBalance = nonExistentPlainDateTime.toZonedDateTime(nyTimeZone, { overflow: 'balance' });
console.log('非存在时间 (balance):', zdtBalance.toString()); // 2023-03-12T03:30:00-04:00[America/New_York]

b. 模糊时间(Ambiguous times): 当时钟“秋季向后拨慢”时,某些本地时间会重复出现。
例如,在America/New_York时区,2023年11月5日2:00 AM跳回1:00 AM。那么1:30 AM这个时间就出现了两次(一次是DST,一次是标准时间)。

const ambiguousPlainDateTime = Temporal.PlainDateTime.from('2023-11-05T01:30:00'); // 这是一个模糊的本地时间
const nyTimeZone = Temporal.TimeZone.from('America/New_York');

// 默认行为:'compatible',通常选择“靠后”的那个有效时间 (即DST后的标准时间)
const zdtCompatibleAmbiguous = ambiguousPlainDateTime.toZonedDateTime(nyTimeZone, { disambiguation: 'compatible' });
console.log('模糊时间 (compatible):', zdtCompatibleAmbiguous.toString()); // 2023-11-05T01:30:00-05:00[America/New_York] (EST)

// 'earlier':选择第一个出现的实例 (DST时间)
const zdtEarlier = ambiguousPlainDateTime.toZonedDateTime(nyTimeZone, { disambiguation: 'earlier' });
console.log('模糊时间 (earlier):', zdtEarlier.toString()); // 2023-11-05T01:30:00-04:00[America/New_York] (EDT)

// 'later':选择第二个出现的实例 (标准时间)
const zdtLater = ambiguousPlainDateTime.toZonedDateTime(nyTimeZone, { disambiguation: 'later' });
console.log('模糊时间 (later):', zdtLater.toString()); // 2023-11-05T01:30:00-05:00[America/New_York] (EST)

// 'reject':抛出错误
try {
    ambiguousPlainDateTime.toZonedDateTime(nyTimeZone, { disambiguation: 'reject' });
} catch (e) {
    console.log('模糊时间 (reject):', e.message); // RangeError: 2023-11-05T01:30:00 is an ambiguous wall-clock time in America/New_York
}

这些选项让开发者能够明确地控制在DST过渡期间的日期时间解析行为,从而避免了Date对象时代的猜测和不确定性。

3. 跨时区事件调度

假设你需要调度一个全球性的在线会议:

  • 开始时间:北京时间2024年1月15日10:00 AM。
  • 持续时间:1小时30分钟。

需要知道在纽约和伦敦的对应时间。

const beijingTimeZone = Temporal.TimeZone.from('Asia/Shanghai'); // 北京
const newYorkTimeZone = Temporal.TimeZone.from('America/New_York');
const londonTimeZone = Temporal.TimeZone.from('Europe/London');

// 创建北京时间的会议开始 ZonedDateTime
const meetingStartBeijing = Temporal.PlainDateTime.from('2024-01-15T10:00:00').toZonedDateTime(beijingTimeZone);
console.log('会议开始 (北京):', meetingStartBeijing.toString());

// 计算会议结束时间(在北京时区)
const meetingDuration = Temporal.Duration.from({ hours: 1, minutes: 30 });
const meetingEndBeijing = meetingStartBeijing.add(meetingDuration);
console.log('会议结束 (北京):', meetingEndBeijing.toString());

// 转换为纽约时区
const meetingStartNY = meetingStartBeijing.withTimeZone(newYorkTimeZone);
const meetingEndNY = meetingEndBeijing.withTimeZone(newYorkTimeZone);
console.log('会议开始 (纽约):', meetingStartNY.toString());
console.log('会议结束 (纽约):', meetingEndNY.toString());

// 转换为伦敦时区
const meetingStartLondon = meetingStartBeijing.withTimeZone(londonTimeZone);
const meetingEndLondon = meetingEndBeijing.withTimeZone(londonTimeZone);
console.log('会议开始 (伦敦):', meetingStartLondon.toString());
console.log('会议结束 (伦敦):', meetingEndLondon.toString());

// 验证纽约和伦敦会议的持续时间是否一致
const nyDuration = meetingStartNY.until(meetingEndNY);
const londonDuration = meetingStartLondon.until(meetingEndLondon);
console.log('纽约会议持续时间:', nyDuration.toString());
console.log('伦敦会议持续时间:', londonDuration.toString());
console.log('持续时间一致:', nyDuration.equals(londonDuration)); // true

通过ZonedDateTime,我们可以轻松地在不同时区之间转换,并进行精确的日期时间算术,而无需担心DST或其他时区规则的复杂性。

4. 格式化和国际化

Temporal对象与Intl.DateTimeFormat完美集成,可以方便地进行本地化格式化。

const zdt = Temporal.ZonedDateTime.from('2023-10-27T10:30:00-04:00[America/New_York]');

// 使用默认 locale 和选项
console.log('默认格式化:', zdt.toLocaleString()); // 10/27/2023, 10:30:00 AM EDT

// 指定 locale 和选项
const formatter = new Intl.DateTimeFormat('zh-CN', {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    timeZoneName: 'short',
    timeZone: 'America/New_York' // 确保使用 ZonedDateTime 的时区
});
console.log('中文格式化:', formatter.format(zdt)); // 2023年10月27日 上午10:30:00 EDT

IX. DateTemporal 的对比总结

特性 Date 对象 Temporal API
可变性 可变(Mutable) 不可变(Immutable)
时区处理 模糊不清,依赖宿主系统本地时区,无法明确指定 IANA 时区。 显式且强大,支持 IANA 时区,提供 ZonedDateTime 类型,精确处理时区转换和 DST。
DST 处理 行为不明确,可能导致本地时间偏移或错误。 提供 disambiguationoverflow 选项,明确处理非存在时间、模糊时间,日期算术在 DST 边界上表现正确。
API 丰富性 有限,需要手动计算毫秒进行日期算术,缺乏高级操作。 丰富且直观,提供 add(), subtract(), until() 等方法,支持多种时间单位,以及不同日期时间类型的转换。
类型系统 单一的 Date 对象,混淆了时间点、日期、时间、时区。 多种专门的类型(Instant, PlainDate, PlainTime, PlainDateTime, ZonedDateTime, Duration, TimeZone, Calendar),分离关注点。
精确度 毫秒级

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注