diff --git a/src/util/number.ts b/src/util/number.ts index 662695f7f7..b58ecd7b85 100644 --- a/src/util/number.ts +++ b/src/util/number.ts @@ -482,12 +482,14 @@ export function isRadianAroundZero(val: number): boolean { // eslint-disable-next-line const TIME_REG = /^(?:(\d{4})(?:[-\/](\d{1,2})(?:[-\/](\d{1,2})(?:[T ](\d{1,2})(?::(\d{1,2})(?::(\d{1,2})(?:[.,](\d+))?)?)?(Z|[\+\-]\d\d:?\d\d)?)?)?)?)?$/; // jshint ignore:line +const ISO_WEEK_REG = /^(\d{4})-W(\d{2})$/; /** * @param value valid type: number | string | Date, otherwise return `new Date(NaN)` * These values can be accepted: * + An instance of Date, represent a time in its own time zone. * + Or string in a subset of ISO 8601, only including: + * + ISO week: '2026-W04', representing Monday at local midnight, * + only year, month, date: '2012-03', '2012-03-01', '2012-03-01 05', '2012-03-01 05:06', * + separated with T or space: '2012-03-01T12:22:33.123', '2012-03-01 12:22:33.123', * + time zone: '2012-03-01T12:22:33Z', '2012-03-01T12:22:33+8000', '2012-03-01T12:22:33-05:00', @@ -504,6 +506,24 @@ export function parseDate(value: unknown): Date { return value; } else if (zrUtil.isString(value)) { + const weekMatch = ISO_WEEK_REG.exec(value); + if (weekMatch) { + const year = +weekMatch[1]; + const week = +weekMatch[2]; + if (week < 1 || week > 53) { + return new Date(NaN); + } + const date = new Date(0); + date.setFullYear(year, 0, 4); + date.setHours(0, 0, 0, 0); + // ISO week 1 contains January 4; its Thursday is in the ISO week year. + date.setDate(4 - (date.getDay() + 6) % 7 + (week - 1) * 7 + 3); + if (date.getFullYear() !== year) { + return new Date(NaN); + } + date.setDate(date.getDate() - 3); + return date; + } // Different browsers parse date in different way, so we parse it manually. // Some other issues: // new Date('1970-01-01') is UTC, diff --git a/test/ut/spec/scale/time.test.ts b/test/ut/spec/scale/time.test.ts new file mode 100644 index 0000000000..a68a50e215 --- /dev/null +++ b/test/ut/spec/scale/time.test.ts @@ -0,0 +1,69 @@ + +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import { createChart, getECModel } from '../../core/utHelper'; +import { parseDate } from '../../../../src/util/number'; +import { EChartsType } from '../../../../src/echarts'; + + +describe('scale/time', function () { + let chart: EChartsType; + + beforeEach(function () { + chart = createChart(); + }); + + afterEach(function () { + chart.dispose(); + }); + + it('uses ISO weeks in time series and dataZoom bounds', function () { + chart.setOption({ + animation: false, + xAxis: { type: 'time' }, + yAxis: { type: 'value' }, + dataZoom: [{ type: 'slider', xAxisIndex: 0 }], + series: [{ + type: 'line', + data: [ + ['2020-W52', 1], + ['2020-W53', 2], + ['2021-W01', 3], + ['2021-W02', 4] + ] + }] + }); + const model = getECModel(chart); + expect(model.getSeriesByIndex(0).getData().count()).toEqual(4); + const pixel = chart.convertToPixel('grid', ['2020-W53', 2]); + expect(isFinite(pixel[0])).toEqual(true); + expect(isFinite(pixel[1])).toEqual(true); + + chart.dispatchAction({ + type: 'dataZoom', + startValue: '2020-W53', + endValue: '2021-W01' + }); + const data = model.getSeriesByIndex(0).getData(); + expect(data.count()).toEqual(2); + expect(data.get('x', 0)).toEqual(+parseDate('2020-12-28')); + expect(data.get('x', 1)).toEqual(+parseDate('2021-01-04')); + }); +}); diff --git a/test/ut/spec/util/number.test.ts b/test/ut/spec/util/number.test.ts index 2c26932148..2ea0f70c82 100755 --- a/test/ut/spec/util/number.test.ts +++ b/test/ut/spec/util/number.test.ts @@ -237,6 +237,36 @@ describe('util/number', function () { }); describe('parseDate', function () { + it('parses ISO weeks as the local Monday', function () { + [ + ['2026-W04', '2026-01-19'], + ['2020-W01', '2019-12-30'], + ['2020-W53', '2020-12-28'], + ['2021-W01', '2021-01-04'], + ['2026-W53', '2026-12-28'], + ['2026-W13', '2026-03-23'], + ['2026-W44', '2026-10-26'] + ].forEach(function (pair) { + expect(+parseDate(pair[0])).toEqual(+parseDate(pair[1])); + expect(parseDate(pair[0]).getDay()).toEqual(1); + expect(parseDate(pair[0]).getHours()).toEqual(0); + }); + }); + + it('rejects invalid ISO weeks', function () { + ['2021-W53', '2026-W00', '2026-W54', '2026-W4', '2026-w04', + '2026-W04extra', '2026-W04-1'].forEach(function (value) { + expect('' + parseDate(value)).toEqual('Invalid Date'); + }); + }); + + it('does not interpret ISO week years below 100 as 1900-based years', function () { + const date = parseDate('0099-W01'); + expect(date.getFullYear()).toEqual(98); + expect(date.getMonth()).toEqual(11); + expect(date.getDate()).toEqual(29); + }); + it('parseDate', function () { // Invalid Date