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的类的概述。它们都可以通过全局变量Temporal (Temporal.Instant,Temporal.ZonedDateTime, 等等)来访问。
本节后给出了使用它们的例子。
Temporal区分了两种时间。给定一个全局的时间瞬间。
- 挂钟时间(也叫_本地时间_或_时钟时间_)在全球范围内变化,取决于一个时钟的时区。
- 精确时间(也叫_UTC时间_)在任何地方都是一样的。
纪元时间_是表示精确时间的一种方式。它是一个计算_Unix纪元(1970年1月1日UTC午夜)之前或之后的时间单位(如纳秒)的数字。
Temporal中的所有日期和/或时间类支持两种直接创建方式。
一方面,构造函数接受完全指定日期时间值所需的最小数据量。例如,在两个精确时间的类中,Instant 和ZonedDateTime ,时间本身是通过纳秒(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不支持解析人类可读的字符串。
在传统的Date 和Temporal 之间进行转换
一方面,我们可以将传统的日期转换为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 转换为ZonedDateTime 和PlainDateTime
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 转换为Instant 和PlainDateTime
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 转换成ZonedDateTime 和Instant
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的更多信息Temporal 和Date
- 官方的Temporal文档目前托管在GitHub上,但最终将被转移到MDN Web Docs。
- 传统的
DateAPI在 "JavaScript for impatient programmers "一书中有一章记录。