From c9fe67a30d61e71f42da70f2f27ba94822cc9407 Mon Sep 17 00:00:00 2001 From: Brent McSharry <2456704+mcshaz@users.noreply.github.com> Date: Tue, 10 Jan 2023 11:09:47 +1300 Subject: [PATCH 1/5] use date-fns-tz library --- package.json | 4 +--- src/CalDate.js | 19 +++++++++---------- test/CalDate.mocha.js | 6 ++++++ 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 08ce704..0a2fd6f 100644 --- a/package.json +++ b/package.json @@ -53,11 +53,9 @@ "checkLeaks": true, "colors": true }, - "dependencies": { - "moment-timezone": "^0.5.39" - }, "devDependencies": { "c8": "^7.12.0", + "date-fns-tz": "^1.3.7", "dtslint": "^4.2.1", "eslint": "^8.28.0", "eslint-config-standard": "^17.0.0", diff --git a/src/CalDate.js b/src/CalDate.js index 96b3095..c7838ae 100644 --- a/src/CalDate.js +++ b/src/CalDate.js @@ -1,6 +1,5 @@ - -import moment from 'moment-timezone' import { toYear, toNumber, isDate, pad0 } from './utils.js' +import { zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz/esm' const PROPS = ['year', 'month', 'day', 'hour', 'minute', 'second'] @@ -182,7 +181,7 @@ export class CalDate { */ toTimezone (timezone) { if (timezone) { - return new Date(moment.tz(this.toString(), timezone).format()) + return zonedTimeToUtc(this.toString(), timezone) } else { return this.toDate() } @@ -196,13 +195,13 @@ export class CalDate { */ fromTimezone (dateUTC, timezone) { if (timezone) { - const m = moment.tz(dateUTC, timezone) - this.year = m.year() - this.month = m.month() + 1 - this.day = m.date() - this.hour = m.hours() - this.minute = m.minutes() - this.second = m.seconds() + const m = utcToZonedTime(dateUTC, timezone) + this.year = m.getFullYear() + this.month = m.getMonth() + 1 + this.day = m.getDate() + this.hour = m.getHours() + this.minute = m.getMinutes() + this.second = m.getSeconds() } else { this.set(dateUTC) } diff --git a/test/CalDate.mocha.js b/test/CalDate.mocha.js index b32d4fa..7a4f8b7 100644 --- a/test/CalDate.mocha.js +++ b/test/CalDate.mocha.js @@ -96,6 +96,12 @@ describe('#CalDate', function () { assert.strictEqual(res, '2000-01-01T05:00:00.000Z') }) + it('can move date by timezone with daylight saving offset', function () { + const caldate = new CalDate(new Date('2000-07-01 00:00:00')) + const res = caldate.toTimezone('America/New_York').toISOString() + assert.strictEqual(res, '2000-07-01T04:00:00.000Z') + }) + it('can return date in current timezone', function () { const caldate = new CalDate({ year: 2000, month: 1, day: 1 }) const exp = new Date('2000-01-01 00:00:00') From e4802fd2c471e98bd2aebeba56a82caebc56ae0e Mon Sep 17 00:00:00 2001 From: Brent McSharry <2456704+mcshaz@users.noreply.github.com> Date: Tue, 10 Jan 2023 16:42:20 +1300 Subject: [PATCH 2/5] import only date-fns-tz files needed --- src/CalDate.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CalDate.js b/src/CalDate.js index c7838ae..0b7c017 100644 --- a/src/CalDate.js +++ b/src/CalDate.js @@ -1,5 +1,6 @@ import { toYear, toNumber, isDate, pad0 } from './utils.js' -import { zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz/esm' +import * as utcToZonedTime from 'date-fns-tz/utcToZonedTime' +import * as zonedTimeToUtc from 'date-fns-tz/zonedTimeToUtc' const PROPS = ['year', 'month', 'day', 'hour', 'minute', 'second'] From 9430978b91f155a2ebc8c34d1dc4f4bca3b9be90 Mon Sep 17 00:00:00 2001 From: Brent McSharry <2456704+mcshaz@users.noreply.github.com> Date: Tue, 10 Jan 2023 17:12:58 +1300 Subject: [PATCH 3/5] single date-fns-tz dependency --- src/CalDate.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CalDate.js b/src/CalDate.js index 0b7c017..caa4a53 100644 --- a/src/CalDate.js +++ b/src/CalDate.js @@ -1,6 +1,6 @@ import { toYear, toNumber, isDate, pad0 } from './utils.js' -import * as utcToZonedTime from 'date-fns-tz/utcToZonedTime' -import * as zonedTimeToUtc from 'date-fns-tz/zonedTimeToUtc' +import utcToZonedTime from 'date-fns-tz/utcToZonedTime' +import zonedTimeToUtc from 'date-fns-tz/zonedTimeToUtc' const PROPS = ['year', 'month', 'day', 'hour', 'minute', 'second'] From dd213231c1576498a656cd32e335bf834cf0dae7 Mon Sep 17 00:00:00 2001 From: Brent McSharry <2456704+mcshaz@users.noreply.github.com> Date: Wed, 11 Jan 2023 13:35:41 +1300 Subject: [PATCH 4/5] import esm --- src/CalDate.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/CalDate.js b/src/CalDate.js index caa4a53..5917318 100644 --- a/src/CalDate.js +++ b/src/CalDate.js @@ -1,6 +1,5 @@ import { toYear, toNumber, isDate, pad0 } from './utils.js' -import utcToZonedTime from 'date-fns-tz/utcToZonedTime' -import zonedTimeToUtc from 'date-fns-tz/zonedTimeToUtc' +import { utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz/esm' const PROPS = ['year', 'month', 'day', 'hour', 'minute', 'second'] From c37a4508d2cd1b4ed776efefd16f6ba613db13e7 Mon Sep 17 00:00:00 2001 From: Brent McSharry <2456704+mcshaz@users.noreply.github.com> Date: Thu, 12 Jan 2023 15:34:34 +1300 Subject: [PATCH 5/5] temp fix for local time doesn't exist Tests added to describe the problem with the date-fns-tz library when the zoned time is set in a daylight savings jump forward, which therefore doesn't exist. A temporary workaround is added but only for Iran (the only country which changed to DST at their local midnight). --- package.json | 7 +++-- src/CalDate.js | 17 +++++++++--- test/CalDate.mocha.js | 61 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 0a2fd6f..f9c777c 100644 --- a/package.json +++ b/package.json @@ -53,9 +53,11 @@ "checkLeaks": true, "colors": true }, + "dependencies": { + "date-fns-tz": "^1.3.7" + }, "devDependencies": { "c8": "^7.12.0", - "date-fns-tz": "^1.3.7", "dtslint": "^4.2.1", "eslint": "^8.28.0", "eslint-config-standard": "^17.0.0", @@ -66,7 +68,8 @@ "npm-run-all": "^4.1.5", "rimraf": "^3.0.2", "rollup": "^3.3.0", - "typescript": "^4.9.3" + "typescript": "^4.9.3", + "date-fns":"^2.29.3" }, "engines": { "node": ">=12.0.0" diff --git a/src/CalDate.js b/src/CalDate.js index 5917318..18b97f0 100644 --- a/src/CalDate.js +++ b/src/CalDate.js @@ -1,5 +1,6 @@ import { toYear, toNumber, isDate, pad0 } from './utils.js' -import { utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz/esm' +import utcToZonedTime from 'date-fns-tz/utcToZonedTime' +import zonedTimeToUtc from 'date-fns-tz/zonedTimeToUtc' const PROPS = ['year', 'month', 'day', 'hour', 'minute', 'second'] @@ -179,9 +180,17 @@ export class CalDate { * @param {String} timezone - e.g. 'America/New_York' * @return {Date} */ - toTimezone (timezone) { - if (timezone) { - return zonedTimeToUtc(this.toString(), timezone) + toTimezone (timeZone) { + if (timeZone) { + const returnVar = zonedTimeToUtc(this.toString(), timeZone) + // hack alert - tehran has the only timezone which starts daylight saving at midnight (although stopped DST in 2022) + // once the bug in zoneTimeToUtc is fixed, delete this hack https://github.com/marnusw/date-fns-tz/issues/222 + if (timeZone === 'Asia/Tehran') { + const i = new Intl.DateTimeFormat('en', { timeZone, hourCycle: 'h23', hour: 'numeric' }) + const f = parseInt(i.format(returnVar), 10) + if (f !== this.hour) returnVar.setHours(returnVar.getHours() + 1) + } + return returnVar } else { return this.toDate() } diff --git a/test/CalDate.mocha.js b/test/CalDate.mocha.js index 7a4f8b7..ab72723 100644 --- a/test/CalDate.mocha.js +++ b/test/CalDate.mocha.js @@ -205,3 +205,64 @@ describe('#CalDate', function () { assert.strictEqual(res, exp) }) }) + +describe('handles daylight saving jumps', function () { + it('finds first time after clock jumps back: -ve UTC offset', function () { + const caldate = new CalDate(new Date('2023-11-05 02:00:00')) + const res = caldate.toTimezone('America/New_York').toISOString() + // UTC 05:59 is NY 01:59 GMT -4 + // UTC 06:00 is NY 01:00 GMT -5 + // therefore the first time NY local 02:00 is struck is UTC 07:00 + assert.strictEqual(res, '2023-11-05T07:00:00.000Z') + }) + + it('finds first time after clock jumps back: +ve UTC offset', function () { + const caldate = new CalDate(new Date('2023-04-02 03:00:00')) + const res = caldate.toTimezone('Australia/Sydney').toISOString() + // UTC 15:59 is SYD 02:59 GMT +11 + // UTC 16:00 is SYD 02:00 GMT +10 + // therefore the first time SYD local 03:00 is struck is UTC 17:00 + assert.strictEqual(res, '2023-04-01T17:00:00.000Z') + }) + + it('handles times that repeat when clock jump back: -ve UTC offset', function () { + // at 02:00 local clock jumps back 1 hour so 01:00 occurs twice + const caldate = new CalDate(new Date('2023-11-05 01:00:00')) + const res = caldate.toTimezone('America/New_York').toISOString() + // UTC 05:00 is NY 01:00 GMT -4 + // UTC 06:00 is NY 01:00 GMT -5 + // this implementation picks the later occurrence of 01:00 @ UTC 06:00 + assert.strictEqual(res, '2023-11-05T06:00:00.000Z') + }) + + it('handles times that repeat when clock jump back: +ve UTC offset', function () { + // at 03:00 local clock jumps back 1 hour so 02:00 occurs twice + const caldate = new CalDate(new Date('2023-04-02 02:00:00')) + const res = caldate.toTimezone('Australia/Sydney').toISOString() + // UTC 15:00 is SYD 02:00 GMT +11 + // UTC 16:00 is SYD 02:00 GMT +10 + // this implementation picks the later occurrence of 02:00 @ UTC 16:00 + assert.strictEqual(res, '2023-04-01T16:00:00.000Z') + }) + + // utc: 2023-09-30T16:00:00.000Z SYD: 03:00 GMT+11 + it('handles times that dont exist with clock jump forward: -ve UTC offset', function () { + // at 02:00 local clock will immediately jump forward to 03:00 + const caldate = new CalDate(new Date('2023-03-12 02:00:00')) + const res = caldate.toTimezone('America/New_York').toISOString() + // UTC 06:59 is NY 01:59 GMT -5 + // UTC 07:00 is NY 03:00 GMT -4 + // !! this is an error - should either throw error or return UTC 07:00 + assert.strictEqual(res, '2023-03-12T06:00:00.000Z') + }) + + it('handles times that dont exist with clock jump forward: +ve UTC offset', function () { + // at 02:00 local clock will immediately jump forward to 03:00 + const caldate = new CalDate(new Date('2023-10-01 02:00:00')) + const res = caldate.toTimezone('Australia/Sydney').toISOString() + // UTC 15:59 is SYD 01:59 GMT +10 + // UTC 16:00 is SYD 03:00 GMT +11 + // !! this is an error - should either throw error or return UTC 16:00 + assert.strictEqual(res, '2023-09-30T15:00:00.000Z') + }) +})