From f5c99a1a2861024f9fb2751fba0b582b893c27b5 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 27 May 2021 17:51:33 -0600 Subject: [PATCH] Add docs, examples, and prepare to publish --- .gitignore | 2 + README.md | 215 ++++++++++++++++++++++++++++++++++++++++++++++++++- examples.js | 89 +++++++++++++++++++++ index.js | 171 ---------------------------------------- package.json | 29 +++++++ xtz.js | 182 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 515 insertions(+), 173 deletions(-) create mode 100644 examples.js delete mode 100644 index.js create mode 100644 package.json create mode 100644 xtz.js diff --git a/.gitignore b/.gitignore index 144585f..be8362d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*.gz + # ---> Node # Logs logs diff --git a/README.md b/README.md index 3eda5a0..6e1568d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,214 @@ -# when.js +# xtz.js -Something about timezones and JavaScript \ No newline at end of file +A fast, lightweight, zero-dependency library to +translate between Time Zones and UTC with native +`Intl.DateTimeFormat` in ~100 LoC. For Node.js & Browsers. + +XTZ is a poor man's Temporal polyfill, but just for time zones. + +> What UTC time will it be when it's 3:15am in New York? + +```js +// Relative New York time to Absolute UTC Time +TZ.toUTCISOString("2021-11-07 03:15:59.000", "America/New_York"); +// "2021-11-07T03:15:59.000-0500" +``` + +```js +var tzDate = TZ.toUTC("2021-11-07 03:15:59.000", "America/New_York"); +// { +// year: 2021, month: 11, day: 7, +// hour: 3, minute: 15, second: 59, millisecond: 0, +// offset: -300, timeZoneName: "Eastern Standard Time" +// } + +tzDate.toISOString(); +// "2021-11-07T03:15:59.000-0500" +// same as "2021-11-07T08:15:59.000Z" +``` + +> What time will it be in New York when it's 7:15am UTC? + +```js +// Absolute UTC time to Relative New York time +TZ.toTimeZoneISOString("2021-11-07T07:15:59.000Z", "America/New_York"); +// "2021-11-07T03:15:59.000-0400" +``` + +```js +var utcDate = TZ.toTimeZone("2021-03-14T07:15:59.000Z", "America/New_York"); +// { +// year: 2021, month: 11, day: 7, +// hour: 3, minute: 15, second: 59, millisecond: 0, +// offset: -240, timeZoneName: "Eastern Daylight Time" +// } + +utcDate.toISOString(); +// "2021-03-14T03:15:59.000-0400" +// same as "2021-11-07T07:15:59.000Z" +``` + +# Features + +- [x] Translate a UTC time to a Time Zone +- [x] Translate a Zoned time to UTC +- [x] Handles **Daylight Savings**, Weird Time Zones, etc... +- [x] Lightweight (No deps) + - 5kb Source + Comments + - 2.5kb Minified + - <1kb `gzip`d + +Compatible with Node.js & Browsers. + +## Node.js & Webpack + +```bash +npm install --save xtz +``` + +```js +var TZ = require("xtz"); +``` + +## Browsers + +```html + +``` + +```js +var TZ = window.XTZ; +``` + +# API + +- `toTimeZone(utcDate, timeZone)` +- `toTimeZoneISOString(isoString, timeZone)` +- `toUTC(dtString, timeZone)` +- `toUTCISOString(dtString, timeZone)` + +## `toTimeZone(utcDate, timeZone)` + +> Convert UTC into a Target Time Zone + +Use ISO timestamps representing the absolute UTC time in the target time zone: + +```txt +"2021-11-07T08:15:59.000Z" +``` + +Convert directly to an ISO String: + +```js +TZ.toTimeZoneISOString("2021-11-07T08:15:59.000Z", "America/New_York"); +// "2021-11-07T03:15:59.000-0500" +``` + +Or use our bespoke (custom) date object: + +```js +var tzDate = TZ.toTimeZone("2021-11-07T08:15:59.000Z", "America/New_York"); +``` + +You can also use a date object with an absolute UTC time: + +```js +var tzDate = TZ.toTimeZone( + new Date("2021-11-07T08:15:59.000Z"), + "America/New_York" +); +``` + +```js +console.log(tzDate.toISOString()); +// "2021-11-07T03:15:59.000-0500" +``` + +Our ISO Strings + Offsets work with JavaScript's native Date object!! + +```js +new Date("2021-11-07T03:15:59.000-0500").toISOString()); +// "2021-11-07T08:15:59.000Z" +``` + +## `toUTC(dtString, timeZone)` + +> Convert a Target Time Zone into UTC + +Use ISO-like timestamps representing the _local_ time in the target time zone: + +```txt +"2021-11-0 T03:15:59.000" +``` + +Convert directly to an offset ISO String: + +```js +TZ.toUTCISOString("2021-11-07 03:15:59.000", "America/New_York"); +// "2021-11-07T03:15:59.000-0500" +``` + +Or our bespoke date object: + +```js +var utcDate = TZ.toUTC("2021-11-07 03:15:59.000", "America/New_York"); +``` + +You can also use a date object as the source time, but the date's UTC time will be treated as **_relative to time zone_** rather than absolute (this is a workaround for JavaScript's lack of bi-directional timezone support). + +```js +var utcDate = TZ.toUTC( + new Date("2021-11-07T03:15:59.000Z"), + "America/New_York" +); +``` + +```js +utcDate.toISOString(); +// "2021-11-07T03:15:59.000-0500" +``` + +# Daylight Savings / Edge Cases + +> In 2021 Daylight Savings (in the US) +> +> - begins at 2am on March 14th +> - ends at 2am on November 7th +> +> See . + +Q: What happens in March when 2am is skipped? + +- A: Although 2am is not a valid time, rather than throwing an error this library will resolve to 1am instead, which is an hour early in real ("tick-tock" or "monotonic") time. + ```js + var utcDate = TZ.toUTC("2021-03-14 02:15:59.000", "America/New_York"); + utcDate.toISOString(); + // "2021-03-14T02:15:59.000-0400" + // (same as "2021-03-14T01:15:59.000-0500") + ``` + +Q: What happens in November when 2am happens twice? + +- A: Although both 2ams are distinguishable with ISO offset times, only the first can be resolved from a local time with this library. + ```js + var utcDate = TZ.toUTC("2021-11-07 01:15:59.000", "America/New_York"); + utcDate.toISOString(); + // "2021-11-07T01:15:59.000-0400", same as "2021-11-07T05:15:59.000Z" + // (an hour before the 2nd 2am at "2021-11-07T01:15:59.000-0500") + ``` + +# List of Time Zones + +See the [Full List of Time Zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) on Wikipedia. + +Common Zones for Testing: + +```txt +America/New_York -0500 +America/Denver -0700 +America/Phoenix -0700 (No DST) +America/Los_Angeles -0800 +Australia/Adelaide +0930 (30-min, has DST) +Asia/Kathmandu +0545 (No DST, 45-min) +Asia/Kolkata +0530 (No DST, 30-min) +``` diff --git a/examples.js b/examples.js new file mode 100644 index 0000000..6fbaa0f --- /dev/null +++ b/examples.js @@ -0,0 +1,89 @@ +var XTZ; + +(function () { + "use strict"; + + if (!XTZ) { + try { + XTZ = require("xtz"); + } catch (e) { + XTZ = require("./xtz.js"); + } + } + + var TZ = XTZ; + var tzDate; + + // + // UTC-absolute time translated to a Time Zone + // + function demo1() { + console.info("What's the UTC equivalent of 8:15am in New York?"); + console.info(); + + console.info("\t// during daylight savings"); + console.info( + `\tXTZ.toUTC("2021-03-14 08:15:59.000", "America/New_York")` + ); + console.info(`\ttzDate.toISOString()`); + tzDate = XTZ.toUTC("2021-03-14 08:15:59.000", "America/New_York"); + console.info( + "\t" + tzDate.toISOString(), + "// same as", + new Date(tzDate.toISOString()).toISOString() + ); + console.info(); + + console.info("\t// during standard time"); + console.info( + `\tXTZ.toUTC("2021-11-07 08:15:59.000", "America/New_York")` + ); + console.info(`\ttzDate.toISOString()`); + tzDate = XTZ.toUTC("2021-11-07 08:15:59.000", "America/New_York"); + console.info( + "\t" + tzDate.toISOString(), + "// same as", + new Date(tzDate.toISOString()).toISOString() + ); + console.info(); + } + + // + // Time Zone-relative time translated to UTC + // + function demo2() { + console.info( + "What time is it in New York at 8:15am on March 14th UTC?" + ); + console.info(); + + console.info("\t// during daylight savings"); + console.info( + `\tXTZ.toTimeZone("2021-03-14T08:15:59.000Z", "America/New_York")` + ); + console.info(`\ttzDate.toISOString()`); + tzDate = XTZ.toTimeZone("2021-03-14T08:15:59.000Z", "America/New_York"); + console.info( + "\t" + tzDate.toISOString(), + "// same as", + new Date(tzDate.toISOString()).toISOString() + ); + console.info(); + + console.info("\t// during standard time"); + console.info( + `\tXTZ.toUTC("2021-11-07T08:15:59.000Z", "America/New_York")` + ); + console.info(`\ttzDate.toISOString()`); + tzDate = XTZ.toUTC("2021-11-07T08:15:59.000Z", "America/New_York"); + console.info( + "\t" + tzDate.toISOString(), + "// same as", + new Date(tzDate.toISOString()).toISOString() + ); + console.info(); + } + + demo1(); + demo2(); +})(); diff --git a/index.js b/index.js deleted file mode 100644 index 74a3ec8..0000000 --- a/index.js +++ /dev/null @@ -1,171 +0,0 @@ -"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); - - var whole = {}; - 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); - } - // 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 toOffsetISOString(whole); -} - -function _toOffsetISOString() { - return toOffsetISOString(this); -} - -function getOffset(utcDate, tzD2) { - var tzDate = new Date(toOffsetISOString(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(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 toUTC(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 = ""; - - 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 toUTCISOString(date, timeZone) { - var whole = toUTC(date, timeZone); - return toOffsetISOString(whole); -} - -module.exports = { - // bespoke date => - // 2021-11-07T3:15:59-0500 - toOffsetISOString: toOffsetISOString, - - // -240 => -0400 - formatOffset: formatOffset, - - // [ "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: toUTC, - toUTCISOString: toUTCISOString, -}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..5c1106e --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "xtz", + "version": "1.0.0", + "description": "A fast, lightweight, zero-dependency library to translate between Time Zones and UTC with native Intl.DateTimeFormat in ~100 LoC. For Node.js & Browsers.", + "main": "xtz.js", + "scripts": { + "test": "node ./test.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/therootcompany/tz.js.git" + }, + "keywords": [ + "tz", + "timezone", + "date", + "intl", + "temporal", + "polyfill", + "convert", + "translate" + ], + "author": "AJ ONeal (https://coolaj86.com/)", + "license": "MPL-2.0", + "bugs": { + "url": "https://github.com/therootcompany/tz.js/issues" + }, + "homepage": "https://github.com/therootcompany/tz.js#readme" +} diff --git a/xtz.js b/xtz.js new file mode 100644 index 0000000..e9d0895 --- /dev/null +++ b/xtz.js @@ -0,0 +1,182 @@ +var XTZ; + +(function () { + "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); + + var whole = {}; + 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); + } + // 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 toOffsetISOString(whole); + } + + function _toOffsetISOString() { + return toOffsetISOString(this); + } + + function getOffset(utcDate, tzD2) { + var tzDate = new Date(toOffsetISOString(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(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 toUTC(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 = ""; + + 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 toUTCISOString(date, timeZone) { + var whole = toUTC(date, timeZone); + return toOffsetISOString(whole); + } + + XTZ = { + // bespoke date => + // 2021-11-07T3:15:59-0500 + toOffsetISOString: toOffsetISOString, + + // -240 => -0400 + formatOffset: formatOffset, + + // [ "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: toUTC, + toUTCISOString: toUTCISOString, + }; + + if ("undefined" != typeof module && module.exports) { + module.exports = XTZ; + } +}());