Skip to content

accept ISO-8601 dates, and rework dateTime2ms #1158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 43 additions & 84 deletions src/lib/dates.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
'use strict';

var d3 = require('d3');
var isNumeric = require('fast-isnumeric');

var logError = require('./loggers').error;

Expand All @@ -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';
Expand All @@ -32,20 +36,33 @@ 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-dd<sep>HH:MM:SS.sss<tzInfo>?
*
* <sep>: space (our normal standard) or T or t (ISO-8601)
* <tzInfo>: 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
* near the present time.
* 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')):
Expand Down Expand Up @@ -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');
Expand Down
18 changes: 16 additions & 2 deletions test/jasmine/tests/lib_date_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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'));
});
});

Expand Down Expand Up @@ -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);
});
Expand Down