diff --git a/circle.yml b/circle.yml index 085c303b25c..584761eefcb 100644 --- a/circle.yml +++ b/circle.yml @@ -6,6 +6,8 @@ general: machine: node: version: 6.1.0 + timezone: + America/Anchorage services: - docker diff --git a/src/components/rangeselector/get_update_object.js b/src/components/rangeselector/get_update_object.js index fe6eacaa454..71978d0a310 100644 --- a/src/components/rangeselector/get_update_object.js +++ b/src/components/rangeselector/get_update_object.js @@ -42,13 +42,13 @@ function getXRange(axisLayout, buttonLayout) { switch(buttonLayout.stepmode) { case 'backward': - range0 = Lib.ms2DateTime(+d3.time[step].offset(base, -count)); + range0 = Lib.ms2DateTime(+d3.time[step].utc.offset(base, -count)); break; case 'todate': - var base2 = d3.time[step].offset(base, -count); + var base2 = d3.time[step].utc.offset(base, -count); - range0 = Lib.ms2DateTime(+d3.time[step].ceil(base2)); + range0 = Lib.ms2DateTime(+d3.time[step].utc.ceil(base2)); break; } diff --git a/src/lib/dates.js b/src/lib/dates.js index 72dfecd5f72..a486bc797ee 100644 --- a/src/lib/dates.js +++ b/src/lib/dates.js @@ -10,7 +10,6 @@ 'use strict'; var d3 = require('d3'); -var isNumeric = require('fast-isnumeric'); var logError = require('./loggers').error; @@ -21,6 +20,11 @@ var ONEHOUR = constants.ONEHOUR; var ONEMIN = constants.ONEMIN; var ONESEC = constants.ONESEC; +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; + +// for 2-digit years, the first year we map them onto +var YFIRST = new Date().getFullYear() - 70; + // is an object a javascript date? exports.isJSDate = function(v) { return typeof v === 'object' && v !== null && typeof v.getTime === 'function'; @@ -32,12 +36,25 @@ exports.isJSDate = function(v) { var MIN_MS, MAX_MS; /** - * 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 BADNUM if it doesn't find a date + * dateTime2ms - turn a date object or string s into milliseconds + * (relative to 1970-01-01, per javascript standard) + * + * Returns BADNUM if it doesn't find a date + * + * strings should have the form: + * + * -?YYYY-mm-ddHH:MM:SS.sss? + * + * : space (our normal standard) or T or t (ISO-8601) + * : Z, z, or [+\-]HH:?MM and we THROW IT AWAY + * this format comes from https://tools.ietf.org/html/rfc3339#section-5.6 + * but we allow it even with a space as the separator + * + * May truncate after any full field, and sss can be any length + * even >3 digits, though javascript dates truncate to milliseconds, + * we keep as much as javascript numeric precision can hold, but we only + * report back up to 100 microsecond precision, because most dates support + * this precision (close to 1970 support more, very far away support less) * * Expanded to support negative years to -9999 but you must always * give 4 digits, except for 2-digit positive years which we assume are @@ -45,7 +62,7 @@ var MIN_MS, MAX_MS; * Note that we follow ISO 8601:2004: there *is* a year 0, which * is 1BC/BCE, and -1===2BC etc. * - * 2-digit to 4-digit year conversion, where to cut off? + * Where to cut off 2-digit years between 1900s and 2000s? * 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')): @@ -70,96 +87,36 @@ var MIN_MS, MAX_MS; exports.dateTime2ms = function(s) { // first check if s is a date object if(exports.isJSDate(s)) { - s = Number(s); + // Convert to the UTC milliseconds that give the same + // hours as this date has in the local timezone + s = Number(s) - s.getTimezoneOffset() * ONEMIN; if(s >= MIN_MS && s <= MAX_MS) return s; return BADNUM; } // otherwise only accept strings and numbers if(typeof s !== 'string' && typeof s !== 'number') return BADNUM; - var y, m, d, h; - // split date and time parts - // TODO: we strip leading/trailing whitespace but not other - // characters like we do for numbers - do we want to? - var datetime = String(s).trim().split(' '); - if(datetime.length > 2) return BADNUM; - - var p = datetime[0].split('-'); // date part - - var CE = true; // common era, ie positive year - if(p[0] === '') { - // first part is blank: year starts with a minus sign - CE = false; - p.splice(0, 1); + var match = String(s).match(DATETIME_REGEXP); + if(!match) return BADNUM; + var y = match[1], + m = Number(match[3] || 1), + d = Number(match[5] || 1), + H = Number(match[7] || 0), + M = Number(match[9] || 0), + S = Number(match[11] || 0); + if(y.length === 2) { + y = (Number(y) + 2000 - YFIRST) % 100 + YFIRST; } - - var plen = p.length; - if(plen > 3 || (plen !== 3 && datetime[1]) || !plen) return BADNUM; - - // year - if(p[0].length === 4) y = Number(p[0]); - else if(p[0].length === 2) { - if(!CE) return BADNUM; - var yNow = new Date().getFullYear(); - y = ((Number(p[0]) - yNow + 70) % 100 + 200) % 100 + yNow - 70; - } - else return BADNUM; - if(!isNumeric(y)) return BADNUM; + else y = Number(y); // javascript takes new Date(0..99,m,d) to mean 1900-1999, so // to support years 0-99 we need to use setFullYear explicitly - var baseDate = new Date(0, 0, 1); - baseDate.setFullYear(CE ? y : -y); - if(p.length > 1) { - - // month - may be 1 or 2 digits - m = Number(p[1]) - 1; // new Date() uses zero-based months - if(p[1].length > 2 || !(m >= 0 && m <= 11)) return BADNUM; - baseDate.setMonth(m); - - if(p.length > 2) { - - // day - may be 1 or 2 digits - d = Number(p[2]); - if(p[2].length > 2 || !(d >= 1 && d <= 31)) return BADNUM; - baseDate.setDate(d); - - // does that date exist in this month? - if(baseDate.getDate() !== d) return BADNUM; - - if(datetime[1]) { + var date = new Date(Date.UTC(2000, m - 1, d, H, M)); + date.setUTCFullYear(y); - p = datetime[1].split(':'); - if(p.length > 3) return BADNUM; + if(date.getUTCDate() !== d) return BADNUM; - // hour - may be 1 or 2 digits - h = Number(p[0]); - if(p[0].length > 2 || !p[0].length || !(h >= 0 && h <= 23)) return BADNUM; - baseDate.setHours(h); - - // does that hour exist in this day? (Daylight time!) - // (TODO: remove this check when we move to UTC) - if(baseDate.getHours() !== h) return BADNUM; - - if(p.length > 1) { - d = baseDate.getTime(); - - // minute - must be 2 digits - m = Number(p[1]); - if(p[1].length !== 2 || !(m >= 0 && m <= 59)) return BADNUM; - d += ONEMIN * m; - if(p.length === 2) return d; - - // second (and milliseconds) - must have 2-digit seconds - if(p[2].split('.')[0].length !== 2) return BADNUM; - s = Number(p[2]); - if(!(s >= 0 && s < 60)) return BADNUM; - return d + s * ONESEC; - } - } - } - } - return baseDate.getTime(); + return date.getTime() + S * ONESEC; }; MIN_MS = exports.MIN_MS = exports.dateTime2ms('-9999'); @@ -191,16 +148,41 @@ exports.ms2DateTime = function(ms, r) { if(!r) r = 0; - var d = new Date(Math.floor(ms)), - dateStr = d3.time.format('%Y-%m-%d')(d), + var msecTenths = Math.round(((ms % 1) + 1) * 10) % 10, + d = new Date(Math.round(ms - msecTenths / 10)), + dateStr = d3.time.format.utc('%Y-%m-%d')(d), // <90 days: add hours and minutes - never *only* add hours - h = (r < NINETYDAYS) ? d.getHours() : 0, - m = (r < NINETYDAYS) ? d.getMinutes() : 0, + h = (r < NINETYDAYS) ? d.getUTCHours() : 0, + m = (r < NINETYDAYS) ? d.getUTCMinutes() : 0, // <3 hours: add seconds - s = (r < THREEHOURS) ? d.getSeconds() : 0, + s = (r < THREEHOURS) ? d.getUTCSeconds() : 0, // <5 minutes: add ms (plus one extra digit, this is msec*10) - msec10 = (r < FIVEMIN) ? Math.round((d.getMilliseconds() + (((ms % 1) + 1) % 1)) * 10) : 0; + msec10 = (r < FIVEMIN) ? d.getUTCMilliseconds() * 10 + msecTenths : 0; + return includeTime(dateStr, h, m, s, msec10); +}; + +// For converting old-style milliseconds to date strings, +// we use the local timezone rather than UTC like we use +// everywhere else, both for backward compatibility and +// because that's how people mostly use javasript date objects. +// Clip one extra day off our date range though so we can't get +// thrown beyond the range by the timezone shift. +exports.ms2DateTimeLocal = function(ms) { + if(!(ms >= MIN_MS + ONEDAY && ms <= MAX_MS - ONEDAY)) return BADNUM; + + var msecTenths = Math.round(((ms % 1) + 1) * 10) % 10, + d = new Date(Math.round(ms - msecTenths / 10)), + dateStr = d3.time.format('%Y-%m-%d')(d), + h = d.getHours(), + m = d.getMinutes(), + s = d.getSeconds(), + msec10 = d.getUTCMilliseconds() * 10 + msecTenths; + + return includeTime(dateStr, h, m, s, msec10); +}; + +function includeTime(dateStr, h, m, s, msec10) { // include each part that has nonzero data in or after it if(h || m || s || msec10) { dateStr += ' ' + lpad(h, 2) + ':' + lpad(m, 2); @@ -217,7 +199,7 @@ exports.ms2DateTime = function(ms, r) { } } return dateStr; -}; +} // normalize date format to date string, in case it starts as // a Date object or milliseconds @@ -227,7 +209,7 @@ exports.cleanDate = function(v, dflt) { // NOTE: if someone puts in a year as a number rather than a string, // this will mistakenly convert it thinking it's milliseconds from 1970 // that is: '2012' -> Jan. 1, 2012, but 2012 -> 2012 epoch milliseconds - v = exports.ms2DateTime(+v); + v = exports.ms2DateTimeLocal(+v); if(!v && dflt !== undefined) return dflt; } else if(!exports.isDateTime(v)) { diff --git a/src/lib/index.js b/src/lib/index.js index 3239693a4d7..023896b0e80 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -28,6 +28,7 @@ var datesModule = require('./dates'); lib.dateTime2ms = datesModule.dateTime2ms; lib.isDateTime = datesModule.isDateTime; lib.ms2DateTime = datesModule.ms2DateTime; +lib.ms2DateTimeLocal = datesModule.ms2DateTimeLocal; lib.cleanDate = datesModule.cleanDate; lib.isJSDate = datesModule.isJSDate; lib.MIN_MS = datesModule.MIN_MS; diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index cb07516caae..559a5574f1c 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -931,9 +931,9 @@ axes.tickIncrement = function(x, dtick, axrev) { // Dates: months (or years) if(tType === 'M') { var y = new Date(x); - // is this browser consistent? setMonth edits a date but + // is this browser consistent? setUTCMonth edits a date but // returns that date's milliseconds - return y.setMonth(y.getMonth() + dtSigned); + return y.setUTCMonth(y.getUTCMonth() + dtSigned); } // Log scales: Linear, Digits @@ -984,9 +984,9 @@ axes.tickFirst = function(ax) { if(tType === 'M') { t0 = new Date(tick0); r0 = new Date(r0); - mdif = (r0.getFullYear() - t0.getFullYear()) * 12 + - r0.getMonth() - t0.getMonth(); - t1 = t0.setMonth(t0.getMonth() + + mdif = (r0.getUTCFullYear() - t0.getUTCFullYear()) * 12 + + r0.getUTCMonth() - t0.getUTCMonth(); + t1 = t0.setUTCMonth(t0.getUTCMonth() + (Math.round(mdif / dtNum) + (axrev ? 1 : -1)) * dtNum); while(axrev ? t1 > r0 : t1 < r0) { @@ -1010,12 +1010,13 @@ axes.tickFirst = function(ax) { else throw 'unrecognized dtick ' + String(dtick); }; -var yearFormat = d3.time.format('%Y'), - monthFormat = d3.time.format('%b %Y'), - dayFormat = d3.time.format('%b %-d'), - yearMonthDayFormat = d3.time.format('%b %-d, %Y'), - minuteFormat = d3.time.format('%H:%M'), - secondFormat = d3.time.format(':%S'); +var utcFormat = d3.time.format.utc, + yearFormat = utcFormat('%Y'), + monthFormat = utcFormat('%b %Y'), + dayFormat = utcFormat('%b %-d'), + yearMonthDayFormat = utcFormat('%b %-d, %Y'), + minuteFormat = utcFormat('%H:%M'), + secondFormat = utcFormat(':%S'); // add one item to d3's vocabulary: // %{n}f where n is the max number of digits @@ -1028,10 +1029,10 @@ function modDateFormat(fmt, x) { var digits = Math.min(+fm[1] || 6, 6), fracSecs = String((x / 1000 % 1) + 2.0000005) .substr(2, digits).replace(/0+$/, '') || '0'; - return d3.time.format(fmt.replace(fracMatch, fracSecs))(d); + return utcFormat(fmt.replace(fracMatch, fracSecs))(d); } else { - return d3.time.format(fmt)(d); + return utcFormat(fmt)(d); } } diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index 0df91d1ec77..2927becef5a 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -298,16 +298,18 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { function zoomAxRanges(axList, r0Fraction, r1Fraction) { var i, axi, - axRangeLinear; + axRangeLinear0, + axRangeLinearSpan; for(i = 0; i < axList.length; i++) { axi = axList[i]; if(axi.fixedrange) continue; - axRangeLinear = axi.range.map(axi.r2l); + axRangeLinear0 = axi._rl[0]; + axRangeLinearSpan = axi._rl[1] - axRangeLinear0; axi.range = [ - axi.l2r(axRangeLinear[0] + (axRangeLinear[1] - axRangeLinear[0]) * r0Fraction), - axi.l2r(axRangeLinear[0] + (axRangeLinear[1] - axRangeLinear[0]) * r1Fraction) + axi.l2r(axRangeLinear0 + axRangeLinearSpan * r0Fraction), + axi.l2r(axRangeLinear0 + axRangeLinearSpan * r1Fraction) ]; } } diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index 69a24545b40..377c638ecf5 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -265,10 +265,10 @@ module.exports = function setConvert(ax) { // NOTE: Changed this behavior: previously we took any numeric value // to be a ms, even if it was a string that could be a bare year. // Now we convert it as a date if at all possible, and only try - // as ms if that fails. + // as (local) ms if that fails. var ms = Lib.dateTime2ms(v); if(ms === BADNUM) { - if(isNumeric(v)) ms = Number(v); + if(isNumeric(v)) ms = Lib.dateTime2ms(new Date(v)); else return BADNUM; } return Lib.constrain(ms, Lib.MIN_MS, Lib.MAX_MS); diff --git a/src/plots/plots.js b/src/plots/plots.js index b8003c66c81..3ad048addd5 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1390,7 +1390,7 @@ plots.graphJson = function(gd, dataonly, mode, output, useDefaults) { // convert native dates to date strings... // mostly for external users exporting to plotly - if(Lib.isJSDate(d)) return Lib.ms2DateTime(+d); + if(Lib.isJSDate(d)) return Lib.ms2DateTimeLocal(+d); return d; } diff --git a/test/image/baselines/date_axes.png b/test/image/baselines/date_axes.png index 7d330eadcb2..bc7a8e367bc 100644 Binary files a/test/image/baselines/date_axes.png and b/test/image/baselines/date_axes.png differ diff --git a/test/image/mocks/date_axes.json b/test/image/mocks/date_axes.json index 4c1cc88be49..b053e29c3fc 100644 --- a/test/image/mocks/date_axes.json +++ b/test/image/mocks/date_axes.json @@ -2,16 +2,89 @@ "data": [ { "x": [ - "2013-10-04 22:23:00", - "2013-11-04 22:23:00", - "2013-12-04 22:23:00" + "1900-01-01", + "2000-01-01", + "2100-01-01" ], - "y": [ - 1, - 3, - 6 + "y": [1, 3, 2] + }, + { + "x": [ + "2013-05-01", + "2013-09-01", + "2014-01-01" + ], + "y": [1, 3, 2], + "xaxis": "x2", + "yaxis": "y2" + }, + { + "x": [ + "2013-11-17", + "2013-12-15", + "2014-01-12" + ], + "y": [1, 3, 2], + "xaxis": "x3", + "yaxis": "y3" + }, + { + "x": [ + "2013-01-01", + "2013-01-02", + "2013-01-03" + ], + "y": [1, 3, 2], + "xaxis": "x4", + "yaxis": "y4" + }, + { + "x": [ + "2013-07-01 18:00", + "2013-07-02 00:00", + "2013-07-02 06:00" + ], + "y": [1, 3, 2], + "xaxis": "x5", + "yaxis": "y5" + }, + { + "x": [ + "2013-01-01 23:59", + "2013-01-02 00:00", + "2013-01-02 00:01" + ], + "y": [1, 3, 2], + "xaxis": "x6", + "yaxis": "y6" + }, + { + "x": [ + "2013-07-01 23:59:59", + "2013-07-02 00:00:00", + "2013-07-02 00:00:01" ], - "type": "scatter" + "y": [1, 3, 2], + "xaxis": "x7", + "yaxis": "y7" } - ] + ], + "layout": { + "showlegend": false, + "width": 600, + "height": 500, + "yaxis": {"domain": [0, 0.04]}, + "yaxis2": {"domain": [0.16, 0.2]}, + "yaxis3": {"domain": [0.32, 0.36]}, + "yaxis4": {"domain": [0.48, 0.52]}, + "yaxis5": {"domain": [0.64, 0.68]}, + "yaxis6": {"domain": [0.80, 0.84]}, + "yaxis7": {"domain": [0.96, 1]}, + "xaxis2": {"anchor": "y2"}, + "xaxis3": {"anchor": "y3"}, + "xaxis4": {"anchor": "y4"}, + "xaxis5": {"anchor": "y5"}, + "xaxis6": {"anchor": "y6"}, + "xaxis7": {"anchor": "y7"} + } } diff --git a/test/jasmine/tests/annotations_test.js b/test/jasmine/tests/annotations_test.js index cf6e282f6da..eeac5f13c9a 100644 --- a/test/jasmine/tests/annotations_test.js +++ b/test/jasmine/tests/annotations_test.js @@ -3,7 +3,6 @@ var Annotations = require('@src/components/annotations'); var Plotly = require('@lib/index'); var Plots = require('@src/plots/plots'); var Lib = require('@src/lib'); -var Dates = require('@src/lib/dates'); var Axes = require('@src/plots/cartesian/axes'); var d3 = require('d3'); @@ -71,7 +70,10 @@ describe('Test annotations', function() { axref: 'x', ayref: 'y', x: '2008-07-01', - ax: Dates.dateTime2ms('2004-07-01'), + // note this is not portable: this generates ms in the local + // timezone, so will work correctly where it was created but + // not if the milliseconds number is moved to another TZ + ax: +(new Date(2004, 6, 1)), y: 0, ay: 50 }] diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 897e9512626..7378c50b376 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -678,7 +678,7 @@ describe('Test axes', function() { it('should handle tick0 and dtick for date axes', function() { var someMs = 123456789, - someMsDate = Lib.ms2DateTime(someMs), + someMsDate = Lib.ms2DateTimeLocal(someMs), oneDay = 24 * 3600 * 1000, axIn = {tick0: someMs, dtick: String(3 * oneDay)}, axOut = {}; @@ -1402,7 +1402,7 @@ describe('Test axes', function() { } function mockHoverText(ax, x) { - var xCalc = (ax.d2l_noadd || ax.d2c)(x); + var xCalc = (ax.d2l_noadd || ax.d2l)(x); var tickTextObj = Axes.tickText(ax, xCalc, true); return tickTextObj.text; } @@ -1433,7 +1433,7 @@ describe('Test axes', function() { 'Feb 12' ]; expect(textOut).toEqual(expectedText); - expect(mockHoverText(ax, ax.d2c('1999-12-18 15:34:33.3'))) + expect(mockHoverText(ax, '1999-12-18 15:34:33.3')) .toBe('Dec 18, 1999, 15:34'); ax = { @@ -1454,7 +1454,7 @@ describe('Test axes', function() { '00:00
Jan 6, 2000' ]; expect(textOut).toEqual(expectedText); - expect(mockHoverText(ax, ax.d2c('2000-01-04 15:34:33.3'))) + expect(mockHoverText(ax, '2000-01-04 15:34:33.3')) .toBe('Jan 4, 2000, 15:34:33'); ax = { @@ -1475,9 +1475,9 @@ describe('Test axes', function() { '00:00:02' ]; expect(textOut).toEqual(expectedText); - expect(mockHoverText(ax, ax.d2c('2000-02-04 00:00:00.123456'))) + expect(mockHoverText(ax, '2000-02-04 00:00:00.123456')) .toBe('Feb 4, 2000, 00:00:00.1235'); - expect(mockHoverText(ax, ax.d2c('2000-02-04 00:00:00'))) + expect(mockHoverText(ax, '2000-02-04 00:00:00')) .toBe('Feb 4, 2000'); }); @@ -1500,9 +1500,9 @@ describe('Test axes', function() { '00:05
Feb 12, 2000' ]; expect(textOut).toEqual(expectedText); - expect(mockHoverText(ax, ax.d2c('2000-02-04 00:00:00.123456'))) + expect(mockHoverText(ax, '2000-02-04 00:00:00.123456')) .toBe('Feb 4, 2000'); - expect(mockHoverText(ax, ax.d2c('2000-02-04 00:00:05.123456'))) + expect(mockHoverText(ax, '2000-02-04 00:00:05.123456')) .toBe('Feb 4, 2000, 00:00:05'); }); @@ -1559,9 +1559,9 @@ describe('Test axes', function() { '00:00:01
Jan 1, 2013' ]; expect(textOut).toEqual(expectedText); - expect(mockHoverText(ax, ax.d2c('2012-01-01'))) + expect(mockHoverText(ax, '2012-01-01')) .toBe('New year'); - expect(mockHoverText(ax, ax.d2c('2012-01-01 12:34:56.1234'))) + expect(mockHoverText(ax, '2012-01-01 12:34:56.1234')) .toBe('Jan 1, 2012, 12:34:56'); }); @@ -1587,8 +1587,8 @@ describe('Test axes', function() { // 10 and 0.1 are off scale ]; expect(textOut).toEqual(expectedText, axType); - expect(mockHoverText(ax, ax.c2l(1))).toBe('One'); - expect(mockHoverText(ax, ax.c2l(19.999))).toBe('19.999'); + expect(mockHoverText(ax, 1)).toBe('One'); + expect(mockHoverText(ax, 19.999)).toBe('19.999'); }); }); diff --git a/test/jasmine/tests/histogram2d_test.js b/test/jasmine/tests/histogram2d_test.js index 45620c68a92..51ad0c4ae4d 100644 --- a/test/jasmine/tests/histogram2d_test.js +++ b/test/jasmine/tests/histogram2d_test.js @@ -76,9 +76,7 @@ describe('Test histogram2d', function() { } // remove tzJan/tzJuly when we move to UTC - var oneDay = 24 * 3600000, - tzJan = (new Date(1970, 0, 1)).getTimezoneOffset(), - tzJuly = (new Date(1970, 6, 1)).getTimezoneOffset(); + var oneDay = 24 * 3600000; it('should handle both uniform and nonuniform date bins', function() { var out = _calc({ @@ -97,7 +95,7 @@ describe('Test histogram2d', function() { // lets also make it display the bins with nonuniform size, // and ensure we don't generate an extra bin on the end (see // first row of z below) - expect(out.y0).toBe(tzJan === tzJuly ? '1969-07-02 14:24' : '1969-07-02 15:24'); + expect(out.y0).toBe('1969-07-02 14:24'); expect(out.dy).toBe(365.2 * oneDay); expect(out.z).toEqual([ diff --git a/test/jasmine/tests/histogram_test.js b/test/jasmine/tests/histogram_test.js index 5263835f67f..7305c8a1d37 100644 --- a/test/jasmine/tests/histogram_test.js +++ b/test/jasmine/tests/histogram_test.js @@ -146,9 +146,7 @@ describe('Test histogram', function() { return out; } - // remove tzOffset when we move to UTC - var tzOffset = (new Date(1970, 0, 1)).getTimezoneOffset() * 60000, - oneDay = 24 * 3600000; + var oneDay = 24 * 3600000; it('should handle auto dates with nonuniform (month) bins', function() { var out = _calc({ @@ -165,7 +163,7 @@ describe('Test histogram', function() { // bars. Now that we have explicit per-bar positioning, perhaps // we should fill the space, rather than insisting on equal-width // bars? - var x0 = tzOffset + 15768000000, + var x0 = 15768000000, x1 = x0 + oneDay * 365, x2 = x1 + oneDay * 365.5, x3 = x2 + oneDay * 365.5; @@ -186,7 +184,7 @@ describe('Test histogram', function() { nbinsx: 4 }); - var x0 = tzOffset, + var x0 = 0, x1 = x0 + oneDay, x2 = x1 + oneDay, x3 = x2 + oneDay; diff --git a/test/jasmine/tests/lib_date_test.js b/test/jasmine/tests/lib_date_test.js index f4771c539a4..a73f79c7b25 100644 --- a/test/jasmine/tests/lib_date_test.js +++ b/test/jasmine/tests/lib_date_test.js @@ -18,6 +18,7 @@ describe('dates', function() { describe('dateTime2ms', function() { it('should accept valid date strings', function() { + var tzOffset; [ ['2016', new Date(2016, 0, 1)], @@ -29,20 +30,38 @@ describe('dates', function() { ['0122-04-08 08:22', new Date(122, 3, 8, 8, 22)], ['-0098-11-19 23:59:59', new Date(-98, 10, 19, 23, 59, 59)], ['-9730-12-01 12:34:56.789', new Date(-9730, 11, 1, 12, 34, 56, 789)], + // random whitespace before and after gets stripped + ['\r\n\t -9730-12-01 12:34:56.789\r\n\t ', new Date(-9730, 11, 1, 12, 34, 56, 789)], // first century, also allow month, day, and hour to be 1-digit, and not all // three digits of milliseconds ['0013-1-1 1:00:00.6', d1c], - // we support more than 4 digits too, though Date objects don't. More than that + // we support tenths of msec too, though Date objects don't. Smaller than that // and we hit the precision limit of js numbers unless we're close to the epoch. // It won't break though. ['0013-1-1 1:00:00.6001', +d1c + 0.1], + ['0013-1-1 1:00:00.60011111111', +d1c + 0.11111111], // 2-digit years get mapped to now-70 -> now+29 [thisYear_2 + '-05', new Date(thisYear, 4, 1)], [nowMinus70_2 + '-10-18', new Date(nowMinus70, 9, 18)], - [nowPlus29_2 + '-02-12 14:29:32', new Date(nowPlus29, 1, 12, 14, 29, 32)] + [nowPlus29_2 + '-02-12 14:29:32', new Date(nowPlus29, 1, 12, 14, 29, 32)], + + // including timezone info (that we discard) + ['2014-03-04 08:15Z', new Date(2014, 2, 4, 8, 15)], + ['2014-03-04 08:15:00.00z', new Date(2014, 2, 4, 8, 15)], + ['2014-03-04 08:15:34+1200', new Date(2014, 2, 4, 8, 15, 34)], + ['2014-03-04 08:15:34.567-05:45', new Date(2014, 2, 4, 8, 15, 34, 567)], ].forEach(function(v) { - expect(Lib.dateTime2ms(v[0])).toBe(+v[1], v[0]); + // just for sub-millisecond precision tests, use timezoneoffset + // from the previous date object + if(v[1].getTimezoneOffset) tzOffset = v[1].getTimezoneOffset(); + + var expected = +v[1] - (tzOffset * 60000); + expect(Lib.dateTime2ms(v[0])).toBe(expected, v[0]); + + // ISO-8601: all the same stuff with t or T as the separator + expect(Lib.dateTime2ms(v[0].trim().replace(' ', 't'))).toBe(expected, v[0].trim().replace(' ', 't')); + expect(Lib.dateTime2ms('\r\n\t ' + v[0].trim().replace(' ', 'T') + '\r\n\t ')).toBe(expected, v[0].trim().replace(' ', 'T')); }); }); @@ -57,7 +76,7 @@ describe('dates', function() { [ 1000, 9999, -1000, -9999 ].forEach(function(v) { - expect(Lib.dateTime2ms(v)).toBe(+(new Date(v, 0, 1)), v); + expect(Lib.dateTime2ms(v)).toBe(Date.UTC(v, 0, 1), v); }); [ @@ -66,7 +85,7 @@ describe('dates', function() { [nowMinus70_2, nowMinus70], [99, 1999] ].forEach(function(v) { - expect(Lib.dateTime2ms(v[0])).toBe(+(new Date(v[1], 0, 1)), v[0]); + expect(Lib.dateTime2ms(v[0])).toBe(Date.UTC(v[1], 0, 1), v[0]); }); }); @@ -81,7 +100,7 @@ describe('dates', function() { d1c, new Date(2015, 8, 7, 23, 34, 45, 567) ].forEach(function(v) { - expect(Lib.dateTime2ms(v)).toBe(+v); + expect(Lib.dateTime2ms(v)).toBe(+v - v.getTimezoneOffset() * 60000); }); }); @@ -105,11 +124,40 @@ describe('dates', function() { '2015-01-00', '2015-01-32', '2015-02-29', '2015-04-31', '2015-01-001', // bad day (incl non-leap year) '2015-01-01 24:00', '2015-01-01 -1:00', '2015-01-01 001:00', // bad hour '2015-01-01 12:60', '2015-01-01 12:-1', '2015-01-01 12:001', '2015-01-01 12:1', // bad minute - '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 + '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 + '2015-01-01T', '2015-01-01TT12:34', // bad ISO separators + '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 ].forEach(function(v) { expect(Lib.dateTime2ms(v)).toBeUndefined(v); }); }); + + var JULY1MS = 181 * 24 * 3600 * 1000; + + it('should use UTC with no timezone offset or daylight saving time', function() { + expect(Lib.dateTime2ms('1970-01-01')).toBe(0); + + // 181 days (and no DST hours) between jan 1 and july 1 in a non-leap-year + // 31 + 28 + 31 + 30 + 31 + 30 + expect(Lib.dateTime2ms('1970-07-01')).toBe(JULY1MS); + }); + + it('should interpret JS dates by local time, not by its getTime()', function() { + // not really part of the test, just to make sure the test is meaningful + // the test should NOT be run in a UTC environment + var local0 = Number(new Date(1970, 0, 1)), + localjuly1 = Number(new Date(1970, 6, 1)); + expect([local0, localjuly1]).not.toEqual([0, JULY1MS], + 'test must not run in UTC'); + // verify that there *is* daylight saving time in the test environment + expect(localjuly1 - local0).not.toEqual(JULY1MS - 0, + 'test must run in a timezone with DST'); + + // now repeat the previous test and show that we throw away + // timezone info from js dates + expect(Lib.dateTime2ms(new Date(1970, 0, 1))).toBe(0); + expect(Lib.dateTime2ms(new Date(1970, 6, 1))).toBe(JULY1MS); + }); }); describe('ms2DateTime', function() { @@ -145,8 +193,8 @@ describe('dates', function() { it('should not accept Date objects beyond our limits or other objects', function() { [ - +(new Date(10000, 0, 1)), - +(new Date(-10000, 11, 31, 23, 59, 59, 999)), + Date.UTC(10000, 0, 1), + Date.UTC(-10000, 11, 31, 23, 59, 59, 999), '', '2016-01-01', '0', @@ -177,19 +225,20 @@ describe('dates', function() { }); describe('cleanDate', function() { - it('should convert any number or js Date within range to a date string', function() { + it('should convert numbers or js Dates to strings based on local TZ', function() { [ new Date(0), new Date(2000), new Date(2000, 0, 1), new Date(), - new Date(-9999, 0, 1), - new Date(9999, 11, 31, 23, 59, 59, 999) + new Date(-9999, 0, 3), // we lose one day of range +/- tzoffset this way + new Date(9999, 11, 29, 23, 59, 59, 999) ].forEach(function(v) { - expect(typeof Lib.ms2DateTime(+v)).toBe('string'); - expect(Lib.cleanDate(v)).toBe(Lib.ms2DateTime(+v)); - expect(Lib.cleanDate(+v)).toBe(Lib.ms2DateTime(+v)); - expect(Lib.cleanDate(v, '2000-01-01')).toBe(Lib.ms2DateTime(+v)); + var expected = Lib.ms2DateTime(Lib.dateTime2ms(v)); + expect(typeof expected).toBe('string'); + expect(Lib.cleanDate(v)).toBe(expected); + expect(Lib.cleanDate(+v)).toBe(expected); + expect(Lib.cleanDate(v, '2000-01-01')).toBe(expected); }); }); diff --git a/test/jasmine/tests/shapes_test.js b/test/jasmine/tests/shapes_test.js index 006675bd490..43714ebd8df 100644 --- a/test/jasmine/tests/shapes_test.js +++ b/test/jasmine/tests/shapes_test.js @@ -489,7 +489,7 @@ describe('Test shapes', function() { title: 'linked to date and category axes', xaxis: { type: 'date', - range: ['2000-01-01', (new Date(2000, 1, 2)).getTime()] + range: ['2000-01-01', '2000-02-02'] }, yaxis: { type: 'category', range: ['a', 'b'] } }