Current UTC ISO Time
new Date().toISOString();
// "2021-05-25T23:04:33.116Z"
Current Timezone and Locale
Timezone:
Intl.DateTimeFormat().resolvedOptions().timeZone;
// America/Denver
Locale:
Intl.DateTimeFormat().resolvedOptions().locale;
// en-US
And the rest of it, for reference:
Intl.DateTimeFormat().resolvedOptions();
/*
{
"locale": "en-US",
"calendar": "gregory",
"numberingSystem": "latn",
"timeZone": "America/Denver",
"year": "numeric",
"month": "numeric",
"day": "numeric"
}
*/
Surprisingly, Intl
has actually made it into ECMAScript proper. It works in Browsers and Node.js.
Intl.DateTimeFormat vs tz.js
While writing this I found that Intl.DateTimeFormat
is difficult to use directly or with short snippets, so I created xtz.js:
Demo at https://therootcompany.github.io/tz.js/
Translate UTC time to a TimeZone
Intl.DateTimeFormat
can translate timezones, but it does not have the ability to output an ISO-compatible time.
Furthermore, new Intl.DateTimeFormat("default", opts).format(date)
cannot be guaranteed to support any particular locale on any particular system.
However, using new Intl.DateTimeFormat("default", opts).formatToParts(date)
makes it possible to get a date in a guaranteed format.
We could create a helper function toTimeZone()
and use it like this:
var tzDate = toTimeZone('2021-01-01T13:59:59Z', 'America/New_York');
console.log(JSON.stringify(tzDate, null, 2));
{
"year": 2021,
"month": 0,
"day": 1,
"hour": 8,
"minute": 59,
"second": 59,
"millisecond": 0,
"timeZoneName": "Eastern Standard Time",
"timeZone": "America/New_York"
}
Here’s an implementation for toTimeZone()
, and another helper partsToWhole
(for converting formatToParts()
array into a more usable object):
function toTimeZone(date, timeZone) {
// ISO string or existing date object
date = new Date(date);
var options = {
timeZone: timeZone,
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour12: false,
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
fractionalSecondDigits: 3
};
var tzOpts = Object.assign({ timeZoneName: 'long' }, options);
var parts = new Intl.DateTimeFormat('default', tzOpts).formatToParts(date);
// [ { type: 'year', value: '2021', ... } ]
var whole = partsToWhole(parts);
whole.timeZone = timeZone;
return whole;
}
function partsToWhole(parts) {
// millisecond explicitly 0 for Safari's lack of fractionalSecond support
var whole = { millisecond: 0 };
parts.forEach(function (part) {
var val = part.value;
switch (part.type) {
case 'literal':
// ignore separators and whitespace characters
return;
case 'timeZoneName':
// keep as is - it's a string
break;
case 'month':
// months are 0-indexed for new Date()
val = parseInt(val, 10) - 1;
break;
case 'hour':
// because sometimes 24 is used instead of 0, make 24 0
// (also hourCycle is not widely supported)
val = parseInt(val, 10) % 24;
break;
case 'fractionalSecond':
// fractionalSecond is a dumb name - should be millisecond
whole.millisecond = parseInt(val, 10);
return;
default:
val = parseInt(val, 10);
}
// ex: whole.month = 0;
whole[part.type] = val;
});
return whole;
}
Snippet adapted from xtz.js.
Intl.DateTimeFormat to ISO Time
Having used the array from formatToParts(date)
to create an object with all of the individual pieces, you can then construct an ISO string.
The ISO string will NOT be correct for the given time zone, as it lacks the necessary offset, but can be used to get that offset (later on below).
toISOString(whole);
// 2021-01-01T08:59:59.000Z
function toISOString(d) {
var YYYY = d.year;
var MM = (d.month + 1).padStart(2, '0');
var DD = d.day.padStart(2, '0');
var hh = d.hour.padStart(2, '0');
var mm = d.minute.padStart(2, '0');
var ss = d.second.padStart(2, '0');
var ms = (d.millisecond || 0).padStart(3, '0');
return `${YYYY}-${MM}-${DD}T${hh}:${mm}:${ss}.${ms}Z`;
}
Snippet adapted from xtz.js.
Get Time Zone offset relative to a UTC time
Once you have ISO string, you can calculate the difference between the source date and the target date to arrive at the offset:
var offset = getOffset('2021-01-01T13:59:59Z', '2021-01-01T08:59:59.000Z');
// -300
formatOffset(offset);
// -0500
Assuming you’re not crossing a daylight savings boundary (that’s covered in xtz.js), getting the offset is a simple matter of subtracting one time from the other:
function getOffset(dateA, dateB) {
var date1 = new Date(dateA);
var date2 = new Date(dateB);
var diff = Math.round((date2.valueOf() - date1.valueOf()) / (60 * 1000));
return diff;
}
Formatting is also pretty simple - just separate the hours using floored division and modulus the total to get the remainder in minutes:
function formatOffset(offset) {
if (!minutes) {
return 'Z';
}
var h = Math.floor(Math.abs(minutes) / 60);
var m = Math.abs(minutes) % 60;
var offset = '';
if (minutes > 0) {
offset = '+';
} else if (minutes < 0) {
offset = '-';
}
// +0500, -0730
return (
offset + h.toString().padStart(2, '0') + m.toString().padStart(2, '0')
);
}
Snippet adapted from xtz.js.
Caveats with Offsets
Keep in mind that the offset may be different based on the time of year.
If you’re starting from a UTC time and calculating an offset in another time zone as if it were UTC, you’ll still get the wrong answer crossing daylight savings boundaries.
Also, offsets are political - they rarely, but sometimes change from one year to another as dictated by local governments.
Get Time Zone offset at it’s own time
If you need accurate timezone offsets, even at 1:30am during a Daylight Savings time switch…
Just use xtz.js:
https://therootcompany.github.io/tz.js/
XTZ.toUTC('2021-03-14 03:30:59.001', 'America/New_York').toISOString();
// 2021-03-14T03:30:59.001-0400
XTZ.toUTC('2021-03-14 03:30:59.001', 'America/Denver').toISOString();
// 2021-03-14T03:30:59.001-0600
XTZ.toUTC('2021-03-14 03:30:59.001', 'America/Phoenix').toISOString();
// 2021-03-14T03:30:59.001-0700
XTZ.toUTC('2021-03-14 03:30:59.001', 'America/Los_Angeles').toISOString();
// 2021-03-14T03:30:59.001-0700
XTZ.toUTC('2021-03-14 03:30:59.001', 'UTC').toISOString();
// 2021-03-14T03:30:59.001Z
XTZ.toUTC('2021-03-14 03:30:59.001', 'Australia/Adelaide').toISOString();
// 2021-03-14T03:30:59.001+1030
XTZ.toUTC('2021-03-14 03:30:59.001', 'Asia/Kathmandu').toISOString();
// 2021-03-14T03:30:59.001+0545
Essentially, the only method by which it is possible to do these lookups without requiring between 100s of kilobytes (or megabytes) of lookup tables (such as moment.js, date-fns-tz, and Temporal Polyfill) is to follow a “guess and check” strategy:
- Get the offset, as mentioned above
-
Create a date from an ISO string, replacing
Z
with the offset- i.e.
new Date('2021-03-14T03:30:59.001-0400')
- i.e.
- Pass that date through
formatToParts(date)
a second time - Create a new date from that, after translating to an ISO string + offset
-
Calculate the difference between the
getUTCHour()
andgetUTCMinute()
of both dates- if 0, you’ve got the correct time
- otherwise, adjust the hour, minute, and offset accordingly
That’s the method XTZ.js uses, and it will always yield the correct time, in all circumstances.
Caveats with Daylight Savings
Wherever Daylight Savings (or equivalent) is used, there is one day at which one hour does not exist at all - 2:30am on 13 March, 2021 in New York, for example - and one day at which an hour exists twice - 1:30am on 7 Nov, 2021 in New York, for example.
When to represent events scheduled at those times on a calendar is up for debate.
Convert between Time Zones
Once you have a method by which to convert from a zoned time to UTC, you can simply then convert that to a different zone:
var nyTime = XTZ.toUTC('2021-01-01 00:59:59', 'America/New_York');
nyTime.toISOString();
// 2021-01-01T00:59:59.000-0500
var kTime = XTZ.toTimeZone(new Date(nyTime.toISOString()), 'Asia/Kathmandu');
kTime.toISOString();
// 2021-01-01T11:44:59.000+0545
Final Notes
Since time changes over time, you’re safest bet for scheduling far-off events (say more than 6 months away), is to store the target Time Zone and Time (i.e. 2021-03-14 01:30:00.000 @ America/New_York
) and then recalculate the absolute UTC time around the 6 month mark.
It is highly unlikely that a government will change a time zone or daylight savings time without well more than 6 months of official notice (plus perhaps years of unofficial notice).
That said, an operating system (wether phone, desktop, or server) must receive updates from time to time that include new Time Zone alterations.