时间性:开始使用JavaScript的新日期时间API

1,116 阅读15分钟

Date ,JavaScript目前的日期时间API是出了名的难用ECMAScript提案 "Temporal "是一个新的、更好的日期时间API,目前处于第三阶段。它是由Philipp Dunkel,Maggie Johnson-Pint,Matt Johnson-Pint,Brian Terlson,Shane Carr,Ujjwal Sharma,Philip Chimento,Jason Williams, andJustin Grant创建的。

这篇博文有两个目标。

  • 让你感受到Temporal是如何工作的
  • 帮助你开始使用它

然而,这并不是一个详尽的文档。对于许多细节,你必须查阅Temporal的(优秀)文档

警告。这些是我对这个API的第一次探索--欢迎反馈!


目录。


Temporal API

Temporal的日期时间API可以通过全局变量Temporal 。它使用起来非常愉快。

  • 所有的对象都是不可改变的。改变它们会产生新的值,类似于JavaScript中字符串的工作方式。
  • 它支持时区和非格雷戈尔式的日历。
  • 有几个专门针对时间值的类(带时区的日期时间值,不带时区的日期时间值,不带时区的日期值,等等)。这有几个好处。
    • 一个值的上下文(是否有时区,等等)更容易理解。
    • 如何实现一个给定的任务往往更加明显。
    • .toString() ,使用时可以少考虑很多。
  • 1月是第1个月。

这篇博文的部分内容。

  • 这篇文章以背景知识开始。这将有助于你完成文章的其余部分,但你不看也应该没事。
  • 接下来,是对Temporal API的所有类的概述,以及它们是如何结合在一起的。
  • 在结尾处,有一个包含实例的综合部分。

从太阳时到标准时

从历史上看,我们测量时间的方式在过去的几年中不断进步。

  • 表观太阳时(本地表观时间)。最早测量时间的方法之一是根据太阳的位置来确定当前时间。例如,正午是太阳正上方的时候。
  • 平均太阳时(当地平均时间)。这种时间表示方法修正了表观太阳时的变化,使一年中的每一天都有相同的长度。
  • 标准时间和时区。标准时间规定了一个地理区域内的时钟是如何同步的。它建立于19世纪,以支持天气预报和火车旅行。在20世纪,标准时间被定义为全球性的,地理区域成为_时区_。

挂钟_时间_是一个时区内的当前时间(由墙上的时钟显示)。挂钟时间也被称为_当地时间_。

时间标准。UTC vs. Z与GMT

UTC、Z和GMT是指定时间的方式,它们相似,但有细微的不同。

  • UTC(协调世界时)是所有时区都基于的时间标准。它们是相对于它而指定的。也就是说,没有一个国家或地区将UTC作为其本地时区。

  • Z(祖鲁时区)是一个军事时区,经常被用于航空和军事领域,是UTC+0的另一个名称。

  • GMT(格林威治时间)是在一些欧洲和非洲国家使用的时区。它是UTC加零时,因此具有与UTC相同的时间。

来源。

Temporal的时区是基于IANA时区数据库(简称:tz数据库)。IANA是Internet Assigned Numbers Authority的缩写。在该数据库中,每个时区都有一个标识符和定义UTC时间的偏移的规则。如果一个时区有标准时间和夏令时,那么偏移量在一年中会发生变化。

在标准时间,America/Los_Angeles 时区的时间偏移是-8:00(A线)。在夏令时,时间偏移为-7:00(B线)。

这是两个不错的时区名称列表。

日历

Temporal支持的日历是基于标准的Unicode统一编码通用地域数据存储库(CLDR)--除此之外

  • buddhist: 泰国佛教日历
  • chinese: 繁体中文日历
  • coptic :科普特日历
  • dangi :韩国传统历法
  • ethiopic :埃塞俄比亚日历,Amete Mihret(大约公元8世纪)。
  • gregory :格雷戈里历
  • hebrew :传统希伯来历
  • indian :印度历法
  • islamic :伊斯兰历法
  • iso8601 :国际标准化组织日历(使用ISO 8601日历周规则的公历)。
  • japanese: 日本帝国历
  • persian: 波斯历
  • roc :中华民国历法

iso8601 被大多数西方国家使用,并在Temporal中得到额外的支持,方法包括:Temporal.now.zonedDateTimeISO() (返回系统时区和ISO-8601日历中的当前日期和壁钟时间)。

ECMAScript扩展ISO-8601/RFC 3339字符串

标准ISO-8601和RFC 3339规定了如何用字符串表示日期。目前,它们缺少Temporal所需要和添加的功能。

  • 以字符串表示月日数据
  • 在日期时间字符串中表示IANA时区名称
  • 在日期时间字符串中表示日历系统

我们的目标是最终使这些新增功能标准化(超越ECMAScript)。

月-日语法

月-日语法看起来像这样。

> Temporal.PlainMonthDay.from('12-24').toString()
'12-24'

带有IANA时区名称和日历系统的日期时间字符串

下面的代码显示了一个完整的日期时间字符串的样子。在实践中,其中许多部分往往会被遗漏。

const zdt = Temporal.ZonedDateTime.from({
  timeZone: 'Africa/Nairobi',
  year: 2019,
  month: 11,
  day: 30,
  hour: 8,
  minute: 55,
  second: 0,
  millisecond: 123,
  microsecond: 456,
  nanosecond: 789,
});

assert.equal(
  zdt.toString({calendarName: 'always', smallestUnit: 'nanosecond'}),
  '2019-11-30T08:55:00.123456789+03:00[Africa/Nairobi][u-ca=iso8601]');

前面例子中的日期时间字符串的部分内容。

  • 日期。'2019-11-30'
  • 日期和时间之间的分隔符。'T'
  • 时间。'08:55:00.123456789'
    • 小时':' 分钟':'
    • '.' (秒和秒的零头之间的分隔符
    • 毫秒(3位数)
    • 微秒(3位数)
    • 纳秒(3位数)
  • 相对于UTC的时间偏移。'+03:00'
    • 替代方案:'Z' ,这意味着'+0:00'
  • 时区。'[Africa/Nairobi]'
  • 日历。'[u-ca=iso8601]'

最后两项目前还没有标准化。

Temporal API的概述

本节给出了一个关于时间API的类的概述。它们都可以通过全局变量TemporalTemporal.Instant,Temporal.ZonedDateTime, 等等)来访问。

本节后给出了使用它们的例子。

Temporal区分了两种时间。给定一个全局的时间瞬间。

  • 挂钟时间(也叫_本地时间_或_时钟时间_)在全球范围内变化,取决于一个时钟的时区。
  • 精确时间(也叫_UTC时间_)在任何地方都是一样的。

纪元时间_是表示精确时间的一种方式。它是一个计算_Unix纪元(1970年1月1日UTC午夜)之前或之后的时间单位(如纳秒)的数字。

Temporal中的所有日期和/或时间类支持两种直接创建方式。

一方面,构造函数接受完全指定日期时间值所需的最小数据量。例如,在两个精确时间的类中,InstantZonedDateTime ,时间本身是通过纳秒(epoch nanosecond)指定的。

const epochNanoseconds = 6046783259163000000n;
const timeZone = 'America/Mexico_City';
const zdt1 = new Temporal.ZonedDateTime(epochNanoseconds, timeZone);
assert.equal(
  zdt1.toString(),
  '2161-08-12T17:00:59.163-05:00[America/Mexico_City]');

静态工厂方法.from() 是重载的。大多数类支持其参数的三种值。

首先,如果参数是同一个类的一个实例,那么这个实例就被克隆了。

const zdt2 = Temporal.ZonedDateTime.from(zdt1);
assert.equal(
  zdt2.toString(),
  '2161-08-12T17:00:59.163-05:00[America/Mexico_City]');

第二,所有其他对象都被解释为指定具有时间相关信息的各种字段。

const zdt3 = Temporal.ZonedDateTime.from({
  timeZone: 'America/Mexico_City',
  year: 2161,
  month: 8,
  day: 12,
  hour: 17,
  minute: 0,
  second: 59,
  millisecond: 163,
  microsecond: 0,
  nanosecond: 0,
});
assert.equal(
  zdt3.toString(),
  '2161-08-12T17:00:59.163-05:00[America/Mexico_City]');

第三,所有的原始值都被胁迫为字符串并进行解析。

请注意,我们不需要在A行指定偏移量,但在B行显示了偏移量。

下表给出了日期时间类的概述。

Temporal.*Cal地区
ZonedDateTime
← 纪元、区域、Cal?
Instant(✓)
← 震旦
PlainDateTime
← Y, M, D, h?, m?, s?, ms?, μs?, ns?, cal?
PlainDate
← Y, M, D, cal?
PlainTime
← h?, m?, s?, ms?, μs?
PlainYearMonth
← Y, M, cal? , refIsoDay?
PlainMonthDay
← M, D, cal?, refIsoYear?

图例。

  • cal:日历
  • zone:时区
  • epoch: 纪元纳秒
  • Y, M, D: ISO年、月、日
  • h, m, s:ISO小时、分钟、秒
  • ms, μs, ns:ISO毫秒、微秒、纳秒
  • refIsoDay:参考ISO日,在使用ISO8601日历以外的日历时用于消除歧义。
  • refIsoYear:参考ISO年,在使用ISO 8601日历以外的其他日历时用于消除歧义。
  • Instant 内部使用一个日历,但该日历不能通过构造函数参数来配置。

命名。

  • Zoned 表示一个明确指定的时区
  • Plain 表示一个值没有相关的时区(抽象时间)。

对象Temporal.now ,有几个工厂方法用于创建代表当前时间的Temporal值。

> Temporal.now.instant().toString()
'2021-06-27T12:51:10.961Z'

> Temporal.now.zonedDateTimeISO('Asia/Shanghai').toString()
'2021-06-27T20:51:10.961+08:00[Asia/Shanghai]'

> Temporal.now.plainDateTimeISO().toString()
'2021-06-27T20:51:10.961'

> Temporal.now.plainTimeISO().toString()
'20:51:10.961'

由系统提供的上下文:时区和日历

我们可以使用Temporal.now 来访问系统的当前时区。这个时区可以改变--例如,当系统旅行时。

> Temporal.now.timeZone().toString()
'Asia/Shanghai'

访问当前的日历则更为复杂。

const sysCal = new Intl.DateTimeFormat().resolvedOptions().calendar;

Temporal以三种方式表示确切的时间。

  • 通过类Instant (UTC时间)。
  • 通过类ZonedDateTime (壁钟时间加上一个时区和一个日历)。
  • 通过一个表达自纪元以来的纳秒数的大int数。

Instant

Instant 代表全球精确时间。它使用UTC作为它的 "时区"。它的日历总是iso8601

使用这个类来表示不显示给终端用户的内部日期(日志中的时间戳,等等)。

const instant = Temporal.now.instant();
assert.equal(
  instant.toString(),
  '2021-06-27T08:32:33.18174345Z');

ZonedDateTime 类通过挂钟时间加上一个时区和一个日历来表示时间。

这个类的使用情况。

  • 表示实际事件
  • 转换不同地区的时间
  • 在夏令时可能起作用的时间计算("晚一小时")。

如果一个类没有时区,Temporal称它为 "普通"。有三个没有时区的类:PlainDateTime,PlainDate, 和PlainTime 。它们是时间的抽象表示。

这些类的用例。

  • 显示特定时区的挂钟时间(见下文)。
  • 当时区不重要时,进行时间计算("1998年5月的第一个星期三")。

PlainYearMonth

PlainYearMonth 的一个实例抽象地指某年的某月。

使用案例。

  • 识别一个每月重复发生的事件("2020年10月的会议")。
const plainYearMonth = Temporal.PlainYearMonth.from(
  {year: 2020, month: 10});
assert.equal(
  plainYearMonth.toString(),
  '2020-10');

PlainMonthDay

PlainMonthDay 的一个实例抽象地指某月的某一天。

使用案例。

  • 识别一个每年重复发生的事件("巴士底狱日是7月14日")。

帮助类

Calendar

所有包含完整日期的时间类都使用日历来帮助它们进行各种计算。大多数代码会使用ISO 8601日历,但也支持其他日历系统。

这是三种常见的指定日历的方式。

const pd1 = new Temporal.PlainDate(1992, 2, 24,
  'iso8601');
const pd2 = new Temporal.PlainDate(1992, 2, 24,
  {calendar: 'iso8601'});
const pd3 = new Temporal.PlainDate(1992, 2, 24,
  new Temporal.Calendar('iso8601'));

TimeZone 的实例代表时区。它们支持IANA时区、UTC和UTC偏移。对于大多数使用情况,IANA时区是最好的选择,因为它们能够正确处理夏令时。

这是指定时区的三种常见方式。

const zdt1 = new Temporal.ZonedDateTime(0n,
  'America/Lima');
const zdt2 = new Temporal.ZonedDateTime(0n,
  {timeZone: 'America/Lima'});
const zdt3 = new Temporal.ZonedDateTime(0n,
  new Temporal.TimeZone('America/Lima'));

Duration

持续时间代表一个时间长度--例如,3小时45分钟。

持续时间用于时间运算。

  • 测量两个时间值之间的差异
  • 将时间加到一个时间值上
  • 等等。
const duration = Temporal.Duration.from({hours: 3, minutes: 45});
assert.equal(
  duration.total({unit: 'second'}),
  13500);

请注意,对于时长没有简单的标准化。

  • 有时,我们指的是 "90分钟"。
  • 有时,我们指的是 "1小时30分"。

前者不应该被自动转换为后者。

例子

输入和输出

从字符串转换到字符串

静态工厂方法.from() 总是接受字符串。

const zdt = Temporal.ZonedDateTime.from(
  '2019-12-01T12:00:00[Pacific/Auckland]');

.toString() 方法的工作是可预测的,并且可以配置。

assert.equal(
  zdt.toString(),
  '2019-12-01T12:00:00+13:00[Pacific/Auckland]');

assert.equal(
  zdt.toString({offset: 'never', timeZoneName: 'never'}),
  '2019-12-01T12:00:00');

assert.equal(
  zdt.toString({smallestUnit: 'minute'}),
  '2019-12-01T12:00+13:00[Pacific/Auckland]');

然而,.toString() 在这种情况下不会让你隐藏分钟--如果你想这样做,你必须将ZonedDateTime 转换为PlainDate

assert.equal(
  zdt.toPlainDate().toString(),
  '2019-12-01');

转换为JSON和从JSON转换

所有的Temporal日期时间值都有一个.toJSON() 方法,因此可以被字符串化为JSON。

如果你想用日期时间值解析JSON,你需要设置一个JSON reviver

转换为人类可读的字符串

Temporal支持将日期时间值转换为人类可读的字符串,这与Intl.DateTimeFormat's类似。

const zdt = Temporal.ZonedDateTime.from(
  '2019-12-01T12:00[Europe/Berlin]');

assert.equal(
  zdt.toLocaleString(),
  '12/1/2019, 12:00:00 PM GMT+1');

assert.equal(
  zdt.toLocaleString('de-DE'),
  '1.12.2019, 12:00:00 MEZ');

assert.equal(
  zdt.toLocaleString('en-GB', {
    weekday: 'long',
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  }),
  'Sunday, 1 December 2019');

Temporal不支持解析人类可读的字符串。

在传统的DateTemporal 之间进行转换

一方面,我们可以将传统的日期转换为Temporal实例。

const legacyDate = new Date('1970-01-01T00:00:01Z');

const instant1 = legacyDate.toTemporalInstant();
assert.equal(
  instant1.toString(),
  '1970-01-01T00:00:01Z');

这是对以前的方法的一种替代。

const ms = legacyDate.getTime();
const instant2 = Temporal.Instant.fromEpochMilliseconds(ms);
assert.equal(
  instant2.toString(),
  '1970-01-01T00:00:01Z');

另一方面,Instant 所暴露的一个字段为我们提供了以毫秒为单位的纪元时间--我们可以用它来创建一个日期。

const instant = Temporal.Instant.from('1970-01-01T00:00:01Z');
const legacyDate = new Date(instant.epochMilliseconds);

assert.equal(
  legacyDate.toISOString(),
  '1970-01-01T00:00:01.000Z');

使用时间值的工作

使用字段值

大多数日期时间类支持丰富的字段,如.dayOfWeek,.month, 和.calendar 。值得注意的例外是Instant ,它的时区和日历是固定的,它的设置和状态是基于历时的。

在所有其他的日期时间类中,我们可以使用静态工厂函数.from() 来配置实例。

const zonedDateTime = Temporal.ZonedDateTime.from({
  timeZone: 'Africa/Lagos',
  year: 1995,
  month: 12,
  day: 7,
  hour: 3,
  minute: 24,
  second: 30,
  millisecond: 0,
  microsecond: 3,
  nanosecond: 500,
});
assert.equal(
  zonedDateTime.toString(),
  '1995-12-07T03:24:30.0000035+01:00[Africa/Lagos]');

这些字段和其他字段也可以作为属性使用,但这些属性是不可改变的。

assert.equal(
  zonedDateTime.year,
  1995);

assert.equal(
  zonedDateTime.month,
  12);

assert.equal(
  zonedDateTime.dayOfWeek,
  4);

assert.equal(
  zonedDateTime.epochNanoseconds,
  818303070000003500n);

如果我们想改变这些字段,我们需要通过.with() ,创建一个新的值。

const newZonedDateTime = zonedDateTime.with({
  year: 2222,
  month: 3,
});
assert.equal(
  zonedDateTime.toString(),
  '1995-12-07T03:24:30.0000035+01:00[Africa/Lagos]');

每个日期时间类D ,提供一个函数D.compare ,用于对实例进行排序D

const dates = [
  Temporal.ZonedDateTime.from('2022-12-01T12:00[Asia/Tehran]'),
  Temporal.ZonedDateTime.from('2001-12-01T12:00[Asia/Tehran]'),
  Temporal.ZonedDateTime.from('2009-12-01T12:00[Asia/Tehran]'),
];
dates.sort(Temporal.ZonedDateTime.compare);
assert.deepEqual(
  dates.map(d => d.toString()),
  [
    '2001-12-01T12:00:00+03:30[Asia/Tehran]',
    '2009-12-01T12:00:00+03:30[Asia/Tehran]',
    '2022-12-01T12:00:00+03:30[Asia/Tehran]',
  ]);

时间值之间的转换

Instant 转换为ZonedDateTimePlainDateTime

const instant = Temporal.Instant.from('1970-01-01T00:00:01Z');

const zonedDateTime = instant.toZonedDateTimeISO('Europe/Madrid');
assert.equal(
  zonedDateTime.toString(),
  '1970-01-01T01:00:01+01:00[Europe/Madrid]');

const plainDateTime1 = zonedDateTime.toPlainDateTime();
assert.equal(
  plainDateTime1.toString(),
  '1970-01-01T01:00:01');

const timeZone = Temporal.TimeZone.from('Europe/Madrid');
const plainDateTime2 = timeZone.getPlainDateTimeFor(instant);
assert.equal(
  plainDateTime2.toString(),
  '1970-01-01T01:00:01');

ZonedDateTime 转换为InstantPlainDateTime

const zonedDateTime = Temporal.ZonedDateTime.from(
  '2019-12-01T12:00[Europe/Minsk]');

const instant = zonedDateTime.toInstant();
assert.equal(
  instant.toString(),
  '2019-12-01T09:00:00Z');

const plainDateTime = zonedDateTime.toPlainDateTime();
assert.equal(
  plainDateTime.toString(),
  '2019-12-01T12:00:00');

PlainDateTime 转换成ZonedDateTimeInstant

const plainDateTime = Temporal.PlainDateTime.from(
  '1995-12-07T03:24:30');

const zonedDateTime = plainDateTime.toZonedDateTime('Europe/Berlin');
assert.equal(
  zonedDateTime.toString(),
  '1995-12-07T03:24:30+01:00[Europe/Berlin]');

const instant = zonedDateTime.toInstant();
assert.equal(
  instant.toString(),
  '1995-12-07T02:24:30Z');
const source = Temporal.ZonedDateTime.from(
  '2020-01-09T02:00[America/Chicago]');
const target = source.withTimeZone('America/Anchorage');

assert.equal(
  target.toString(),
  '2020-01-08T23:00:00-09:00[America/Anchorage]');
const plainDate = Temporal.PlainDate.from('2020-03-08');
assert.equal(
  plainDate.add({days: 1}).toString(),
  '2020-03-09');

九月的第一个星期一

为了计算某一年的劳动节(9月的第一个星期一),我们需要计算出要在9月1日的基础上增加多少天才能达到工作日1(星期一)。

Temporal的Polyfill

npm软件包proposal-temporal,包含了一个初步的Temporal的Polyfill。

关于API的更多信息TemporalDate