From af6d1bdb5fe4102bd3bf56ac89069e2ea3897075 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 27 May 2021 16:25:55 -0600 Subject: [PATCH] TZ => UTC passes tests --- index.js | 65 ++++++++++++++++--- test.js | 193 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 237 insertions(+), 21 deletions(-) diff --git a/index.js b/index.js index 5fbb3ad..74a3ec8 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,6 @@ "use strict"; -function fromUTCToTimeZone(date, timeZone) { +function toTimeZone(date, timeZone) { // ISO string or existing date object date = new Date(date); var options = { @@ -15,12 +15,7 @@ function fromUTCToTimeZone(date, timeZone) { fractionalSecondDigits: 3, }; - var tzOptions = Object.assign( - { - timeZoneName: "long", - }, - options - ); + var tzOptions = Object.assign({ timeZoneName: "long" }, options); // Every country uses the same year and months, right? var formater = new Intl.DateTimeFormat("default", tzOptions); @@ -61,6 +56,11 @@ function fromUTCToTimeZone(date, timeZone) { return whole; } +function toTimeZoneISOString(date, timeZone) { + var whole = toTimeZone(date, timeZone); + return toOffsetISOString(whole); +} + function _toOffsetISOString() { return toOffsetISOString(this); } @@ -109,18 +109,63 @@ function toOffsetISOString(d) { ); } -function fromZonedToUTC(dt, tz) {} +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 - fromUTCToTimeZone: fromUTCToTimeZone, + toTimeZone: toTimeZone, + toTimeZoneISOString: toTimeZoneISOString, + // [ "2021-11-07 03:15:59", "America/New_York" ] // => "2021-11-07T03:15:59-0500" // 2021-11-07T08:15:59Z - fromZonedToUTC: fromZonedToUTC, + toUTC: toUTC, + toUTCISOString: toUTCISOString, }; diff --git a/test.js b/test.js index 7afce5f..42bc5fb 100644 --- a/test.js +++ b/test.js @@ -2,6 +2,29 @@ var TZ = require("./"); +function testUtcToTz(t) { + var result = TZ.toTimeZone.apply(TZ, t.inputs).toISOString(); + if (t.result !== result) { + throw new Error( + `Invalid UTC to TZ conversion for ${t.desc}:\n` + + `\tExpected: ${t.result}\n` + + `\tActual: ${result}\n` + ); + } +} + +function testTzToUtc(t) { + var result = TZ.toUTC.apply(TZ, t.inputs); + if (t.result !== result.toISOString()) { + console.log(result); + throw new Error( + `Invalid TZ to UTC conversion for ${t.desc}:\n` + + `\tExpected: ${t.result}\n` + + `\tActual: ${result.toISOString()}\n` + ); + } +} + // At this real UTC time, what does the timezone translate it to? [ // @@ -21,19 +44,23 @@ var TZ = require("./"); // 12:15am NY -0500 => -0400 { + desc: "UTC to 12:15am NY EST", inputs: ["2021-03-14T05:15:59.000Z", "America/New_York"], result: "2021-03-14T00:15:59.000-0500", }, { + desc: "UTC to 12:15am NY EST (2)", inputs: ["2021-03-14T00:15:59.000-0500", "America/New_York"], result: "2021-03-14T00:15:59.000-0500", }, // 1:15am NY (non-DST) { + desc: "UTC to 1:15am NY EST", inputs: ["2021-03-14T06:15:59.000Z", "America/New_York"], result: "2021-03-14T01:15:59.000-0500", }, { + desc: "UTC to 1:15am NY EST (2)", inputs: ["2021-03-14T01:15:59.000-0500", "America/New_York"], result: "2021-03-14T01:15:59.000-0500", }, @@ -42,19 +69,23 @@ var TZ = require("./"); // 3:15am NY (DST) { + desc: "UTC to 3:15am NY EDT", inputs: ["2021-03-14T07:15:59.000Z", "America/New_York"], result: "2021-03-14T03:15:59.000-0400", }, { + desc: "UTC to 3:15am NY EDT (2)", inputs: ["2021-03-14T03:15:59.000-0400", "America/New_York"], result: "2021-03-14T03:15:59.000-0400", }, // 4:15am NY { + desc: "UTC to 4:15am NY EDT", inputs: ["2021-03-14T08:15:59.000Z", "America/New_York"], result: "2021-03-14T04:15:59.000-0400", }, { + desc: "UTC to 4:15am NY EDT (2)", inputs: ["2021-03-14T04:15:59.000-0400", "America/New_York"], result: "2021-03-14T04:15:59.000-0400", }, @@ -75,47 +106,57 @@ var TZ = require("./"); // 12:15am NY -0400 => -0500 { + desc: "UTC to 2021 Nov 7, 12:15am NY EDT", inputs: ["2021-11-07T04:15:59.000Z", "America/New_York"], result: "2021-11-07T00:15:59.000-0400", }, { + desc: "UTC to 2021 Nov 7, 12:15am NY EDT (2)", inputs: ["2021-11-07T00:15:59.000-0400", "America/New_York"], result: "2021-11-07T00:15:59.000-0400", }, // 1:15am NY (DST) -0400 // NOTE: 1:15am happens TWICE (with different offsets) { + desc: "UTC to 2021 Nov 7, 1:15am NY EDT", inputs: ["2021-11-07T05:15:59.000Z", "America/New_York"], result: "2021-11-07T01:15:59.000-0400", }, { + desc: "UTC to 2021 Nov 7, 1:15am NY EDT (2)", inputs: ["2021-11-07T01:15:59.000-0400", "America/New_York"], result: "2021-11-07T01:15:59.000-0400", }, // 1:15am NY (non-DST) -0500 { + desc: "UTC to 2021 Nov 7, 1:15am NY EST", inputs: ["2021-11-07T06:15:59.000Z", "America/New_York"], result: "2021-11-07T01:15:59.000-0500", }, { + desc: "UTC to 2021 Nov 7, 1:15am NY EST (2)", inputs: ["2021-11-07T01:15:59.000-0500", "America/New_York"], result: "2021-11-07T01:15:59.000-0500", }, // 2:15am NY -0500 { + desc: "UTC to 2021 Nov 7, 2:15am NY EST", inputs: ["2021-11-07T07:15:59.000Z", "America/New_York"], result: "2021-11-07T02:15:59.000-0500", }, { + desc: "UTC to 2021 Nov 7, 2:15am NY EST (2)", inputs: ["2021-11-07T02:15:59.000-0500", "America/New_York"], result: "2021-11-07T02:15:59.000-0500", }, // 3:15am NY { + desc: "UTC to 2021 Nov 7, 3:15am NY EST", inputs: ["2021-11-07T08:15:59.000Z", "America/New_York"], result: "2021-11-07T03:15:59.000-0500", }, { + desc: "UTC to 2021 Nov 7, 3:15am NY EST (2)", inputs: ["2021-11-07T03:15:59.000-0500", "America/New_York"], result: "2021-11-07T03:15:59.000-0500", }, @@ -124,22 +165,152 @@ var TZ = require("./"); // Positive Offset Test // - // 4:15am Colombo +0530 (not DST) + // Colombo +0530 (not DST) { + desc: "UTC to Asia/Colombo (1)", inputs: ["2021-03-14T08:15:59.000Z", "Asia/Colombo"], result: "2021-03-14T13:45:59.000+0530", }, { + desc: "UTC to Asia/Colombo (2)", inputs: ["2021-03-14T13:45:59.000+0530", "Asia/Colombo"], result: "2021-03-14T13:45:59.000+0530", }, -].forEach(function (t) { - var result = TZ.fromUTCToTimeZone.apply(TZ, t.inputs).toISOString(); - if (t.result !== result) { - throw new Error( - "Invalid Conversion:\n" + - `\tExpected: ${t.result}\n` + - `\tActual: ${result}\n` - ); - } -}); + { + desc: "UTC to Asia/Colombo (3)", + inputs: ["2021-11-07T08:15:59.000Z", "Asia/Colombo"], + result: "2021-11-07T13:45:59.000+0530", + }, + { + desc: "UTC to Asia/Colombo (4)", + inputs: ["2021-11-07T13:45:59.000+0530", "Asia/Colombo"], + result: "2021-11-07T13:45:59.000+0530", + }, +].forEach(testUtcToTz); +console.info("Pass: UTC to TZ for America/New_York and Asia/Colombo"); + +[ + // + // Start-of-DST Tests + // + + // [Start] + // What time is '2021-03-14 01:15:59.000 in New York' in UTC? // 2021-03-14 06:15:59.000 + // // 2021-03-14T01:15:59.000-0500 + // What time is '2021-03-14 02:15:59.000 in New York' in UTC? // 2021-03-14 07:15:59.000 + // // 2021-03-14T03:15:59.000-0400 + // What time is '2021-03-14 03:15:59.000 in New York' in UTC? // 2021-03-14 07:15:59.000 + // // 2021-03-14T03:15:59.000-0400 + // What time is '2021-03-14 04:15:59.000 in New York' in UTC? // 2021-03-14 08:15:59.000 + // // 2021-03-14T04:15:59.000-0400 + // [End] + + // 12:15am NY -0500 => -0400 + { + // 2021-03-14T05:15:59.000Z + desc: "2021 Mar 14, 12:15am NY EST to UTC", + inputs: ["2021-03-14 00:15:59.000", "America/New_York"], + result: "2021-03-14T00:15:59.000-0500", + }, + // 1:15am NY (non-DST) + { + // 2021-03-14T06:15:59.000Z + desc: "2021 Mar 14, 1:15am NY EST to UTC", + inputs: ["2021-03-14 01:15:59.000", "America/New_York"], + result: "2021-03-14T01:15:59.000-0500", + }, + + // NOTE: Can't 2:15am NY, because it does not exist (skipped by DST) + // This test is here to document the "undefined" behavior + { + // Both 2021-03-14T06:15:59.000Z and 2021-03-14T07:15:59.000Z + // would be reasonable substitutions, I think + desc: "2021 Mar 14, 2:15am NY ExT to UTC", + inputs: ["2021-03-14 02:15:59.000", "America/New_York"], + //result: "2021-03-14T01:15:59.000-0500", // 2021-03-14T06:15:59.000Z + result: "2021-03-14T02:15:59.000-0400", // 2021-03-14T06:15:59.000Z + //result: "2021-03-14T02:15:59.000-0500", // 2021-03-14T07:15:59.000Z + }, + + // 3:15am NY (DST) + { + // 2021-03-14T07:15:59.000Z + desc: "2021 Mar 14, 3:15am NY EDT to UTC", + inputs: ["2021-03-14 03:15:59.000", "America/New_York"], + result: "2021-03-14T03:15:59.000-0400", + }, + // 4:15am NY + { + // 2021-03-14T08:15:59.000Z + desc: "2021 Mar 14, 4:15am NY EDT to UTC", + inputs: ["2021-03-14 04:15:59.000", "America/New_York"], + result: "2021-03-14T04:15:59.000-0400", + }, + + // + // End-of-DST Tests + // + + // [Start] + // What time is '2021-11-07 01:15:59.000 in New York' in UTC? // 2021-11-07 05:15:59.000 + // // 2021-11-07T01:15:59.000-0400 + // // 2021-11-07 06:15:59.000 + // // 2021-11-07T01:15:59.000-0500 + // What time is '2021-11-07 02:15:59.000 in New York' in UTC? // 2021-11-07 07:15:59.000 + // // 2021-11-07T02:15:59.000-0500 + // What time is '2021-11-07 03:15:59.000 in New York' in UTC? // 2021-11-07 08:15:59.000 + // [End] + + // 12:15am NY -0400 => -0500 + { + // 2021-11-07T04:15:59.000Z + desc: "2021 Nov 7, 12:15am NY EDT to UTC", + inputs: ["2021-11-07 00:15:59.000", "America/New_York"], + result: "2021-11-07T00:15:59.000-0400", + }, + + // 1:15am NY (DST) -0400 + // NOTE: 1:15am happens TWICE (with different offsets), so we skip one + { + // ==> 2021-11-07T05:15:59.000Z + // [Skip] 2021-11-07T06:15:59.000Z + desc: "2021 Nov 7, 1:15am NY ExT to UTC", + inputs: ["2021-11-07 01:15:59.000", "America/New_York"], + result: "2021-11-07T01:15:59.000-0400", // 2021-11-07T05:15:59.000Z + //result: "2021-11-07T01:15:59.000-0500", // 2021-11-07T06:15:59.000Z + }, + + // 2:15am NY -0500 + { + // 2021-11-07T07:15:59.000Z + desc: "2021 Nov 7, 2:15am NY EST to UTC", + inputs: ["2021-11-07 02:15:59.000", "America/New_York"], + result: "2021-11-07T02:15:59.000-0500", + }, + // 3:15am NY + { + // 2021-11-07T08:15:59.000Z + desc: "2021 Nov 7, 3:15am NY EST to UTC", + inputs: ["2021-11-07T03:15:59.000", "America/New_York"], + result: "2021-11-07T03:15:59.000-0500", + }, + + // + // Positive Offset Test + // + + // Colombo +0530 (not DST) + { + // 2021-03-14T08:15:59.000Z + desc: "Asia/Colombo to UTC (1)", + inputs: ["2021-03-14 13:45:59.000", "Asia/Colombo"], + result: "2021-03-14T13:45:59.000+0530", + }, + { + // 2021-03-14T08:15:59.000Z + desc: "Asia/Colombo to UTC (2)", + inputs: ["2021-11-07 13:45:59.000", "Asia/Colombo"], + result: "2021-11-07T13:45:59.000+0530", + }, +].forEach(testTzToUtc); +console.info("Pass: TZ to UTC for America/New_York and Asia/Colombo");