diff --git a/src/lib/moment/start-end-of.js b/src/lib/moment/start-end-of.js index 02f982479a..dadfc03b61 100644 --- a/src/lib/moment/start-end-of.js +++ b/src/lib/moment/start-end-of.js @@ -1,7 +1,19 @@ 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) { @@ -9,11 +21,28 @@ export function startOf (units) { 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); @@ -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; @@ -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; } diff --git a/src/test/moment/start_end_of.js b/src/test/moment/start_end_of.js index 4b47620d63..60988af875 100644 --- a/src/test/moment/start_end_of.js +++ b/src/test/moment/start_end_of.js @@ -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) {