1010'use strict' ;
1111
1212var d3 = require ( 'd3' ) ;
13- var isNumeric = require ( 'fast-isnumeric' ) ;
1413
1514var logError = require ( './loggers' ) . error ;
1615
@@ -21,6 +20,11 @@ var ONEHOUR = constants.ONEHOUR;
2120var ONEMIN = constants . ONEMIN ;
2221var ONESEC = constants . ONESEC ;
2322
23+ var DATETIME_REGEXP = / ^ \s * ( - ? \d \d \d \d | \d \d ) ( - ( 0 ? [ 1 - 9 ] | 1 [ 0 1 2 ] ) ( - ( [ 0 - 3 ] ? \d ) ( [ T t ] ( [ 0 1 ] ? \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?
2529exports . isJSDate = function ( v ) {
2630 return typeof v === 'object' && v !== null && typeof v . getTime === 'function' ;
@@ -32,20 +36,33 @@ exports.isJSDate = function(v) {
3236var 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')):
@@ -70,96 +87,36 @@ var MIN_MS, MAX_MS;
7087exports . dateTime2ms = function ( s ) {
7188 // first check if s is a date object
7289 if ( exports . isJSDate ( s ) ) {
73- s = Number ( s ) ;
90+ // Convert to the UTC milliseconds that give the same
91+ // hours as this date has in the local timezone
92+ s = Number ( s ) - s . getTimezoneOffset ( ) * ONEMIN ;
7493 if ( s >= MIN_MS && s <= MAX_MS ) return s ;
7594 return BADNUM ;
7695 }
7796 // otherwise only accept strings and numbers
7897 if ( typeof s !== 'string' && typeof s !== 'number' ) return BADNUM ;
7998
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 ) ;
99+ var match = String ( s ) . match ( DATETIME_REGEXP ) ;
100+ if ( ! match ) return BADNUM ;
101+ var y = match [ 1 ] ,
102+ m = Number ( match [ 3 ] || 1 ) ,
103+ d = Number ( match [ 5 ] || 1 ) ,
104+ H = Number ( match [ 7 ] || 0 ) ,
105+ M = Number ( match [ 9 ] || 0 ) ,
106+ S = Number ( match [ 11 ] || 0 ) ;
107+ if ( y . length === 2 ) {
108+ y = ( Number ( y ) + 2000 - YFIRST ) % 100 + YFIRST ;
94109 }
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 ;
105- }
106- else return BADNUM ;
107- if ( ! isNumeric ( y ) ) return BADNUM ;
110+ else y = Number ( y ) ;
108111
109112 // javascript takes new Date(0..99,m,d) to mean 1900-1999, so
110113 // 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 ) {
114-
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 ) ;
119-
120- if ( p . length > 2 ) {
121-
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 ] ) {
114+ var date = new Date ( Date . UTC ( 2000 , m - 1 , d , H , M ) ) ;
115+ date . setUTCFullYear ( y ) ;
131116
132- p = datetime [ 1 ] . split ( ':' ) ;
133- if ( p . length > 3 ) return BADNUM ;
117+ if ( date . getUTCDate ( ) !== d ) return BADNUM ;
134118
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 ( ) ;
119+ return date . getTime ( ) + S * ONESEC ;
163120} ;
164121
165122MIN_MS = exports . MIN_MS = exports . dateTime2ms ( '-9999' ) ;
@@ -191,16 +148,41 @@ exports.ms2DateTime = function(ms, r) {
191148
192149 if ( ! r ) r = 0 ;
193150
194- var d = new Date ( Math . floor ( ms ) ) ,
195- dateStr = d3 . time . format ( '%Y-%m-%d' ) ( d ) ,
151+ var msecTenths = Math . round ( ( ( ms % 1 ) + 1 ) * 10 ) % 10 ,
152+ d = new Date ( Math . round ( ms - msecTenths / 10 ) ) ,
153+ dateStr = d3 . time . format . utc ( '%Y-%m-%d' ) ( d ) ,
196154 // <90 days: add hours and minutes - never *only* add hours
197- h = ( r < NINETYDAYS ) ? d . getHours ( ) : 0 ,
198- m = ( r < NINETYDAYS ) ? d . getMinutes ( ) : 0 ,
155+ h = ( r < NINETYDAYS ) ? d . getUTCHours ( ) : 0 ,
156+ m = ( r < NINETYDAYS ) ? d . getUTCMinutes ( ) : 0 ,
199157 // <3 hours: add seconds
200- s = ( r < THREEHOURS ) ? d . getSeconds ( ) : 0 ,
158+ s = ( r < THREEHOURS ) ? d . getUTCSeconds ( ) : 0 ,
201159 // <5 minutes: add ms (plus one extra digit, this is msec*10)
202- msec10 = ( r < FIVEMIN ) ? Math . round ( ( d . getMilliseconds ( ) + ( ( ( ms % 1 ) + 1 ) % 1 ) ) * 10 ) : 0 ;
160+ msec10 = ( r < FIVEMIN ) ? d . getUTCMilliseconds ( ) * 10 + msecTenths : 0 ;
203161
162+ return includeTime ( dateStr , h , m , s , msec10 ) ;
163+ } ;
164+
165+ // For converting old-style milliseconds to date strings,
166+ // we use the local timezone rather than UTC like we use
167+ // everywhere else, both for backward compatibility and
168+ // because that's how people mostly use javasript date objects.
169+ // Clip one extra day off our date range though so we can't get
170+ // thrown beyond the range by the timezone shift.
171+ exports . ms2DateTimeLocal = function ( ms ) {
172+ if ( ! ( ms >= MIN_MS + ONEDAY && ms <= MAX_MS - ONEDAY ) ) return BADNUM ;
173+
174+ var msecTenths = Math . round ( ( ( ms % 1 ) + 1 ) * 10 ) % 10 ,
175+ d = new Date ( Math . round ( ms - msecTenths / 10 ) ) ,
176+ dateStr = d3 . time . format ( '%Y-%m-%d' ) ( d ) ,
177+ h = d . getHours ( ) ,
178+ m = d . getMinutes ( ) ,
179+ s = d . getSeconds ( ) ,
180+ msec10 = d . getUTCMilliseconds ( ) * 10 + msecTenths ;
181+
182+ return includeTime ( dateStr , h , m , s , msec10 ) ;
183+ } ;
184+
185+ function includeTime ( dateStr , h , m , s , msec10 ) {
204186 // include each part that has nonzero data in or after it
205187 if ( h || m || s || msec10 ) {
206188 dateStr += ' ' + lpad ( h , 2 ) + ':' + lpad ( m , 2 ) ;
@@ -217,7 +199,7 @@ exports.ms2DateTime = function(ms, r) {
217199 }
218200 }
219201 return dateStr ;
220- } ;
202+ }
221203
222204// normalize date format to date string, in case it starts as
223205// a Date object or milliseconds
@@ -227,7 +209,7 @@ exports.cleanDate = function(v, dflt) {
227209 // NOTE: if someone puts in a year as a number rather than a string,
228210 // this will mistakenly convert it thinking it's milliseconds from 1970
229211 // that is: '2012' -> Jan. 1, 2012, but 2012 -> 2012 epoch milliseconds
230- v = exports . ms2DateTime ( + v ) ;
212+ v = exports . ms2DateTimeLocal ( + v ) ;
231213 if ( ! v && dflt !== undefined ) return dflt ;
232214 }
233215 else if ( ! exports . isDateTime ( v ) ) {
0 commit comments