From a5726777e7211380bbf2b1819440290c003f55cb Mon Sep 17 00:00:00 2001 From: Michael Peirce Date: Sun, 12 Jul 2020 17:50:22 -0700 Subject: [PATCH] Fix keeping time through tz to correctly handle being near DST boundaries --- moment-timezone.js | 76 +++++++++- tests/moment-timezone/manipulate.js | 211 ++++++++++++++++++++++++++++ tests/moment-timezone/utc.js | 2 +- 3 files changed, 287 insertions(+), 2 deletions(-) diff --git a/moment-timezone.js b/moment-timezone.js index 23046fad..f70931bd 100644 --- a/moment-timezone.js +++ b/moment-timezone.js @@ -628,14 +628,88 @@ } }; + function isRepeatedTime(mom) { + if (mom._UTC) { + return false; + } + + var zone = mom._z || moment.defaultZone || getZone(guess()); + + if (!zone) { + return false; + } + + var timestamp = mom.valueOf(); + var index = zone._index(timestamp); + + // There are no transitions before this one, so it cannot have been repeated. + if (index === 0) { + return false; + } + + var offset = zone.offsets[index]; + var previousOffset = zone.offsets[index - 1]; + var msChange = (previousOffset - offset) * 60000; + + var potentialPreviousTimestamp = timestamp + msChange; + return potentialPreviousTimestamp < zone.untils[index - 1]; + } + + function adjustToRepeatedTime(mom) { + if (mom._UTC) { + return; + } + + var zone = mom._z || moment.defaultZone || getZone(guess()); + + if (!zone) { + return; + } + + var timestamp = mom.valueOf(); + var index = zone._index(timestamp); + + // There are no transitions after this one, so it is not repeatable. + if (index === zone.offsets.length - 1) { + return; + } + + var offset = zone.offsets[index]; + var nextOffset = zone.offsets[index + 1]; + var msChange = (nextOffset - offset) * 60000; + + var potentialNextTimestamp = timestamp + msChange; + if (potentialNextTimestamp > zone.untils[index]) { + mom.add(msChange, 'milliseconds'); + } + } + fn.tz = function (name, keepTime) { if (name) { if (typeof name !== 'string') { throw new Error('Time zone name must be a string, got ' + name + ' [' + typeof name + ']'); } + + if (keepTime) { + // If the original time was a repeat of a local time (after a DST shift), and the new zone has the same shift, + // the new time should also be the repeat. + var wasRepeated = isRepeatedTime(this); + + var adjusted = moment.tz(this.toArray(), name); + this._z = adjusted._z; + this._offset = adjusted._offset; + this._isUTC = adjusted._isUTC; + this._d = adjusted._d; + + if (wasRepeated) { + adjustToRepeatedTime(this); + } + + return this; + } this._z = getZone(name); if (this._z) { - moment.updateOffset(this, keepTime); + moment.updateOffset(this); } else { logError("Moment Timezone has no data for " + name + ". See http://momentjs.com/timezone/docs/#/data-loading/."); } diff --git a/tests/moment-timezone/manipulate.js b/tests/moment-timezone/manipulate.js index a448a95b..8104abd8 100644 --- a/tests/moment-timezone/manipulate.js +++ b/tests/moment-timezone/manipulate.js @@ -32,6 +32,7 @@ exports.manipulate = { ); t.done(); }, + subtract : function (t) { t.equal( moment('2012-10-29T00:00:00+00:00').tz('Europe/London').subtract(1, 'days').format(), @@ -50,6 +51,7 @@ exports.manipulate = { ); t.done(); }, + month : function (t) { t.equal( moment("2014-03-09T00:00:00-08:00").tz('America/Los_Angeles').add(1, 'month').format(), @@ -65,6 +67,215 @@ exports.manipulate = { t.done(); }, + tz : function (t) { + t.equal( + moment.tz("2014-03-09T01:59:59.999", 'America/Los_Angeles').tz('America/New_York', true).toISOString(true), + '2014-03-09T01:59:59.999-05:00', + 'keeping times between zones with DST before springing forward should work' + ); + t.equal( + moment.tz("2014-03-09T03:00:00", 'America/Los_Angeles').tz('America/New_York', true).toISOString(true), + '2014-03-09T03:00:00.000-04:00', + 'keeping times between zones with DST after springing forward should work' + ); + t.equal( + moment.tz("2014-11-02T01:59:59.999", 'America/Los_Angeles').tz('America/New_York', true).toISOString(true), + '2014-11-02T01:59:59.999-04:00', + 'keeping times between zones with DST before falling back should work' + ); + t.equal( + moment.tz("2014-11-02T01:00:00-07:00", 'America/Los_Angeles').tz('America/New_York', true).toISOString(true), + '2014-11-02T01:00:00.000-04:00', + 'keeping times between zones with DST at start of first repeated section falling back should work' + ); + t.equal( + moment.tz("2014-11-02T01:59:59.999-07:00", 'America/Los_Angeles').tz('America/New_York', true).toISOString(true), + '2014-11-02T01:59:59.999-04:00', + 'keeping times between zones with DST at end of first repeated section falling back should work' + ); + t.equal( + moment.tz("2014-11-02T01:00:00-08:00", 'America/Los_Angeles').tz('America/New_York', true).toISOString(true), + '2014-11-02T01:00:00.000-04:00', + 'keeping times between zones with DST at start of second repeated section falling back should work' + ); + t.equal( + moment.tz("2014-11-02T01:59:59.999-08:00", 'America/Los_Angeles').tz('America/New_York', true).toISOString(true), + '2014-11-02T01:59:59.999-05:00', + 'keeping times between zones with DST at end of second repeated section falling back should work' + ); + t.equal( + moment.tz("2014-11-02T02:00:00", 'America/Los_Angeles').tz('America/New_York', true).toISOString(true), + '2014-11-02T02:00:00.000-05:00', + 'keeping times between zones with DST after falling back should work' + ); + + t.equal( + moment.utc("2014-03-09T01:59:59.999").tz('America/New_York', true).toISOString(true), + '2014-03-09T01:59:59.999-05:00', + 'keeping times from UTC to a zone with DST before springing forward should work' + ); + t.equal( + moment.utc("2014-03-09T02:00:00").tz('America/New_York', true).toISOString(true), + '2014-03-09T03:00:00.000-04:00', + 'keeping times from UTC to a zone with DST at the start of springing forward should jump by an hour' + ); + t.equal( + moment.utc("2014-03-09T02:59:59.999").tz('America/New_York', true).toISOString(true), + '2014-03-09T03:59:59.999-04:00', + 'keeping times from UTC to a zone with DST at the end of springing forward should jump by an hour' + ); + t.equal( + moment.utc("2014-03-09T03:00:00").tz('America/New_York', true).toISOString(true), + '2014-03-09T03:00:00.000-04:00', + 'keeping times from UTC to a zone with DST after springing forward should work' + ); + t.equal( + moment.utc("2014-11-02T01:59:59.999").tz('America/New_York', true).toISOString(true), + '2014-11-02T01:59:59.999-04:00', + 'keeping times from UTC to a zone with DST before falling back should work' + ); + t.equal( + moment.utc("2014-11-02T01:00:00").tz('America/New_York', true).toISOString(true), + '2014-11-02T01:00:00.000-04:00', + 'keeping times from UTC to a zone with DST at start of first repeated section falling back should work' + ); + t.equal( + moment.utc("2014-11-02T01:59:59.999").tz('America/New_York', true).toISOString(true), + '2014-11-02T01:59:59.999-04:00', + 'keeping times from UTC to a zone with DST at end of repeated section falling back should use first section' + ); + t.equal( + moment.utc("2014-11-02T02:00:00").tz('America/New_York', true).toISOString(true), + '2014-11-02T02:00:00.000-05:00', + 'keeping times from UTC to a zone with DST after falling back should work' + ); + + t.equal( + moment.tz("2014-03-09T01:59:59.999", 'America/Los_Angeles').tz('UTC', true).toISOString(true), + '2014-03-09T01:59:59.999+00:00', + 'keeping times from a zone with DST to UTC before springing forward should work' + ); + t.equal( + moment.tz("2014-03-09T03:00:00", 'America/Los_Angeles').tz('UTC', true).toISOString(true), + '2014-03-09T03:00:00.000+00:00', + 'keeping times from a zone with DST to UTC after springing forward should work' + ); + t.equal( + moment.tz("2014-11-02T01:59:59.999", 'America/Los_Angeles').tz('UTC', true).toISOString(true), + '2014-11-02T01:59:59.999+00:00', + 'keeping times from a zone with DST to UTC before falling back should work' + ); + t.equal( + moment.tz("2014-11-02T01:00:00-07:00", 'America/Los_Angeles').tz('UTC', true).toISOString(true), + '2014-11-02T01:00:00.000+00:00', + 'keeping times from a zone with DST to UTC at start of first repeated section falling back should work' + ); + t.equal( + moment.tz("2014-11-02T01:59:59.999-07:00", 'America/Los_Angeles').tz('UTC', true).toISOString(true), + '2014-11-02T01:59:59.999+00:00', + 'keeping times from a zone with DST to UTC at end of first repeated section falling back should work' + ); + t.equal( + moment.tz("2014-11-02T01:00:00-08:00", 'America/Los_Angeles').tz('UTC', true).toISOString(true), + '2014-11-02T01:00:00.000+00:00', + 'keeping times from a zone with DST to UTC at start of second repeated section falling back should work' + ); + t.equal( + moment.tz("2014-11-02T01:59:59.999-08:00", 'America/Los_Angeles').tz('UTC', true).toISOString(true), + '2014-11-02T01:59:59.999+00:00', + 'keeping times from a zone with DST to UTC at end of second repeated section falling back should work' + ); + t.equal( + moment.tz("2014-11-02T02:00:00", 'America/Los_Angeles').tz('UTC', true).toISOString(true), + '2014-11-02T02:00:00.000+00:00', + 'keeping times from a zone with DST to UTC after falling back should work' + ); + + t.equal( + moment.tz("2014-03-09T01:59:59.999", 'America/New_York').tz('America/Phoenix', true).toISOString(true), + '2014-03-09T01:59:59.999-07:00', + 'keeping times from a zone with DST to one without before springing forward should work' + ); + t.equal( + moment.tz("2014-03-09T03:00:00", 'America/New_York').tz('America/Phoenix', true).toISOString(true), + '2014-03-09T03:00:00.000-07:00', + 'keeping times from a zone with DST to one without after springing forward should work' + ); + t.equal( + moment.tz("2014-11-02T01:59:59.999", 'America/New_York').tz('America/Phoenix', true).toISOString(true), + '2014-11-02T01:59:59.999-07:00', + 'keeping times from a zone with DST to one without before falling back should work' + ); + t.equal( + moment.tz("2014-11-02T01:00:00-04:00", 'America/New_York').tz('America/Phoenix', true).toISOString(true), + '2014-11-02T01:00:00.000-07:00', + 'keeping times from a zone with DST to one without at start of first repeated section falling back should work' + ); + t.equal( + moment.tz("2014-11-02T01:59:59.999-04:00", 'America/New_York').tz('America/Phoenix', true).toISOString(true), + '2014-11-02T01:59:59.999-07:00', + 'keeping times from a zone with DST to one without at end of first repeated section falling back should work' + ); + t.equal( + moment.tz("2014-11-02T01:00:00-05:00", 'America/New_York').tz('America/Phoenix', true).toISOString(true), + '2014-11-02T01:00:00.000-07:00', + 'keeping times from a zone with DST to one without at start of second repeated section falling back should work' + ); + t.equal( + moment.tz("2014-11-02T01:59:59.999-05:00", 'America/New_York').tz('America/Phoenix', true).toISOString(true), + '2014-11-02T01:59:59.999-07:00', + 'keeping times from a zone with DST to one without at end of second repeated section falling back should work' + ); + t.equal( + moment.tz("2014-11-02T02:00:00", 'America/New_York').tz('America/Phoenix', true).toISOString(true), + '2014-11-02T02:00:00.000-07:00', + 'keeping times from a zone with DST to one without after falling back should work' + ); + + t.equal( + moment.tz("2014-03-09T01:59:59.999", 'America/Phoenix').tz('America/New_York', true).toISOString(true), + '2014-03-09T01:59:59.999-05:00', + 'keeping times from a zone without DST to one with before springing forward should work' + ); + t.equal( + moment.tz("2014-03-09T02:00:00", 'America/Phoenix').tz('America/New_York', true).toISOString(true), + '2014-03-09T03:00:00.000-04:00', + 'keeping times from a zone without DST to one with at the start of springing forward should jump by an hour' + ); + t.equal( + moment.tz("2014-03-09T02:59:59.999", 'America/Phoenix').tz('America/New_York', true).toISOString(true), + '2014-03-09T03:59:59.999-04:00', + 'keeping times from a zone without DST to one with at the end of springing forward should jump by an hour' + ); + t.equal( + moment.tz("2014-03-09T03:00:00", 'America/Phoenix').tz('America/New_York', true).toISOString(true), + '2014-03-09T03:00:00.000-04:00', + 'keeping times from a zone without DST to one with after springing forward should work' + ); + t.equal( + moment.tz("2014-11-02T01:59:59.999", 'America/Phoenix').tz('America/New_York', true).toISOString(true), + '2014-11-02T01:59:59.999-04:00', + 'keeping times from a zone without DST to one with before falling back should work' + ); + t.equal( + moment.tz("2014-11-02T01:00:00", 'America/Phoenix').tz('America/New_York', true).toISOString(true), + '2014-11-02T01:00:00.000-04:00', + 'keeping times from a zone without DST to one with at start of first repeated section falling back should work' + ); + t.equal( + moment.tz("2014-11-02T01:59:59.999", 'America/Phoenix').tz('America/New_York', true).toISOString(true), + '2014-11-02T01:59:59.999-04:00', + 'keeping times from a zone without DST to one with at end of first repeated section falling back should work' + ); + t.equal( + moment.tz("2014-11-02T02:00:00", 'America/Phoenix').tz('America/New_York', true).toISOString(true), + '2014-11-02T02:00:00.000-05:00', + 'keeping times from a zone without DST to one with after falling back should work' + ); + + t.done(); + }, + isSame : function(t) { var m1 = moment.tz('2014-10-01T00:00:00', 'Europe/London'); var m2 = moment.tz('2014-10-01T00:00:00', 'Europe/London'); diff --git a/tests/moment-timezone/utc.js b/tests/moment-timezone/utc.js index 6fa0c184..6553d83e 100644 --- a/tests/moment-timezone/utc.js +++ b/tests/moment-timezone/utc.js @@ -71,7 +71,7 @@ exports.utc = { var utcWallTimeFormat = m.clone().utcOffset('-05:00', true).format(); m.tz('America/New_York', true); test.equal(m.format(), utcWallTimeFormat, "Should change the offset while keeping wall time when passing an optional parameter to moment.fn.tz"); - + test.done(); } };