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;
+ }
+}());