Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix keeping time through tz to correctly handle being near DST boundaries #872

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 75 additions & 1 deletion moment-timezone.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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();
}
};