Skip to content

Commit

Permalink
Merge pull request #297 from Kashoo/issue-292-date-workaround
Browse files Browse the repository at this point in the history
Issue 292: Do not depend on buggy Date object for date value comparisons
  • Loading branch information
dkichler authored Apr 9, 2018
2 parents cc20db0 + ba3100b commit 827c81c
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 64 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
187 changes: 123 additions & 64 deletions templates/sync-function/time-module.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -50,7 +52,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) {
Expand All @@ -67,29 +69,37 @@ 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
case 7: // Jul
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(year, month, day) {
if (day < 1) {
return false;
} 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 (day === 29) ? isLeapYear(year) : day <= 28;
} else {
return day <= numDaysInMonth(year, month);
}
}

Expand Down Expand Up @@ -141,31 +151,27 @@ 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) {
if (typeof a !== 'string' || typeof b !== 'string') {
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) {
Expand Down Expand Up @@ -220,57 +226,110 @@ 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 as necessary
var msPerDay = 86400000;
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 as necessary
if (calculatedDay < 1) {
calculatedMonth--;

// 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) {
calculatedDay = numDaysInMonth(calculatedYear - 1, 12);
} 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;
}

var timezoneOffsetMinutes = normalizeIso8601TimeZone(timezone);
// 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;
}

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;
return null;
}
}

// 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) {
Expand Down
Loading

0 comments on commit 827c81c

Please sign in to comment.