Skip to content

Commit

Permalink
Fix handling of DST jumps in Brazilian timezones in startOf/endOf, fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
Seldaek committed Sep 8, 2017
1 parent 7ff10bd commit 47b853a
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 15 deletions.
104 changes: 90 additions & 14 deletions src/lib/moment/start-end-of.js
@@ -1,19 +1,48 @@
import { normalizeUnits } from '../units/aliases';

export function startOf (units) {
var initialDate = this.date(),
checkForDST = true,
hour, clone;

units = normalizeUnits(units);

// in week/isoWeek operations, jump to noon to make sure that we do not hit DST bugs when
// switching the weekday/isoWeekday and can collect a valid initialDate to use for comparison
// after the time is adjusted
if (units === 'week' || units === 'isoWeek') {
this.hours(12);
}

// the following switch intentionally omits break keywords
// to utilize falling through the cases.
switch (units) {
case 'year':
this.month(0);
/* falls through */
case 'quarter':
// quarters is a special case
if (units === 'quarter') {
this.month(Math.floor(this.month() / 3) * 3);
}
/* falls through */
case 'month':
this.date(1);
// for month/quarter/year changes, no DST can interfere so we do not check for it
checkForDST = false;
/* falls through */
case 'week':
case 'isoWeek':
// weeks are a special case
if (units === 'week') {
this.weekday(0);
initialDate = this.date();
}
if (units === 'isoWeek') {
this.isoWeekday(1);
initialDate = this.date();
}
/* falls through */
case 'day':
case 'date':
this.hours(0);
Expand All @@ -28,17 +57,24 @@ export function startOf (units) {
this.milliseconds(0);
}

// weeks are a special case
if (units === 'week') {
this.weekday(0);
}
if (units === 'isoWeek') {
this.isoWeekday(1);
}
// check if day of month changed when setting the time
if (checkForDST && this.date() !== initialDate) {
// note: DST adjustments are assumed to occur in multiples of 1 hour (this is almost always the case)
// refer to http://www.timeanddate.com/time/aboutdst.html for the (rare) exceptions to this rule

// quarters are also special
if (units === 'quarter') {
this.month(Math.floor(this.month() / 3) * 3);
// depending on JS implementations, the time can jump 1day ahead or be in the past
if (this.date() > initialDate) {
this.date(initialDate);
} else {
// increment hour until cloned date == current date
hour = 1;
do {
clone = this.clone().add(hour++, 'hour');
} while (clone.date() < initialDate);

this.date(initialDate);
this.hours(clone.hours());
}
}

return this;
Expand All @@ -50,10 +86,50 @@ export function endOf (units) {
return this;
}

// 'date' is an alias for 'day', so it should be considered as such.
if (units === 'date') {
units = 'day';
// in week/isoWeek operations, jump to noon to make sure that we do not hit DST bugs when
// switching the weekday/isoWeekday
if (units === 'week' || units === 'isoWeek') {
this.hours(12);
}

// the following switch intentionally omits break keywords
// to utilize falling through the cases.
switch (units) {
case 'year':
this.month(11);
/* falls through */
case 'quarter':
// quarters is a special case
if (units === 'quarter') {
this.month((Math.floor(this.month() / 3) * 3) + 2);
}
/* falls through */
case 'month':
this.date(this.month() === 1 ? 28 : ([0, 2, 4, 6, 7, 9, 11].indexOf(this.month()) === -1 ? 30 : 31));
/* falls through */
case 'week':
case 'isoWeek':
// weeks are a special case
if (units === 'week') {
this.weekday(6);
}
if (units === 'isoWeek') {
this.isoWeekday(7);
}
/* falls through */
case 'day':
case 'date':
this.hours(23);
/* falls through */
case 'hour':
this.minutes(59);
/* falls through */
case 'minute':
this.seconds(59);
/* falls through */
case 'second':
this.milliseconds(999);
}

return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms');
return this;
}
83 changes: 82 additions & 1 deletion src/test/moment/start_end_of.js
Expand Up @@ -186,7 +186,88 @@ test('end of day', function (assert) {
assert.equal(m.hours(), 23, 'set the hours');
assert.equal(m.minutes(), 59, 'set the minutes');
assert.equal(m.seconds(), 59, 'set the seconds');
assert.equal(m.milliseconds(), 999, 'set the seconds');
assert.equal(m.milliseconds(), 999, 'set the milliseconds');
});

test('start/end of week with timezone on day DST switch occurs and weekstart being day of DST switch', function (assert) {
var oldOffset = moment.updateOffset,
fmt = 'YYYY-MM-DD HH:mm:ss.SSS',
m = moment('2017-10-15 02:03:04'),
expectedStart = '2017-10-15 01:00:00.000';

moment.updateOffset = function (mom, keepTime) {
// mimick Brazil DST which happens at midnight and in which 00:00:00 - 00:59:59 does not exist on Sunday 15th Oct 2017
if (mom.format(fmt) === '2017-10-15 00:00:00.000') {
mom.hour(1);
}
};

m.startOf('week');
assert.equal(m.format(fmt), expectedStart, 'start of week jumps correctly');

moment.updateOffset = oldOffset;
});

test('start/end of day with timezone on day DST switch occurs', function (assert) {
var oldOffset = moment.updateOffset,
fmt = 'YYYY-MM-DD HH:mm:ss.SSS',
m = moment('2017-10-15 02:03:04'),
m2 = moment('2017-10-15 02:03:04'),
expectedStart = '2017-10-15 01:00:00.000',
expectedEnd = '2017-10-15 23:59:59.999';

moment.updateOffset = function (mom, keepTime) {
// mimick Brazil DST which happens at midnight and in which 00:00:00 - 00:59:59 does not exist on Sunday 15th Oct 2017
if (mom.format(fmt) === '2017-10-15 00:00:00.000') {
mom.hour(1);
}
};

m.startOf('day');

assert.equal(m.format(fmt), expectedStart, 'start of day jumps correctly');

m.endOf('day');
m2.endOf('day');

assert.equal(m.format(fmt), expectedEnd, 'start + end of day jumps correctly');
assert.equal(m2.format(fmt), expectedEnd, 'end of day jumps correctly');

m.startOf('day');
assert.equal(m.format(fmt), expectedStart, 'start + end + start of day jumps correctly');

moment.updateOffset = oldOffset;
});

test('start/end of day with timezone on day before DST switch occurs', function (assert) {
var oldOffset = moment.updateOffset,
fmt = 'YYYY-MM-DD HH:mm:ss.SSS',
m = moment('2017-10-14 02:03:04'),
m2 = moment('2017-10-14 02:03:04'),
expectedStart = '2017-10-14 00:00:00.000',
expectedEnd = '2017-10-14 23:59:59.999';

moment.updateOffset = function (mom, keepTime) {
// mimick Brazil DST which happens at midnight and in which 00:00:00 - 00:59:59 does not exist on Sunday 15th Oct 2017
if (mom.format(fmt) === '2017-10-15 00:00:00.000') {
mom.hour(1);
}
};

m.startOf('day');

assert.equal(m.format(fmt), expectedStart, 'start of day jumps correctly');

m.endOf('day');
m2.endOf('day');

assert.equal(m.format(fmt), expectedEnd, 'start + end of day jumps correctly');
assert.equal(m2.format(fmt), expectedEnd, 'end of day jumps correctly');

m.startOf('day');
assert.equal(m.format(fmt), expectedStart, 'start + end + start of day jumps correctly');

moment.updateOffset = oldOffset;
});

test('start of date', function (assert) {
Expand Down

0 comments on commit 47b853a

Please sign in to comment.