From caa8f5a5252ccf0c693987af5579fcc96ac78ec7 Mon Sep 17 00:00:00 2001 From: Ash Searle Date: Wed, 29 Nov 2017 16:43:29 +0000 Subject: [PATCH 01/12] Fix startOf/endOf DST issues --- src/lib/moment/start-end-of.js | 59 ++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/src/lib/moment/start-end-of.js b/src/lib/moment/start-end-of.js index 02f982479a..dab22bde83 100644 --- a/src/lib/moment/start-end-of.js +++ b/src/lib/moment/start-end-of.js @@ -1,6 +1,18 @@ import { normalizeUnits } from '../units/aliases'; +import { hooks } from '../utils/hooks'; + +var MS_PER_SECOND = 1000; +var MS_PER_MINUTE = 60 * MS_PER_SECOND; +var MS_PER_HOUR = 60 * MS_PER_MINUTE; +var MS_PER_24_HOUR_DAY = 24 * MS_PER_HOUR; + +// actual modulo - handles negative numbers (for dates before 1970): +function mod(dividend, divisor) { + return (dividend % divisor + divisor) % divisor; +} export function startOf (units) { + var time; units = normalizeUnits(units); // the following switch intentionally omits break keywords // to utilize falling through the cases. @@ -16,16 +28,36 @@ export function startOf (units) { case 'isoWeek': case 'day': case 'date': - this.hours(0); - /* falls through */ + if (this.isValid()) { + if (this._isUTC) { + time = this._d.valueOf(); + this._d.setTime(time - mod(time, MS_PER_24_HOUR_DAY)); + } else { + this._d.setTime(new Date(this.year(), this.month(), this.date()).valueOf()); + } + hooks.updateOffset(this, true); + } + break; case 'hour': - this.minutes(0); - /* falls through */ + if (this.isValid()) { + var offset = this._isUTC ? 0 : this.utcOffset(); + time = this._d.valueOf(); + this._d.setTime(time - mod(time + offset * MS_PER_MINUTE, MS_PER_HOUR)); + hooks.updateOffset(this, true); + } + break; case 'minute': - this.seconds(0); - /* falls through */ + if (this.isValid()) { + time = this._d.valueOf(); + this._d.setTime(time - mod(time, MS_PER_MINUTE)); + } + break; case 'second': - this.milliseconds(0); + if (this.isValid()) { + time = this._d.valueOf(); + this._d.setTime(time - mod(time, MS_PER_SECOND)); + } + break; } // weeks are a special case @@ -55,5 +87,18 @@ export function endOf (units) { units = 'day'; } + if (units === 'day') { + if (this.isValid()) { + if (this._isUTC) { + var time = this._d.valueOf(); + this._d.setTime(time - mod(time, MS_PER_24_HOUR_DAY) + MS_PER_24_HOUR_DAY - 1); + } else { + this._d.setTime(new Date(this.year(), this.month(), this.date() + 1).valueOf() - 1); + } + hooks.updateOffset(this, true); + } + return this; + } + return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms'); } From 321a7cebaefd1545327a750c6873e115931e5956 Mon Sep 17 00:00:00 2001 From: Ash Searle Date: Wed, 29 Nov 2017 17:38:09 +0000 Subject: [PATCH 02/12] Performance improvements --- src/lib/moment/start-end-of.js | 209 ++++++++++++++++++++++----------- 1 file changed, 143 insertions(+), 66 deletions(-) diff --git a/src/lib/moment/start-end-of.js b/src/lib/moment/start-end-of.js index dab22bde83..168c83e1c3 100644 --- a/src/lib/moment/start-end-of.js +++ b/src/lib/moment/start-end-of.js @@ -14,91 +14,168 @@ function mod(dividend, divisor) { export function startOf (units) { var time; units = normalizeUnits(units); - // the following switch intentionally omits break keywords - // to utilize falling through the cases. - switch (units) { - case 'year': - this.month(0); - /* falls through */ - case 'quarter': - case 'month': - this.date(1); - /* falls through */ - case 'week': - case 'isoWeek': - case 'day': - case 'date': - if (this.isValid()) { - if (this._isUTC) { - time = this._d.valueOf(); - this._d.setTime(time - mod(time, MS_PER_24_HOUR_DAY)); - } else { - this._d.setTime(new Date(this.year(), this.month(), this.date()).valueOf()); + if (units === undefined || units === 'millisecond' || !this.isValid()) { + return this; + } + + if (this._isUTC) { + switch (units) { + case 'year': + this._d.setTime(Date.UTC(this.year(), 0)); + hooks.updateOffset(this, true); + break; + case 'quarter': + this._d.setTime(Date.UTC(this.year(), this.month() - (this.month() % 3))); + hooks.updateOffset(this, true); + break; + case 'month': + this._d.setTime(Date.UTC(this.year(), this.month())); + hooks.updateOffset(this, true); + break; + case 'week': + case 'isoWeek': + case 'day': + case 'date': + time = this._d.valueOf(); + this._d.setTime(time - mod(time, MS_PER_24_HOUR_DAY)); + hooks.updateOffset(this, true); + // Note: there may be a better/faster way of doing this with UTC... + if (units === 'week') { + this.weekday(0); + } else if (units === 'isoWeek') { + this.isoWeekday(1); } + break; + case 'hour': + time = this._d.valueOf(); + this._d.setTime(time - mod(time, MS_PER_HOUR)); hooks.updateOffset(this, true); - } - break; - case 'hour': - if (this.isValid()) { - var offset = this._isUTC ? 0 : this.utcOffset(); + break; + case 'minute': + time = this._d.valueOf(); + this._d.setTime(time - mod(time, MS_PER_MINUTE)); + break; + case 'second': time = this._d.valueOf(); - this._d.setTime(time - mod(time + offset * MS_PER_MINUTE, MS_PER_HOUR)); + this._d.setTime(time - mod(time, MS_PER_SECOND)); + break; + } + } else { + switch (units) { + case 'year': + this._d.setTime(new Date(this.year(), 0).valueOf()); hooks.updateOffset(this, true); - } - break; - case 'minute': - if (this.isValid()) { + break; + case 'quarter': + this._d.setTime(new Date(this.year(), this.month() - (this.month() % 3)).valueOf()); + hooks.updateOffset(this, true); + break; + case 'month': + this._d.setTime(new Date(this.year(), this.month()).valueOf()); + hooks.updateOffset(this, true); + break; + case 'week': + case 'isoWeek': + case 'day': + case 'date': + this._d.setTime(new Date(this.year(), this.month(), this.date()).valueOf()); + hooks.updateOffset(this, true); + // weeks are a special case + if (units === 'week') { + this.weekday(0); + } else if (units === 'isoWeek') { + this.isoWeekday(1); + } + break; + case 'hour': + time = this._d.valueOf(); + this._d.setTime(time - mod(time + this.utcOffset() * MS_PER_MINUTE, MS_PER_HOUR)); + hooks.updateOffset(this, true); + break; + case 'minute': time = this._d.valueOf(); this._d.setTime(time - mod(time, MS_PER_MINUTE)); - } - break; - case 'second': - if (this.isValid()) { + break; + case 'second': time = this._d.valueOf(); this._d.setTime(time - mod(time, MS_PER_SECOND)); - } - break; - } - - // weeks are a special case - if (units === 'week') { - this.weekday(0); - } - if (units === 'isoWeek') { - this.isoWeekday(1); - } - - // quarters are also special - if (units === 'quarter') { - this.month(Math.floor(this.month() / 3) * 3); + break; + } } return this; } export function endOf (units) { + var time; units = normalizeUnits(units); - if (units === undefined || units === 'millisecond') { + if (units === undefined || units === 'millisecond' || !this.isValid()) { return this; } - // 'date' is an alias for 'day', so it should be considered as such. - if (units === 'date') { - units = 'day'; - } - - if (units === 'day') { - if (this.isValid()) { - if (this._isUTC) { - var time = this._d.valueOf(); - this._d.setTime(time - mod(time, MS_PER_24_HOUR_DAY) + MS_PER_24_HOUR_DAY - 1); - } else { - this._d.setTime(new Date(this.year(), this.month(), this.date() + 1).valueOf() - 1); - } + switch (units) { + case 'year': + this._d.setTime((this._isUTC ? Date.UTC(this.year() + 1, 0) : new Date(this.year() + 1, 0).valueOf()) - 1); hooks.updateOffset(this, true); - } - return this; + break; + case 'quarter': + this._d.setTime( + (this._isUTC ? + Date.UTC(this.year(), 3 + this.month() - this.month() % 3) : + new Date(this.year(), 3 + this.month() - this.month() % 3).valueOf()) - 1 + ); + hooks.updateOffset(this, true); + break; + case 'month': + this._d.setTime( + (this._isUTC ? + Date.UTC(this.year(), 1 + this.month()) : + new Date(this.year(), 1 + this.month()).valueOf()) - 1 + ); + hooks.updateOffset(this, true); + break; + case 'week': + // start of week should be: this.date() - this.weekday(), so end of week would be: this.date() - this.weekday() + 7 + this._d.setTime( + (this._isUTC ? + Date.UTC(this.year(), this.month(), this.date() + 7 - this.weekday()) : + new Date(this.year(), this.month(), this.date() + 7 - this.weekday()).valueOf()) - 1 + ); + hooks.updateOffset(this, true); + break; + case 'isoWeek': + // start of iso week should be: this.date() - (this.isoWeekday - 1), so end would be: this.date() - (this.isoWeekday() - 1) + 7 + this._d.setTime( + (this._isUTC ? + Date.UTC(this.year(), this.month(), this.date() + 7 - (this.isoWeekday() - 1)) : + new Date(this.year(), this.month(), this.date() + 7 - (this.isoWeekday() - 1)).valueOf()) - 1 + ); + hooks.updateOffset(this, true); + break; + case 'day': + case 'date': + time = this._d.valueOf(); + this._d.setTime( + (this._isUTC ? + time - mod(time, MS_PER_24_HOUR_DAY) + MS_PER_24_HOUR_DAY : + new Date(this.year(), this.month(), this.date() + 1).valueOf()) - 1 + ); + hooks.updateOffset(this, true); + break; + case 'hour': + time = this._d.valueOf(); + this._d.setTime(time - mod(time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), MS_PER_HOUR) + MS_PER_HOUR - 1); + hooks.updateOffset(this, true); + break; + case 'minute': + time = this._d.valueOf(); + this._d.setTime(time - mod(time, MS_PER_MINUTE) + MS_PER_MINUTE - 1); + break; + case 'second': + time = this._d.valueOf(); + this._d.setTime(time - mod(time, MS_PER_SECOND) + MS_PER_SECOND - 1); + break; } - return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms'); + return this; } From b3f5e3b04523004a09ac03cdf404e846c0808eb1 Mon Sep 17 00:00:00 2001 From: Ash Searle Date: Thu, 30 Nov 2017 21:48:15 +0000 Subject: [PATCH 03/12] Rewrite for legibility --- src/lib/moment/start-end-of.js | 175 ++++++++++++++------------------- 1 file changed, 76 insertions(+), 99 deletions(-) diff --git a/src/lib/moment/start-end-of.js b/src/lib/moment/start-end-of.js index 168c83e1c3..b897adacb7 100644 --- a/src/lib/moment/start-end-of.js +++ b/src/lib/moment/start-end-of.js @@ -18,91 +18,69 @@ export function startOf (units) { return this; } - if (this._isUTC) { - switch (units) { - case 'year': - this._d.setTime(Date.UTC(this.year(), 0)); - hooks.updateOffset(this, true); - break; - case 'quarter': - this._d.setTime(Date.UTC(this.year(), this.month() - (this.month() % 3))); - hooks.updateOffset(this, true); - break; - case 'month': - this._d.setTime(Date.UTC(this.year(), this.month())); - hooks.updateOffset(this, true); - break; - case 'week': - case 'isoWeek': - case 'day': - case 'date': - time = this._d.valueOf(); - this._d.setTime(time - mod(time, MS_PER_24_HOUR_DAY)); - hooks.updateOffset(this, true); - // Note: there may be a better/faster way of doing this with UTC... - if (units === 'week') { - this.weekday(0); - } else if (units === 'isoWeek') { - this.isoWeekday(1); - } - break; - case 'hour': - time = this._d.valueOf(); - this._d.setTime(time - mod(time, MS_PER_HOUR)); - hooks.updateOffset(this, true); - break; - case 'minute': - time = this._d.valueOf(); - this._d.setTime(time - mod(time, MS_PER_MINUTE)); - break; - case 'second': - time = this._d.valueOf(); - this._d.setTime(time - mod(time, MS_PER_SECOND)); - break; - } - } else { - switch (units) { - case 'year': - this._d.setTime(new Date(this.year(), 0).valueOf()); - hooks.updateOffset(this, true); - break; - case 'quarter': - this._d.setTime(new Date(this.year(), this.month() - (this.month() % 3)).valueOf()); - hooks.updateOffset(this, true); - break; - case 'month': - this._d.setTime(new Date(this.year(), this.month()).valueOf()); - hooks.updateOffset(this, true); - break; - case 'week': - case 'isoWeek': - case 'day': - case 'date': - this._d.setTime(new Date(this.year(), this.month(), this.date()).valueOf()); - hooks.updateOffset(this, true); - // weeks are a special case - if (units === 'week') { - this.weekday(0); - } else if (units === 'isoWeek') { - this.isoWeekday(1); - } - break; - case 'hour': - time = this._d.valueOf(); - this._d.setTime(time - mod(time + this.utcOffset() * MS_PER_MINUTE, MS_PER_HOUR)); - hooks.updateOffset(this, true); - break; - case 'minute': - time = this._d.valueOf(); - this._d.setTime(time - mod(time, MS_PER_MINUTE)); - break; - case 'second': - time = this._d.valueOf(); - this._d.setTime(time - mod(time, MS_PER_SECOND)); - break; - } + switch (units) { + case 'year': + time = ( + (this._isUTC ? + Date.UTC(this.year(), 0) : + new Date(this.year(), 0).valueOf()) + ); + break; + case 'quarter': + time = ( + (this._isUTC ? + Date.UTC(this.year(), this.month() - this.month() % 3) : + new Date(this.year(), this.month() - this.month() % 3).valueOf()) + ); + break; + case 'month': + time = ( + (this._isUTC ? + Date.UTC(this.year(), this.month()) : + new Date(this.year(), this.month()).valueOf()) + ); + break; + case 'week': + // start of week should be: this.date() - this.weekday(): + time = ( + (this._isUTC ? + Date.UTC(this.year(), this.month(), this.date() - this.weekday()) : + new Date(this.year(), this.month(), this.date() - this.weekday()).valueOf()) + ); + break; + case 'isoWeek': + // start of iso week should be: this.date() - (this.isoWeekday - 1): + time = ( + (this._isUTC ? + Date.UTC(this.year(), this.month(), this.date() - (this.isoWeekday() - 1)) : + new Date(this.year(), this.month(), this.date() - (this.isoWeekday() - 1)).valueOf()) + ); + break; + case 'day': + case 'date': + time = this._d.valueOf(); + time = ( + (this._isUTC ? + time - mod(time, MS_PER_24_HOUR_DAY) : + new Date(this.year(), this.month(), this.date()).valueOf()) + ); + break; + case 'hour': + time = this._d.valueOf(); + time = (time - mod(time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), MS_PER_HOUR)); + break; + case 'minute': + time = this._d.valueOf(); + this._d.setTime(time - mod(time, MS_PER_MINUTE)); + return this; + case 'second': + time = this._d.valueOf(); + this._d.setTime(time - mod(time, MS_PER_SECOND)); + return this; } + this._d.setTime(time); + hooks.updateOffset(this, true); return this; } @@ -115,67 +93,66 @@ export function endOf (units) { switch (units) { case 'year': - this._d.setTime((this._isUTC ? Date.UTC(this.year() + 1, 0) : new Date(this.year() + 1, 0).valueOf()) - 1); - hooks.updateOffset(this, true); + time = ( + (this._isUTC ? + Date.UTC(this.year() + 1, 0) : + new Date(this.year() + 1, 0).valueOf()) - 1 + ); break; case 'quarter': - this._d.setTime( + time = ( (this._isUTC ? Date.UTC(this.year(), 3 + this.month() - this.month() % 3) : new Date(this.year(), 3 + this.month() - this.month() % 3).valueOf()) - 1 ); - hooks.updateOffset(this, true); break; case 'month': - this._d.setTime( + time = ( (this._isUTC ? Date.UTC(this.year(), 1 + this.month()) : new Date(this.year(), 1 + this.month()).valueOf()) - 1 ); - hooks.updateOffset(this, true); break; case 'week': // start of week should be: this.date() - this.weekday(), so end of week would be: this.date() - this.weekday() + 7 - this._d.setTime( + time = ( (this._isUTC ? Date.UTC(this.year(), this.month(), this.date() + 7 - this.weekday()) : new Date(this.year(), this.month(), this.date() + 7 - this.weekday()).valueOf()) - 1 ); - hooks.updateOffset(this, true); break; case 'isoWeek': // start of iso week should be: this.date() - (this.isoWeekday - 1), so end would be: this.date() - (this.isoWeekday() - 1) + 7 - this._d.setTime( + time = ( (this._isUTC ? Date.UTC(this.year(), this.month(), this.date() + 7 - (this.isoWeekday() - 1)) : new Date(this.year(), this.month(), this.date() + 7 - (this.isoWeekday() - 1)).valueOf()) - 1 ); - hooks.updateOffset(this, true); break; case 'day': case 'date': time = this._d.valueOf(); - this._d.setTime( + time = ( (this._isUTC ? time - mod(time, MS_PER_24_HOUR_DAY) + MS_PER_24_HOUR_DAY : new Date(this.year(), this.month(), this.date() + 1).valueOf()) - 1 ); - hooks.updateOffset(this, true); break; case 'hour': time = this._d.valueOf(); - this._d.setTime(time - mod(time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), MS_PER_HOUR) + MS_PER_HOUR - 1); - hooks.updateOffset(this, true); + time = (time - mod(time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), MS_PER_HOUR) + MS_PER_HOUR - 1); break; case 'minute': time = this._d.valueOf(); this._d.setTime(time - mod(time, MS_PER_MINUTE) + MS_PER_MINUTE - 1); - break; + return this; case 'second': time = this._d.valueOf(); this._d.setTime(time - mod(time, MS_PER_SECOND) + MS_PER_SECOND - 1); - break; + return this; } + this._d.setTime(time); + hooks.updateOffset(this, true); return this; } From c447c0c32eb25532c1df3e078fbc7ed3b0899750 Mon Sep 17 00:00:00 2001 From: Ash Searle Date: Thu, 30 Nov 2017 22:32:24 +0000 Subject: [PATCH 04/12] Fix for years in range 0-99 --- src/lib/create/date-from-array.js | 31 ++++++--- src/lib/moment/start-end-of.js | 103 +++++++++++------------------- src/test/moment/start_end_of.js | 24 +++++++ 3 files changed, 84 insertions(+), 74 deletions(-) diff --git a/src/lib/create/date-from-array.js b/src/lib/create/date-from-array.js index 59b57b0365..acf82073c2 100644 --- a/src/lib/create/date-from-array.js +++ b/src/lib/create/date-from-array.js @@ -1,21 +1,36 @@ export function createDate (y, m, d, h, M, s, ms) { + var date; + // the date constructor remaps years 0-99 to 1900-1999 + var remapYears = (y < 100 && y >= 0); // can't just apply() to create a date: // https://stackoverflow.com/q/181348 - var date = new Date(y, m, d, h, M, s, ms); + if (arguments.length === 3) { + // Special-case: handles dates that don't start at midnight + date = new Date(remapYears ? y + 400 : y, m, d); + } else { + date = new Date(remapYears ? y + 400 : y, m, d, h, M, s, ms); + } - // the date constructor remaps years 0-99 to 1900-1999 - if (y < 100 && y >= 0 && isFinite(date.getFullYear())) { + if (remapYears && isFinite(date.getFullYear())) { date.setFullYear(y); } + return date; } export function createUTCDate (y) { - var date = new Date(Date.UTC.apply(null, arguments)); - - // the Date.UTC function remaps years 0-99 to 1900-1999 - if (y < 100 && y >= 0 && isFinite(date.getUTCFullYear())) { - date.setUTCFullYear(y); + var date; + // the date constructor remaps years 0-99 to 1900-1999 + if (y < 100 && y >= 0) { + var args = Array.prototype.slice.call(arguments); + args[0] = y + 400; + date = new Date(Date.UTC.apply(null, args)); + if (isFinite(date.getUTCFullYear())) { + date.setUTCFullYear(y); + } + } else { + date = new Date(Date.UTC.apply(null, arguments)); } + return date; } diff --git a/src/lib/moment/start-end-of.js b/src/lib/moment/start-end-of.js index b897adacb7..a46b7e1292 100644 --- a/src/lib/moment/start-end-of.js +++ b/src/lib/moment/start-end-of.js @@ -5,12 +5,31 @@ var MS_PER_SECOND = 1000; var MS_PER_MINUTE = 60 * MS_PER_SECOND; var MS_PER_HOUR = 60 * MS_PER_MINUTE; var MS_PER_24_HOUR_DAY = 24 * MS_PER_HOUR; +var MS_PER_400_YEARS = (365 * 400 + 97) * MS_PER_24_HOUR_DAY; // actual modulo - handles negative numbers (for dates before 1970): function mod(dividend, divisor) { return (dividend % divisor + divisor) % divisor; } +function localStartOfDate(y, m, d) { + // the date constructor remaps years 0-99 to 1900-1999 + if (0 <= y && y <= 99) { + return new Date(y + 400, m, d) - MS_PER_400_YEARS; + } else { + return new Date(y, m, d).getTime(); + } +} + +function utcStartOfDate(y, m, d) { + // Date.UTC remaps years 0-99 to 1900-1999 + if (0 <= y && y <= 99) { + return Date.UTC(y + 400, m, d) - MS_PER_400_YEARS; + } else { + return Date.UTC(y, m, d); + } +} + export function startOf (units) { var time; units = normalizeUnits(units); @@ -18,56 +37,32 @@ export function startOf (units) { return this; } + var startOfDate = this._isUTC ? utcStartOfDate : localStartOfDate; + switch (units) { case 'year': - time = ( - (this._isUTC ? - Date.UTC(this.year(), 0) : - new Date(this.year(), 0).valueOf()) - ); + time = startOfDate(this.year(), 0, 1); break; case 'quarter': - time = ( - (this._isUTC ? - Date.UTC(this.year(), this.month() - this.month() % 3) : - new Date(this.year(), this.month() - this.month() % 3).valueOf()) - ); + time = startOfDate(this.year(), this.month() - this.month() % 3, 1); break; case 'month': - time = ( - (this._isUTC ? - Date.UTC(this.year(), this.month()) : - new Date(this.year(), this.month()).valueOf()) - ); + time = startOfDate(this.year(), this.month(), 1); break; case 'week': - // start of week should be: this.date() - this.weekday(): - time = ( - (this._isUTC ? - Date.UTC(this.year(), this.month(), this.date() - this.weekday()) : - new Date(this.year(), this.month(), this.date() - this.weekday()).valueOf()) - ); + time = startOfDate(this.year(), this.month(), this.date() - this.weekday()); break; case 'isoWeek': - // start of iso week should be: this.date() - (this.isoWeekday - 1): - time = ( - (this._isUTC ? - Date.UTC(this.year(), this.month(), this.date() - (this.isoWeekday() - 1)) : - new Date(this.year(), this.month(), this.date() - (this.isoWeekday() - 1)).valueOf()) - ); + time = startOfDate(this.year(), this.month(), this.date() - (this.isoWeekday() - 1)); break; case 'day': case 'date': - time = this._d.valueOf(); - time = ( - (this._isUTC ? - time - mod(time, MS_PER_24_HOUR_DAY) : - new Date(this.year(), this.month(), this.date()).valueOf()) - ); + time = startOfDate(this.year(), this.month(), this.date()); break; case 'hour': time = this._d.valueOf(); time = (time - mod(time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), MS_PER_HOUR)); + // FIXME: Special case: handle fractional DST changes: break; case 'minute': time = this._d.valueOf(); @@ -91,56 +86,32 @@ export function endOf (units) { return this; } + var startOfDate = this._isUTC ? utcStartOfDate : localStartOfDate; + switch (units) { case 'year': - time = ( - (this._isUTC ? - Date.UTC(this.year() + 1, 0) : - new Date(this.year() + 1, 0).valueOf()) - 1 - ); + time = startOfDate(this.year() + 1, 0, 1) - 1; break; case 'quarter': - time = ( - (this._isUTC ? - Date.UTC(this.year(), 3 + this.month() - this.month() % 3) : - new Date(this.year(), 3 + this.month() - this.month() % 3).valueOf()) - 1 - ); + time = startOfDate(this.year(), this.month() - this.month() % 3 + 3, 1) - 1; break; case 'month': - time = ( - (this._isUTC ? - Date.UTC(this.year(), 1 + this.month()) : - new Date(this.year(), 1 + this.month()).valueOf()) - 1 - ); + time = startOfDate(this.year(), this.month() + 1, 1) - 1; break; case 'week': - // start of week should be: this.date() - this.weekday(), so end of week would be: this.date() - this.weekday() + 7 - time = ( - (this._isUTC ? - Date.UTC(this.year(), this.month(), this.date() + 7 - this.weekday()) : - new Date(this.year(), this.month(), this.date() + 7 - this.weekday()).valueOf()) - 1 - ); + time = startOfDate(this.year(), this.month(), this.date() - this.weekday() + 7) - 1; break; case 'isoWeek': - // start of iso week should be: this.date() - (this.isoWeekday - 1), so end would be: this.date() - (this.isoWeekday() - 1) + 7 - time = ( - (this._isUTC ? - Date.UTC(this.year(), this.month(), this.date() + 7 - (this.isoWeekday() - 1)) : - new Date(this.year(), this.month(), this.date() + 7 - (this.isoWeekday() - 1)).valueOf()) - 1 - ); + time = startOfDate(this.year(), this.month(), this.date() - (this.isoWeekday() - 1) + 7) - 1; break; case 'day': case 'date': - time = this._d.valueOf(); - time = ( - (this._isUTC ? - time - mod(time, MS_PER_24_HOUR_DAY) + MS_PER_24_HOUR_DAY : - new Date(this.year(), this.month(), this.date() + 1).valueOf()) - 1 - ); + time = startOfDate(this.year(), this.month(), this.date() + 1) - 1; break; case 'hour': time = this._d.valueOf(); time = (time - mod(time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), MS_PER_HOUR) + MS_PER_HOUR - 1); + // FIXME: Special case: handle fractional DST changes: break; case 'minute': time = this._d.valueOf(); diff --git a/src/test/moment/start_end_of.js b/src/test/moment/start_end_of.js index 4b47620d63..71ba88d809 100644 --- a/src/test/moment/start_end_of.js +++ b/src/test/moment/start_end_of.js @@ -393,3 +393,27 @@ test('endOf millisecond and no-arg', function (assert) { assert.equal(+m, +m.clone().endOf('millisecond'), 'endOf with millisecond argument should change time'); assert.equal(+m, +m.clone().endOf('milliseconds'), 'endOf with milliseconds argument should change time'); }); + +test('startOf for year zero', function (assert) { + var m = moment('0000-02-29T12:34:56.789Z').parseZone(); + assert.equal(m.clone().startOf('ms').toISOString(), '0000-02-29T12:34:56.789Z', 'startOf millisecond should preserver year'); + assert.equal(m.clone().startOf('s').toISOString(), '0000-02-29T12:34:56.000Z', 'startOf second should preserver year'); + assert.equal(m.clone().startOf('m').toISOString(), '0000-02-29T12:34:00.000Z', 'startOf minute should preserver year'); + assert.equal(m.clone().startOf('h').toISOString(), '0000-02-29T12:00:00.000Z', 'startOf hour should preserver year'); + assert.equal(m.clone().startOf('d').toISOString(), '0000-02-29T00:00:00.000Z', 'startOf day should preserver year'); + assert.equal(m.clone().startOf('M').toISOString(), '0000-02-01T00:00:00.000Z', 'startOf month should preserver year'); + assert.equal(m.clone().startOf('Q').toISOString(), '0000-01-01T00:00:00.000Z', 'startOf quarter should preserver year'); + assert.equal(m.clone().startOf('y').toISOString(), '0000-01-01T00:00:00.000Z', 'startOf year should preserver year'); +}); + +test('endOf for year zero', function (assert) { + var m = moment('0000-02-29T12:34:56.789Z').parseZone(); + assert.equal(m.clone().endOf('ms').toISOString(), '0000-02-29T12:34:56.789Z', 'endOf millisecond should preserver year'); + assert.equal(m.clone().endOf('s').toISOString(), '0000-02-29T12:34:56.999Z', 'endOf second should preserver year'); + assert.equal(m.clone().endOf('m').toISOString(), '0000-02-29T12:34:59.999Z', 'endOf minute should preserver year'); + assert.equal(m.clone().endOf('h').toISOString(), '0000-02-29T12:59:59.999Z', 'endOf hour should preserver year'); + assert.equal(m.clone().endOf('d').toISOString(), '0000-02-29T23:59:59.999Z', 'endOf day should preserver year'); + assert.equal(m.clone().endOf('M').toISOString(), '0000-02-29T23:59:59.999Z', 'endOf month should preserver year'); + assert.equal(m.clone().endOf('Q').toISOString(), '0000-03-31T23:59:59.999Z', 'endOf quarter should preserver year'); + assert.equal(m.clone().endOf('y').toISOString(), '0000-12-31T23:59:59.999Z', 'endOf year should preserver year'); +}); From 69ccb8311263704df4fa5525235913e4bec9d990 Mon Sep 17 00:00:00 2001 From: Ash Searle Date: Thu, 30 Nov 2017 23:40:27 +0000 Subject: [PATCH 05/12] Remove placeholder comments --- src/lib/moment/start-end-of.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/moment/start-end-of.js b/src/lib/moment/start-end-of.js index a46b7e1292..afae9790bb 100644 --- a/src/lib/moment/start-end-of.js +++ b/src/lib/moment/start-end-of.js @@ -17,7 +17,7 @@ function localStartOfDate(y, m, d) { if (0 <= y && y <= 99) { return new Date(y + 400, m, d) - MS_PER_400_YEARS; } else { - return new Date(y, m, d).getTime(); + return new Date(y, m, d).valueOf(); } } @@ -62,7 +62,6 @@ export function startOf (units) { case 'hour': time = this._d.valueOf(); time = (time - mod(time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), MS_PER_HOUR)); - // FIXME: Special case: handle fractional DST changes: break; case 'minute': time = this._d.valueOf(); @@ -111,7 +110,6 @@ export function endOf (units) { case 'hour': time = this._d.valueOf(); time = (time - mod(time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), MS_PER_HOUR) + MS_PER_HOUR - 1); - // FIXME: Special case: handle fractional DST changes: break; case 'minute': time = this._d.valueOf(); From 1cdb7cacb98dc91dd9ada73a656802bb9051a125 Mon Sep 17 00:00:00 2001 From: Ash Searle Date: Fri, 1 Dec 2017 00:36:35 +0000 Subject: [PATCH 06/12] Revert comment --- src/lib/create/date-from-array.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/create/date-from-array.js b/src/lib/create/date-from-array.js index acf82073c2..d89481fc9e 100644 --- a/src/lib/create/date-from-array.js +++ b/src/lib/create/date-from-array.js @@ -20,7 +20,7 @@ export function createDate (y, m, d, h, M, s, ms) { export function createUTCDate (y) { var date; - // the date constructor remaps years 0-99 to 1900-1999 + // the Date.UTC function remaps years 0-99 to 1900-1999 if (y < 100 && y >= 0) { var args = Array.prototype.slice.call(arguments); args[0] = y + 400; From 09203bbaae8708438d5175d4225ce2b91bf67386 Mon Sep 17 00:00:00 2001 From: Ash Searle Date: Wed, 6 Dec 2017 08:56:46 +0000 Subject: [PATCH 07/12] Remove useless special-case --- src/lib/create/date-from-array.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/lib/create/date-from-array.js b/src/lib/create/date-from-array.js index d89481fc9e..1d577207fe 100644 --- a/src/lib/create/date-from-array.js +++ b/src/lib/create/date-from-array.js @@ -1,15 +1,9 @@ export function createDate (y, m, d, h, M, s, ms) { - var date; // the date constructor remaps years 0-99 to 1900-1999 var remapYears = (y < 100 && y >= 0); // can't just apply() to create a date: // https://stackoverflow.com/q/181348 - if (arguments.length === 3) { - // Special-case: handles dates that don't start at midnight - date = new Date(remapYears ? y + 400 : y, m, d); - } else { - date = new Date(remapYears ? y + 400 : y, m, d, h, M, s, ms); - } + var date = new Date(remapYears ? y + 400 : y, m, d, h, M, s, ms); if (remapYears && isFinite(date.getFullYear())) { date.setFullYear(y); From 119e0b84f520ca395bc6ef1b7da843873ce5a95f Mon Sep 17 00:00:00 2001 From: Ash Searle Date: Fri, 22 Dec 2017 12:22:01 +0000 Subject: [PATCH 08/12] Code review change: use break consistenly --- src/lib/moment/start-end-of.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lib/moment/start-end-of.js b/src/lib/moment/start-end-of.js index afae9790bb..5c1a752120 100644 --- a/src/lib/moment/start-end-of.js +++ b/src/lib/moment/start-end-of.js @@ -61,16 +61,16 @@ export function startOf (units) { break; case 'hour': time = this._d.valueOf(); - time = (time - mod(time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), MS_PER_HOUR)); + time -= mod(time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), MS_PER_HOUR); break; case 'minute': time = this._d.valueOf(); - this._d.setTime(time - mod(time, MS_PER_MINUTE)); - return this; + time -= mod(time, MS_PER_MINUTE); + break; case 'second': time = this._d.valueOf(); - this._d.setTime(time - mod(time, MS_PER_SECOND)); - return this; + time -= mod(time, MS_PER_SECOND); + break; } this._d.setTime(time); @@ -109,16 +109,16 @@ export function endOf (units) { break; case 'hour': time = this._d.valueOf(); - time = (time - mod(time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), MS_PER_HOUR) + MS_PER_HOUR - 1); + time += MS_PER_HOUR - mod(time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), MS_PER_HOUR) - 1; break; case 'minute': time = this._d.valueOf(); - this._d.setTime(time - mod(time, MS_PER_MINUTE) + MS_PER_MINUTE - 1); - return this; + time += MS_PER_MINUTE - mod(time, MS_PER_MINUTE) - 1; + break; case 'second': time = this._d.valueOf(); - this._d.setTime(time - mod(time, MS_PER_SECOND) + MS_PER_SECOND - 1); - return this; + time += MS_PER_SECOND - mod(time, MS_PER_SECOND) - 1; + break; } this._d.setTime(time); From 1b433f7b97301e030cb417795cc2a68f5ce29011 Mon Sep 17 00:00:00 2001 From: Ash Searle Date: Fri, 5 Jan 2018 16:11:14 +0000 Subject: [PATCH 09/12] Rename per code review --- src/lib/moment/start-end-of.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/moment/start-end-of.js b/src/lib/moment/start-end-of.js index 5c1a752120..0f4bc8b87e 100644 --- a/src/lib/moment/start-end-of.js +++ b/src/lib/moment/start-end-of.js @@ -4,8 +4,8 @@ import { hooks } from '../utils/hooks'; var MS_PER_SECOND = 1000; var MS_PER_MINUTE = 60 * MS_PER_SECOND; var MS_PER_HOUR = 60 * MS_PER_MINUTE; -var MS_PER_24_HOUR_DAY = 24 * MS_PER_HOUR; -var MS_PER_400_YEARS = (365 * 400 + 97) * MS_PER_24_HOUR_DAY; +var MS_PER_DAY = 24 * MS_PER_HOUR; +var MS_PER_400_YEARS = (365 * 400 + 97) * MS_PER_DAY; // actual modulo - handles negative numbers (for dates before 1970): function mod(dividend, divisor) { From 657db785f8ea6c23fc4e37378953f751518441e4 Mon Sep 17 00:00:00 2001 From: Ash Searle Date: Mon, 16 Apr 2018 08:52:25 +0100 Subject: [PATCH 10/12] Fix typo --- src/test/moment/start_end_of.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/test/moment/start_end_of.js b/src/test/moment/start_end_of.js index 71ba88d809..e3db910474 100644 --- a/src/test/moment/start_end_of.js +++ b/src/test/moment/start_end_of.js @@ -396,24 +396,24 @@ test('endOf millisecond and no-arg', function (assert) { test('startOf for year zero', function (assert) { var m = moment('0000-02-29T12:34:56.789Z').parseZone(); - assert.equal(m.clone().startOf('ms').toISOString(), '0000-02-29T12:34:56.789Z', 'startOf millisecond should preserver year'); - assert.equal(m.clone().startOf('s').toISOString(), '0000-02-29T12:34:56.000Z', 'startOf second should preserver year'); - assert.equal(m.clone().startOf('m').toISOString(), '0000-02-29T12:34:00.000Z', 'startOf minute should preserver year'); - assert.equal(m.clone().startOf('h').toISOString(), '0000-02-29T12:00:00.000Z', 'startOf hour should preserver year'); - assert.equal(m.clone().startOf('d').toISOString(), '0000-02-29T00:00:00.000Z', 'startOf day should preserver year'); - assert.equal(m.clone().startOf('M').toISOString(), '0000-02-01T00:00:00.000Z', 'startOf month should preserver year'); - assert.equal(m.clone().startOf('Q').toISOString(), '0000-01-01T00:00:00.000Z', 'startOf quarter should preserver year'); - assert.equal(m.clone().startOf('y').toISOString(), '0000-01-01T00:00:00.000Z', 'startOf year should preserver year'); + assert.equal(m.clone().startOf('ms').toISOString(), '0000-02-29T12:34:56.789Z', 'startOf millisecond should preserve year'); + assert.equal(m.clone().startOf('s').toISOString(), '0000-02-29T12:34:56.000Z', 'startOf second should preserve year'); + assert.equal(m.clone().startOf('m').toISOString(), '0000-02-29T12:34:00.000Z', 'startOf minute should preserve year'); + assert.equal(m.clone().startOf('h').toISOString(), '0000-02-29T12:00:00.000Z', 'startOf hour should preserve year'); + assert.equal(m.clone().startOf('d').toISOString(), '0000-02-29T00:00:00.000Z', 'startOf day should preserve year'); + assert.equal(m.clone().startOf('M').toISOString(), '0000-02-01T00:00:00.000Z', 'startOf month should preserve year'); + assert.equal(m.clone().startOf('Q').toISOString(), '0000-01-01T00:00:00.000Z', 'startOf quarter should preserve year'); + assert.equal(m.clone().startOf('y').toISOString(), '0000-01-01T00:00:00.000Z', 'startOf year should preserve year'); }); test('endOf for year zero', function (assert) { var m = moment('0000-02-29T12:34:56.789Z').parseZone(); - assert.equal(m.clone().endOf('ms').toISOString(), '0000-02-29T12:34:56.789Z', 'endOf millisecond should preserver year'); - assert.equal(m.clone().endOf('s').toISOString(), '0000-02-29T12:34:56.999Z', 'endOf second should preserver year'); - assert.equal(m.clone().endOf('m').toISOString(), '0000-02-29T12:34:59.999Z', 'endOf minute should preserver year'); - assert.equal(m.clone().endOf('h').toISOString(), '0000-02-29T12:59:59.999Z', 'endOf hour should preserver year'); - assert.equal(m.clone().endOf('d').toISOString(), '0000-02-29T23:59:59.999Z', 'endOf day should preserver year'); - assert.equal(m.clone().endOf('M').toISOString(), '0000-02-29T23:59:59.999Z', 'endOf month should preserver year'); - assert.equal(m.clone().endOf('Q').toISOString(), '0000-03-31T23:59:59.999Z', 'endOf quarter should preserver year'); - assert.equal(m.clone().endOf('y').toISOString(), '0000-12-31T23:59:59.999Z', 'endOf year should preserver year'); + assert.equal(m.clone().endOf('ms').toISOString(), '0000-02-29T12:34:56.789Z', 'endOf millisecond should preserve year'); + assert.equal(m.clone().endOf('s').toISOString(), '0000-02-29T12:34:56.999Z', 'endOf second should preserve year'); + assert.equal(m.clone().endOf('m').toISOString(), '0000-02-29T12:34:59.999Z', 'endOf minute should preserve year'); + assert.equal(m.clone().endOf('h').toISOString(), '0000-02-29T12:59:59.999Z', 'endOf hour should preserve year'); + assert.equal(m.clone().endOf('d').toISOString(), '0000-02-29T23:59:59.999Z', 'endOf day should preserve year'); + assert.equal(m.clone().endOf('M').toISOString(), '0000-02-29T23:59:59.999Z', 'endOf month should preserve year'); + assert.equal(m.clone().endOf('Q').toISOString(), '0000-03-31T23:59:59.999Z', 'endOf quarter should preserve year'); + assert.equal(m.clone().endOf('y').toISOString(), '0000-12-31T23:59:59.999Z', 'endOf year should preserve year'); }); From e2c82e110373a3684bdb818c434a13f25af718e6 Mon Sep 17 00:00:00 2001 From: Ash Searle Date: Mon, 16 Apr 2018 09:00:28 +0100 Subject: [PATCH 11/12] Inline variable to end naming debate --- src/lib/moment/start-end-of.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/moment/start-end-of.js b/src/lib/moment/start-end-of.js index 0f4bc8b87e..75942674e4 100644 --- a/src/lib/moment/start-end-of.js +++ b/src/lib/moment/start-end-of.js @@ -4,8 +4,7 @@ import { hooks } from '../utils/hooks'; var MS_PER_SECOND = 1000; var MS_PER_MINUTE = 60 * MS_PER_SECOND; var MS_PER_HOUR = 60 * MS_PER_MINUTE; -var MS_PER_DAY = 24 * MS_PER_HOUR; -var MS_PER_400_YEARS = (365 * 400 + 97) * MS_PER_DAY; +var MS_PER_400_YEARS = (365 * 400 + 97) * 24 * MS_PER_HOUR; // actual modulo - handles negative numbers (for dates before 1970): function mod(dividend, divisor) { From 6abd6fde48f7136067f77c70d1c4d64195733f23 Mon Sep 17 00:00:00 2001 From: Ash Searle Date: Fri, 18 Jan 2019 10:16:26 +0000 Subject: [PATCH 12/12] Address review comments --- src/lib/create/date-from-array.js | 17 +++++++++++------ src/lib/moment/start-end-of.js | 6 ++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/lib/create/date-from-array.js b/src/lib/create/date-from-array.js index 1d577207fe..7fabbc05f1 100644 --- a/src/lib/create/date-from-array.js +++ b/src/lib/create/date-from-array.js @@ -1,12 +1,16 @@ export function createDate (y, m, d, h, M, s, ms) { - // the date constructor remaps years 0-99 to 1900-1999 - var remapYears = (y < 100 && y >= 0); // can't just apply() to create a date: // https://stackoverflow.com/q/181348 - var date = new Date(remapYears ? y + 400 : y, m, d, h, M, s, ms); - - if (remapYears && isFinite(date.getFullYear())) { - date.setFullYear(y); + var date; + // the date constructor remaps years 0-99 to 1900-1999 + if (y < 100 && y >= 0) { + // preserve leap years using a full 400 year cycle, then reset + date = new Date(y + 400, m, d, h, M, s, ms); + if (isFinite(date.getFullYear())) { + date.setFullYear(y); + } + } else { + date = new Date(y, m, d, h, M, s, ms); } return date; @@ -17,6 +21,7 @@ export function createUTCDate (y) { // the Date.UTC function remaps years 0-99 to 1900-1999 if (y < 100 && y >= 0) { var args = Array.prototype.slice.call(arguments); + // preserve leap years using a full 400 year cycle, then reset args[0] = y + 400; date = new Date(Date.UTC.apply(null, args)); if (isFinite(date.getUTCFullYear())) { diff --git a/src/lib/moment/start-end-of.js b/src/lib/moment/start-end-of.js index 75942674e4..42c19cbc3b 100644 --- a/src/lib/moment/start-end-of.js +++ b/src/lib/moment/start-end-of.js @@ -13,7 +13,8 @@ function mod(dividend, divisor) { function localStartOfDate(y, m, d) { // the date constructor remaps years 0-99 to 1900-1999 - if (0 <= y && y <= 99) { + if (y < 100 && y >= 0) { + // preserve leap years using a full 400 year cycle, then reset return new Date(y + 400, m, d) - MS_PER_400_YEARS; } else { return new Date(y, m, d).valueOf(); @@ -22,7 +23,8 @@ function localStartOfDate(y, m, d) { function utcStartOfDate(y, m, d) { // Date.UTC remaps years 0-99 to 1900-1999 - if (0 <= y && y <= 99) { + if (y < 100 && y >= 0) { + // preserve leap years using a full 400 year cycle, then reset return Date.UTC(y + 400, m, d) - MS_PER_400_YEARS; } else { return Date.UTC(y, m, d);