Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix ISO-8601 timezone parsing issue #3421

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontend/src/metabase/lib/time.js
@@ -1,11 +1,11 @@
import moment from "moment";

// only attempt to parse the timezone if we're sure we have one (either Z or ±hh:mm)
// only attempt to parse the timezone if we're sure we have one (either Z or ±hh:mm or +-hhmm)
// moment normally interprets the DD in YYYY-MM-DD as an offset :-/
export function parseTimestamp(value, unit) {
if (moment.isMoment(value)) {
return value;
} else if (typeof value === "string" && /(Z|[+-]\d\d:\d\d)$/.test(value)) {
} else if (typeof value === "string" && /(Z|[+-]\d\d:?\d\d)$/.test(value)) {
return moment.parseZone(value);
} else if (unit === "year") {
// workaround for https://github.com/metabase/metabase/issues/1992
Expand Down
38 changes: 38 additions & 0 deletions frontend/test/unit/lib/time.spec.js
@@ -0,0 +1,38 @@
import { parseTimestamp } from 'metabase/lib/time';
import moment from 'moment';

describe('time', () => {
describe('parseTimestamp', () => {
const NY15_TOKYO = moment(1420038000000); // 2014-12-31 15:00 UTC
const NY15_UTC = moment(1420070400000); // 2015-01-01 00:00 UTC
const NY15_LA = moment(1420099200000); // 2015-01-01 00:00 UTC


const TEST_CASES = [
['2015-01-01T00:00:00.000Z', 0, NY15_UTC],
['2015-01-01T00:00:00.000+00:00', 0, NY15_UTC],
['2015-01-01T00:00:00.000+0000', 0, NY15_UTC],
['2015-01-01T00:00:00Z', 0, NY15_UTC],

['2015-01-01T00:00:00.000+09:00', 540, NY15_TOKYO],
['2015-01-01T00:00:00.000+0900', 540, NY15_TOKYO],
['2015-01-01T00:00:00+09:00', 540, NY15_TOKYO],
['2015-01-01T00:00:00+0900', 540, NY15_TOKYO],

['2015-01-01T00:00:00.000-08:00', -480, NY15_LA],
['2015-01-01T00:00:00.000-0800', -480, NY15_LA],
['2015-01-01T00:00:00-08:00', -480, NY15_LA],
['2015-01-01T00:00:00-0800', -480, NY15_LA]]

TEST_CASES.map(([str, expectedOffset, expectedMoment]) => {
it(str + ' should be parsed as moment reprsenting' + expectedMoment + "with the offset " + expectedOffset, () => {
let result = parseTimestamp(str);

expect(moment.isMoment(result)).toBe(true);
expect(result.utcOffset()).toBe(expectedOffset);
expect(result.unix()).toEqual(expectedMoment.unix());
});
});

});
});
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -32,7 +32,7 @@
"isomorphic-fetch": "^2.2.1",
"js-cookie": "^2.1.2",
"leaflet": "^0.7.7",
"moment": "^2.12.0",
"moment": "2.14.1",
"node-libs-browser": "^0.5.3",
"normalizr": "^2.0.0",
"number-to-locale-string": "^1.0.1",
Expand Down
8 changes: 4 additions & 4 deletions src/metabase/driver/druid/query_processor.clj
Expand Up @@ -194,12 +194,12 @@
;; don't try to make this a ^:const map -- extract:timeFormat looks up timezone info at query time
(defn- unit->extraction-fn [unit]
(case unit
:default (extract:timeFormat "yyyy-MM-dd'T'HH:mm:ssZ")
:minute (extract:timeFormat "yyyy-MM-dd'T'HH:mm:00Z")
:default (extract:timeFormat "yyyy-MM-dd'T'HH:mm:ssZZ")
:minute (extract:timeFormat "yyyy-MM-dd'T'HH:mm:00ZZ")
:minute-of-hour (extract:timeFormat "mm")
:hour (extract:timeFormat "yyyy-MM-dd'T'HH:00:00Z")
:hour (extract:timeFormat "yyyy-MM-dd'T'HH:00:00ZZ")
:hour-of-day (extract:timeFormat "HH")
:day (extract:timeFormat "yyyy-MM-ddZ")
:day (extract:timeFormat "yyyy-MM-ddZZ")
:day-of-week (extract:js "function (timestamp) {"
" var date = new Date(timestamp);"
" return date.getDay() + 1;"
Expand Down
40 changes: 20 additions & 20 deletions test/metabase/timeseries_query_processor_test.clj
Expand Up @@ -388,11 +388,11 @@
;;; date bucketing - default (day)
(expect-with-timeseries-dbs
{:columns ["timestamp" "count"]
:rows [["2013-01-03+0000" 1]
["2013-01-10+0000" 1]
["2013-01-19+0000" 1]
["2013-01-22+0000" 1]
["2013-01-23+0000" 1]]}
:rows [["2013-01-03+00:00" 1]
["2013-01-10+00:00" 1]
["2013-01-19+00:00" 1]
["2013-01-22+00:00" 1]
["2013-01-23+00:00" 1]]}
(data (data/run-query checkins
(ql/aggregation (ql/count))
(ql/breakout $timestamp)
Expand All @@ -401,11 +401,11 @@
;;; date bucketing - minute
(expect-with-timeseries-dbs
{:columns ["timestamp" "count"]
:rows [["2013-01-03T08:00:00+0000" 1]
["2013-01-10T08:00:00+0000" 1]
["2013-01-19T08:00:00+0000" 1]
["2013-01-22T08:00:00+0000" 1]
["2013-01-23T08:00:00+0000" 1]]}
:rows [["2013-01-03T08:00:00+00:00" 1]
["2013-01-10T08:00:00+00:00" 1]
["2013-01-19T08:00:00+00:00" 1]
["2013-01-22T08:00:00+00:00" 1]
["2013-01-23T08:00:00+00:00" 1]]}
(data (data/run-query checkins
(ql/aggregation (ql/count))
(ql/breakout (ql/datetime-field $timestamp :minute))
Expand All @@ -423,11 +423,11 @@
;;; date bucketing - hour
(expect-with-timeseries-dbs
{:columns ["timestamp" "count"]
:rows [["2013-01-03T08:00:00+0000" 1]
["2013-01-10T08:00:00+0000" 1]
["2013-01-19T08:00:00+0000" 1]
["2013-01-22T08:00:00+0000" 1]
["2013-01-23T08:00:00+0000" 1]]}
:rows [["2013-01-03T08:00:00+00:00" 1]
["2013-01-10T08:00:00+00:00" 1]
["2013-01-19T08:00:00+00:00" 1]
["2013-01-22T08:00:00+00:00" 1]
["2013-01-23T08:00:00+00:00" 1]]}
(data (data/run-query checkins
(ql/aggregation (ql/count))
(ql/breakout (ql/datetime-field $timestamp :hour))
Expand Down Expand Up @@ -459,11 +459,11 @@
;;; date bucketing - day
(expect-with-timeseries-dbs
{:columns ["timestamp" "count"]
:rows [["2013-01-03+0000" 1]
["2013-01-10+0000" 1]
["2013-01-19+0000" 1]
["2013-01-22+0000" 1]
["2013-01-23+0000" 1]]}
:rows [["2013-01-03+00:00" 1]
["2013-01-10+00:00" 1]
["2013-01-19+00:00" 1]
["2013-01-22+00:00" 1]
["2013-01-23+00:00" 1]]}
(data (data/run-query checkins
(ql/aggregation (ql/count))
(ql/breakout (ql/datetime-field $timestamp :day))
Expand Down