10
10
'use strict' ;
11
11
12
12
var d3 = require ( 'd3' ) ;
13
- var isNumeric = require ( 'fast-isnumeric' ) ;
14
13
15
14
var logError = require ( './loggers' ) . error ;
16
15
@@ -21,6 +20,11 @@ var ONEHOUR = constants.ONEHOUR;
21
20
var ONEMIN = constants . ONEMIN ;
22
21
var ONESEC = constants . ONESEC ;
23
22
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
+
24
28
// is an object a javascript date?
25
29
exports . isJSDate = function ( v ) {
26
30
return typeof v === 'object' && v !== null && typeof v . getTime === 'function' ;
@@ -32,20 +36,33 @@ exports.isJSDate = function(v) {
32
36
var MIN_MS , MAX_MS ;
33
37
34
38
/**
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)
41
58
*
42
59
* Expanded to support negative years to -9999 but you must always
43
60
* give 4 digits, except for 2-digit positive years which we assume are
44
61
* near the present time.
45
62
* Note that we follow ISO 8601:2004: there *is* a year 0, which
46
63
* is 1BC/BCE, and -1===2BC etc.
47
64
*
48
- * 2-digit to 4 -digit year conversion, where to cut off ?
65
+ * Where to cut off 2 -digit years between 1900s and 2000s ?
49
66
* from http://support.microsoft.com/kb/244664:
50
67
* 1930-2029 (the most retro of all...)
51
68
* but in my mac chrome from eg. d=new Date(Date.parse('8/19/50')):
@@ -70,96 +87,36 @@ var MIN_MS, MAX_MS;
70
87
exports . dateTime2ms = function ( s ) {
71
88
// first check if s is a date object
72
89
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 ;
74
93
if ( s >= MIN_MS && s <= MAX_MS ) return s ;
75
94
return BADNUM ;
76
95
}
77
96
// otherwise only accept strings and numbers
78
97
if ( typeof s !== 'string' && typeof s !== 'number' ) return BADNUM ;
79
98
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 ;
94
109
}
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 ) ;
108
111
109
112
// javascript takes new Date(0..99,m,d) to mean 1900-1999, so
110
113
// 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 ) ;
131
116
132
- p = datetime [ 1 ] . split ( ':' ) ;
133
- if ( p . length > 3 ) return BADNUM ;
117
+ if ( date . getUTCDate ( ) !== d ) return BADNUM ;
134
118
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 ;
163
120
} ;
164
121
165
122
MIN_MS = exports . MIN_MS = exports . dateTime2ms ( '-9999' ) ;
@@ -191,16 +148,41 @@ exports.ms2DateTime = function(ms, r) {
191
148
192
149
if ( ! r ) r = 0 ;
193
150
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 ) ,
196
154
// <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 ,
199
157
// <3 hours: add seconds
200
- s = ( r < THREEHOURS ) ? d . getSeconds ( ) : 0 ,
158
+ s = ( r < THREEHOURS ) ? d . getUTCSeconds ( ) : 0 ,
201
159
// <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 ;
203
161
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 ) {
204
186
// include each part that has nonzero data in or after it
205
187
if ( h || m || s || msec10 ) {
206
188
dateStr += ' ' + lpad ( h , 2 ) + ':' + lpad ( m , 2 ) ;
@@ -217,7 +199,7 @@ exports.ms2DateTime = function(ms, r) {
217
199
}
218
200
}
219
201
return dateStr ;
220
- } ;
202
+ }
221
203
222
204
// normalize date format to date string, in case it starts as
223
205
// a Date object or milliseconds
@@ -227,7 +209,7 @@ exports.cleanDate = function(v, dflt) {
227
209
// NOTE: if someone puts in a year as a number rather than a string,
228
210
// this will mistakenly convert it thinking it's milliseconds from 1970
229
211
// that is: '2012' -> Jan. 1, 2012, but 2012 -> 2012 epoch milliseconds
230
- v = exports . ms2DateTime ( + v ) ;
212
+ v = exports . ms2DateTimeLocal ( + v ) ;
231
213
if ( ! v && dflt !== undefined ) return dflt ;
232
214
}
233
215
else if ( ! exports . isDateTime ( v ) ) {
0 commit comments