From c76c9bea2908da8dd3bde55ddf96b7f48104ccb3 Mon Sep 17 00:00:00 2001 From: Joel Date: Sat, 7 Apr 2018 18:51:22 -0700 Subject: [PATCH 1/5] Issue #292: Stop depending on Date object for date value comparisons The Date object implementation provided by otto has become even less reliable in the version included in Sync Gateway 2.0.0 Beta 2. This change insulates a user of synctos from its buggy behaviour while continuing to provide the same features. --- CHANGELOG.md | 1 + templates/sync-function/time-module.js | 181 ++++++++++++++------- test/datetime.spec.js | 128 +++++++++++++++ test/resources/datetime-doc-definitions.js | 31 ++++ 4 files changed, 279 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a0efbe5..f8ea2d65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). All notable c - [#281](https://github.com/Kashoo/synctos/issues/281): Mechanism to reset test environment between test cases - [#278](https://github.com/Kashoo/synctos/issues/278): Extended year format in date strings - [#282](https://github.com/Kashoo/synctos/issues/282): Support hour 24 in date and time validation types +- [#292](https://github.com/Kashoo/synctos/issues/292): Permanent workaround for bugs in Date object implementation ### Fixed - [#276](https://github.com/Kashoo/synctos/issues/276): Date range validation is incorrect for dates between years 0 and 99 diff --git a/templates/sync-function/time-module.js b/templates/sync-function/time-module.js index d016011c..4b906bdf 100644 --- a/templates/sync-function/time-module.js +++ b/templates/sync-function/time-module.js @@ -67,12 +67,8 @@ function timeModule(utils) { timezone.minute <= 59; } - function isValidDayOfMonth(date) { - if (date.day < 1) { - return false; - } - - switch (date.month) { + function numDaysInMonth(year, month) { + switch (month) { case 1: // Jan case 3: // Mar case 5: // May @@ -80,16 +76,28 @@ function timeModule(utils) { case 8: // Aug case 10: // Oct case 12: // Dec - return date.day <= 31; + return 31; case 4: // Apr case 6: // Jun case 9: // Sep case 11: // Nov - return date.day <= 30; + return 30; case 2: // Feb - return (date.day === 29) ? isLeapYear(date.year) : date.day <= 28; + return isLeapYear(year) ? 29 : 28; default: - return false; + return NaN; + } + } + + function isValidDayOfMonth(date) { + if (date.day < 1) { + return false; + } else if (date.month === 2) { + // This is a small optimization; checking whether February has a leap day is a moderately expensive operation, so + // only perform that check if the day of the month is 29. + return (date.day === 29) ? isLeapYear(date.year) : date.day <= 28; + } else { + return date.day <= numDaysInMonth(date.year, date.month); } } @@ -141,6 +149,19 @@ function timeModule(utils) { } } + // Converts the given time to the number of milliseconds since hour 0 + function normalizeIso8601Time(time, timezoneOffsetMinutes) { + var msPerSecond = 1000; + var msPerMinute = 60000; + var msPerHour = 3600000; + var effectiveTimezoneOffset = timezoneOffsetMinutes || 0; + + var rawTimeMs = + (time.hour * msPerHour) + (time.minute * msPerMinute) + (time.second * msPerSecond) + time.millisecond; + + return rawTimeMs - (effectiveTimezoneOffset * msPerMinute); + } + // Compares the given time strings. Returns a negative number if a is less than b, a positive number if a is greater // than b, or zero if a and b are equal. function compareTimes(a, b) { @@ -148,24 +169,7 @@ function timeModule(utils) { return NaN; } - var aTime = parseIso8601Time(a); - var bTime = parseIso8601Time(b); - if (!isValidTimeStructure(aTime) || !isValidTimeStructure(bTime)) { - return NaN; - } - - var aTimePieces = [ aTime.hour, aTime.minute, aTime.second, aTime.millisecond ]; - var bTimePieces = [ bTime.hour, bTime.minute, bTime.second, bTime.millisecond ]; - for (var timePieceIndex = 0; timePieceIndex < aTimePieces.length; timePieceIndex++) { - if (aTimePieces[timePieceIndex] < bTimePieces[timePieceIndex]) { - return -1; - } else if (aTimePieces[timePieceIndex] > bTimePieces[timePieceIndex]) { - return 1; - } - } - - // If we got here, the two parameters represent the same time of day - return 0; + return normalizeIso8601Time(parseIso8601Time(a)) - normalizeIso8601Time(parseIso8601Time(b)); } function parseIso8601Date(value) { @@ -220,41 +224,85 @@ function timeModule(utils) { } } - // Converts the given date representation to a timestamp that represents the number of ms since the Unix epoch - function convertToTimestamp(value) { - if (value instanceof Date) { - return value.getTime(); - } else if (typeof value === 'number') { - return Math.floor(value); - } else if (typeof value === 'string') { - var dateAndTimePieces = splitDateAndTime(value); + function extractDatePiecesFromDateObject(value) { + var timeStructure = { + hour: value.getUTCHours(), + minute: value.getUTCMinutes(), + second: value.getUTCSeconds(), + millisecond: value.getUTCMilliseconds() + }; + var timeOfDayMs = normalizeIso8601Time(timeStructure); - var date = extractDateStructureFromDateAndTime(dateAndTimePieces); - if (!isValidDateStructure(date)) { - return NaN; - } + return [ value.getUTCFullYear(), value.getUTCMonth() + 1, value.getUTCDate(), timeOfDayMs ]; + } - var timeAndTimezone = extractTimeStructuresFromDateAndTime(dateAndTimePieces); - var time = timeAndTimezone.time; - var timezone = timeAndTimezone.timezone; - if (!isValidTimeStructure(time)) { - return NaN; - } else if (timezone !== null && !isValidTimeZoneStructure(timezone)) { - return NaN; + function extractDatePiecesFromIso8601String(value) { + var dateAndTimePieces = splitDateAndTime(value); + + var date = extractDateStructureFromDateAndTime(dateAndTimePieces); + if (!isValidDateStructure(date)) { + return null; + } + + var timeAndTimezone = extractTimeStructuresFromDateAndTime(dateAndTimePieces); + var time = timeAndTimezone.time; + var timezone = timeAndTimezone.timezone; + if (!isValidTimeStructure(time)) { + return null; + } else if (timezone !== null && !isValidTimeZoneStructure(timezone)) { + return null; + } + + var calculatedYear = date.year; + var calculatedMonth = date.month; + var calculatedDay = date.day; + var calculatedTimeOfDayMs = normalizeIso8601Time(time, normalizeIso8601TimeZone(timezone)); + + // Carry the overflow/underflow of the time of day to the day of the month + var msPerDay = 86400000; + if (calculatedTimeOfDayMs >= msPerDay) { + calculatedDay++; + calculatedTimeOfDayMs -= msPerDay; + } else if (calculatedTimeOfDayMs < 0) { + calculatedDay--; + calculatedTimeOfDayMs += msPerDay; + } + + // Carry the overflow/underflow of the day of the month to the month + var maxDaysInMonth = numDaysInMonth(calculatedYear, calculatedMonth); + if (calculatedDay > maxDaysInMonth) { + calculatedMonth++; + calculatedDay = 1; + } else if (calculatedDay < 1) { + calculatedMonth--; + + // There was an underflow, so set the day to the last day of the new month + if (calculatedMonth > 12) { + calculatedDay = numDaysInMonth(calculatedYear + 1, 1); + } else if (calculatedMonth < 1) { + calculatedDay = numDaysInMonth(calculatedYear - 1, 12); + } else { + calculatedDay = numDaysInMonth(calculatedYear, calculatedMonth); } + } - var timezoneOffsetMinutes = normalizeIso8601TimeZone(timezone); + // Carry the overflow/underflow of the month to the year + if (calculatedMonth > 12) { + calculatedYear++; + calculatedMonth = 1; + } else if (calculatedMonth < 1) { + calculatedYear--; + calculatedMonth = 12; + } - var dateAndTime = new Date(0); - dateAndTime.setUTCFullYear(date.year); - dateAndTime.setUTCMonth(date.month - 1); - dateAndTime.setUTCDate(date.day); - dateAndTime.setUTCHours(time.hour); - dateAndTime.setUTCMinutes(time.minute - timezoneOffsetMinutes); - dateAndTime.setUTCSeconds(time.second); - dateAndTime.setUTCMilliseconds(time.millisecond); + return [ calculatedYear, calculatedMonth, calculatedDay, calculatedTimeOfDayMs ]; + } - return dateAndTime.getTime(); + function extractDatePieces(value) { + if (value instanceof Date) { + return extractDatePiecesFromDateObject(value); + } else if (typeof value === 'string') { + return extractDatePiecesFromIso8601String(value); } else { return NaN; } @@ -263,14 +311,23 @@ function timeModule(utils) { // Compares the given date representations. Returns a negative number if a is less than b, a positive number if a is // greater than b, or zero if a and b are equal. function compareDates(a, b) { - var aTimestamp = convertToTimestamp(a); - var bTimestamp = convertToTimestamp(b); + var aPieces = extractDatePieces(a); + var bPieces = extractDatePieces(b); - if (isNaN(aTimestamp) || isNaN(bTimestamp)) { + if (aPieces === null || bPieces === null) { return NaN; - } else { - return aTimestamp - bTimestamp; } + + for (var pieceIndex = 0; pieceIndex < aPieces.length; pieceIndex++) { + if (aPieces[pieceIndex] < bPieces[pieceIndex]) { + return -1; + } else if (aPieces[pieceIndex] > bPieces[pieceIndex]) { + return 1; + } + } + + // If we got here, the two parameters represent the same date/point in time + return 0; } function parseIso8601TimeZone(value) { diff --git a/test/datetime.spec.js b/test/datetime.spec.js index 52c88c21..9c43733b 100644 --- a/test/datetime.spec.js +++ b/test/datetime.spec.js @@ -719,6 +719,134 @@ describe('Date/time validation type', () => { }); }); + describe('dynamic range validation', () => { + it('allows a datetime that matches expected datetimes that fall on different days because they have different time zones', () => { + const doc = { + _id: 'dynamicDatetimeDocType', + dynamicRangeValidationProp: '0900-06-09T19:30Z', + expectedMinimumValue: '+000900-06-10T01:30:00+06:00', + expectedMaximumValue: '0900-06-08T22:00:00.000-21:30', + expectedEqualityValue: '0900-06-10T19:29:00.0+2359' + }; + + testFixture.verifyDocumentCreated(doc); + }); + + it('rejects a datetime that exceeds a maximum datetime that falls on a different day because it has a different time zone', () => { + const doc = { + _id: 'dynamicDatetimeDocType', + dynamicRangeValidationProp: '+000900-06-08T22:00:00.001-21:30', + expectedMinimumValue: '0900-06-10T01:30:00+06:00', + expectedMaximumValue: '0900-06-09T19:30Z', + expectedEqualityValue: '0900-06-09T15:00:00.001-04:30' + }; + + testFixture.verifyDocumentNotCreated( + doc, + 'dynamicDatetimeDocType', + [ errorFormatter.maximumValueViolation('dynamicRangeValidationProp', '0900-06-09T19:30Z') ]); + }); + + it('rejects a datetime that precedes a minimum datetime that falls on a different day because it has a different time zone', () => { + const doc = { + _id: 'dynamicDatetimeDocType', + dynamicRangeValidationProp: '0900-06-10T01:29:59+06:00', + expectedMinimumValue: '+000900-06-09T19:30Z', + expectedMaximumValue: '0900-06-08T22:00:00.000-21:30', + expectedEqualityValue: '0900-06-08T21:59:59.000-21:30' + }; + + testFixture.verifyDocumentNotCreated( + doc, + 'dynamicDatetimeDocType', + [ errorFormatter.minimumValueViolation('dynamicRangeValidationProp', '+000900-06-09T19:30Z') ]); + }); + + it('allows a datetime that matches expected datetimes that fall on different months because they have different time zones', () => { + const doc = { + _id: 'dynamicDatetimeDocType', + dynamicRangeValidationProp: '-001337-08-31T22:15:00.000-0800', + expectedMinimumValue: '-001337-09-01T06:15:00Z', + expectedMaximumValue: '-001337-09-02T00:45+1830', + expectedEqualityValue: '-001337-08-31T12:45:00.000-17:30' + }; + + testFixture.verifyDocumentCreated(doc); + }); + + it('rejects a datetime that exceeds a maximum datetime that falls on a different month because it has a different time zone', () => { + const doc = { + _id: 'dynamicDatetimeDocType', + dynamicRangeValidationProp: '1600-03-01T00:46+1830', + expectedMinimumValue: '1600-02-29T06:15:00Z', + expectedMaximumValue: '1600-02-28T22:15:00.000-0800', + expectedEqualityValue: '1600-02-28T22:16:00.000-0800' + }; + + testFixture.verifyDocumentNotCreated( + doc, + 'dynamicDatetimeDocType', + [ errorFormatter.maximumValueViolation('dynamicRangeValidationProp', '1600-02-28T22:15:00.000-0800') ]); + }); + + it('rejects a datetime that precedes a minimum datetime that falls on a different month because it has a different time zone', () => { + const doc = { + _id: 'dynamicDatetimeDocType', + dynamicRangeValidationProp: '-001337-09-01T06:14:59.999+08:00', + expectedMinimumValue: '-001337-08-30T23:45-22:30', + expectedMaximumValue: '-001337-08-31T22:15:00.000Z', + expectedEqualityValue: '-001337-08-30T23:44:59.999-22:30' + }; + + testFixture.verifyDocumentNotCreated( + doc, + 'dynamicDatetimeDocType', + [ errorFormatter.minimumValueViolation('dynamicRangeValidationProp', '-001337-08-30T23:45-22:30') ]); + }); + + it('allows a datetime that matches expected datetimes that fall on different years because they have different time zones', () => { + const doc = { + _id: 'dynamicDatetimeDocType', + dynamicRangeValidationProp: '3248-01-01T09:45:32.000+16:45', + expectedMinimumValue: '3247-12-30T23:30:32-17:30', + expectedMaximumValue: '3247-12-31T17:00:32.0Z', + expectedEqualityValue: '3247-12-30T23:30:32-17:30' + }; + + testFixture.verifyDocumentCreated(doc); + }); + + it('rejects a datetime that exceeds a maximum datetime that falls on a different year because it has a different time zone', () => { + const doc = { + _id: 'dynamicDatetimeDocType', + dynamicRangeValidationProp: '1900-01-01T12:01Z', + expectedMinimumValue: '1900-01-02T04:00:00.000+16:00', + expectedMaximumValue: '1899-12-31T15:15:00-20:45', + expectedEqualityValue: '1899-12-31T15:16:00-20:45' + }; + + testFixture.verifyDocumentNotCreated( + doc, + 'dynamicDatetimeDocType', + [ errorFormatter.maximumValueViolation('dynamicRangeValidationProp', '1899-12-31T15:15:00-20:45') ]); + }); + + it('rejects a datetime that precedes a minimum datetime that falls on a different year because it has a different time zone', () => { + const doc = { + _id: 'dynamicDatetimeDocType', + dynamicRangeValidationProp: '9999-12-31T22:59:59.9-1445', + expectedMinimumValue: '+010000-01-01T13:45Z', + expectedMaximumValue: '9999-12-31T24:00:00.0-13:45', + expectedEqualityValue: '+010000-01-01T13:44:59.900Z' + }; + + testFixture.verifyDocumentNotCreated( + doc, + 'dynamicDatetimeDocType', + [ errorFormatter.minimumValueViolation('dynamicRangeValidationProp', '+010000-01-01T13:45Z') ]); + }); + }); + describe('intelligent equality constraint', () => { it('allows a datetime that exactly matches the expected datetime', () => { const doc = { diff --git a/test/resources/datetime-doc-definitions.js b/test/resources/datetime-doc-definitions.js index a11eda97..9709e17b 100644 --- a/test/resources/datetime-doc-definitions.js +++ b/test/resources/datetime-doc-definitions.js @@ -44,5 +44,36 @@ mustEqual: '2018-01-01T11:00:00.000+09:30' } } + }, + dynamicDatetimeDocType: { + typeFilter: function(doc) { + return doc._id === 'dynamicDatetimeDocType'; + }, + channels: { write: 'write' }, + propertyValidators: function(doc, oldDoc) { + return { + dynamicRangeValidationProp: { + type: 'datetime', + minimumValue: function(doc, oldDoc, value, oldValue) { + return doc.expectedMinimumValue; + }, + maximumValue: function(doc, oldDoc, value, oldValue) { + return doc.expectedMaximumValue; + }, + mustEqual: function(doc, oldDoc, value, oldValue) { + return doc.expectedEqualityValue; + } + }, + expectedMinimumValue: { + type: 'datetime' + }, + expectedMaximumValue: { + type: 'datetime' + }, + expectedEqualityValue: { + type: 'datetime' + } + }; + } } } From 5c9418a7abfd01629228331152b738910764d76e Mon Sep 17 00:00:00 2001 From: Joel Date: Sun, 8 Apr 2018 11:54:45 -0700 Subject: [PATCH 2/5] Issue #292: Update test cases for custom date comparisons --- templates/sync-function/time-module.js | 2 +- test/datetime.spec.js | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/templates/sync-function/time-module.js b/templates/sync-function/time-module.js index 4b906bdf..4dcb440c 100644 --- a/templates/sync-function/time-module.js +++ b/templates/sync-function/time-module.js @@ -304,7 +304,7 @@ function timeModule(utils) { } else if (typeof value === 'string') { return extractDatePiecesFromIso8601String(value); } else { - return NaN; + return null; } } diff --git a/test/datetime.spec.js b/test/datetime.spec.js index 9c43733b..7b41f24e 100644 --- a/test/datetime.spec.js +++ b/test/datetime.spec.js @@ -726,7 +726,7 @@ describe('Date/time validation type', () => { dynamicRangeValidationProp: '0900-06-09T19:30Z', expectedMinimumValue: '+000900-06-10T01:30:00+06:00', expectedMaximumValue: '0900-06-08T22:00:00.000-21:30', - expectedEqualityValue: '0900-06-10T19:29:00.0+2359' + expectedEqualityValue: '0900-06-10T19:29:00.0+23:59' }; testFixture.verifyDocumentCreated(doc); @@ -765,10 +765,10 @@ describe('Date/time validation type', () => { it('allows a datetime that matches expected datetimes that fall on different months because they have different time zones', () => { const doc = { _id: 'dynamicDatetimeDocType', - dynamicRangeValidationProp: '-001337-08-31T22:15:00.000-0800', - expectedMinimumValue: '-001337-09-01T06:15:00Z', - expectedMaximumValue: '-001337-09-02T00:45+1830', - expectedEqualityValue: '-001337-08-31T12:45:00.000-17:30' + dynamicRangeValidationProp: '-001337-08-31T24:00:00Z', + expectedMinimumValue: '-001337-09-01T18:30+18:30', + expectedMaximumValue: '-001337-08-31T16:00:00.000-08:00', + expectedEqualityValue: '-001337-09-01' }; testFixture.verifyDocumentCreated(doc); @@ -777,16 +777,16 @@ describe('Date/time validation type', () => { it('rejects a datetime that exceeds a maximum datetime that falls on a different month because it has a different time zone', () => { const doc = { _id: 'dynamicDatetimeDocType', - dynamicRangeValidationProp: '1600-03-01T00:46+1830', + dynamicRangeValidationProp: '1600-03-01T00:46+18:30', expectedMinimumValue: '1600-02-29T06:15:00Z', - expectedMaximumValue: '1600-02-28T22:15:00.000-0800', - expectedEqualityValue: '1600-02-28T22:16:00.000-0800' + expectedMaximumValue: '1600-02-28T22:15:00.000-08:00', + expectedEqualityValue: '1600-02-28T22:16:00.000-08:00' }; testFixture.verifyDocumentNotCreated( doc, 'dynamicDatetimeDocType', - [ errorFormatter.maximumValueViolation('dynamicRangeValidationProp', '1600-02-28T22:15:00.000-0800') ]); + [ errorFormatter.maximumValueViolation('dynamicRangeValidationProp', '1600-02-28T22:15:00.000-08:00') ]); }); it('rejects a datetime that precedes a minimum datetime that falls on a different month because it has a different time zone', () => { @@ -834,7 +834,7 @@ describe('Date/time validation type', () => { it('rejects a datetime that precedes a minimum datetime that falls on a different year because it has a different time zone', () => { const doc = { _id: 'dynamicDatetimeDocType', - dynamicRangeValidationProp: '9999-12-31T22:59:59.9-1445', + dynamicRangeValidationProp: '9999-12-31T22:59:59.9-14:45', expectedMinimumValue: '+010000-01-01T13:45Z', expectedMaximumValue: '9999-12-31T24:00:00.0-13:45', expectedEqualityValue: '+010000-01-01T13:44:59.900Z' From f84742535519a7113e05aefc08e32dac7748163a Mon Sep 17 00:00:00 2001 From: Joel Date: Sun, 8 Apr 2018 12:12:38 -0700 Subject: [PATCH 3/5] Issue #292: Test case for month overflow of a non-leap year February --- test/datetime.spec.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/datetime.spec.js b/test/datetime.spec.js index 7b41f24e..296fdb6e 100644 --- a/test/datetime.spec.js +++ b/test/datetime.spec.js @@ -792,16 +792,16 @@ describe('Date/time validation type', () => { it('rejects a datetime that precedes a minimum datetime that falls on a different month because it has a different time zone', () => { const doc = { _id: 'dynamicDatetimeDocType', - dynamicRangeValidationProp: '-001337-09-01T06:14:59.999+08:00', - expectedMinimumValue: '-001337-08-30T23:45-22:30', - expectedMaximumValue: '-001337-08-31T22:15:00.000Z', - expectedEqualityValue: '-001337-08-30T23:44:59.999-22:30' + dynamicRangeValidationProp: '-001337-03-01T06:14:59.999+08:00', + expectedMinimumValue: '-001337-02-27T23:45-22:30', + expectedMaximumValue: '-001337-02-28T22:15:00.000Z', + expectedEqualityValue: '-001337-02-27T23:44:59.999-22:30' }; testFixture.verifyDocumentNotCreated( doc, 'dynamicDatetimeDocType', - [ errorFormatter.minimumValueViolation('dynamicRangeValidationProp', '-001337-08-30T23:45-22:30') ]); + [ errorFormatter.minimumValueViolation('dynamicRangeValidationProp', '-001337-02-27T23:45-22:30') ]); }); it('allows a datetime that matches expected datetimes that fall on different years because they have different time zones', () => { From 5345582c776f05a8829c7093f7df50706358dc60 Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 9 Apr 2018 11:50:34 -0700 Subject: [PATCH 4/5] Issue #292: Minor optimization for month overflow --- templates/sync-function/time-module.js | 46 +++++++++++++------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/templates/sync-function/time-module.js b/templates/sync-function/time-module.js index 2262ee49..392fc756 100644 --- a/templates/sync-function/time-module.js +++ b/templates/sync-function/time-module.js @@ -50,7 +50,7 @@ function timeModule(utils) { function isValidDateStructure(date) { return isSupportedYear(date.year) && date.month >= 1 && date.month <= 12 && - isValidDayOfMonth(date); + isValidDayOfMonth(date.year, date.month, date.day); } function isValidTimeStructure(time) { @@ -89,15 +89,15 @@ function timeModule(utils) { } } - function isValidDayOfMonth(date) { - if (date.day < 1) { + function isValidDayOfMonth(year, month, day) { + if (day < 1) { return false; - } else if (date.month === 2) { + } else if (month === 2) { // This is a small optimization; checking whether February has a leap day is a moderately expensive operation, so // only perform that check if the day of the month is 29. - return (date.day === 29) ? isLeapYear(date.year) : date.day <= 28; + return (day === 29) ? isLeapYear(year) : day <= 28; } else { - return date.day <= numDaysInMonth(date.year, date.month); + return day <= numDaysInMonth(year, month); } } @@ -258,25 +258,21 @@ function timeModule(utils) { var calculatedDay = date.day; var calculatedTimeOfDayMs = normalizeIso8601Time(time, normalizeIso8601TimeZone(timezone)); - // Carry the overflow/underflow of the time of day to the day of the month + // Carry the overflow/underflow of the time of day to the day of the month as necessary var msPerDay = 86400000; - if (calculatedTimeOfDayMs >= msPerDay) { - calculatedDay++; - calculatedTimeOfDayMs -= msPerDay; - } else if (calculatedTimeOfDayMs < 0) { + if (calculatedTimeOfDayMs < 0) { calculatedDay--; calculatedTimeOfDayMs += msPerDay; + } else if (calculatedTimeOfDayMs >= msPerDay) { + calculatedDay++; + calculatedTimeOfDayMs -= msPerDay; } - // Carry the overflow/underflow of the day of the month to the month - var maxDaysInMonth = numDaysInMonth(calculatedYear, calculatedMonth); - if (calculatedDay > maxDaysInMonth) { - calculatedMonth++; - calculatedDay = 1; - } else if (calculatedDay < 1) { + // Carry the overflow/underflow of the day of the month to the month as necessary + if (calculatedDay < 1) { calculatedMonth--; - // There was an underflow, so set the day to the last day of the new month + // There was an underflow, so roll back to the last day of the previous month if (calculatedMonth > 12) { calculatedDay = numDaysInMonth(calculatedYear + 1, 1); } else if (calculatedMonth < 1) { @@ -284,15 +280,19 @@ function timeModule(utils) { } else { calculatedDay = numDaysInMonth(calculatedYear, calculatedMonth); } + } else if (!isValidDayOfMonth(calculatedYear, calculatedMonth, calculatedDay)) { + // There was an overflow, so roll over to the first day of the next month + calculatedMonth++; + calculatedDay = 1; } - // Carry the overflow/underflow of the month to the year - if (calculatedMonth > 12) { - calculatedYear++; - calculatedMonth = 1; - } else if (calculatedMonth < 1) { + // Carry the overflow/underflow of the month to the year as necessary + if (calculatedMonth < 1) { calculatedYear--; calculatedMonth = 12; + } else if (calculatedMonth > 12) { + calculatedYear++; + calculatedMonth = 1; } return [ calculatedYear, calculatedMonth, calculatedDay, calculatedTimeOfDayMs ]; From ba3100be86e2a55ec902b39af53ee821da867852 Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 9 Apr 2018 12:54:00 -0700 Subject: [PATCH 5/5] Issue #292: Add comment explaining reason for custom date handling --- templates/sync-function/time-module.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/sync-function/time-module.js b/templates/sync-function/time-module.js index 392fc756..d6c90a5d 100644 --- a/templates/sync-function/time-module.js +++ b/templates/sync-function/time-module.js @@ -5,6 +5,8 @@ function timeModule(utils) { minute: 0 }; + // NOTE: Many of the following functions make use of custom logic, rather than relying on the built-in Date object, to + // work around a number of deficiencies in the Date implementation from Sync Gateway's JavaScript engine (otto) return { isIso8601DateTimeString: isIso8601DateTimeString, isIso8601DateString: isIso8601DateString,