Skip to content

Commit 44f147d

Browse files
committed
Fix keeping time through tz to correctly handle being near DST boundaries
1 parent 063a950 commit 44f147d

File tree

3 files changed

+287
-2
lines changed

3 files changed

+287
-2
lines changed

moment-timezone.js

+75-1
Original file line numberDiff line numberDiff line change
@@ -628,14 +628,88 @@
628628
}
629629
};
630630

631+
function isRepeatedTime(mom) {
632+
if (mom._UTC) {
633+
return false;
634+
}
635+
636+
var zone = mom._z || moment.defaultZone || getZone(guess());
637+
638+
if (!zone) {
639+
return false;
640+
}
641+
642+
var timestamp = mom.valueOf();
643+
var index = zone._index(timestamp);
644+
645+
// There are no transitions before this one, so it cannot have been repeated.
646+
if (index === 0) {
647+
return false;
648+
}
649+
650+
var offset = zone.offsets[index];
651+
var previousOffset = zone.offsets[index - 1];
652+
var msChange = (previousOffset - offset) * 60000;
653+
654+
var potentialPreviousTimestamp = timestamp + msChange;
655+
return potentialPreviousTimestamp < zone.untils[index - 1];
656+
}
657+
658+
function adjustToRepeatedTime(mom) {
659+
if (mom._UTC) {
660+
return;
661+
}
662+
663+
var zone = mom._z || moment.defaultZone || getZone(guess());
664+
665+
if (!zone) {
666+
return;
667+
}
668+
669+
var timestamp = mom.valueOf();
670+
var index = zone._index(timestamp);
671+
672+
// There are no transitions after this one, so it is not repeatable.
673+
if (index === zone.offsets.length - 1) {
674+
return;
675+
}
676+
677+
var offset = zone.offsets[index];
678+
var nextOffset = zone.offsets[index + 1];
679+
var msChange = (nextOffset - offset) * 60000;
680+
681+
var potentialNextTimestamp = timestamp + msChange;
682+
if (potentialNextTimestamp > zone.untils[index]) {
683+
mom.add(msChange, 'milliseconds');
684+
}
685+
}
686+
631687
fn.tz = function (name, keepTime) {
632688
if (name) {
633689
if (typeof name !== 'string') {
634690
throw new Error('Time zone name must be a string, got ' + name + ' [' + typeof name + ']');
635691
}
692+
693+
if (keepTime) {
694+
// If the original time was a repeat of a local time (after a DST shift), and the new zone has the same shift,
695+
// the new time should also be the repeat.
696+
var wasRepeated = isRepeatedTime(this);
697+
698+
var adjusted = moment.tz(this.toArray(), name);
699+
this._z = adjusted._z;
700+
this._offset = adjusted._offset;
701+
this._isUTC = adjusted._isUTC;
702+
this._d = adjusted._d;
703+
704+
if (wasRepeated) {
705+
adjustToRepeatedTime(this);
706+
}
707+
708+
return this;
709+
}
636710
this._z = getZone(name);
637711
if (this._z) {
638-
moment.updateOffset(this, keepTime);
712+
moment.updateOffset(this);
639713
} else {
640714
logError("Moment Timezone has no data for " + name + ". See http://momentjs.com/timezone/docs/#/data-loading/.");
641715
}

tests/moment-timezone/manipulate.js

+211
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ exports.manipulate = {
3232
);
3333
t.done();
3434
},
35+
3536
subtract : function (t) {
3637
t.equal(
3738
moment('2012-10-29T00:00:00+00:00').tz('Europe/London').subtract(1, 'days').format(),
@@ -50,6 +51,7 @@ exports.manipulate = {
5051
);
5152
t.done();
5253
},
54+
5355
month : function (t) {
5456
t.equal(
5557
moment("2014-03-09T00:00:00-08:00").tz('America/Los_Angeles').add(1, 'month').format(),
@@ -65,6 +67,215 @@ exports.manipulate = {
6567
t.done();
6668
},
6769

70+
tz : function (t) {
71+
t.equal(
72+
moment.tz("2014-03-09T01:59:59.999", 'America/Los_Angeles').tz('America/New_York', true).toISOString(true),
73+
'2014-03-09T01:59:59.999-05:00',
74+
'keeping times between zones with DST before springing forward should work'
75+
);
76+
t.equal(
77+
moment.tz("2014-03-09T03:00:00", 'America/Los_Angeles').tz('America/New_York', true).toISOString(true),
78+
'2014-03-09T03:00:00.000-04:00',
79+
'keeping times between zones with DST after springing forward should work'
80+
);
81+
t.equal(
82+
moment.tz("2014-11-02T01:59:59.999", 'America/Los_Angeles').tz('America/New_York', true).toISOString(true),
83+
'2014-11-02T01:59:59.999-04:00',
84+
'keeping times between zones with DST before falling back should work'
85+
);
86+
t.equal(
87+
moment.tz("2014-11-02T01:00:00-07:00", 'America/Los_Angeles').tz('America/New_York', true).toISOString(true),
88+
'2014-11-02T01:00:00.000-04:00',
89+
'keeping times between zones with DST at start of first repeated section falling back should work'
90+
);
91+
t.equal(
92+
moment.tz("2014-11-02T01:59:59.999-07:00", 'America/Los_Angeles').tz('America/New_York', true).toISOString(true),
93+
'2014-11-02T01:59:59.999-04:00',
94+
'keeping times between zones with DST at end of first repeated section falling back should work'
95+
);
96+
t.equal(
97+
moment.tz("2014-11-02T01:00:00-08:00", 'America/Los_Angeles').tz('America/New_York', true).toISOString(true),
98+
'2014-11-02T01:00:00.000-04:00',
99+
'keeping times between zones with DST at start of second repeated section falling back should work'
100+
);
101+
t.equal(
102+
moment.tz("2014-11-02T01:59:59.999-08:00", 'America/Los_Angeles').tz('America/New_York', true).toISOString(true),
103+
'2014-11-02T01:59:59.999-05:00',
104+
'keeping times between zones with DST at end of second repeated section falling back should work'
105+
);
106+
t.equal(
107+
moment.tz("2014-11-02T02:00:00", 'America/Los_Angeles').tz('America/New_York', true).toISOString(true),
108+
'2014-11-02T02:00:00.000-05:00',
109+
'keeping times between zones with DST after falling back should work'
110+
);
111+
112+
t.equal(
113+
moment.utc("2014-03-09T01:59:59.999").tz('America/New_York', true).toISOString(true),
114+
'2014-03-09T01:59:59.999-05:00',
115+
'keeping times from UTC to a zone with DST before springing forward should work'
116+
);
117+
t.equal(
118+
moment.utc("2014-03-09T02:00:00").tz('America/New_York', true).toISOString(true),
119+
'2014-03-09T03:00:00.000-04:00',
120+
'keeping times from UTC to a zone with DST at the start of springing forward should jump by an hour'
121+
);
122+
t.equal(
123+
moment.utc("2014-03-09T02:59:59.999").tz('America/New_York', true).toISOString(true),
124+
'2014-03-09T03:59:59.999-04:00',
125+
'keeping times from UTC to a zone with DST at the end of springing forward should jump by an hour'
126+
);
127+
t.equal(
128+
moment.utc("2014-03-09T03:00:00").tz('America/New_York', true).toISOString(true),
129+
'2014-03-09T03:00:00.000-04:00',
130+
'keeping times from UTC to a zone with DST after springing forward should work'
131+
);
132+
t.equal(
133+
moment.utc("2014-11-02T01:59:59.999").tz('America/New_York', true).toISOString(true),
134+
'2014-11-02T01:59:59.999-04:00',
135+
'keeping times from UTC to a zone with DST before falling back should work'
136+
);
137+
t.equal(
138+
moment.utc("2014-11-02T01:00:00").tz('America/New_York', true).toISOString(true),
139+
'2014-11-02T01:00:00.000-04:00',
140+
'keeping times from UTC to a zone with DST at start of first repeated section falling back should work'
141+
);
142+
t.equal(
143+
moment.utc("2014-11-02T01:59:59.999").tz('America/New_York', true).toISOString(true),
144+
'2014-11-02T01:59:59.999-04:00',
145+
'keeping times from UTC to a zone with DST at end of repeated section falling back should use first section'
146+
);
147+
t.equal(
148+
moment.utc("2014-11-02T02:00:00").tz('America/New_York', true).toISOString(true),
149+
'2014-11-02T02:00:00.000-05:00',
150+
'keeping times from UTC to a zone with DST after falling back should work'
151+
);
152+
153+
t.equal(
154+
moment.tz("2014-03-09T01:59:59.999", 'America/Los_Angeles').tz('UTC', true).toISOString(true),
155+
'2014-03-09T01:59:59.999+00:00',
156+
'keeping times from a zone with DST to UTC before springing forward should work'
157+
);
158+
t.equal(
159+
moment.tz("2014-03-09T03:00:00", 'America/Los_Angeles').tz('UTC', true).toISOString(true),
160+
'2014-03-09T03:00:00.000+00:00',
161+
'keeping times from a zone with DST to UTC after springing forward should work'
162+
);
163+
t.equal(
164+
moment.tz("2014-11-02T01:59:59.999", 'America/Los_Angeles').tz('UTC', true).toISOString(true),
165+
'2014-11-02T01:59:59.999+00:00',
166+
'keeping times from a zone with DST to UTC before falling back should work'
167+
);
168+
t.equal(
169+
moment.tz("2014-11-02T01:00:00-07:00", 'America/Los_Angeles').tz('UTC', true).toISOString(true),
170+
'2014-11-02T01:00:00.000+00:00',
171+
'keeping times from a zone with DST to UTC at start of first repeated section falling back should work'
172+
);
173+
t.equal(
174+
moment.tz("2014-11-02T01:59:59.999-07:00", 'America/Los_Angeles').tz('UTC', true).toISOString(true),
175+
'2014-11-02T01:59:59.999+00:00',
176+
'keeping times from a zone with DST to UTC at end of first repeated section falling back should work'
177+
);
178+
t.equal(
179+
moment.tz("2014-11-02T01:00:00-08:00", 'America/Los_Angeles').tz('UTC', true).toISOString(true),
180+
'2014-11-02T01:00:00.000+00:00',
181+
'keeping times from a zone with DST to UTC at start of second repeated section falling back should work'
182+
);
183+
t.equal(
184+
moment.tz("2014-11-02T01:59:59.999-08:00", 'America/Los_Angeles').tz('UTC', true).toISOString(true),
185+
'2014-11-02T01:59:59.999+00:00',
186+
'keeping times from a zone with DST to UTC at end of second repeated section falling back should work'
187+
);
188+
t.equal(
189+
moment.tz("2014-11-02T02:00:00", 'America/Los_Angeles').tz('UTC', true).toISOString(true),
190+
'2014-11-02T02:00:00.000+00:00',
191+
'keeping times from a zone with DST to UTC after falling back should work'
192+
);
193+
194+
t.equal(
195+
moment.tz("2014-03-09T01:59:59.999", 'America/New_York').tz('America/Phoenix', true).toISOString(true),
196+
'2014-03-09T01:59:59.999-07:00',
197+
'keeping times from a zone with DST to one without before springing forward should work'
198+
);
199+
t.equal(
200+
moment.tz("2014-03-09T03:00:00", 'America/New_York').tz('America/Phoenix', true).toISOString(true),
201+
'2014-03-09T03:00:00.000-07:00',
202+
'keeping times from a zone with DST to one without after springing forward should work'
203+
);
204+
t.equal(
205+
moment.tz("2014-11-02T01:59:59.999", 'America/New_York').tz('America/Phoenix', true).toISOString(true),
206+
'2014-11-02T01:59:59.999-07:00',
207+
'keeping times from a zone with DST to one without before falling back should work'
208+
);
209+
t.equal(
210+
moment.tz("2014-11-02T01:00:00-04:00", 'America/New_York').tz('America/Phoenix', true).toISOString(true),
211+
'2014-11-02T01:00:00.000-07:00',
212+
'keeping times from a zone with DST to one without at start of first repeated section falling back should work'
213+
);
214+
t.equal(
215+
moment.tz("2014-11-02T01:59:59.999-04:00", 'America/New_York').tz('America/Phoenix', true).toISOString(true),
216+
'2014-11-02T01:59:59.999-07:00',
217+
'keeping times from a zone with DST to one without at end of first repeated section falling back should work'
218+
);
219+
t.equal(
220+
moment.tz("2014-11-02T01:00:00-05:00", 'America/New_York').tz('America/Phoenix', true).toISOString(true),
221+
'2014-11-02T01:00:00.000-07:00',
222+
'keeping times from a zone with DST to one without at start of second repeated section falling back should work'
223+
);
224+
t.equal(
225+
moment.tz("2014-11-02T01:59:59.999-05:00", 'America/New_York').tz('America/Phoenix', true).toISOString(true),
226+
'2014-11-02T01:59:59.999-07:00',
227+
'keeping times from a zone with DST to one without at end of second repeated section falling back should work'
228+
);
229+
t.equal(
230+
moment.tz("2014-11-02T02:00:00", 'America/New_York').tz('America/Phoenix', true).toISOString(true),
231+
'2014-11-02T02:00:00.000-07:00',
232+
'keeping times from a zone with DST to one without after falling back should work'
233+
);
234+
235+
t.equal(
236+
moment.tz("2014-03-09T01:59:59.999", 'America/Phoenix').tz('America/New_York', true).toISOString(true),
237+
'2014-03-09T01:59:59.999-05:00',
238+
'keeping times from a zone without DST to one with before springing forward should work'
239+
);
240+
t.equal(
241+
moment.tz("2014-03-09T02:00:00", 'America/Phoenix').tz('America/New_York', true).toISOString(true),
242+
'2014-03-09T03:00:00.000-04:00',
243+
'keeping times from a zone without DST to one with at the start of springing forward should jump by an hour'
244+
);
245+
t.equal(
246+
moment.tz("2014-03-09T02:59:59.999", 'America/Phoenix').tz('America/New_York', true).toISOString(true),
247+
'2014-03-09T03:59:59.999-04:00',
248+
'keeping times from a zone without DST to one with at the end of springing forward should jump by an hour'
249+
);
250+
t.equal(
251+
moment.tz("2014-03-09T03:00:00", 'America/Phoenix').tz('America/New_York', true).toISOString(true),
252+
'2014-03-09T03:00:00.000-04:00',
253+
'keeping times from a zone without DST to one with after springing forward should work'
254+
);
255+
t.equal(
256+
moment.tz("2014-11-02T01:59:59.999", 'America/Phoenix').tz('America/New_York', true).toISOString(true),
257+
'2014-11-02T01:59:59.999-04:00',
258+
'keeping times from a zone without DST to one with before falling back should work'
259+
);
260+
t.equal(
261+
moment.tz("2014-11-02T01:00:00", 'America/Phoenix').tz('America/New_York', true).toISOString(true),
262+
'2014-11-02T01:00:00.000-04:00',
263+
'keeping times from a zone without DST to one with at start of first repeated section falling back should work'
264+
);
265+
t.equal(
266+
moment.tz("2014-11-02T01:59:59.999", 'America/Phoenix').tz('America/New_York', true).toISOString(true),
267+
'2014-11-02T01:59:59.999-04:00',
268+
'keeping times from a zone without DST to one with at end of first repeated section falling back should work'
269+
);
270+
t.equal(
271+
moment.tz("2014-11-02T02:00:00", 'America/Phoenix').tz('America/New_York', true).toISOString(true),
272+
'2014-11-02T02:00:00.000-05:00',
273+
'keeping times from a zone without DST to one with after falling back should work'
274+
);
275+
276+
t.done();
277+
},
278+
68279
isSame : function(t) {
69280
var m1 = moment.tz('2014-10-01T00:00:00', 'Europe/London');
70281
var m2 = moment.tz('2014-10-01T00:00:00', 'Europe/London');

tests/moment-timezone/utc.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ exports.utc = {
7171
var utcWallTimeFormat = m.clone().utcOffset('-05:00', true).format();
7272
m.tz('America/New_York', true);
7373
test.equal(m.format(), utcWallTimeFormat, "Should change the offset while keeping wall time when passing an optional parameter to moment.fn.tz");
74-
74+
7575
test.done();
7676
}
7777
};

0 commit comments

Comments
 (0)