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 handling of DST jumps in Brazilian timezones in startOf/endOf #4164

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 ? (this.isLeapYear() ? 29 : 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