Dates in Javascript Will Finally Be Fixed

What is the problem?

Of all the recent changes to be implemented in ECMAScript, my favorite by far is Temporal offer. This proposal is very progressive, we can already use this API with polyfill developed by the FullCalendar team.

This API is so incredible that I'll probably spend several posts covering its main features. However, in this first post I'll cover one of its main advantages: we finally have a native object describing Zoned Date Time.

But what is Zoned Date Time?

Human Dates and JS Dates

When we talk about human dates, we usually say something like “I have a doctor's appointment for August 4, 2024 at 10:30” but we don't mention the time zone. This is logical, because most of the time our interlocutor knows us and understands that when I talk about dates, I mean the context of my time zone (Europe/Madrid).

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

IN official specification it says the following:

“An ECMAScript time value is a number; either a finite integer describing an instant in time with millisecond precision, or NaN, describing the absence of a particular instant.”

Besides the fact that JavaScript dates are not represented in UTC but in POSIX (this is VERY IMPORTANT) where leap seconds are completely ignored, the problem with describing time as a number is that the original semantics of the data is lost. That is, given a human date, we can get an equivalent JS date, but not vice versa.

Let's look at an example: let's say I need to record the moment a payment is made from my card. Many developers are tempted to write something like this:

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

Because my browser is in the time zone CETwhen I write this, the browser just “calculates the number of milliseconds since the start of EPOX for this CET moment.”

Here's what we actually store in the date:

paymentDate.getTime();
// 1721464200000

That is, depending on how we read this information, we will get different “human dates”:

If we count them in terms of CET, we get 10:30:

d.toLocaleString()
// '20/07/2024, 10:30:00'

and if you count from the ISO point of view, then 8:30:

d.toISOString()
// '2024-07-20T08:30:00.000Z'

Many people think that by working with UTC or transmitting data in ISO format they are providing security, however This is not true, information is still lost.

UTC format is not enough

Even when working with ISO dates with offsets, the next time we want to display a date, we only know the number of milliseconds since the UNIX epoch and the offset. But that's still not enough to know the “human” moment and time zone of the payment.

Strictly speaking, having a timestamp t0we can get n human-readable dates describing it…

In other words, the function responsible for converting a timestamp into a human-readable date is not injectivesince each element in the set of timestamps corresponds to more than one element in the set of “human dates”.

Exactly the same thing happens when storing dates in ISO, since timestamps and ISO are two descriptions of the same moment:

This happens when working with offsets toobecause different time zones can have the same offset.

If you still don't fully understand the problem, let me illustrate it with an example. Let's imagine that you live in Madrid and go to Sydney.

A few weeks later you return to Madrid and see a strange charge you can't remember… I was charged 3.50 at 2am on the 16th? What was I doing? I went to bed early that night!.. I don't understand.

After a bit of worry, you realize that this is a payment for the coffee you had the next morning, because after reading the article, you already realize that your bank stores all transactions in UTC, and the app converts them to the phone's time zone.

This may seem like an innocent story, but what if your bank allows you to withdraw cash once a day for free? When does the day start and end? UTC? Australia?… It gets complicated, trust me…

Hopefully by now you've realized that working solely with timestamps is a problem; fortunately, there is a solution.

ZonedDateTime

Among other things, the new Temporal API introduces the concept of an object Temporal.ZonedDateTime specifically designed to describe dates and times in the corresponding time zone. The developers also proposed RFC 3339 extension to standardize serialization and deserialization of strings describing data:

Here is an example:

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

This string describes 39 minutes and 57 seconds past the 16th hour of December 19, 1996, with an offset of -08:00 from UTC, and additionally specifies the time zone associated with the date (“Pacific Time”) so that it can be used by time zone-aware applications.

In addition, this API allows you to work with various calendars, including:

  • Buddhist

  • Chinese

  • Coptic

  • Korean

  • Ethiopian

  • Gregorian

  • Jewish

  • Indian

  • islamic

  • islamic-umalqura

  • islamic-tbla

  • islamic-civil

  • islamic-rgsa

  • Japanese

  • Persian

  • Mingo calendar

Among them all, the most popular will be iso8601 (the standard adaptation of the Gregorian calendar) that you will work with most often.

Basic Operations

Creating dates

The Temporal API offers a great advantage when creating dates, especially with the Temporal.ZonedDateTime object. One of its standout features is its ability to handle time zones seamlessly, including the complexities of Daylight Saving Time (DST). For example, if 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 specifying a date and time; you're providing an accurate description of the date in the specified time zone. This accuracy ensures that, regardless of DST changes or any other local time changes, your date will always reflect the correct moment in time.

This feature is especially useful when scheduling events or logging actions that are coordinated across multiple regions. By building the time zone directly into the date creation process, Temporal eliminates common problems with traditional Date objects, such as unexpected time shifts due to DST or time zone differences. So Temporal is not just a way to make your life easier, it’s a necessity for modern web development, where global time consistency is critical.

If you are curious about what is so great about this API, read on an article explaining how to deal with changes in time zone definitions.

Comparison of dates

ZonedDateTime has a static method comparewhich takes two ZonedDateTime and returns:

  • −1if the first is less than the second

  • 0if both describe exactly the same moment without taking into account the time zone and calendar

  • 1if the first is greater than the second.

It is easy to compare dates in unusual cases, for example when an hour repeats after DST ends, later values ​​may be earlier in the clock time, and 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);
  // => -1
  // (потому что `one` в реальном мире происходит раньше)

Great built-in features

ZonedDateTime has pre-computed attributes to make your life easier, such as:

hoursInDay

The hoursInDay read-only property returns the number of real hours between the start of the current day (usually midnight) in zonedDateTime.timeZone and 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;
  // => 24
  // (обычныый день)
Temporal.ZonedDateTime.from('2020-03-08T12:00-07:00[America/Los_Angeles]').hoursInDay;
  // => 23
  // (в этот день начинается DST)
Temporal.ZonedDateTime.from('2020-11-01T12:00-08:00[America/Los_Angeles]').hoursInDay;
  // => 25
  // (в этот день завершается DST)

ZonedDateTime also has some great attributes daysInYear, inLeapYear

Time Zone Conversion

ZonedDateTimes has a method .withTimeZone allowing you to change ZonedDateTime as needed:

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

Arithmetic

You can use the method .add to add the date portion of a time interval using calendar arithmetic. The result automatically takes Daylight Saving Time into account based on the rules of the timeZone field of this instance.

The great thing about this is that it supports the ability to do arithmetic with both calendar arithmetic and simple durations.

  • Adding or subtracting days must accommodate clock times when DST transitions occur. For example, if you have a meeting scheduled for Saturday at 1:00 PM and you want to move it forward one day, you would expect the meeting to be scheduled for 1:00 PM again, even if DST transitioned overnight.

  • Adding or subtracting a duration time portion should ignore DST transitions. For example, if you agree to meet a friend in two hours, they will be upset if you show up an hour or three hours later.

  • There must be a consistent and reasonably expected order of operations. If the results fall on or near a DST transition, then the uncertainty must be resolved automatically (without failure) and deterministically.

zdt = Temporal.ZonedDateTime.from('2020-03-08T00:00-08:00[America/Los_Angeles]');
// Прибавляем день, чтобы получить полночь в день после дня начала DST
laterDay = zdt.add({ days: 1 });
  // => 2020-03-09T00:00:00-07:00[America/Los_Angeles]
  // Обратите внимание, что новое смещение отличается, это показывает, что результат учитывает DST.
laterDay.since(zdt, { largestUnit: 'hour' }).hours;
  // => 23
  // потому что один час потерялся из-за DST

laterHours = zdt.add({ hours: 24 });
  // => 2020-03-09T01:00:00-07:00[America/Los_Angeles]
  // Прибавление единиц времени не учитывает DST. Результат равен 1:00: спустя 24 часов
  // реального времени, потому что один час был пропущен из-за DST.
laterHours.since(zdt, { largestUnit: 'hour' }).hours; // => 24

Calculating differences between dates

Temporal has a method .until which calculates the difference between two times represented by a zonedDateTime, optionally rounds it, and returns it as a Temporal.Duration object. If the second time was earlier than the zonedDateTime, the resulting duration will be negative. With the default options, adding the returned Temporal.Duration to the zonedDateTime will yield the second value.

This may seem like a trivial operation, but I recommend read full specificationto understand its nuances.

Conclusion

The Temporal API is a revolutionary change to how JavaScript handles time, making it one of the few languages ​​to have a comprehensive solution to this problem. In this article, I've only scratched the surface, covering the difference between human-readable dates (or clock times) and UTC dates, and how the Temporal.ZonedDateTime object can be used to accurately describe the former.

In future articles we'll look at other great objects like Instant, PlainDate, and Duration.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *