Temporal: Modern dates and times in JavaScript

I'm speaking today about the Temporal proposal, which adds modern date and time handling to JavaScript.

In this presentation I'll give a tour through the API and show you what you can do with Temporal by means of an easy, medium and complicated programming task.

Temporal is an API that has been proposed to become part of JavaScript.

It's currently making its way through the standardization process.

It will add a built-in library for dates and times to JavaScript like many other programming languages already have.

Temporal is built into the browser or the JS engine.

That's important because many developers included dependency on Moment or some similar library in their app to achieve the same thing that you could with Temporal.

But depending on how much locale data you include, this may add a payload of anywhere from a dozen to 200 kilobytes to an app.

Temporal is strongly typed.

It means that for different kinds of data, such as a CalendarDate with, or without an associated time, there are different types to represent them, instead of one type fits all.

Temporaral objects are also immutable.

That means that you don't have to worry about code that you don't own modifying your objects without your knowledge.

Temporal is designed to work together with JavaScript's internationalization facilities and provides things that a lot of other libraries don't such as non Gregorian calendars.

A fair question for any proposal is, do we really need this?

JavaScript already has the global Date object.

It's not perfect, but it's done okay for all these years, hasn't it?

Well, I disagree.

We do actually have a good list of Date's deficiencies.

This here is a list of problems with Date that were identified way back at the beginning, that Temporal aims to solve.

You can read about them in the link blog posts by Maggie Johnson Pint, one of the Moment developers, and also one of the original architects of Temporal.

She lists them in order of relative disgust.

I won't go into all of these.

Some of them such as the unwiedly APIs could be fixed.

Others, like the mutability cannot be fixed without breaking the web because so much code out there already relies on the existing behavior.

JavaScript Date, which I'm going to optimistically call Legacy Date was based on a date class from Java, which Java deprecated in 1997 and replaced by something better.

I mentioned strong typing.

For the next few slides I'm going to give you a tour through these types that Temporal introduces.

The first type you should know about is Instant.

It represents what we call an "exact time", an instantaneous point on the timeline with nanosecond resolution.

There's no calendar.

So no months, weeks, years, or days.

No time zone, so no daylight saving adjustments, just purely an ever increasing number.

Next, we have a family of types called the "plain types".

These represent a date on your wall calendar at a time on your wall clock, which we call "wall time" for short.

They represent your local date and time independent of the time zone.

And so there are no daylight savings adjustments either.

But unlike instant, they are not exact moments in time.

Why do we have this family of types with progressively less information?

It's so that you can correctly represent the information that you have.

For example, with legacy Date, if you want it to represent a date without a time, you might use midnight in the user's local time zone.

But there are actually days in certain times zones where midnight is skipped due to daylight saving time.

This can cause hard to track down bugs.

For example, midnight on November 4th, 2018, didn't exist in Brazil.In Temporal We have these types that don't carry all the information.

PlainDate is a day without a specific clock time.

It's a very common use case.

PlainTime is a clock time not on any specific day.

PlainYearMonth is a month without a specific day, you could think of using it to refer to an event that happens occasionally, like I have there-the September, 2021 board meeting.

It also corresponds to HTML input type month.

Finally, PlainMonthDay is a calendar date, but without a specific year.

You could think of it as referring to a birthday or anniversary.

Completing the family of types that represent dates and times is another exact time type: ZoneDateTime.

Just like instant, this type represents an exact moment in time, but it is also coupled to a location with a time zone rules, because it includes a timezone.

The time zone means that this type does account for daylight saving time changes and other changes in the time zone.

It also has a calendar, which means that unlike Instant, he has a year, a month, and day.

We say that ZoneDateTime represents a calendar event that happened or will happen at a place on earth.

There are some other auxiliary types that don't represent a date or a time.

Duration is used in arithmetic with the other types and has some methods of its own for rounding and converting between units.

Finally, there are the TimeZone and Calendar types.

Usually you won't need to use these directly because you'll be using the built-in ones implicitly when you use the other types.

However, you can write your own custom time zones and calendars for specialized applications.

I won't go into that here.

This diagram is taken from the Temporal documentation.

It shows the relationships between the types.

You can see there are the two types on the left that know the exact time and the other types on the right that know the wall time.

So in ZoneDateTime spans both categories because it knows both the exact time and the wall time.

Now that you've been introduced to the cast of characters in Temporal it's time to show how to accomplish a programming task.

I've picked three tasks to walk through: an easy one, a medium one, and a complicated one.

The easy task is to get the current time as a Unix timestamp in milliseconds.

This is the number one top voted question for legacy Date on stack overflow.

So naturally we'll want to be able to do the same thing in Temporal.

First we consider what type we have to use.

A timestamp represents an exact time without regard to time zones.

So we use Instant.

In fact, Instant was very nearly named 'timestamp' earlier in the Temporal design process.

The next thing, maybe a meta thing to consider is do we really want to timestamp and milliseconds, or will the instant object itself work just as well?

Going forward as Temporal gains wider adoption, we do expect that numerical timestamps are going to be mainly necessary for interoperation, not so much for use within JavaScript applications where instant will be used.

But for the sake of this example, let's say that we do need a numerical timestamp in millisecond.

So we know what type we need and next we need to know how to fill it with the current Time.

We do this, with the functions in the Now namespace, any of these functions will give you the current date, time and or time zone as one of the Temporal types.

There are shortcuts for the calendar types for when you are using the ISO calendar.

Since we need the current time as an instant, we'll use the top one from this list: Temporal.

Now.Instant.

Next we'll need to figure out how to get a number of milliseconds since the Unix epoch from the instant.

Temporal types have read only properties with which we can get this sort of information.

Here is a full list of these properties.

Not every type has each property, only the ones that make sense for that type.

So for example, year exists only on types that have calendars, offset only exists on ZoneDateTime, and the epochNanoseconds and similar properties are only on the exact time types.

In our case, we need the epochMilliseconds property.

Putting the previous slides together into this line of code, we get the result of Temporal.Now.Instant and examine its epochMilliseconds property to obtain a Unix timestamp in milliseconds, which is a big number.

Now for the medium task.

The question we have to answer is "what is the date one month from today?" We already saw in the previous task, how to figure out what the date is today.

So let's skip that for the moment.

Although I'll talk more about how that interacts with calendars in a moment.

How do we add one month to a date?

When I mentioned duration a few slides ago, I mentioned arithmetic methods.

This is where we will need to use one of those arithmetic methods.

We have add and subtract for adding or subtracting a duration.

We also have since and until that determined the elapsed time between two Temporal objects.

In our case, we use the add method and pass it a property bag that automatically gets converted to a duration representing one month, like in this line of code.

Now this date variable, where does it come from?

We know we need to use one of the now methods and we have two choices, PlainDate, which takes a calendar and PlaneDateISO, which does not.

So first a little bit about calendars.

The ISO calendar can be thought of as the machine calendar, how it works is standardized with no regional or language dependence.

The Gregorian calendar is a human calendar that's used in a large part of the world with minor variations, but it's similar enough to the ISO calendar that you can switch between the two without having to do any mental calculations.

However, though a large part of the world does use the Gregorian calendar, that doesn't mean that the whole world uses it.

Some regions don't and in some regions, people use the Gregorian calendar for business purposes and another one for religious purposes.

Already without Temporal, you can print a legacy date object in a non ISO non-Gregorian calendar if the user wants it.

That's good enough for some applications, but not for answering this question because how long one month is depends on which month you're talking about in which calendar.

Here's an example of how that might go wrong.

Today's date in the Gregorian calendar is August 6th and one month later is September six.

But in the Hebrew calendar, those two dates are not one month apart this year, they are one month and one day apart.

So it is not enough just to add a month and print the dates in another calendar.

You actually, you actually have to consider the calendar when performing the addition.

The lesson here is when working with dates that the user will see to use the user's calendar, not the machine ISO calendar.

Putting it all together.

The proper way to get the date one month from today is to define exactly what month you mean by choosing the calendar.

You can get the calendar from the user's preferences in your app or from their locale using the resolvedOptions method of the Intl.DateTimeFormat object, though beware this doesn't always correspond to what the user actually wants.

Then you can get today's date in that calendar using Now.plainDate.

Add one month to it using the add method and format it for the user with the two locale string method.

Here's an example of what it looks like in my locale, 2021-09-06.

Now for the complicated task, we need to answer the question using the information on the conference website "what time does Global Scope happen for me?" I'd like to know which sessions I can attend when I'm not asleep.

This question turns out to be surprisingly hard to answer if we don't use a computer, because it requires a lot of mental gymnastics with time zones and subtracting or adding hours, most people, myself included just put 11 o'clock EST into a search engine and hope that the search engine correctly guesses what I want.

Temporal's strongly typed approach is perfect for solving this problem.

Here are the facts that we know: we know from the website, the two calendar dates of the conference, August 6th and 13th, 2021, there are three sessions on each conference day.

We know the local start times of each of the three sessions and what time zones those local time start times are given in.

I also know my time zone and I know the hours in my local time zone during which I'm willing to be online, watching a conference.

Here's some pseudo-code of what we need to do :for each date in each session.

We know the wall time and the time zone, which allows us to figure out the exact time that the session starts and ends.

And we need to go from exact time back to wall time in my time zone to check whether I'm awake or not.

If I am, then we print out that session.

Here's the first part of the code where we set things up.

We have the calendar dates, the session time zones, and start hours.

The length of the sessions and my time zone.

For this calculation, it's okay to use the ISO calendar for the dates because I'm reckoning, my awake times in the ISO calendar as well.

There are a few things to explain here.

One of them is how I determined these time zone names.

On the website we had Australia Eastern standard time, British summertime and Eastern standard time with North America implied in that last one, but those are human names, not machine identifiers.

The strings with the slashes like Australia slash Brisbane are the official identifiers of the IANA timezone database, that's how you refer to time zones in Temporal.

If you don't know the IANA identifier for a particular time zone, you can usually find it out on Wikipedia.

The identifier is always at least as precise.

For example, in our case, some of the Eastern part of Australia uses AEST for half a year, but Queensland uses it the whole year round.

So AEST can be ambiguous if you have dates scattered throughout the year.

The next question is why are we using these from methods to create our Temporal objects?

As you might expect, you could also use a Temporal types constructor to create an instance of it, but the constructors are intended for low-level use where you have all the data in exactly the right format.

Like in these examples.

On the other hand, there are these from methods which are high level and accept many more kinds of data, in particular, they're more readable, either for someone else who's reading your code or for you, who's rebuilding your own code later.

In most cases I'd recommend using from, to create a Temporal object.

Now, back to writing code.

The other piece of data that I mentioned we know is what times I'm willing to be online for the conference.

I want to write a little function that takes a wall clock time and returns true if the time is within my online hours.

For that, we have to do comparisons.

Each type has a compare static method that we use for this.

It's a static method so that you can use it as an argument to the sort method of arrays.

If instead, you just want the common case of checking whether two temporal objects are equal, don't use compare, use the equals method.

Here's the function that tells us whether I can be online.

We are using Temporal.PlainTime.Compare to implement it.

Here I am taking advantage of the fact that my online hours do not cross midnight.

We'd have to be slightly more complicated to support the case where they did.

Now this code here is how the actual calculation works, that I previously outlined in pseudocode.

We loop through each conference date and the info for each session and convert it to the exact time session start as a ZoneDateTime.

Then we use that WithTimeZone method to get a new ZoneDateTime object for the start of the session in my time zone, instead of the conference session's time zone, then we add the session length using the add method that we're already familiar with to get the end of the session in my time zone.

These we pass to our comparison function.

And if they are during my online times, we print out the start and the end of the session using formatRange.

If you're watching a recording of this talk, you might want to pause it at this point to examine the code in more detail.

I'll quickly go into details about the new methods that we've seen here.

The toZoneDateTime method that we saw is an example of one of the methods for conversion between Temoral types.

To convert from one Temporal type to another, you either need to add information or drop information.

If you drop information like the year, when you go from PlainDate to PlainMonthDay, the method doesn't take any argument.

On the other hand, if you add information like the clock time, when you go from PlainDate to PlainDateTime, the method will take the required information as an argument.

In our case, we convert from PlainDate to ZoneDateTime.

So we had to add both a time zone and a time which the ToZoneDate method takes in a property bag.

The other new method you saw was withTimeZone.

This is a good time to get into how to modify a Temporal object.

Technically you can't.

Temporal objects are immutable, but you can get a new object of the same type with one or more components replaced.

This is what the with method is for.

It takes a property bag with one or more components to replace and returns a new object.

Here you can see an example.

We take a date and replace the day with the last day of the month.

We have separate methods for withCalendar and withTimeZone, which we're using here because calendars and timesZones can't be provided at the same time as any other properties-it would be ambiguous which property you'd have to replace first.

So take that code, put it all together, run it.

And here's the nicely formatted answer.

This is the answer in my time zone with my locales formatting of course, your answer will be different.

This is a seemingly simple question answered in more lines of code than you might expect, but I seriously would not have wanted to even try to write this code with legacy Date.

It would have been possible by carefully making sure that the dates were all in UTC and converting accordingly by manually adding or subtracting the right number of hours, but really susceptible to bugs.

I hope you found this a useful tour of Temporal.

And if you deal with legacy Dates in your code base, I hope you're excited to replace them with Temporal objects.

You can check out the documentation at this link.

At some point it will graduate and you'll be able to find it on MDN instead, along with the rest of JavaScript's built-ins.

If you're interested in learning some of the things that people widely believe about dates and times, but that aren't true.

You can check out yourcalendricalfallacyis.com for an interesting read.

Thank you for your attention.

Temporal

Temporal

Temporal

Modern dates and times in JavaScript

Philip Chimento ptomato • @therealptomato
Igalia, in partnership with Bloomberg
Global Scope, 6 August 2021

What is Temporal?

  • A proposal for enhancing JavaScript
  • A new, built-in, date-time library
  • Strongly typed
  • Immutable
  • Strong support for internationalization

🤔 But we already have Date!

Date is not a good API

Date is not a good API

Date is not a good API

Source: Maggie Johnson-Pint

  1. No support for time zones other than the user’s local time and UTC
  2. Parser behavior so unreliable it is unusable
  3. Date object is mutable
  4. DST behavior is unpredictable
  5. Computation APIs are unwieldy
  6. No support for non-Gregorian calendars

The selection of types

Temporal.Instant

Temporal.Instant

Temporal.Instant

  • An exact moment in time*
  • No time zone, no daylight saving
  • No calendar
  • Data model: elapsed nanoseconds since midnight UTC, Jan. 1, 1970
*disregarding leap seconds, though
  • ‘Wall’ time
  • No time zone, no daylight saving
  • Several types in this family with progressively less information

Plain types

  • PlainDateTime
  • PlainDate
  • PlainTime
  • PlainYearMonth
  • PlainMonthDay

Plain types

  • Represent the information you have
  • Avoid buggy pattern of filling in 0 or UTC for missing info
  • Do appropriate calculations based on type

Plain types

  • Temporal.PlainDate: “The conference session on August 6th, 2021”
  • Temporal.PlainTime: “The break is at 12:05”
  • Temporal.PlainYearMonth: “The September 2021 board meeting”
  • Temporal.PlainMonthDay: “My birthday is December 15th”

Temporal.ZonedDateTime

  • Like Temporal.Instant, an exact time
  • But also with a calendar and time zone
  • Correctly accounts for the time zone's daylight saving rules
  • “The Global Scope session is on Friday, August 13th, 2021, at 11 AM, Australian Eastern Standard Time”

Other types

  • Temporal.Duration
    • returned from other types' since() and until() methods
    • passed to their add() and subtract() methods

Other types

  • Temporal.TimeZone
    • contains rules for UTC offset changes in a particular time zone
    • converts between Instant and PlainDateTime
  • Temporal.Calendar
    • contains rules for converting dates from a particular calendar to ISO 8601 and back

Type relationships

Slide shows two groups, and a space between them.

On the left is a group labelled Exact Time Types.

It contains ZonedDateTime which spans both groups and the space between. Under ZonedDateTime is Instant, and below that the text "These types know time since Epoch".

In the gap between the two groups are

From top to bottom: ZonedDateTime which spans the gap and both groups then TimeZone, Calendar and Duration.

A line from the right edge of Instant branches and joins to the left edegs of TimeZone and Calendar.

In the group on the right, labelled Calendar Date Wall-Clock Time Types in addition to ZonedDateTime there are 5 types, arranged in a hierarchy that is displayed left to right.

Lines emerging from the righthand edges of TimeZone and Calendar join and then connect to the left hand edge of PlainDateTime. A line emerges from the right of PlainDateTime an branches and connects above and to the right to PlainDate on its left hand edge. A line emerges fromthe right edge of PlainDate, branches and them connects withPlainMonthDay and below it the second branch connects to PlainYearMonth.

The second branch that emerged from the right edge of PlainDateTime connects with the left edge of PlainTime

At the botton of this group is the text "These have a calendar (except PlainTime) and know wall-clock time"

Show me the code!

An easy one

Get a Unix timestamp in ms

Temporal.Now

Temporal.Now

Temporal.Now

  • A namespace with functions that give you Temporal objects representing the current date, time, or time zone
  • Shortcuts if you are using the ISO 8601 calendar
Temporal.Now.instant()
Temporal.Now.timeZone()
Temporal.Now.zonedDateTime(calendar)
Temporal.Now.zonedDateTimeISO()
Temporal.Now.plainDateTime(calendar)
Temporal.Now.plainDateTimeISO()
Temporal.Now.plainDate(calendar)
Temporal.Now.plainDateISO()
Temporal.Now.plainTimeISO()

Properties

  • Each Temporal object has read-only properties
  • year, month, monthCode, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, timeZone, offset, era, eraYear, dayOfWeek, dayOfYear, weekOfYear, daysInWeek, daysInMonth, daysInYear, monthsInYear, inLeapYear, hoursInDay, startOfDay, offsetNanoseconds, epochSeconds, epochMilliseconds, epochMicroseconds, epochNanoseconds
  • Only ZonedDateTime has all these at once, the others have subsets
> Temporal.Now.instant().epochMilliseconds
1627682409993

Get a Unix timestamp in ms

A medium one

What is the date one month from today?

Arithmetic

  • add()/subtract() take a Temporal.Duration and return a new Temporal object of the same type, that far in the future or the past
  • since()/until() take another Temporal object of the same type and return a Temporal.Duration representing the amount of time that elapsed from one to the other
const monthLater = date.add({ months: 1 })

Calendar support

  • The standard "machine" calendar is the ISO 8601 calendar
  • "Human" readable calendars are common in user-facing use cases
  • Much of the world uses the Gregorian calendar
  • Can print non-ISO dates with toLocaleString(), but that is not good enough

What is the date, one month from today?

> d1 = Temporal.Now.plainDateISO();
> d1.toLocaleString('en', { calendar: 'hebrew' });
'28 Av 5781'

> d2 = d1.add({ months: 1 });
> d2.toLocaleString('en', { calendar: 'hebrew' });
'29 Elul 5781'  // WRONG!

What is the date, one month from today?

const calendar = ... // choose the appropriate calendar
const date = Temporal.Now.plainDate(calendar);
console.log(date.add({ months: 1 }).toLocaleString());
// example in my locale: "2021-09-06"

A complicated one

What time is Global Scope for me?

What time is Global Scope for me?

What we know:

  • The two calendar dates that the sessions occur on (August 6 & 13)
  • The start times of the sessions and their time zones (11 AM AEST, noon BST, 1 PM EDT)
  • My time zone
  • The hours (in my local time) at which I am willing to be at my computer

What time is Global Scope for me?

  • For each session date,
    • For each session,
      • Calculate the exact time of that session start and end on that date;
      • If the local time of the session in my time zone falls within the hours that I'm willing to be at my computer,
        • Print the local time of the session in my time zone.

What time is Global Scope for me?

const dates = [
  Temporal.PlainDate.from({ year: 2021, month: 8, day: 6 }),
  Temporal.PlainDate.from({ year: 2021, month: 8, day: 13 })
];
const sessions = [
  { timeZone: 'Australia/Brisbane', hour: 11 },
  { timeZone: 'Europe/London', hour: 12 },
  { timeZone: 'America/New_York', hour: 13 }
];
const sessionLength = Temporal.Duration.from({ hours: 4 });
const myTimeZone = Temporal.Now.timeZone();

Construction

  • Constructors are low-level
  • Accept numerical arguments for the date/time data
new Temporal.PlainTime(13, 37)  // ⇒ 13:37:00
new Temporal.Instant(946684860000000000n)  // ⇒ 2000-01-01T00:01:00Z
new Temporal.PlainDate(2019, 6, 24);  // ⇒ 2019-06-24

Construction

  • from() static methods are high-level and friendly
  • Accept property bags, ISO strings, instances
  • Recommended in most cases
Temporal.PlainYearMonth.from({ year: 2019, month: 6 });
Temporal.PlainYearMonth.from('2019-06-24T15:43:27');
Temporal.PlainYearMonth.from(myPlainDate);

Comparison

  • equals() lets you know whether two Temporal objects of the same type are exactly equal (including their calendars and time zones, if applicable)
  • Temporal.{type}.compare(obj1, obj2) is for sorting and ordering
dates.sort(Temporal.PlainDate.compare);

What time is Global Scope for me?

const myCoffeeTime = Temporal.PlainTime.from('08:00');
const myBedtime = Temporal.PlainTime.from('23:00');

function iCanBeOnlineAt(time) {
  return Temporal.PlainTime.compare(myCoffeeTime, time) <= 0 &&
         Temporal.PlainTime.compare(time, myBedtime) <= 0;
}

What time is Global Scope for me?

const formatter = new Intl.DateTimeFormat(/* your preferred format here */);
console.log('Put Global Scope in your calendar:');
dates.forEach((date) => {
  sessions.forEach(({ timeZone, hour }) => {
    const sessionStart = date.toZonedDateTime({ timeZone, plainTime: { hour } });
    const mySessionStart = sessionStart.withTimeZone(myTimeZone);
    const mySessionEnd = mySessionStart.add(sessionLength);
    if (iCanBeOnlineAt(mySessionStart) && iCanBeOnlineAt(mySessionEnd)) {
      console.log(formatter.formatRange(
        mySessionStart.toPlainDateTime(), mySessionEnd.toPlainDateTime()));
    }
  });
});

Conversion

  • Conversion methods can remove or add information
  • Argument: any information that needs to be added
  • Some types contain all the information of some other types
const date = Temporal.PlainDate.from('2006-08-24');
const time = Temporal.PlainTime.from('15:23:30.003');
date.toPlainDateTime(time);  // ⇒ 2006-08-24T15:23:30.003
date.toPlainMonthDay();  // ⇒ 08-24

Modification

  • with() methods
  • Argument: a property bag with some components of the same type
  • Returns a new Temporal object with the provided fields replaced
  • Remember, Temporal objects are immutable!
  • Separate withCalendar() and withTimeZone() methods
const date = Temporal.PlainDate.from('2006-01-24');
// What's the last day of this month?
date.with({ day: date.daysInMonth });  // ⇒ 2006-01-31
date.withCalendar('islamic-civil');  // ⇒ 1426-12-24 AH

What time is Global Scope for me?

Put Global Scope in your calendar:
Thursday, August 5, 6:00 – 10:00 p.m.
Friday, August 6, 10:00 a.m. – 2:00 p.m.
Thursday, August 12, 6:00 – 10:00 p.m.
Friday, August 13, 10:00 a.m. – 2:00 p.m.

Learning more