forked from plotly/plotly.js
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdates.js
More file actions
334 lines (301 loc) · 10.8 KB
/
dates.js
File metadata and controls
334 lines (301 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
var d3 = require('d3');
var isNumeric = require('fast-isnumeric');
/**
* 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 false if it doesn't find a date
*
* 2-digit to 4-digit year conversion, where to cut off?
* 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')):
* 1950-2049
* by Java, from http://stackoverflow.com/questions/2024273/:
* now-80 - now+20
* or FileMaker Pro, from
* http://www.filemaker.com/12help/html/add_view_data.4.21.html:
* now-70 - now+30
* but python strptime etc, via
* http://docs.python.org/py3k/library/time.html:
* 1969-2068 (super forward-looking, but static, not sliding!)
*
* lets go with now-70 to now+30, and if anyone runs into this problem
* they can learn the hard way not to use 2-digit years, as no choice we
* make now will cover all possibilities. mostly this will all be taken
* care of in initial parsing, should only be an issue for hand-entered data
* currently (2012) this range is:
* 1942-2041
*/
exports.dateTime2ms = function(s) {
// first check if s is a date object
try {
if (s.getTime) return +s;
}
catch(e) {
return false;
}
var y, m, d, h;
// split date and time parts
var datetime = String(s).split(' ');
if (datetime.length > 2) return false;
var p = datetime[0].split('-'); // date part
if (p.length > 3 || (p.length !== 3 && datetime[1])) return false;
// year
if (p[0].length === 4) y = Number(p[0]);
else if (p[0].length === 2) {
var yNow = new Date().getFullYear();
y = ((Number(p[0]) - yNow + 70)%100 + 200)%100 + yNow - 70;
}
else return false;
if (!isNumeric(y)) return false;
if (p.length === 1) return new Date(y,0,1).getTime(); // year only
// month
m = Number(p[1]) - 1; // new Date() uses zero-based months
if (p[1].length > 2 || !(m >= 0 && m <= 11)) return false;
if (p.length === 2) return new Date(y, m, 1).getTime(); // year-month
// day
d = Number(p[2]);
if (p[2].length > 2 || !(d >= 1 && d <= 31)) return false;
// now save the date part
d = new Date(y, m, d).getTime();
if (!datetime[1]) return d; // year-month-day
p = datetime[1].split(':');
if (p.length > 3) return false;
// hour
h = Number(p[0]);
if (p[0].length > 2 || !(h >= 0 && h <= 23)) return false;
d += 3600000*h;
if (p.length === 1) return d;
// minute
m = Number(p[1]);
if (p[1].length > 2 || !(m >= 0 && m <= 59)) return false;
d += 60000*m;
if (p.length === 2) return d;
// second
s = Number(p[2]);
if (!(s >= 0 && s < 60)) return false;
return d+s*1000;
};
// is string s a date? (see above)
exports.isDateTime = function(s) {
return (exports.dateTime2ms(s) !== false);
};
// pad a number with zeroes, to given # of digits before the decimal point
function lpad(val, digits){
return String(val + Math.pow(10, digits)).substr(1);
}
/**
* Turn ms into string of the form YYYY-mm-dd HH:MM:SS.sss
* Crop any trailing zeros in time, but always leave full date
* (we could choose to crop '-01' from date too)...
* Optional range r is the data range that applies, also in ms.
* If rng is big, the later parts of time will be omitted
*/
exports.ms2DateTime = function(ms, r) {
if(typeof(d3)==='undefined'){
console.log('d3 is not defined');
return;
}
if(!r) r=0;
var d = new Date(ms),
s = d3.time.format('%Y-%m-%d')(d);
if(r<7776000000) {
// <90 days: add hours
s+=' '+lpad(d.getHours(),2);
if(r<432000000) {
// <5 days: add minutes
s+=':'+lpad(d.getMinutes(),2);
if(r<10800000) {
// <3 hours: add seconds
s+=':'+lpad(d.getSeconds(),2);
if(r<300000) {
// <5 minutes: add ms
s+='.'+lpad(d.getMilliseconds(),3);
}
}
}
// strip trailing zeros
return s.replace(/([:\s]00)*\.?[0]*$/,'');
}
return s;
};
/**
* parseDate: forgiving attempt to turn any date string
* into a javascript date object
*
* first collate all the date formats we want to support, precompiled
* to d3 format objects see below for the string cleaning that happens
* before this separate out 2-digit (y) and 4-digit-year (Y) formats,
* formats with month names (b), and formats with am/pm (I) or no time (D)
* (also includes hour only, as the test is really for a colon) so we can
* cut down the number of tests we need to run for any given string
* (right now all are between 15 and 32 tests)
*/
// TODO: this is way out of date vs. the server-side version
var timeFormats = {
// 24 hour
H:['%H:%M:%S~%L', '%H:%M:%S', '%H:%M'],
// with am/pm
I:['%I:%M:%S~%L%p', '%I:%M:%S%p', '%I:%M%p'],
// no colon, ie only date or date with hour (could also support eg 12h34m?)
D:['%H', '%I%p', '%Hh']
};
var dateFormats = {
Y:[
'%Y~%m~%d',
'%Y%m%d',
'%y%m%d', // YYMMDD, has 6 digits together so will match Y, not y
'%m~%d~%Y', // MM/DD/YYYY has first precedence
'%d~%m~%Y' // then DD/MM/YYYY
],
Yb:[
'%b~%d~%Y', // eg nov 21 2013
'%d~%b~%Y', // eg 21 nov 2013
'%Y~%d~%b', // eg 2013 21 nov (or 2013 q3, after replacement)
'%Y~%b~%d' // eg 2013 nov 21
],
/**
* the two-digit year cases have so many potential ambiguities
* it's not even funny, but we'll try them anyway.
*/
y:[
'%m~%d~%y',
'%d~%m~%y',
'%y~%m~%d'
],
yb:[
'%b~%d~%y',
'%d~%b~%y',
'%y~%d~%b',
'%y~%b~%d'
]
};
// use utc formatter since we're ignoring timezone info
var formatter = d3.time.format.utc;
/**
* ISO8601 and YYYYMMDDHHMMSS are the only ones where date and time
* are not separated by a space, so they get inserted specially here.
* Also a couple formats with no day (so time makes no sense)
*/
var dateTimeFormats = {
Y: {
H: ['%Y~%m~%dT%H:%M:%S', '%Y~%m~%dT%H:%M:%S~%L'].map(formatter),
I: [],
D: ['%Y%m%d%H%M%S', '%Y~%m', '%m~%Y'].map(formatter)
},
Yb: {H: [], I: [], D: ['%Y~%b', '%b~%Y'].map(formatter)},
y: {H: [], I: [], D: []},
yb: {H: [], I: [], D: []}
};
// all others get inserted in all possible combinations from dateFormats and timeFormats
['Y', 'Yb', 'y', 'yb'].forEach(function(dateType) {
dateFormats[dateType].forEach(function(dateFormat) {
// just a date (don't do just a time)
dateTimeFormats[dateType].D.push(formatter(dateFormat));
['H', 'I', 'D'].forEach(function(timeType) {
timeFormats[timeType].forEach(function(timeFormat) {
var a = dateTimeFormats[dateType][timeType];
// 'date time', then 'time date'
a.push(formatter(dateFormat+'~'+timeFormat));
a.push(formatter(timeFormat+'~'+dateFormat));
});
});
});
});
// precompiled regexps for performance
var matchword = /[a-z]*/g,
shortenword = function(m) { return m.substr(0,3); },
weekdaymatch = /(mon|tue|wed|thu|fri|sat|sun|the|of|st|nd|rd|th)/g,
separatormatch = /[\s,\/\-\.\(\)]+/g,
ampmmatch = /~?([ap])~?m(~|$)/,
replaceampm = function(m,ap) { return ap+'m '; },
match4Y = /\d\d\d\d/,
matchMonthName = /(^|~)[a-z]{3}/,
matchAMPM = /[ap]m/,
matchcolon = /:/,
matchquarter = /q([1-4])/,
quarters = ['31~mar','30~jun','30~sep','31~dec'],
replacequarter = function(m,n) { return quarters[n-1]; },
matchTZ = / ?([+\-]\d\d:?\d\d|Z)$/;
function getDateType(v) {
var dateType;
dateType = (match4Y.test(v) ? 'Y' : 'y');
dateType = dateType + (matchMonthName.test(v) ? 'b' : '');
return dateType;
}
function getTimeType(v) {
var timeType;
timeType = matchcolon.test(v) ? (matchAMPM.test(v) ? 'I' : 'H') : 'D';
return timeType;
}
exports.parseDate = function(v) {
// is it already a date? just return it
if (v.getTime) return v;
/**
* otherwise, if it's not a string, return nothing
* the case of numbers that just have years will get
* dealt with elsewhere.
*/
if (typeof v !== 'string') return false;
// first clean up the string a bit to reduce the number of formats we have to test
v = v.toLowerCase()
/**
* cut all words down to 3 characters - this will result in
* some spurious matches, ie whenever the first three characters
* of a word match a month or weekday but that seems more likely
* to fix typos than to make dates where they shouldn't be...
* and then we can omit the long form of months from our testing
*/
.replace(matchword, shortenword)
/**
* remove weekday names, as they get overridden anyway if they're
* inconsistent also removes a few more words
* (ie "tuesday the 26th of november")
* TODO: language support?
* for months too, but these seem to be built into d3
*/
.replace(weekdaymatch, '')
/**
* collapse all separators one ~ at a time, except : which seems
* pretty consistent for the time part use ~ instead of space or
* something since d3 can eat a space as padding on 1-digit numbers
*/
.replace(separatormatch, '~')
// in case of a.m. or p.m. (also take off any space before am/pm)
.replace(ampmmatch, replaceampm)
// turn quarters Q1-4 into dates (quarter ends)
.replace(matchquarter, replacequarter)
.trim()
// also try to ignore timezone info, at least for now
.replace(matchTZ, '');
// now test against the various formats that might match
var out = null,
dateType = getDateType(v),
timeType = getTimeType(v),
formatList,
len;
formatList = dateTimeFormats[dateType][timeType];
len = formatList.length;
for (var i = 0; i < len; i++) {
out = formatList[i].parse(v);
if (out) break;
}
// If not an instance of Date at this point, just return it.
if (!(out instanceof Date)) return false;
// parse() method interprets arguments with local time zone.
var tzoff = out.getTimezoneOffset();
// In general (default) this is not what we want, so force into UTC:
out.setTime(out.getTime() + tzoff * 60 * 1000);
return out;
};