A fast, lightweight, zero-dependency library to translate between Time Zones and UTC with native Intl.DateTimeFormat in ~100 LoC.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

218 lines
5.6 KiB

(function (exports) {
"use strict";
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 tzOptions = Object.assign({ timeZoneName: "long" }, options);
// Every country uses the same year and months, right?
var formater = new Intl.DateTimeFormat("default", tzOptions);
var parts = formater.formatToParts(date);
// millisecond is explicitly 0 for iOS' 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
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;
});
whole.timeZone = timeZone;
whole.offset = getOffset(date, whole);
whole.toISOString = _toOffsetISOString;
return whole;
}
function toTimeZoneISOString(date, timeZone) {
var whole = toTimeZone(date, timeZone);
return formatAsOffsetISOString(whole);
}
function _toOffsetISOString() {
/* jshint validthis: true */
return formatAsOffsetISOString(this);
}
function getOffset(utcDate, tzD2) {
var tzDate = new Date(formatAsOffsetISOString(tzD2));
var diff = Math.round((tzDate.valueOf() - utcDate.valueOf()) / (60 * 1000));
return diff;
}
function p2(x) {
return String(x).padStart(2, "0");
}
function p3(x) {
return String(x).padStart(3, "0");
}
function formatOffset(minutes) {
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")
);
}
function toOffsetISOString(date, timeZone) {
if ("object" === typeof date && "offset" in date && "year" in date) {
return formatAsOffsetISOString(date);
}
var whole = fromTimeZone(date, timeZone);
return formatAsOffsetISOString(whole);
}
function formatAsOffsetISOString(d) {
var offset = formatOffset(d.offset);
return (
`${d.year}-${p2(d.month + 1)}-${p2(d.day)}` +
`T${p2(d.hour)}:${p2(d.minute)}:${p2(d.second)}.${p3(
d.millisecond
)}${offset}`
);
}
function fromTimeZone(dt, tz) {
if ("string" === typeof dt) {
// Either of these formats should work:
// 2021-03-14 01:15:59
// 2021-03-14T01:15:59Z
dt = dt
.replace("T", " ")
.replace("Z", "")
.replace(" ", "T")
.replace(/$/, "Z");
}
var utcDate = new Date(dt);
var tzD2 = toTimeZone(utcDate, tz);
var offset = tzD2.offset;
tzD2.offset = 0;
var deltaDate = new Date(utcDate);
deltaDate.setUTCMinutes(deltaDate.getUTCMinutes() - offset);
var tzD3 = toTimeZone(deltaDate, tz);
if (
tzD3.hour === utcDate.getUTCHours() &&
tzD3.minute === utcDate.getUTCMinutes()
) {
return tzD3;
}
var diff = tzD3.offset - offset;
var h = Math.floor(Math.abs(diff) / 60);
var m = Math.abs(diff) % 60;
var sign = Math.abs(diff) / diff;
tzD3.hour -= h * sign;
tzD3.minute -= m * sign;
return tzD3;
}
function toLocalISOString(dateOrStr) {
var d;
if (dateOrStr) {
d = new Date(dateOrStr);
} else {
d = new Date();
}
var YYYY = d.getFullYear();
var MM = p2(d.getMonth() + 1);
var DD = p2(d.getDate());
var hh = p2(d.getHours());
var mm = p2(d.getMinutes());
var ss = p2(d.getSeconds());
var sss = d.getMilliseconds().toString().padStart(3, "0");
var offset = formatOffset(-d.getTimezoneOffset());
return `${YYYY}-${MM}-${DD}T${hh}:${mm}:${ss}.${sss}${offset}`;
}
function getTimeZone() {
return new Intl.DateTimeFormat().resolvedOptions().timeZone;
}
exports.XTZ = {
// bespoke date =>
// 2021-11-07T3:15:59-0500
// (todo?)
// xtzToISOString: formatAsOffsetISOString,
// (deprecated)
toOffsetISOString: toOffsetISOString,
// -240 => -0400
formatOffset: formatOffset,
// "America/New_York"
timeZone: getTimeZone,
// "2021-11-07T03:15:59-0500"
toLocalISOString: toLocalISOString,
// [ "2021-11-07T08:15:59Z", "America/New_York" ]
// => "2021-11-07T03:15:59-0500" // 2021-11-07 03:15:59
toTimeZone: toTimeZone,
toTimeZoneISOString: toTimeZoneISOString,
// [ "2021-11-07 03:15:59", "America/New_York" ]
// => "2021-11-07T03:15:59-0500" // 2021-11-07T08:15:59Z
toUTC: fromTimeZone,
fromTimeZone: fromTimeZone,
// deprecated
toUTCISOString: toOffsetISOString,
};
if ("undefined" != typeof module && module.exports) {
module.exports = exports.XTZ;
}
})(("undefined" === typeof module && window) || exports);