The core concept in Temporal is the distinction between wall-clock time (also called "local time" or "clock time") which depends on the time zone of the clock and exact time (also called "UTC time") which is the same everywhere.
Wall-clock time is controlled by local governmental authorities, so it can abruptly change. When Daylight Saving Time (DST) starts or if a country moves to another time zone, then local clocks will instantly change. Exact time however has a consistent global definition and is represented by a special time zone called UTC (from Wikipedia):
Coordinated Universal Time (or UTC) is the primary time standard by which the world regulates clocks and time. It is within about 1 second of mean solar time at 0° longitude, and is not adjusted for daylight saving time. It is effectively a successor to Greenwich Mean Time (GMT).
Every wall-clock time is defined using a UTC Offset: the amount of exact time that a particular clock is set ahead or behind UTC.
For example, on January 19, 2020 in California, the UTC Offset (or "offset" for short) was -08:00
which means that wall-clock time in San Francisco was 8 hours behind UTC, so 10:00AM locally on that day was 18:00 UTC.
However the same calendar date and wall-clock time India would have an offset of +05:30
: 5½ hours later than UTC.
ISO 8601 and RFC 3339 define standard representations for exact times as a date and time value, e.g. 2020-09-06T17:35:24.485Z
. The Z
suffix indicates that this is an exact UTC time.
Temporal has two types that store exact time: Temporal.Instant
(which only stores exact time and no other information) and Temporal.ZonedDateTime
which stores exact time, a time zone, and a calendar system
Another way to represent exact time is using a single number representing temporal distance from the Unix epoch of January 1, 1970 at 00:00 UTC.
For example, Temporal.Instant
(an exact-time type) can be constructed using only a BigInt
value of nanoseconds since epoch, ignoring leap seconds.
Another term developers often encounter is "timestamp".
This most often refers to an exact time represented by the number of seconds since Unix epoch.
Temporal avoids using this terminology, however, because of historical ambiguity surrounding the term "timestamp".
For example, many databases have a type called TIMESTAMP
, but its meaning varies: in MySQL, it is an exact time; in Oracle Database, it is the number of seconds since the wall-clock time 00:00 on January 1, 1970 (a quantity one might call a "local timestamp"); and in Microsoft SQL Server, it is a monotonically increasing value unrelated to date and time.
A Time Zone defines the rules that control how local wall-clock time relates to UTC. You can think of a time zone as a function that accepts an exact time and returns a UTC offset, and a corresponding function for conversions in the opposite direction. (See below for why exact → local conversions are 1:1, but local → exact conversions can be ambiguous.)
Temporal uses the IANA Time Zone Database (or "TZ database"), which you can think of as a global repository of time zone functions. Each IANA time zone has:
Europe/Paris
or Africa/Kampala
) but can also denote single-offset time zones like UTC
(a consistent +00:00
offset) or Etc/GMT+5
(which for historical reasons is a negative offset -05:00
).The IANA Time Zone Database is updated several times per year in response to political changes around the world. Each update contains changes to time zone definitions. These changes usually affect only future date/time values, but occasionally fixes are made to past ranges too, for example when new historical sources are discovered about early-20th century timekeeping.
In Temporal:
Temporal.Instant
type represents exact time only.Temporal.PlainDateTime
type represents calendar date and wall-clock time, as do other narrower types: Temporal.PlainDate
, Temporal.PlainTime
, Temporal.PlainYearMonth
, and Temporal.PlainMonthDay
.
These types all carry a calendar system, which by default is 'iso8601'
(the ISO 8601 calendar) but can be overridden for other calendars like 'islamic'
or 'japanese'
.Temporal.ZonedDateTime
type encapsulates all of the types above: an exact time (like a Temporal.Instant
), its wall-clock equivalent (like a Temporal.PlainDateTime
), and the time zone that links the two.There are two ways to get a human-readable calendar date and clock time from a Temporal
type that stores exact time.
Temporal.ZonedDateTime
instance then the wall-clock time values are trivially available using the properties and methods of that type, e.g. .year
, .hour
, or .toLocaleString()
.Temporal.Instant
, use a time zone and optional calendar to create a Temporal.ZonedDateTime
. Example:instant = Temporal.Instant.from('2019-09-03T08:34:05Z');
formatOptions = {
era: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
};
zdt = instant.toZonedDateTimeISO('Asia/Tokyo');
// => 2019-09-03T17:34:05+09:00[Asia/Tokyo]
zdt.toLocaleString('en-us', { ...formatOptions, calendar: zdt.calendar });
// => 'Sep 3, 2019 AD, 5:34:05 PM'
zdt.year;
// => 2019
zdt.toLocaleString('ja-jp', formatOptions);
// => '西暦2019年9月3日 17:34:05'
zdt = zdt.withCalendar('japanese');
// => 2019-09-03T17:34:05+09:00[Asia/Tokyo][u-ca=japanese]
zdt.toLocaleString('en-us', { ...formatOptions, calendar: zdt.calendar });
// => 'Sep 3, 1 Reiwa, 5:34:05 PM'
zdt.eraYear;
// => 1
Conversions from calendar date and/or wall clock time to exact time are also supported:
// Convert various local time types to an exact time type by providing a time zone
date = Temporal.PlainDate.from('2019-12-17');
// If time is omitted, local time defaults to start of day
zdt = date.toZonedDateTime('Asia/Tokyo');
// => 2019-12-17T00:00:00+09:00[Asia/Tokyo]
zdt = date.toZonedDateTime({ timeZone: 'Asia/Tokyo', plainTime: '10:00' });
// => 2019-12-17T10:00:00+09:00[Asia/Tokyo]
time = Temporal.PlainTime.from('14:35');
zdt = time.toZonedDateTime({ timeZone: 'Asia/Tokyo', plainDate: Temporal.PlainDate.from('2020-08-27') });
// => 2020-08-27T14:35:00+09:00[Asia/Tokyo]
dateTime = Temporal.PlainDateTime.from('2019-12-17T07:48');
zdt = dateTime.toZonedDateTime('Asia/Tokyo');
// => 2019-12-17T07:48:00+09:00[Asia/Tokyo]
// Get the exact time in seconds, milliseconds or nanoseconds since the UNIX epoch.
inst = zdt.toInstant();
epochNano = inst.epochNanoseconds; // => 1576536480000000000n
epochMilli = inst.epochMilliseconds; // => 1576536480000
epochSecs = Math.floor(inst.epochMilliseconds / 1000); // => 1576536480
Usually, a time zone definition provides a bidirectional 1:1 mapping between any particular local date and clock time and its corresponding UTC date and time. However, near a time zone offset transition there can be time ambiguity where it's not clear what offset should be used to convert a wall-clock time into an exact time. This ambiguity leads to two possible UTC times for one clock time.
-07:00
).
30 exact minutes later, DST ended and Pacific Standard Time (offset -08:00
) became active.
After 30 more exact minutes, the "second" 1:30AM happened.
This means that "1:30AM on Sunday, 4 November 2018" is not sufficient to know which 1:30AM it is.
The clock time is ambiguous.In both cases, resolving the ambiguity when converting the local time into exact time requires choosing which of two possible offsets to use, or deciding to throw an exception.
In Temporal
, if the exact time or time zone offset is known, then there is no ambiguity possible. For example:
// No ambiguity possible because source is exact time in UTC
inst = Temporal.Instant.from('2020-09-06T17:35:24.485Z');
// => 2020-09-06T17:35:24.485Z
// An offset can make a local time "exact" with no ambiguity possible.
inst = Temporal.Instant.from('2020-09-06T10:35:24.485-07:00');
// => 2020-09-06T17:35:24.485Z
zdt = Temporal.ZonedDateTime.from('2020-09-06T10:35:24.485-07:00[America/Los_Angeles]');
// => 2020-09-06T10:35:24.485-07:00[America/Los_Angeles]
// if the source is an exact Temporal object, then no ambiguity is possible.
zdt = inst.toZonedDateTimeISO('America/Los_Angeles');
// => 2020-09-06T10:35:24.485-07:00[America/Los_Angeles]
inst2 = zdt.toInstant();
// => 2020-09-06T17:35:24.485Z
However, opportunities for ambiguity are present when creating an exact-time type (Temporal.ZonedDateTime
or Temporal.Instant
) from a non-exact source. For example:
// Offset is not known. Ambiguity is possible!
zdt = Temporal.PlainDate.from('2019-02-19').toZonedDateTime('America/Sao_Paulo'); // can be ambiguous
zdt = Temporal.PlainDateTime.from('2019-02-19T00:00').toZonedDateTime('America/Sao_Paulo'); // can be ambiguous
// Even if the offset is present in the source string, if the type (like PlainDateTime)
// isn't an exact type then the offset is ignored when parsing so ambiguity is possible.
dt = Temporal.PlainDateTime.from('2019-02-19T00:00-03:00');
zdt = dt.toZonedDateTime('America/Sao_Paulo'); // can be ambiguous
// the offset is lost when converting from an exact type to a non-exact type
zdt = Temporal.ZonedDateTime.from('2020-11-01T01:30-08:00[America/Los_Angeles]');
// => 2020-11-01T01:30:00-08:00[America/Los_Angeles]
dt = zdt.toPlainDateTime(); // offset is lost!
// => 2020-11-01T01:30:00
zdtAmbiguous = dt.toZonedDateTime('America/Los_Angeles'); // can be ambiguous
// => 2020-11-01T01:30:00-07:00[America/Los_Angeles]
// note that the offset is now -07:00 (Pacific Daylight Time) which is the "first" 1:30AM
// not -08:00 (Pacific Standard Time) like the original time which was the "second" 1:30AM
To resolve this possible ambiguity, Temporal
methods that create exact types from inexact sources accept a disambiguation
option, which controls what exact time to return in the case of ambiguity:
'compatible'
(the default): Acts like 'earlier'
for backward transitions and 'later'
for forward transitions.'earlier'
: The earlier of two possible exact times will be returned.'later'
: The later of two possible exact times will be returned.'reject'
: A RangeError
will be thrown.When interoperating with existing code or services, 'compatible'
mode matches the behavior of legacy Date
as well as libraries like moment.js, Luxon, and date-fns.
This mode also matches the behavior of cross-platform standards like RFC 5545 (iCalendar).
Methods where this option is present include:
Temporal.ZonedDateTime.from
with object argumentTemporal.ZonedDateTime.prototype.with
Temporal.PlainDateTime.prototype.toZonedDateTime
This explanation was adapted from the moment-timezone documentation.
When entering DST, clocks move forward an hour. In reality, it is not time that is moving, it is the offset moving. Moving the offset forward gives the illusion that an hour has disappeared. If you watch your computer's digital clock, you can see it move from 1:58 to 1:59 to 3:00. It is easier to see what is actually happening when you include the offset.
1:58 -08:00
1:59 -08:00
3:00 -07:00
3:01 -07:00
The result is that any time between 1:59:59 and 3:00:00 never actually happened.
In 'earlier'
mode, the exact time that is returned will be as if the post-change UTC offset had continued before the change, effectively skipping backwards by the amount of the DST gap (usually 1 hour).
In 'later'
mode, the exact time that is returned will be as if the pre-change UTC offset had continued after the change, effectively skipping forwards by the amount of the DST gap.
In 'compatible'
mode, the same time is returned as 'later'
mode, which matches the behavior of existing JavaScript code that uses legacy Date
.
// Different disambiguation modes for times in the skipped clock hour after DST starts in the Spring
// Offset of -07:00 is Daylight Saving Time, while offset of -08:00 indicates Standard Time.
props = { timeZone: 'America/Los_Angeles', year: 2020, month: 3, day: 8, hour: 2, minute: 30 };
zdt = Temporal.ZonedDateTime.from(props, { disambiguation: 'compatible' });
// => 2020-03-08T03:30:00-07:00[America/Los_Angeles]
zdt = Temporal.ZonedDateTime.from(props);
// => 2020-03-08T03:30:00-07:00[America/Los_Angeles]
// ('compatible' is the default)
earlier = Temporal.ZonedDateTime.from(props, { disambiguation: 'earlier' });
// => 2020-03-08T01:30:00-08:00[America/Los_Angeles]
// (1:30 clock time; still in Standard Time)
later = Temporal.ZonedDateTime.from(props, { disambiguation: 'later' });
// => 2020-03-08T03:30:00-07:00[America/Los_Angeles]
// ('later' is same as 'compatible' for backwards transitions)
later.toPlainDateTime().since(earlier.toPlainDateTime());
// => PT2H
// (2 hour difference in clock time...
later.since(earlier);
// => PT1H
// ... but 1 hour later in real-world time)
Likewise, at the end of DST, clocks move backward an hour.
In this case, the illusion is that an hour repeats itself.
In 'earlier'
mode, the exact time will be the earlier instance of the duplicated wall-clock time.
In 'later'
mode, the exact time will be the later instance of the duplicated time.
In 'compatible'
mode, the same time is returned as 'earlier'
mode, which matches the behavior of existing JavaScript code that uses legacy Date
.
// Different disambiguation modes for times in the repeated clock hour after DST ends in the Fall
// Offset of -07:00 is Daylight Saving Time, while offset of -08:00 indicates Standard Time.
props = { timeZone: 'America/Los_Angeles', year: 2020, month: 11, day: 1, hour: 1, minute: 30 };
zdt = Temporal.ZonedDateTime.from(props, { disambiguation: 'compatible' });
// => 2020-11-01T01:30:00-07:00[America/Los_Angeles]
zdt = Temporal.ZonedDateTime.from(props);
// => 2020-11-01T01:30:00-07:00[America/Los_Angeles]
// 'compatible' is the default.
earlier = Temporal.ZonedDateTime.from(props, { disambiguation: 'earlier' });
// => 2020-11-01T01:30:00-07:00[America/Los_Angeles]
// 'earlier' is same as 'compatible' for backwards transitions.
later = Temporal.ZonedDateTime.from(props, { disambiguation: 'later' });
// => 2020-11-01T01:30:00-08:00[America/Los_Angeles]
// Same clock time, but one hour later.
// -08:00 offset indicates Standard Time.
later.toPlainDateTime().since(earlier.toPlainDateTime());
// => PT0S
// (same clock time...
later.since(earlier);
// => PT1H
// ... but 1 hour later in real-world time)
Time zone definitions can change. Almost always these changes are forward-looking so don't affect historical data. But computers sometimes store data about the future! For example, a calendar app might record that a user wants to be reminded of a friend's birthday next year.
When date/time data for future times is stored with both the offset and the time zone, and if the time zone definition changes, then it's possible that the new time zone definition may conflict with previously-stored data.
In this case, then the offset
option to Temporal.ZonedDateTime.from
is used to resolve the conflict:
'use'
: Evaluate date/time values using the time zone offset if it's provided in the input.
This will keep the exact time unchanged even if local time will be different than what was originally stored.'ignore'
: Never use the time zone offset provided in the input. Instead, calculate the offset from the time zone.
This will keep local time unchanged but may result in a different exact time than was originally stored.'prefer'
: Evaluate date/time values using the offset if it's valid for this time zone.
If the offset is invalid, then calculate the offset from the time zone.
This option is rarely used when calling from()
.
See the documentation of with()
for more details about why this option is used.'reject'
: Throw a RangeError
if the offset is not valid for the provided date and time in the provided time zone.The default is 'reject'
for Temporal.ZonedDateTime.from
because there is no obvious default solution.
Instead, the developer needs to decide how to fix the now-invalid data.
For Temporal.ZonedDateTime.with
the default is 'prefer'
.
This default is helpful to prevent DST disambiguation from causing unexpected one-hour changes in exact time after making small changes to clock time fields.
For example, if a Temporal.ZonedDateTime
is set to the "second" 1:30AM on a day where the 1-2AM clock hour is repeated after a backwards DST transition, then calling .with({minute: 45})
will result in an ambiguity which is resolved using the default offset: 'prefer'
option.
Because the existing offset is valid for the new time, it will be retained so the result will be the "second" 1:45AM.
However, if the existing offset is not valid for the new result (e.g. .with({hour: 0})
), then the default behavior will change the offset to match the new local time in that time zone.
Note that offset vs. timezone conflicts only matter for Temporal.ZonedDateTime
because no other Temporal type accepts both an IANA time zone and a time zone offset as an input to any method.
For example, Temporal.Instant.from
will never run into conflicts because the Temporal.Instant
type ignores the time zone in the input and only uses the offset.
offset
optionThe primary reason to use the offset
option is for parsing values which were saved before a change to that time zone's time zone definition.
For example, Brazil stopped observing Daylight Saving Time in 2019, with the final transition out of DST on February 16, 2019.
The change to stop DST permanently was announced in April 2019.
Now imagine that an app running in 2018 (before these changes were announced) had saved a far-future time in a string format that contained both offset and IANA time zone.
Such a format is used by Temporal.ZonedDateTime.prototype.toString
as well as other platforms and libraries that use the same format like Java.time.ZonedDateTime
.
Let's assume the stored future time was noon on January 15, 2020 in São Paulo:
zdt = Temporal.ZonedDateTime.from({ year: 2020, month: 1, day: 15, hour: 12, timeZone: 'America/Sao_Paulo' });
zdt.toString();
// => '2020-01-15T12:00:00-02:00[America/Sao_Paulo]'
// Assume this string is saved in an external database.
// Note that the offset is `-02:00` which is Daylight Saving Time
// Also note that if you run the code above today, it will return an offset
// of `-03:00` because that reflects the current time zone definition after
// DST was abolished. But this code running in 2018 would have returned `-02:00`
// which corresponds to the then-current Daylight Saving Time in Brazil.
This string was valid at the time is was created and saved in 2018.
But after the time zone rules were changed in April 2019, 2020-01-15T12:00-02:00[America/Sao_Paulo]
is no longer valid because the correct offset for this time is now -03:00
.
When parsing this string using current time zone rules, Temporal
needs to know how to interpret it.
The offset
option helps deal with this case.
savedUsingOldTzDefinition = '2020-01-01T12:00-02:00[America/Sao_Paulo]'; // string that was saved earlier
/* WRONG */ zdt = Temporal.ZonedDateTime.from(savedUsingOldTzDefinition);
// => RangeError: Offset is invalid for '2020-01-01T12:00' in 'America/Sao_Paulo'. Provided: -02:00, expected: -03:00.
// Default is to throw when the offset and time zone conflict.
/* WRONG */ zdt = Temporal.ZonedDateTime.from(savedUsingOldTzDefinition, { offset: 'reject' });
// => RangeError: Offset is invalid for '2020-01-01T12:00' in 'America/Sao_Paulo'. Provided: -02:00, expected: -03:00.
zdt = Temporal.ZonedDateTime.from(savedUsingOldTzDefinition, { offset: 'use' });
// => 2020-01-01T11:00:00-03:00[America/Sao_Paulo]
// Evaluate date/time string using old offset, which keeps UTC time constant as local time changes to 11:00
zdt = Temporal.ZonedDateTime.from(savedUsingOldTzDefinition, { offset: 'ignore' });
// => 2020-01-01T12:00:00-03:00[America/Sao_Paulo]
// Use current time zone rules to calculate offset, ignoring any saved offset
zdt = Temporal.ZonedDateTime.from(savedUsingOldTzDefinition, { offset: 'prefer' });
// => 2020-01-01T12:00:00-03:00[America/Sao_Paulo]
// Saved offset is invalid for current time zone rules, so use time zone to to calculate offset.