diff --git a/src/lib/dates.js b/src/lib/dates.js index 72dfecd5f72..43de12f5719 100644 --- a/src/lib/dates.js +++ b/src/lib/dates.js @@ -10,7 +10,6 @@ 'use strict'; var d3 = require('d3'); -var isNumeric = require('fast-isnumeric'); var logError = require('./loggers').error; @@ -21,6 +20,11 @@ var ONEHOUR = constants.ONEHOUR; var ONEMIN = constants.ONEMIN; var ONESEC = constants.ONESEC; +var DATETIME_REGEXP = /^\s*(-?\d\d\d\d|\d\d)(-(0?[1-9]|1[012])(-([0-3]?\d)([ Tt]([01]?\d|2[0-3])(:([0-5]\d)(:([0-5]\d(\.\d+)?))?(Z|z|[+\-]\d\d:?\d\d)?)?)?)?)?\s*$/m; + +// for 2-digit years, the first year we map them onto +var YFIRST = new Date().getFullYear() - 70; + // is an object a javascript date? exports.isJSDate = function(v) { return typeof v === 'object' && v !== null && typeof v.getTime === 'function'; @@ -32,12 +36,25 @@ exports.isJSDate = function(v) { var MIN_MS, MAX_MS; /** - * dateTime2ms - turn a date object or string s of the form - * YYYY-mm-dd HH:MM:SS.sss into milliseconds (relative to 1970-01-01, - * per javascript standard) - * may truncate after any full field, and sss can be any length - * even >3 digits, though javascript dates truncate to milliseconds - * returns BADNUM if it doesn't find a date + * dateTime2ms - turn a date object or string s into milliseconds + * (relative to 1970-01-01, per javascript standard) + * + * Returns BADNUM if it doesn't find a date + * + * strings should have the form: + * + * -?YYYY-mm-ddHH:MM:SS.sss? + * + * : space (our normal standard) or T or t (ISO-8601) + * : Z, z, or [+\-]HH:?MM and we THROW IT AWAY + * this format comes from https://tools.ietf.org/html/rfc3339#section-5.6 + * but we allow it even with a space as the separator + * + * May truncate after any full field, and sss can be any length + * even >3 digits, though javascript dates truncate to milliseconds, + * we keep as much as javascript numeric precision can hold, but we only + * report back up to 100 microsecond precision, because most dates support + * this precision (close to 1970 support more, very far away support less) * * Expanded to support negative years to -9999 but you must always * give 4 digits, except for 2-digit positive years which we assume are @@ -45,7 +62,7 @@ var MIN_MS, MAX_MS; * Note that we follow ISO 8601:2004: there *is* a year 0, which * is 1BC/BCE, and -1===2BC etc. * - * 2-digit to 4-digit year conversion, where to cut off? + * Where to cut off 2-digit years between 1900s and 2000s? * from http://support.microsoft.com/kb/244664: * 1930-2029 (the most retro of all...) * but in my mac chrome from eg. d=new Date(Date.parse('8/19/50')): @@ -77,89 +94,31 @@ exports.dateTime2ms = function(s) { // otherwise only accept strings and numbers if(typeof s !== 'string' && typeof s !== 'number') return BADNUM; - var y, m, d, h; - // split date and time parts - // TODO: we strip leading/trailing whitespace but not other - // characters like we do for numbers - do we want to? - var datetime = String(s).trim().split(' '); - if(datetime.length > 2) return BADNUM; - - var p = datetime[0].split('-'); // date part - - var CE = true; // common era, ie positive year - if(p[0] === '') { - // first part is blank: year starts with a minus sign - CE = false; - p.splice(0, 1); - } - - var plen = p.length; - if(plen > 3 || (plen !== 3 && datetime[1]) || !plen) return BADNUM; - - // year - if(p[0].length === 4) y = Number(p[0]); - else if(p[0].length === 2) { - if(!CE) return BADNUM; - var yNow = new Date().getFullYear(); - y = ((Number(p[0]) - yNow + 70) % 100 + 200) % 100 + yNow - 70; + var match = String(s).match(DATETIME_REGEXP); + if(!match) return BADNUM; + var y = match[1], + m = Number(match[3] || 1), + d = Number(match[5] || 1), + H = Number(match[7] || 0), + M = Number(match[9] || 0), + S = Number(match[11] || 0); + if(y.length === 2) { + y = (Number(y) + 2000 - YFIRST) % 100 + YFIRST; } - else return BADNUM; - if(!isNumeric(y)) return BADNUM; + else y = Number(y); // javascript takes new Date(0..99,m,d) to mean 1900-1999, so // to support years 0-99 we need to use setFullYear explicitly - var baseDate = new Date(0, 0, 1); - baseDate.setFullYear(CE ? y : -y); - if(p.length > 1) { + var date = new Date(2000, m - 1, d, H, M); + date.setFullYear(y); - // month - may be 1 or 2 digits - m = Number(p[1]) - 1; // new Date() uses zero-based months - if(p[1].length > 2 || !(m >= 0 && m <= 11)) return BADNUM; - baseDate.setMonth(m); + if(date.getDate() !== d) return BADNUM; - if(p.length > 2) { + // does that hour exist in this day? (Daylight time!) + // (TODO: remove this check when we move to UTC) + if(date.getHours() !== H) return BADNUM; - // day - may be 1 or 2 digits - d = Number(p[2]); - if(p[2].length > 2 || !(d >= 1 && d <= 31)) return BADNUM; - baseDate.setDate(d); - - // does that date exist in this month? - if(baseDate.getDate() !== d) return BADNUM; - - if(datetime[1]) { - - p = datetime[1].split(':'); - if(p.length > 3) return BADNUM; - - // hour - may be 1 or 2 digits - h = Number(p[0]); - if(p[0].length > 2 || !p[0].length || !(h >= 0 && h <= 23)) return BADNUM; - baseDate.setHours(h); - - // does that hour exist in this day? (Daylight time!) - // (TODO: remove this check when we move to UTC) - if(baseDate.getHours() !== h) return BADNUM; - - if(p.length > 1) { - d = baseDate.getTime(); - - // minute - must be 2 digits - m = Number(p[1]); - if(p[1].length !== 2 || !(m >= 0 && m <= 59)) return BADNUM; - d += ONEMIN * m; - if(p.length === 2) return d; - - // second (and milliseconds) - must have 2-digit seconds - if(p[2].split('.')[0].length !== 2) return BADNUM; - s = Number(p[2]); - if(!(s >= 0 && s < 60)) return BADNUM; - return d + s * ONESEC; - } - } - } - } - return baseDate.getTime(); + return date.getTime() + S * ONESEC; }; MIN_MS = exports.MIN_MS = exports.dateTime2ms('-9999'); diff --git a/test/jasmine/tests/lib_date_test.js b/test/jasmine/tests/lib_date_test.js index f4771c539a4..d0d5c46bb54 100644 --- a/test/jasmine/tests/lib_date_test.js +++ b/test/jasmine/tests/lib_date_test.js @@ -29,6 +29,8 @@ describe('dates', function() { ['0122-04-08 08:22', new Date(122, 3, 8, 8, 22)], ['-0098-11-19 23:59:59', new Date(-98, 10, 19, 23, 59, 59)], ['-9730-12-01 12:34:56.789', new Date(-9730, 11, 1, 12, 34, 56, 789)], + // random whitespace before and after gets stripped + ['\r\n\t -9730-12-01 12:34:56.789\r\n\t ', new Date(-9730, 11, 1, 12, 34, 56, 789)], // first century, also allow month, day, and hour to be 1-digit, and not all // three digits of milliseconds ['0013-1-1 1:00:00.6', d1c], @@ -40,9 +42,19 @@ describe('dates', function() { // 2-digit years get mapped to now-70 -> now+29 [thisYear_2 + '-05', new Date(thisYear, 4, 1)], [nowMinus70_2 + '-10-18', new Date(nowMinus70, 9, 18)], - [nowPlus29_2 + '-02-12 14:29:32', new Date(nowPlus29, 1, 12, 14, 29, 32)] + [nowPlus29_2 + '-02-12 14:29:32', new Date(nowPlus29, 1, 12, 14, 29, 32)], + + // including timezone info (that we discard) + ['2014-03-04 08:15Z', new Date(2014, 2, 4, 8, 15)], + ['2014-03-04 08:15:00.00z', new Date(2014, 2, 4, 8, 15)], + ['2014-03-04 08:15:34+1200', new Date(2014, 2, 4, 8, 15, 34)], + ['2014-03-04 08:15:34.567-05:45', new Date(2014, 2, 4, 8, 15, 34, 567)], ].forEach(function(v) { expect(Lib.dateTime2ms(v[0])).toBe(+v[1], v[0]); + + // ISO-8601: all the same stuff with t or T as the separator + expect(Lib.dateTime2ms(v[0].trim().replace(' ', 't'))).toBe(+v[1], v[0].trim().replace(' ', 't')); + expect(Lib.dateTime2ms('\r\n\t ' + v[0].trim().replace(' ', 'T') + '\r\n\t ')).toBe(+v[1], v[0].trim().replace(' ', 'T')); }); }); @@ -105,7 +117,9 @@ describe('dates', function() { '2015-01-00', '2015-01-32', '2015-02-29', '2015-04-31', '2015-01-001', // bad day (incl non-leap year) '2015-01-01 24:00', '2015-01-01 -1:00', '2015-01-01 001:00', // bad hour '2015-01-01 12:60', '2015-01-01 12:-1', '2015-01-01 12:001', '2015-01-01 12:1', // bad minute - '2015-01-01 12:00:60', '2015-01-01 12:00:-1', '2015-01-01 12:00:001', '2015-01-01 12:00:1' // bad second + '2015-01-01 12:00:60', '2015-01-01 12:00:-1', '2015-01-01 12:00:001', '2015-01-01 12:00:1', // bad second + '2015-01-01T', '2015-01-01TT12:34', // bad ISO separators + '2015-01-01Z', '2015-01-01T12Z', '2015-01-01T12:34Z05:00', '2015-01-01 12:34+500', '2015-01-01 12:34-5:00' // bad TZ info ].forEach(function(v) { expect(Lib.dateTime2ms(v)).toBeUndefined(v); });