diff --git a/package.json b/package.json index 08ce704..f9c777c 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "colors": true }, "dependencies": { - "moment-timezone": "^0.5.39" + "date-fns-tz": "^1.3.7" }, "devDependencies": { "c8": "^7.12.0", @@ -68,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 96b3095..18b97f0 100644 --- a/src/CalDate.js +++ b/src/CalDate.js @@ -1,6 +1,6 @@ - -import moment from 'moment-timezone' import { toYear, toNumber, isDate, pad0 } from './utils.js' +import utcToZonedTime from 'date-fns-tz/utcToZonedTime' +import zonedTimeToUtc from 'date-fns-tz/zonedTimeToUtc' const PROPS = ['year', 'month', 'day', 'hour', 'minute', 'second'] @@ -180,9 +180,17 @@ export class CalDate { * @param {String} timezone - e.g. 'America/New_York' * @return {Date} */ - toTimezone (timezone) { - if (timezone) { - return new Date(moment.tz(this.toString(), timezone).format()) + 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() } @@ -196,13 +204,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..ab72723 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') @@ -199,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') + }) +})