Skip to content

Commit

Permalink
Fix keeping time through tz to correctly handle being near DST bounda…
Browse files Browse the repository at this point in the history
…ries
  • Loading branch information
mongoose700 committed Jul 13, 2020
1 parent 063a950 commit deb1a65
Show file tree
Hide file tree
Showing 3 changed files with 287 additions and 2 deletions.
76 changes: 75 additions & 1 deletion moment-timezone.js
Expand Up @@ -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/.");
}
Expand Down
211 changes: 211 additions & 0 deletions tests/moment-timezone/manipulate.js
Expand Up @@ -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(),
Expand All @@ -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(),
Expand All @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion tests/moment-timezone/utc.js
Expand Up @@ -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();
}
};

0 comments on commit deb1a65

Please sign in to comment.