Skip to content

Commit 899866b

Browse files
committed
accept ISO-8601 dates, and rework dateTime2ms
1 parent ded2339 commit 899866b

File tree

2 files changed

+59
-86
lines changed

2 files changed

+59
-86
lines changed

src/lib/dates.js

Lines changed: 43 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
'use strict';
1111

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

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

@@ -21,6 +20,11 @@ var ONEHOUR = constants.ONEHOUR;
2120
var ONEMIN = constants.ONEMIN;
2221
var ONESEC = constants.ONESEC;
2322

23+
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;
24+
25+
// for 2-digit years, the first year we map them onto
26+
var YFIRST = new Date().getFullYear() - 70;
27+
2428
// is an object a javascript date?
2529
exports.isJSDate = function(v) {
2630
return typeof v === 'object' && v !== null && typeof v.getTime === 'function';
@@ -32,20 +36,33 @@ exports.isJSDate = function(v) {
3236
var MIN_MS, MAX_MS;
3337

3438
/**
35-
* dateTime2ms - turn a date object or string s of the form
36-
* YYYY-mm-dd HH:MM:SS.sss into milliseconds (relative to 1970-01-01,
37-
* per javascript standard)
38-
* may truncate after any full field, and sss can be any length
39-
* even >3 digits, though javascript dates truncate to milliseconds
40-
* returns BADNUM if it doesn't find a date
39+
* dateTime2ms - turn a date object or string s into milliseconds
40+
* (relative to 1970-01-01, per javascript standard)
41+
*
42+
* Returns BADNUM if it doesn't find a date
43+
*
44+
* strings should have the form:
45+
*
46+
* -?YYYY-mm-dd<sep>HH:MM:SS.sss<tzInfo>?
47+
*
48+
* <sep>: space (our normal standard) or T or t (ISO-8601)
49+
* <tzInfo>: Z, z, or [+\-]HH:?MM and we THROW IT AWAY
50+
* this format comes from https://tools.ietf.org/html/rfc3339#section-5.6
51+
* but we allow it even with a space as the separator
52+
*
53+
* May truncate after any full field, and sss can be any length
54+
* even >3 digits, though javascript dates truncate to milliseconds,
55+
* we keep as much as javascript numeric precision can hold, but we only
56+
* report back up to 100 microsecond precision, because most dates support
57+
* this precision (close to 1970 support more, very far away support less)
4158
*
4259
* Expanded to support negative years to -9999 but you must always
4360
* give 4 digits, except for 2-digit positive years which we assume are
4461
* near the present time.
4562
* Note that we follow ISO 8601:2004: there *is* a year 0, which
4663
* is 1BC/BCE, and -1===2BC etc.
4764
*
48-
* 2-digit to 4-digit year conversion, where to cut off?
65+
* Where to cut off 2-digit years between 1900s and 2000s?
4966
* from http://support.microsoft.com/kb/244664:
5067
* 1930-2029 (the most retro of all...)
5168
* but in my mac chrome from eg. d=new Date(Date.parse('8/19/50')):
@@ -77,89 +94,31 @@ exports.dateTime2ms = function(s) {
7794
// otherwise only accept strings and numbers
7895
if(typeof s !== 'string' && typeof s !== 'number') return BADNUM;
7996

80-
var y, m, d, h;
81-
// split date and time parts
82-
// TODO: we strip leading/trailing whitespace but not other
83-
// characters like we do for numbers - do we want to?
84-
var datetime = String(s).trim().split(' ');
85-
if(datetime.length > 2) return BADNUM;
86-
87-
var p = datetime[0].split('-'); // date part
88-
89-
var CE = true; // common era, ie positive year
90-
if(p[0] === '') {
91-
// first part is blank: year starts with a minus sign
92-
CE = false;
93-
p.splice(0, 1);
94-
}
95-
96-
var plen = p.length;
97-
if(plen > 3 || (plen !== 3 && datetime[1]) || !plen) return BADNUM;
98-
99-
// year
100-
if(p[0].length === 4) y = Number(p[0]);
101-
else if(p[0].length === 2) {
102-
if(!CE) return BADNUM;
103-
var yNow = new Date().getFullYear();
104-
y = ((Number(p[0]) - yNow + 70) % 100 + 200) % 100 + yNow - 70;
97+
var match = String(s).match(DATETIME_REGEXP);
98+
if(!match) return BADNUM;
99+
var y = match[1],
100+
m = Number(match[3] || 1),
101+
d = Number(match[5] || 1),
102+
H = Number(match[7] || 0),
103+
M = Number(match[9] || 0),
104+
S = Number(match[11] || 0);
105+
if(y.length === 2) {
106+
y = (Number(y) + 2000 - YFIRST) % 100 + YFIRST;
105107
}
106-
else return BADNUM;
107-
if(!isNumeric(y)) return BADNUM;
108+
else y = Number(y);
108109

109110
// javascript takes new Date(0..99,m,d) to mean 1900-1999, so
110111
// to support years 0-99 we need to use setFullYear explicitly
111-
var baseDate = new Date(0, 0, 1);
112-
baseDate.setFullYear(CE ? y : -y);
113-
if(p.length > 1) {
112+
var date = new Date(2000, m - 1, d, H, M);
113+
date.setFullYear(y);
114114

115-
// month - may be 1 or 2 digits
116-
m = Number(p[1]) - 1; // new Date() uses zero-based months
117-
if(p[1].length > 2 || !(m >= 0 && m <= 11)) return BADNUM;
118-
baseDate.setMonth(m);
115+
if(date.getDate() !== d) return BADNUM;
119116

120-
if(p.length > 2) {
117+
// does that hour exist in this day? (Daylight time!)
118+
// (TODO: remove this check when we move to UTC)
119+
if(date.getHours() !== H) return BADNUM;
121120

122-
// day - may be 1 or 2 digits
123-
d = Number(p[2]);
124-
if(p[2].length > 2 || !(d >= 1 && d <= 31)) return BADNUM;
125-
baseDate.setDate(d);
126-
127-
// does that date exist in this month?
128-
if(baseDate.getDate() !== d) return BADNUM;
129-
130-
if(datetime[1]) {
131-
132-
p = datetime[1].split(':');
133-
if(p.length > 3) return BADNUM;
134-
135-
// hour - may be 1 or 2 digits
136-
h = Number(p[0]);
137-
if(p[0].length > 2 || !p[0].length || !(h >= 0 && h <= 23)) return BADNUM;
138-
baseDate.setHours(h);
139-
140-
// does that hour exist in this day? (Daylight time!)
141-
// (TODO: remove this check when we move to UTC)
142-
if(baseDate.getHours() !== h) return BADNUM;
143-
144-
if(p.length > 1) {
145-
d = baseDate.getTime();
146-
147-
// minute - must be 2 digits
148-
m = Number(p[1]);
149-
if(p[1].length !== 2 || !(m >= 0 && m <= 59)) return BADNUM;
150-
d += ONEMIN * m;
151-
if(p.length === 2) return d;
152-
153-
// second (and milliseconds) - must have 2-digit seconds
154-
if(p[2].split('.')[0].length !== 2) return BADNUM;
155-
s = Number(p[2]);
156-
if(!(s >= 0 && s < 60)) return BADNUM;
157-
return d + s * ONESEC;
158-
}
159-
}
160-
}
161-
}
162-
return baseDate.getTime();
121+
return date.getTime() + S * ONESEC;
163122
};
164123

165124
MIN_MS = exports.MIN_MS = exports.dateTime2ms('-9999');

test/jasmine/tests/lib_date_test.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ describe('dates', function() {
2929
['0122-04-08 08:22', new Date(122, 3, 8, 8, 22)],
3030
['-0098-11-19 23:59:59', new Date(-98, 10, 19, 23, 59, 59)],
3131
['-9730-12-01 12:34:56.789', new Date(-9730, 11, 1, 12, 34, 56, 789)],
32+
// random whitespace before and after gets stripped
33+
['\r\n\t -9730-12-01 12:34:56.789\r\n\t ', new Date(-9730, 11, 1, 12, 34, 56, 789)],
3234
// first century, also allow month, day, and hour to be 1-digit, and not all
3335
// three digits of milliseconds
3436
['0013-1-1 1:00:00.6', d1c],
@@ -40,9 +42,19 @@ describe('dates', function() {
4042
// 2-digit years get mapped to now-70 -> now+29
4143
[thisYear_2 + '-05', new Date(thisYear, 4, 1)],
4244
[nowMinus70_2 + '-10-18', new Date(nowMinus70, 9, 18)],
43-
[nowPlus29_2 + '-02-12 14:29:32', new Date(nowPlus29, 1, 12, 14, 29, 32)]
45+
[nowPlus29_2 + '-02-12 14:29:32', new Date(nowPlus29, 1, 12, 14, 29, 32)],
46+
47+
// including timezone info (that we discard)
48+
['2014-03-04 08:15Z', new Date(2014, 2, 4, 8, 15)],
49+
['2014-03-04 08:15:00.00z', new Date(2014, 2, 4, 8, 15)],
50+
['2014-03-04 08:15:34+1200', new Date(2014, 2, 4, 8, 15, 34)],
51+
['2014-03-04 08:15:34.567-05:45', new Date(2014, 2, 4, 8, 15, 34, 567)],
4452
].forEach(function(v) {
4553
expect(Lib.dateTime2ms(v[0])).toBe(+v[1], v[0]);
54+
55+
// ISO-8601: all the same stuff with t or T as the separator
56+
expect(Lib.dateTime2ms(v[0].trim().replace(' ', 't'))).toBe(+v[1], v[0].trim().replace(' ', 't'));
57+
expect(Lib.dateTime2ms('\r\n\t ' + v[0].trim().replace(' ', 'T') + '\r\n\t ')).toBe(+v[1], v[0].trim().replace(' ', 'T'));
4658
});
4759
});
4860

@@ -105,7 +117,9 @@ describe('dates', function() {
105117
'2015-01-00', '2015-01-32', '2015-02-29', '2015-04-31', '2015-01-001', // bad day (incl non-leap year)
106118
'2015-01-01 24:00', '2015-01-01 -1:00', '2015-01-01 001:00', // bad hour
107119
'2015-01-01 12:60', '2015-01-01 12:-1', '2015-01-01 12:001', '2015-01-01 12:1', // bad minute
108-
'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
120+
'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
121+
'2015-01-01T', '2015-01-01TT12:34', // bad ISO separators
122+
'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
109123
].forEach(function(v) {
110124
expect(Lib.dateTime2ms(v)).toBeUndefined(v);
111125
});

0 commit comments

Comments
 (0)