JavaScript 日期即将修复
JavaScript dates are about to be fixed

原始链接: https://docs.timetime.in/blog/js-dates-finally-fixed

时间提案是 ECMAScript 中的一项新功能,它提供了一个本机对象来表示“Zoned DateTime”,即特定的日期和时间及其各自的时区。 在此更新之前,开发人员在使用 JavaScript 中的纯数字“日期”对象时会遇到不一致和数据丢失的问题。 通过 Temporal API,开发人员可以利用“Temporal.ZonedDateTime”和“Temporal.PlainDate”等对象,从而允许他们执行复杂的计算和操作,而不会丢失重要的上下文数据。 主要优点包括: * 提高了处理时区的准确性,特别是在夏令时 (DST) 前后 * 能够轻松可靠地比较日期 * 内置属性,如“hoursInDay”和“daysInYear”,以简化计算 * 支持各种历法,如伊斯兰教历、佛教历、中国历等,尽管 ISO8601(公历)仍然是最常用的 * 能够使用“.withTimeZone()”等方法更改时区 * 即使 DST 转换也能保持一致性的算术运算,支持日历和简单的持续时间数学 * 像`.until()`这样的方法来计算日期之间的差异,返回一个`Temporal.Duration`对象 总体而言,新的 Temporal API 显着改进了 JavaScript 中的日期管理,解决了与正确处理多个时区相关的长期存在的问题。

使用 JavaScript 日期的开发人员建议避免将日期和时间视为日期时间字符串之外的独立构造。 在处理时区、夏令时 (DST) 或在不同时区托管服务器或与客户端交互时,将现实世界事件表示为当天的午夜 (00:00) 可能会导致问题。 相反,建议在 DateTime 字符串中包含时间部分,以保持对事件的准确跟踪。 还要注意的是确保输入日期时间字符串符合日期时间字符串格式 (RFC 822) 以最大限度地提高跨浏览器兼容性的重要性。 虽然 JavaScript 支持多种日期时间字符串格式,但仅支持日期时间字符串格式可确保广泛的兼容性。 最后,讨论强调了在应用程序中考虑时区意识的重要性。 特别是在管理特定于区域的事件时,建议存储本地时间和相应的时区信息,以及撰写本文时的 UTC 偏移量。 使用可靠的时区数据库可以帮助管理时区更改和不一致。 总之,考虑使用具有显式时间组件的 DateTime 字符串,并确保输入字符串符合日期时间字符串格式,以在跨浏览器和设备的日期管理中实现更高的一致性和准确性。 此外,优先考虑区域特定事件的时区感知,并利用可靠的时区数据库来处理时区更改和不一致。
相关文章

原文

Of all the recent changes coming to ECMAScript, my favorite by far is the Temporal proposal. This proposal is very advanced, and we can already use this API through the polyfill provided by the FullCalendar team.

This API is so incredible that I will likely dedicate several blog posts to highlighting its key features. However, in this first post, I will focus on explaining one of its main advantages: we finally have a native object to represent a "Zoned Date Time".

But... What is a "Zoned Date Time"?

Well, when we talk about human dates, we usually say something like, "I have a doctor's appointment on August 4th, 2024, at 10:30 AM," but we omit the time zone. This omission makes sense because, generally, our interlocutor knows us and understands that when I talk about dates, I usually do so in the context of my time zone, Europe/Madrid.

Unfortunately, with computers, this is not the case. When we work with "Date" objects in JavaScript we are dealing with plain numbers.

If we read the official specification, it states:

"An ECMAScript time value is a Number, either a finite integral Number representing an instant in time to millisecond precision or NaN representing no specific instant"

Aside from the VERY IMPORTANT fact that dates in JavaScript are not UTC but POSIX, where leap seconds are completely ignored, the problem with having only numbers is that the original semantics of the date are lost. This is becaus given an human date we can get the equivalent js date but not the other way arround.

Let's consider an example: suppose I want to record the moment I make a payment with my card. Many people might be tempted to do something like this:

const paymentDate = new Date('2024-07-20T10:30:00');

Since my browser is on an CET timezone, when I write this the browser just "computes the number of milliseconds since the EPOX given this CET instant"

This is what we actually store in a date:

This means that depending on how you read this information you will get a different "human date":

If we read this from the CET perspective I get 10:30:

and if we read this from the ISO perspective we get 8:30:

Many people think that by working with UTC or communicating dates in ISO format, they are safe; however, this is not correct, as information is still lost.

Even when working with dates on an ISO format, including the offset, the next time we want to display that date, we only know the number of milliseconds that have passed since the UNIX epoch and the offset. But this is still not enough to know the human moment and time zone in which the payment was made.

Strictly speaking, given a timestamp t0, we can obtain n human-readable dates that represent it...

In other words, the function responsible for transforming a timestamp into a human-readable date is not injective, as each element of the set of timestamps corresponds to more than one element of the "human dates" set.

This happens in exactly the same way when storing ISO dates, as timestamps and ISO are two representations of the same instant:

And this also happens if you work with offsets because different timezones might have the same offset.

If you still don't see the problem clearly, let me illustrate it with an example. Imagine you live in Madrid and take a trip to Sydney.

A few weeks later, you return to Madrid and see a charge on your transaction statement that you don't recognize... a charge of 3.50 at 2 AM on the 16th? What was I doing? That night I went to bed early!... I don't understand.

After a while of being worried, you realize that the charge corresponds to the coffee you had the following morning since, as you've read this article, you've deduced that your bank stores all transactions in UTC, and the application translates them to the phone's time zone.

This may end up as an anecdote, but what if your bank applies a promotion of one free cash withdrawal per day? When does that day start and end? UTC? Australia?... Things get complicated, believe me...

At this point, I hope you're convinced that working with only timestamps is a problem that, fortunately, now has a solution.

In addition to many other things, the new Temporal API introduces a Temporal.ZonedDateTime object specifically designed to represent dates and times with their corresponding time zone. They have also proposed an extension to RFC 3339 to standardize the serialization and deserialization of strings representing dates:

As an example:

   1996-12-19T16:39:57-08:00[America/Los_Angeles]

This string represents 39 minutes and 57 seconds after the 16th hour of December 19, 1996, with an offset of -08:00 from UTC, and additionally specifies the human time zone associated with it ("Pacific Time") for time-zone-aware applications to take into account.

Additionally, this API allows working with different calendars such as:

  • buddhist
  • chinese
  • coptic
  • dangi
  • ethioaa
  • ethiopic
  • gregory
  • hebrew
  • indian
  • islamic
  • islamic-umalqura
  • islamic-tbla
  • islamic-civil
  • islamic-rgsa
  • japanese
  • persian
  • roc

Among all these, the most common will be iso8601 (the standard adaptation of the Gregorian calendar) with which you will work most frequently.

Basic operations

Creating Dates

The Temporal API offers a significant advantage when creating dates, particularly with its Temporal.ZonedDateTime object. One of the standout features is its ability to effortlessly handle time zones, including those tricky situations involving Daylight Saving Time (DST). For example, when you create a Temporal.ZonedDateTime object like this:

const zonedDateTime = Temporal.ZonedDateTime.from({
year: 2024,
month: 8,
day: 16,
hour: 12,
minute: 30,
second: 0,
timeZone: 'Europe/Madrid'
});

You’re not just setting a date and time; you're ensuring that this date is accurately represented within the specified time zone. This precision means that regardless of DST changes or any other local time adjustments, your date will always reflect the correct moment in time.

This feature is especially powerful when scheduling events or logging actions that need to be consistent across different regions. By incorporating the time zone directly into the date creation process, Temporal eliminates the common pitfalls of working with traditional Date objects, such as unexpected shifts in time due to DST or time zone differences. This makes Temporal not just a convenience but a necessity for modern web development where global time consistency is crucial.

If you are curious about why this API is great read this article explaining how to deal with changes on Time Zone definitions.

Comparing dates

ZonedDateTime offers an static method named compare which given 2 ZonedDateTimes one and two will return:

  • −1 if one is less than two
  • 0 if the two instances describe the same exact instant, ignoring the time zone and calendar
  • 1 if one is greater than two.

You can easily compare dates on unusual cases like the repeated clock hour after DST ends, values that are later in the real world can be earlier in clock time, or vice versa:

const one = Temporal.ZonedDateTime.from('2020-11-01T01:45-07:00[America/Los_Angeles]');
const two = Temporal.ZonedDateTime.from('2020-11-01T01:15-08:00[America/Los_Angeles]');

Temporal.ZonedDateTime.compare(one, two);


Cool built-ins

A ZonedDateTime has some precomputed attributes that will make your life easier, for example:

hoursInDay

The hoursInDay read-only property returns the number of real-world hours between the start of the current day (usually midnight) in zonedDateTime.timeZone to the start of the next calendar day in the same time zone.

Temporal.ZonedDateTime.from('2020-01-01T12:00-08:00[America/Los_Angeles]').hoursInDay;


Temporal.ZonedDateTime.from('2020-03-08T12:00-07:00[America/Los_Angeles]').hoursInDay;


Temporal.ZonedDateTime.from('2020-11-01T12:00-08:00[America/Los_Angeles]').hoursInDay;


Another cool attributes are daysInYear, inLeapYear

Transforming timezones

ZonedDateTimes offer a .withTimeZone method which allows to change a ZonedDateTime as we desire:

zdt = Temporal.ZonedDateTime.from('1995-12-07T03:24:30+09:00[Asia/Tokyo]');
zdt.toString();
zdt.withTimeZone('Africa/Accra').toString();

Basic arithmetics

We can use the .add method to add the date portion of a duration using calendar arithmetics. The result will automatically adjust for Daylight Saving Time using the rules of this instance's timeZone field.

The GREAT PART of this is that it supports playing with calendar arithmetics or plain durations.

  • Adding or subtracting days should keep clock time consistent across DST transitions. For example, if you have an appointment on Saturday at 1:00PM and you ask to reschedule it 1 day later, you would expect the reschedule appointment to still be at 1:00PM, even if there was a DST transition overnight.
  • Adding or subtracting the time portion of a duration should ignore DST transitions. For example, a friend you've asked to meet in in 2 hours will be annoyed if you show up 1 hour or 3 hours later.
  • There should be a consistent and relatively-unsurprising order of operations. If results are at or near a DST transition, ambiguities should be handled automatically (no crashing) and deterministically.
zdt = Temporal.ZonedDateTime.from('2020-03-08T00:00-08:00[America/Los_Angeles]');

laterDay = zdt.add({ days: 1 });


laterDay.since(zdt, { largestUnit: 'hour' }).hours;



laterHours = zdt.add({ hours: 24 });



laterHours.since(zdt, { largestUnit: 'hour' }).hours;

Computing differences between dates.

Temporal offers a method named .until which computes the difference between the two times represented by zonedDateTime and other, optionally rounds it, and returns it as a Temporal.Duration object. If other is earlier than zonedDateTime then the resulting duration will be negative. If using the default options, adding the returned Temporal.Duration to zonedDateTime will yield other.

This might look like an obvious operation but I encourage you to read the full spec to understand the nuances of it.

The Temporal API represents a revolutionary shift in how time is handled in JavaScript, making it one of the few languages that address this issue comprehensively. In this article, we've only scratched the surface by discussing the difference between human-readable dates (or wall clock time) and UTC dates, and how the Temporal.ZonedDateTime object can be used to accurately represent the former.

In future articles, we'll explore other fascinating objects such as Instant, PlainDate, and Duration.

I hope you enjoyed this introduction.

Happy coding! :)

联系我们 contact @ memedata.com