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
const formatter = newIntl.DateTimeFormat(/* your preferred format here */);
console.log('Put the conference 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 the conference for me?
Put the conference 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.
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.
This is an adaptation of a presentation that I gave last year at a conference
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-in to the browser or JS engine. That's important because many developers include a 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 two hundred kilobytes to the app.
Temporal is strongly typed. It means that for different kinds of data, such as a calendar date with or without an associated time, there are different types to represent them, instead of one type fits all.
Temporal objects are 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 OK for all these years.
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 linked blog post 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 unwieldy 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 behaviour.
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 is no calendar, so no months, weeks, years, or days; no time zone, so no daylight saving adjustments. Just purely an ever-increasing number.
mermaid.js
Next we have a family of types called the "Plain" types. These represent a date on your wall calendar and a time on your wall clock, which we call "wall" time for short.
They represent your local date and time independent of time zone, and so there are no daylight saving adjustments.
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 wanted 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 time 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.
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, ZonedDateTime. Just like Instant, this type represents an exact moment in time. But it is also coupled to a location with time zone rules, because it includes a time zone. 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 it has a year, month, and day.
We say that ZonedDateTime 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. ZonedDateTime 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 #1 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.)
The next thing, maybe a meta-thing, to consider, is do we really want a timestamp in milliseconds or will the Instant object itself work just as well? Going forward, as Temporal gets wider adoption, we expect that numerical timestamps are going to be mainly necessary for interoperation, and not so much for JavaScript applications, where Instant will be used. But for the sake of this example let's say that we do need a numerical timestamp.
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.
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. This 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" is only on types that have calendars, "offset" only exists on ZonedDateTime, and the "epochNanosconds" 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.
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 determine 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 plainDateISO() which does not.
So, 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 is used in a large part of the world with minor variations, but it is similar enough to the ISO calendar that you can switch between the two without having to do any mental calculations.
However, although a large part of the world uses the Gregorian calendar, that does not 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 could go wrong. Let's say today's date in the Gregorian calendar is August 6th, and one month later is September 6th. But in the Hebrew calendar, these 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 have to actually consider the calendar when performing the addition.
The lesson here is when working with dates that the user will see, is 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 Intl.DateTimeFormat (though beware; this does not 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 toLocaleString method. Here's an example of what it looks like in my locale: 2021 dash 09 dash 06.
Now for the complicated task. We need to answer the question, using the information on this conference website, what times do these sessions happen for me? Which sessions can I 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 AEST" 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 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 pseudocode of what we need to do. For each date and 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. Then 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 OK 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 Summer Time, and Eastern Standard Time (with North America implied in that last one.) But those are human names, not machine identifiers. These strings with the slashes, like Australia-slash-Brisbane, are the official identifiers of the IANA time zone database, and 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 the 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 type's 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 are more readable, either for someone else who's reading your code, or for you who's re-reading it 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, use the equals method.
Here is the function that tells 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.
And now this code 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 sessionStart, as a ZonedDateTime. Then we use the withTimeZone method to get a new ZonedDateTime 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 toZonedDateTime method that we saw is an example of one of the methods for conversion between Temporal 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 converted from PlainDate to ZonedDateTime, so we had to add both a time zone and a time, which the toZonedDateTime 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, because these two can't be provided at the same time as any other properties. It would be ambiguous which property you'd have to replace first.
Take that code, put it all together, run it, and here is the nicely formatted answer! (This is the answer in my time zone, with my locale's 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've been possible, by carefully making sure 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've 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.