Skip to content

Commit d4bb89d

Browse files
committed
improve dateTime2ms edge cases and standardize BADNUM and FP_SAFE constants
1 parent d90d4e1 commit d4bb89d

File tree

6 files changed

+125
-56
lines changed

6 files changed

+125
-56
lines changed

src/lib/constants.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Copyright 2012-2016, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
12+
module.exports = {
13+
/**
14+
* Standardize all missing data in calcdata to use undefined
15+
* never null or NaN.
16+
* That way we can use !==undefined, or !== BADNUM,
17+
* to test for real data
18+
*/
19+
BADNUM: undefined,
20+
21+
/*
22+
* Limit certain operations to well below floating point max value
23+
* to avoid glitches: Make sure that even when you multiply it by the
24+
* number of pixels on a giant screen it still works
25+
*/
26+
FP_SAFE: Number.MAX_VALUE / 10000
27+
};

src/lib/dates.js

+86-45
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,32 @@
1111

1212
var d3 = require('d3');
1313
var isNumeric = require('fast-isnumeric');
14-
14+
var BADNUM = require('./constants').BADNUM;
1515

1616
/**
1717
* dateTime2ms - turn a date object or string s of the form
1818
* YYYY-mm-dd HH:MM:SS.sss into milliseconds (relative to 1970-01-01,
1919
* per javascript standard)
2020
* may truncate after any full field, and sss can be any length
2121
* even >3 digits, though javascript dates truncate to milliseconds
22-
* returns false if it doesn't find a date
22+
* returns BADNUM if it doesn't find a date
23+
*
24+
* Expanded to support negative years to -9999 but you must always
25+
* give 4 digits, except for 2-digit positive years which we assume are
26+
* near the present time.
27+
* Note that we follow ISO 8601:2004: there *is* a year 0, which
28+
* is 1BC/BCE, and -1===2BC etc.
2329
*
2430
* 2-digit to 4-digit year conversion, where to cut off?
2531
* from http://support.microsoft.com/kb/244664:
2632
* 1930-2029 (the most retro of all...)
2733
* but in my mac chrome from eg. d=new Date(Date.parse('8/19/50')):
2834
* 1950-2049
2935
* by Java, from http://stackoverflow.com/questions/2024273/:
30-
* now-80 - now+20
36+
* now-80 - now+19
3137
* or FileMaker Pro, from
3238
* http://www.filemaker.com/12help/html/add_view_data.4.21.html:
33-
* now-70 - now+30
39+
* now-70 - now+29
3440
* but python strptime etc, via
3541
* http://docs.python.org/py3k/library/time.html:
3642
* 1969-2068 (super forward-looking, but static, not sliding!)
@@ -39,8 +45,8 @@ var isNumeric = require('fast-isnumeric');
3945
* they can learn the hard way not to use 2-digit years, as no choice we
4046
* make now will cover all possibilities. mostly this will all be taken
4147
* care of in initial parsing, should only be an issue for hand-entered data
42-
* currently (2012) this range is:
43-
* 1942-2041
48+
* currently (2016) this range is:
49+
* 1946-2045
4450
*/
4551

4652
exports.dateTime2ms = function(s) {
@@ -49,63 +55,96 @@ exports.dateTime2ms = function(s) {
4955
if(s.getTime) return +s;
5056
}
5157
catch(e) {
52-
return false;
58+
return BADNUM;
5359
}
5460

5561
var y, m, d, h;
5662
// split date and time parts
5763
var datetime = String(s).split(' ');
58-
if(datetime.length > 2) return false;
64+
if(datetime.length > 2) return BADNUM;
5965

6066
var p = datetime[0].split('-'); // date part
61-
if(p.length > 3 || (p.length !== 3 && datetime[1])) return false;
67+
68+
var CE = true; // common era, ie positive year
69+
if(p[0] === '') {
70+
// first part is blank: year starts with a minus sign
71+
CE = false;
72+
p.splice(0, 1);
73+
}
74+
75+
if(p.length > 3 || (p.length !== 3 && datetime[1])) return BADNUM;
6276

6377
// year
6478
if(p[0].length === 4) y = Number(p[0]);
6579
else if(p[0].length === 2) {
80+
if(!CE) return BADNUM;
6681
var yNow = new Date().getFullYear();
6782
y = ((Number(p[0]) - yNow + 70) % 100 + 200) % 100 + yNow - 70;
6883
}
69-
else return false;
70-
if(!isNumeric(y)) return false;
71-
if(p.length === 1) return new Date(y, 0, 1).getTime(); // year only
72-
73-
// month
74-
m = Number(p[1]) - 1; // new Date() uses zero-based months
75-
if(p[1].length > 2 || !(m >= 0 && m <= 11)) return false;
76-
if(p.length === 2) return new Date(y, m, 1).getTime(); // year-month
77-
78-
// day
79-
d = Number(p[2]);
80-
if(p[2].length > 2 || !(d >= 1 && d <= 31)) return false;
81-
82-
// now save the date part
83-
d = new Date(y, m, d).getTime();
84-
if(!datetime[1]) return d; // year-month-day
85-
p = datetime[1].split(':');
86-
if(p.length > 3) return false;
87-
88-
// hour
89-
h = Number(p[0]);
90-
if(p[0].length > 2 || !(h >= 0 && h <= 23)) return false;
91-
d += 3600000 * h;
92-
if(p.length === 1) return d;
93-
94-
// minute
95-
m = Number(p[1]);
96-
if(p[1].length > 2 || !(m >= 0 && m <= 59)) return false;
97-
d += 60000 * m;
98-
if(p.length === 2) return d;
99-
100-
// second
101-
s = Number(p[2]);
102-
if(!(s >= 0 && s < 60)) return false;
103-
return d + s * 1000;
84+
else return BADNUM;
85+
if(!isNumeric(y)) return BADNUM;
86+
87+
// javascript takes new Date(0..99,m,d) to mean 1900-1999, so
88+
// to support years 0-99 we need to use setFullYear explicitly
89+
var baseDate = new Date(0, 0, 1);
90+
baseDate.setFullYear(CE ? y : -y);
91+
if(p.length > 1) {
92+
93+
// month
94+
m = Number(p[1]) - 1; // new Date() uses zero-based months
95+
if(p[1].length > 2 || !(m >= 0 && m <= 11)) return BADNUM;
96+
baseDate.setMonth(m);
97+
98+
if(p.length > 2) {
99+
100+
// day
101+
d = Number(p[2]);
102+
if(p[2].length > 2 || !(d >= 1 && d <= 31)) return BADNUM;
103+
baseDate.setDate(d);
104+
105+
// does that date exist in this month?
106+
if(baseDate.getDate() !== d) return BADNUM;
107+
108+
if(datetime[1]) {
109+
110+
p = datetime[1].split(':');
111+
if(p.length > 3) return BADNUM;
112+
113+
// hour
114+
h = Number(p[0]);
115+
if(p[0].length > 2 || !(h >= 0 && h <= 23)) return BADNUM;
116+
baseDate.setHours(h);
117+
118+
// does that hour exist in this day? (Daylight time!)
119+
// (TODO: remove this check when we move to UTC)
120+
if(baseDate.getHours() !== h) return BADNUM;
121+
122+
if(p.length > 1) {
123+
d = baseDate.getTime();
124+
125+
// minute
126+
m = Number(p[1]);
127+
if(p[1].length > 2 || !(m >= 0 && m <= 59)) return BADNUM;
128+
d += 60000 * m;
129+
if(p.length === 2) return d;
130+
131+
// second (and milliseconds)
132+
s = Number(p[2]);
133+
if(!(s >= 0 && s < 60)) return BADNUM;
134+
return d + s * 1000;
135+
}
136+
}
137+
}
138+
}
139+
return baseDate.getTime();
104140
};
105141

142+
exports.MIN_MS = exports.dateTime2ms('-9999');
143+
exports.MAX_MS = exports.dateTime2ms('9999');
144+
106145
// is string s a date? (see above)
107146
exports.isDateTime = function(s) {
108-
return (exports.dateTime2ms(s) !== false);
147+
return (exports.dateTime2ms(s) !== BADNUM);
109148
};
110149

111150
// pad a number with zeroes, to given # of digits before the decimal point
@@ -123,6 +162,8 @@ function lpad(val, digits) {
123162
exports.ms2DateTime = function(ms, r) {
124163
if(!r) r = 0;
125164

165+
if(ms < exports.MIN_MS || ms > exports.MAX_MS) return BADNUM;
166+
126167
var d = new Date(ms),
127168
s = d3.time.format('%Y-%m-%d')(d);
128169

src/lib/index.js

+6
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ lib.dateTime2ms = datesModule.dateTime2ms;
3636
lib.isDateTime = datesModule.isDateTime;
3737
lib.ms2DateTime = datesModule.ms2DateTime;
3838
lib.parseDate = datesModule.parseDate;
39+
lib.MIN_MS = datesModule.MIN_MS;
40+
lib.MAX_MS = datesModule.MAX_MS;
3941

4042
var searchModule = require('./search');
4143
lib.findBin = searchModule.findBin;
@@ -75,6 +77,10 @@ lib.error = loggersModule.error;
7577

7678
lib.notifier = require('./notifier');
7779

80+
var constantsModule = require('./constants');
81+
lib.BADNUM = constantsModule.BADNUM,
82+
lib.FP_SAFE = constantsModule.FP_SAFE;
83+
7884
/**
7985
* swap x and y of the same attribute in container cont
8086
* specify attr with a ? in place of x/y

src/plots/cartesian/axes.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ var Titles = require('../../components/titles');
1919
var Color = require('../../components/color');
2020
var Drawing = require('../../components/drawing');
2121

22+
var FP_SAFE = Lib.FP_SAFE;
23+
2224

2325
var axes = module.exports = {};
2426

@@ -304,7 +306,6 @@ axes.saveRangeInitial = function(gd, overwrite) {
304306
// (unless one end is overridden by tozero)
305307
// tozero: (boolean) make sure to include zero if axis is linear,
306308
// and make it a tight bound if possible
307-
var FP_SAFE = Number.MAX_VALUE / 2;
308309
axes.expand = function(ax, data, options) {
309310
if(!(ax.autorange || ax._needsExpand) || !data) return;
310311
if(!ax._min) ax._min = [];

src/plots/cartesian/constants.js

-8
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,6 @@ module.exports = {
2121
y: /^yaxis([2-9]|[1-9][0-9]+)?$/
2222
},
2323

24-
/**
25-
* standardize all missing data in calcdata to use undefined
26-
* never null or NaN.
27-
* that way we can use !==undefined, or !== BADNUM,
28-
* to test for real data
29-
*/
30-
BADNUM: undefined,
31-
3224
// axis match regular expression
3325
xAxisMatch: /^xaxis[0-9]*$/,
3426
yAxisMatch: /^yaxis[0-9]*$/,

src/plots/cartesian/set_convert.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ var d3 = require('d3');
1313
var isNumeric = require('fast-isnumeric');
1414

1515
var Lib = require('../../lib');
16+
var FP_SAFE = Lib.FP_SAFE;
17+
var BADNUM = Lib.BADNUM;
1618

1719
var constants = require('./constants');
1820
var cleanDatum = require('./clean_datum');
@@ -128,7 +130,7 @@ module.exports = function setConvert(ax) {
128130
};
129131

130132
ax.l2p = function(v) {
131-
if(!isNumeric(v)) return constants.BADNUM;
133+
if(!isNumeric(v)) return BADNUM;
132134

133135
// include 2 fractional digits on pixel, for PDF zooming etc
134136
return d3.round(ax._b + ax._m * v, 2);
@@ -197,7 +199,7 @@ module.exports = function setConvert(ax) {
197199
}
198200

199201
var c = ax._categories.indexOf(v);
200-
return c === -1 ? constants.BADNUM : c;
202+
return c === -1 ? BADNUM : c;
201203
};
202204

203205
ax.d2l = ax.d2c;

0 commit comments

Comments
 (0)