diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/TimeFormatsForGranularity.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/TimeFormatsForGranularity.ts new file mode 100644 index 0000000000..7fdb07a4a3 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/TimeFormatsForGranularity.ts @@ -0,0 +1,30 @@ +import TimeFormats from './TimeFormats'; +import { TimeGranularity } from './types'; + +const { DATABASE_DATE, DATABASE_DATETIME } = TimeFormats; +const MINUTE = '%Y-%m-%d %H:%M'; + +/** + * Map time granularity to d3-format string + */ +const TimeFormatsForGranularity: Record = { + [TimeGranularity.DATE]: DATABASE_DATE, + [TimeGranularity.SECOND]: DATABASE_DATETIME, + [TimeGranularity.MINUTE]: MINUTE, + [TimeGranularity.FIVE_MINUTES]: MINUTE, + [TimeGranularity.TEN_MINUTES]: MINUTE, + [TimeGranularity.FIFTEEN_MINUTES]: MINUTE, + [TimeGranularity.HALF_HOUR]: MINUTE, + [TimeGranularity.HOUR]: '%Y-%m-%d %H:00', + [TimeGranularity.DAY]: DATABASE_DATE, + [TimeGranularity.WEEK]: DATABASE_DATE, + [TimeGranularity.MONTH]: '%b %Y', + [TimeGranularity.QUARTER]: '%Y Q%q', + [TimeGranularity.YEAR]: '%Y', + [TimeGranularity.WEEK_STARTING_SUNDAY]: DATABASE_DATE, + [TimeGranularity.WEEK_STARTING_MONDAY]: DATABASE_DATE, + [TimeGranularity.WEEK_ENDING_SATURDAY]: DATABASE_DATE, + [TimeGranularity.WEEK_ENDING_SUNDAY]: DATABASE_DATE, +}; + +export default TimeFormatsForGranularity; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/TimeFormatter.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/TimeFormatter.ts index 0896ea05ad..a49b1fec82 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/TimeFormatter.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/TimeFormatter.ts @@ -1,5 +1,6 @@ import { ExtensibleFunction, isRequired } from '@superset-ui/core'; import { TimeFormatFunction } from './types'; +import stringifyTimeInput from './utils/stringifyTimeInput'; export const PREVIEW_TIME = new Date(Date.UTC(2017, 1, 14, 11, 22, 33)); @@ -45,11 +46,7 @@ class TimeFormatter extends ExtensibleFunction { } format(value: Date | number | null | undefined) { - if (value === null || value === undefined) { - return `${value}`; - } - - return this.formatFunc(value instanceof Date ? value : new Date(value)); + return stringifyTimeInput(value, time => this.formatFunc(time)); } preview(value: Date = PREVIEW_TIME) { diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/TimeFormatterRegistrySingleton.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/TimeFormatterRegistrySingleton.ts index bd7f9c4f56..6c7e08f9b1 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/TimeFormatterRegistrySingleton.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/TimeFormatterRegistrySingleton.ts @@ -1,14 +1,63 @@ import { makeSingleton } from '@superset-ui/core'; import TimeFormatterRegistry from './TimeFormatterRegistry'; +import TimeFormatter from './TimeFormatter'; +import TimeFormatsForGranularity from './TimeFormatsForGranularity'; +import { LOCAL_PREFIX } from './TimeFormats'; +import { TimeGranularity } from './types'; +import createTimeRangeFromGranularity from './utils/createTimeRangeFromGranularity'; +import TimeRangeFormatter from './TimeRangeFormatter'; const getInstance = makeSingleton(TimeFormatterRegistry); export default getInstance; -export function getTimeFormatter(formatId?: string) { +export function getTimeRangeFormatter(formatId?: string) { + return new TimeRangeFormatter({ + id: formatId || 'undefined', + formatFunc: (range: (Date | number | null | undefined)[]) => { + const format = getInstance().get(formatId); + const [start, end] = range.map(value => format(value)); + return start === end ? start : [start, end].join(' — '); + }, + useLocalTime: formatId?.startsWith(LOCAL_PREFIX), + }); +} + +export function formatTimeRange(formatId: string | undefined, range: (Date | null | undefined)[]) { + return getTimeRangeFormatter(formatId)(range); +} + +export function getTimeFormatter(formatId?: string, granularity?: TimeGranularity) { + if (granularity) { + const formatString = formatId || TimeFormatsForGranularity[granularity]; + const timeRangeFormatter = getTimeRangeFormatter(formatString); + + return new TimeFormatter({ + id: [formatString, granularity].join('/'), + formatFunc: (value: Date) => + timeRangeFormatter.format( + createTimeRangeFromGranularity(value, granularity, timeRangeFormatter.useLocalTime), + ), + useLocalTime: timeRangeFormatter.useLocalTime, + }); + } + return getInstance().get(formatId); } -export function formatTime(formatId: string | undefined, value: Date | null | undefined) { - return getInstance().format(formatId, value); +/** + * Syntactic sugar for backward compatibility + * TODO: Deprecate this in the next breaking change. + * @param granularity + */ +export function getTimeFormatterForGranularity(granularity?: TimeGranularity) { + return getTimeFormatter(undefined, granularity); +} + +export function formatTime( + formatId: string | undefined, + value: Date | null | undefined, + granularity?: TimeGranularity, +) { + return getTimeFormatter(formatId, granularity)(value); } diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/TimeRangeFormatter.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/TimeRangeFormatter.ts new file mode 100644 index 0000000000..0d199d91ea --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/TimeRangeFormatter.ts @@ -0,0 +1,44 @@ +import { ExtensibleFunction } from '@superset-ui/core'; +import { TimeRangeFormatFunction } from './types'; + +// Use type augmentation to indicate that +// an instance of TimeFormatter is also a function +interface TimeRangeFormatter { + (value: (Date | number | null | undefined)[]): string; +} + +class TimeRangeFormatter extends ExtensibleFunction { + id: string; + + label: string; + + description: string; + + formatFunc: TimeRangeFormatFunction; + + useLocalTime: boolean; + + constructor(config: { + id: string; + label?: string; + description?: string; + formatFunc: TimeRangeFormatFunction; + useLocalTime?: boolean; + }) { + super((value: (Date | number | null | undefined)[]) => this.format(value)); + + const { id, label, description = '', formatFunc, useLocalTime = false } = config; + + this.id = id; + this.label = label ?? id; + this.description = description; + this.formatFunc = formatFunc; + this.useLocalTime = useLocalTime; + } + + format(values: (Date | number | null | undefined)[]) { + return this.formatFunc(values); + } +} + +export default TimeRangeFormatter; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/factories/createMultiFormatter.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/factories/createMultiFormatter.ts index d01a844980..2a339d4be9 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/factories/createMultiFormatter.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/factories/createMultiFormatter.ts @@ -1,5 +1,5 @@ import { utcFormat, timeFormat } from 'd3-time-format'; -import { utcUtils, localTimeUtils } from '../utils'; +import { utcUtils, localTimeUtils } from '../utils/d3Time'; import TimeFormatter from '../TimeFormatter'; type FormatsByStep = Partial<{ diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/factories/getTimeFormatterForGranularity.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/factories/getTimeFormatterForGranularity.ts deleted file mode 100644 index 871c196aff..0000000000 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/factories/getTimeFormatterForGranularity.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * 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 TimeFormats from '../TimeFormats'; -import { getTimeFormatter } from '../TimeFormatterRegistrySingleton'; -import smartDateVerboseFormatter from '../formatters/smartDateVerbose'; -import { TimeGranularity } from '../types'; - -// Translate time granularity to d3-format -const MINUTE = '%Y-%m-%d %H:%M'; -const { DATABASE_DATE, DATABASE_DATETIME } = TimeFormats; - -// search for `builtin_time_grains` in incubator-superset/superset/db_engine_specs/base.py -const formats = { - date: DATABASE_DATE, - PT1S: DATABASE_DATETIME, // second - PT1M: MINUTE, // minute - PT5M: MINUTE, // 5 minute - PT10M: MINUTE, // 10 minute - PT15M: MINUTE, // 15 minute - 'PT0.5H': MINUTE, // half hour - PT1H: '%Y-%m-%d %H:00', // hour - P1D: DATABASE_DATE, // day - P1W: DATABASE_DATE, // week - P1M: '%Y-%m', // month - 'P0.25Y': '%Y Q%q', // quarter - P1Y: '%Y', // year - '1969-12-28T00:00:00Z/P1W': DATABASE_DATE, // 'week_start_sunday' - '1969-12-29T00:00:00Z/P1W': DATABASE_DATE, // 'week_start_monday' - 'P1W/1970-01-03T00:00:00Z': DATABASE_DATE, // 'week_ending_saturday' - 'P1W/1970-01-04T00:00:00Z': DATABASE_DATE, // 'week_ending_sunday' -}; - -export default function getTimeFormatterForGranularity(granularity?: TimeGranularity) { - return granularity && granularity in formats - ? getTimeFormatter(formats[granularity]) - : smartDateVerboseFormatter; -} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/index.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/index.ts index 3c964ef133..fcb39df25b 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/index.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/index.ts @@ -4,12 +4,14 @@ export { default as TimeFormatter, PREVIEW_TIME } from './TimeFormatter'; export { default as getTimeFormatterRegistry, formatTime, + formatTimeRange, getTimeFormatter, + getTimeFormatterForGranularity, + getTimeRangeFormatter, } from './TimeFormatterRegistrySingleton'; export { default as createD3TimeFormatter } from './factories/createD3TimeFormatter'; export { default as createMultiFormatter } from './factories/createMultiFormatter'; -export { default as getTimeFormatterForGranularity } from './factories/getTimeFormatterForGranularity'; export { default as smartDateFormatter } from './formatters/smartDate'; export { default as smartDateVerboseFormatter } from './formatters/smartDateVerbose'; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/types.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/types.ts index 44ed03ee3b..866baa5c18 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/types.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/types.ts @@ -1,20 +1,30 @@ export type TimeFormatFunction = (value: Date) => string; -export type TimeGranularity = - | 'date' - | 'PT1S' - | 'PT1M' - | 'PT5M' - | 'PT10M' - | 'PT15M' - | 'PT0.5H' - | 'PT1H' - | 'P1D' - | 'P1W' - | 'P1M' - | 'P0.25Y' - | 'P1Y' - | '1969-12-28T00:00:00Z/P1W' - | '1969-12-29T00:00:00Z/P1W' - | 'P1W/1970-01-03T00:00:00Z' - | 'P1W/1970-01-04T00:00:00Z'; +export type TimeRangeFormatFunction = (values: (Date | number | undefined | null)[]) => string; + +/** + * search for `builtin_time_grains` in incubator-superset/superset/db_engine_specs/base.py + */ +export const TimeGranularity = { + DATE: 'date', + SECOND: 'PT1S', + MINUTE: 'PT1M', + FIVE_MINUTES: 'PT5M', + TEN_MINUTES: 'PT10M', + FIFTEEN_MINUTES: 'PT15M', + HALF_HOUR: 'PT0.5H', + HOUR: 'PT1H', + DAY: 'P1D', + WEEK: 'P1W', + WEEK_STARTING_SUNDAY: '1969-12-28T00:00:00Z/P1W', + WEEK_STARTING_MONDAY: '1969-12-29T00:00:00Z/P1W', + WEEK_ENDING_SATURDAY: 'P1W/1970-01-03T00:00:00Z', + WEEK_ENDING_SUNDAY: 'P1W/1970-01-04T00:00:00Z', + MONTH: 'P1M', + QUARTER: 'P0.25Y', + YEAR: 'P1Y', +} as const; + +type ValueOf = T[keyof T]; + +export type TimeGranularity = ValueOf; diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/utils/createTime.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/utils/createTime.ts new file mode 100644 index 0000000000..95ebf693ff --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/utils/createTime.ts @@ -0,0 +1,13 @@ +export default function createTime( + mode: 'local' | 'utc', + year: number, + month: number = 0, + date: number = 1, + hours: number = 0, + minutes: number = 0, + seconds: number = 0, + milliseconds: number = 0, +): Date { + const args = [year, month, date, hours, minutes, seconds, milliseconds] as const; + return mode === 'local' ? new Date(...args) : new Date(Date.UTC(...args)); +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/utils/createTimeRangeFromGranularity.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/utils/createTimeRangeFromGranularity.ts new file mode 100644 index 0000000000..9e3807fecc --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/utils/createTimeRangeFromGranularity.ts @@ -0,0 +1,81 @@ +import { TimeGranularity } from '../types'; +import createTime from './createTime'; + +const MS_IN_SECOND = 1000; +const MS_IN_MINUTE = 60 * MS_IN_SECOND; +const MS_IN_HOUR = 60 * MS_IN_MINUTE; + +function deductOneMs(time: Date) { + return new Date(time.getTime() - 1); +} + +function computeEndTimeFromGranularity( + time: Date, + granularity: TimeGranularity, + useLocalTime: boolean, +) { + const date = useLocalTime ? time.getDate() : time.getUTCDate(); + const month = useLocalTime ? time.getMonth() : time.getUTCMonth(); + const year = useLocalTime ? time.getFullYear() : time.getUTCFullYear(); + const mode = useLocalTime ? 'local' : 'utc'; + + switch (granularity) { + case TimeGranularity.SECOND: + return new Date(time.getTime() + MS_IN_SECOND - 1); + case TimeGranularity.MINUTE: + return new Date(time.getTime() + MS_IN_MINUTE - 1); + case TimeGranularity.FIVE_MINUTES: + return new Date(time.getTime() + MS_IN_MINUTE * 5 - 1); + case TimeGranularity.TEN_MINUTES: + return new Date(time.getTime() + MS_IN_MINUTE * 10 - 1); + case TimeGranularity.FIFTEEN_MINUTES: + return new Date(time.getTime() + MS_IN_MINUTE * 15 - 1); + case TimeGranularity.HALF_HOUR: + return new Date(time.getTime() + MS_IN_MINUTE * 30 - 1); + case TimeGranularity.HOUR: + return new Date(time.getTime() + MS_IN_HOUR - 1); + // For the day granularity and above, using Date overflow is better than adding timestamp + // because it will also handle daylight saving. + case TimeGranularity.WEEK: + case TimeGranularity.WEEK_STARTING_SUNDAY: + case TimeGranularity.WEEK_STARTING_MONDAY: + return deductOneMs(createTime(mode, year, month, date + 7)); + case TimeGranularity.MONTH: + return deductOneMs(createTime(mode, year, month + 1)); + case TimeGranularity.QUARTER: + return deductOneMs(createTime(mode, year, (Math.floor(month / 3) + 1) * 3)); + case TimeGranularity.YEAR: + return deductOneMs(createTime(mode, year + 1)); + // For the WEEK_ENDING_XXX cases, + // currently assume "time" returned from database is supposed to be the end time + // (in contrast to all other granularities that the returned time is start time). + // However, the returned "time" is at 00:00:00.000, so have to add 23:59:59.999. + case TimeGranularity.WEEK_ENDING_SATURDAY: + case TimeGranularity.WEEK_ENDING_SUNDAY: + case TimeGranularity.DATE: + case TimeGranularity.DAY: + default: + return deductOneMs(createTime(mode, year, month, date + 1)); + } +} + +export default function createTimeRangeFromGranularity( + time: Date, + granularity: TimeGranularity, + useLocalTime: boolean = false, +) { + const endTime = computeEndTimeFromGranularity(time, granularity, useLocalTime); + + if ( + granularity === TimeGranularity.WEEK_ENDING_SATURDAY || + granularity === TimeGranularity.WEEK_ENDING_SUNDAY + ) { + const date = useLocalTime ? time.getDate() : time.getUTCDate(); + const month = useLocalTime ? time.getMonth() : time.getUTCMonth(); + const year = useLocalTime ? time.getFullYear() : time.getUTCFullYear(); + const startTime = createTime(useLocalTime ? 'local' : 'utc', year, month, date - 6); + return [startTime, endTime]; + } + + return [time, endTime]; +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/utils.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/utils/d3Time.ts similarity index 100% rename from superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/utils.ts rename to superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/utils/d3Time.ts diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/utils/stringifyTimeInput.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/utils/stringifyTimeInput.ts new file mode 100644 index 0000000000..0fd7c24673 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/src/utils/stringifyTimeInput.ts @@ -0,0 +1,10 @@ +export default function stringifyTimeInput( + value: Date | number | undefined | null, + fn: (time: Date) => string, +) { + if (value === null || value === undefined) { + return `${value}`; + } + + return fn(value instanceof Date ? value : new Date(value)); +} diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/test/TimeFormatterRegistrySingleton.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/test/TimeFormatterRegistrySingleton.test.ts index 466bf58a07..59ccfbafc2 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/test/TimeFormatterRegistrySingleton.test.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/test/TimeFormatterRegistrySingleton.test.ts @@ -1,9 +1,12 @@ import getTimeFormatterRegistry, { getTimeFormatter, formatTime, + getTimeFormatterForGranularity, + formatTimeRange, } from '../src/TimeFormatterRegistrySingleton'; import TimeFormatterRegistry from '../src/TimeFormatterRegistry'; import { PREVIEW_TIME } from '../src/TimeFormatter'; +import { TimeGranularity, LOCAL_PREFIX } from '../src'; describe('TimeFormatterRegistrySingleton', () => { describe('getTimeFormatterRegistry()', () => { @@ -17,17 +20,114 @@ describe('TimeFormatterRegistrySingleton', () => { expect(format(PREVIEW_TIME)).toEqual('14/02/2017'); }); it('falls back to default format if format is not specified', () => { - const formatter = getTimeFormatter(); - expect(formatter.format(PREVIEW_TIME)).toEqual('2017-02-14 11:22:33'); + const format = getTimeFormatter(); + expect(format(PREVIEW_TIME)).toEqual('2017-02-14 11:22:33'); + }); + it(`use local time when format string has LOCAL_PREFIX (${LOCAL_PREFIX})`, () => { + const format = getTimeFormatter('local!%m-%d %H:%M'); + expect(format(new Date(2019, 5, 18, 11, 23))).toEqual('06-18 11:23'); }); }); - describe('formatTime(format, value)', () => { - it('format the given time using the specified format', () => { - const output = formatTime('%Y-%m-%d', PREVIEW_TIME); - expect(output).toEqual('2017-02-14'); + describe('getTimeFormatterForGranularity(granularity?)', () => { + it('returns the default formatter for that granularity', () => { + const date = new Date(Date.UTC(2020, 4, 10)); // May 10, 2020 is Sunday + expect(getTimeFormatterForGranularity(TimeGranularity.DATE)(date)).toEqual('2020-05-10'); }); - it('falls back to the default formatter if the format is undefined', () => { - expect(formatTime(undefined, PREVIEW_TIME)).toEqual('2017-02-14 11:22:33'); + }); + describe('formatTimeRange(format?, values)', () => { + it('format the given time range with specified format', () => { + expect( + formatTimeRange('%m-%d', [new Date(Date.UTC(2017, 1, 1)), new Date(Date.UTC(2017, 1, 2))]), + ).toEqual('02-01 — 02-02'); + }); + it('show only one value if start and end are equal after formatting', () => { + expect( + formatTimeRange('%m-%d', [ + new Date(Date.UTC(2017, 1, 1)), + new Date(Date.UTC(2017, 1, 1, 10)), + ]), + ).toEqual('02-01'); + }); + it('falls back to default format if format is not specified', () => { + expect( + formatTimeRange(undefined, [ + new Date(Date.UTC(2017, 1, 1)), + new Date(Date.UTC(2017, 1, 2)), + ]), + ).toEqual('2017-02-01 00:00:00 — 2017-02-02 00:00:00'); + }); + }); + describe('formatTime(format?, value, granularity?)', () => { + describe('without granularity', () => { + it('format the given time using the specified format', () => { + const output = formatTime('%Y-%m-%d', PREVIEW_TIME); + expect(output).toEqual('2017-02-14'); + }); + it('falls back to the default formatter if the format is undefined', () => { + expect(formatTime(undefined, PREVIEW_TIME)).toEqual('2017-02-14 11:22:33'); + }); + }); + describe('with granularity', () => { + it('format the given time using specified format', () => { + const output = formatTime('%-m/%d', new Date(Date.UTC(2017, 4, 10)), TimeGranularity.WEEK); + expect(output).toEqual('5/10 — 5/16'); + }); + it('format the given time using default format if format is not specified', () => { + const date = new Date(Date.UTC(2020, 4, 10)); // May 10, 2020 is Sunday + expect(formatTime(undefined, date, TimeGranularity.DATE)).toEqual('2020-05-10'); + expect(formatTime(undefined, date, TimeGranularity.SECOND)).toEqual('2020-05-10 00:00:00'); + expect(formatTime(undefined, date, TimeGranularity.MINUTE)).toEqual('2020-05-10 00:00'); + expect(formatTime(undefined, date, TimeGranularity.FIVE_MINUTES)).toEqual( + '2020-05-10 00:00 — 2020-05-10 00:04', + ); + expect(formatTime(undefined, date, TimeGranularity.TEN_MINUTES)).toEqual( + '2020-05-10 00:00 — 2020-05-10 00:09', + ); + expect(formatTime(undefined, date, TimeGranularity.FIFTEEN_MINUTES)).toEqual( + '2020-05-10 00:00 — 2020-05-10 00:14', + ); + expect(formatTime(undefined, date, TimeGranularity.HALF_HOUR)).toEqual( + '2020-05-10 00:00 — 2020-05-10 00:29', + ); + expect(formatTime(undefined, date, TimeGranularity.HOUR)).toEqual('2020-05-10 00:00'); + expect(formatTime(undefined, date, TimeGranularity.DAY)).toEqual('2020-05-10'); + expect(formatTime(undefined, date, TimeGranularity.WEEK)).toEqual( + '2020-05-10 — 2020-05-16', + ); + expect(formatTime(undefined, date, TimeGranularity.WEEK_STARTING_SUNDAY)).toEqual( + '2020-05-10 — 2020-05-16', + ); + expect( + formatTime( + undefined, + new Date(Date.UTC(2020, 4, 11)), + TimeGranularity.WEEK_STARTING_MONDAY, + ), + ).toEqual('2020-05-11 — 2020-05-17'); + expect( + formatTime( + undefined, + new Date(Date.UTC(2020, 4, 10)), + TimeGranularity.WEEK_ENDING_SUNDAY, + ), + ).toEqual('2020-05-04 — 2020-05-10'); + expect( + formatTime( + undefined, + new Date(Date.UTC(2020, 4, 9)), + TimeGranularity.WEEK_ENDING_SATURDAY, + ), + ).toEqual('2020-05-03 — 2020-05-09'); + expect( + formatTime(undefined, new Date(Date.UTC(2020, 3, 1)), TimeGranularity.MONTH), + ).toEqual('Apr 2020'); + expect( + formatTime(undefined, new Date(Date.UTC(2020, 3, 1)), TimeGranularity.QUARTER), + ).toEqual('2020 Q2'); + expect(formatTime(undefined, new Date(Date.UTC(2020, 0, 1)), TimeGranularity.YEAR)).toEqual( + '2020', + ); + }); }); }); }); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/test/factories/getTimeFormatterForGranularity.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/test/factories/getTimeFormatterForGranularity.test.ts deleted file mode 100644 index 7c006a9057..0000000000 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/test/factories/getTimeFormatterForGranularity.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import getFormatter from '../../src/factories/getTimeFormatterForGranularity'; -import smartDateVerbose from '../../src/formatters/smartDateVerbose'; - -describe('getTimeFormatterForGranularity()', () => { - it('use smartDate when granularity unknown or undefined', () => { - expect(getFormatter(undefined)).toBe(smartDateVerbose); - // @ts-ignore - expect(getFormatter('random-string')).toBe(smartDateVerbose); - }); - - it('format time for known granularities', () => { - // JS Date constructor month is zero-based - const date = new Date(2020, 4, 10, 11, 10, 1); // May 10, 2020 is Sunday - expect(getFormatter('date')(date)).toBe('2020-05-10'); - expect(getFormatter('PT1S')(date)).toBe('2020-05-10 11:10:01'); - expect(getFormatter('PT1M')(date)).toBe('2020-05-10 11:10'); - expect(getFormatter('PT5M')(date)).toBe('2020-05-10 11:10'); - expect(getFormatter('PT10M')(date)).toBe('2020-05-10 11:10'); - expect(getFormatter('PT15M')(date)).toBe('2020-05-10 11:10'); - expect(getFormatter('PT0.5H')(date)).toBe('2020-05-10 11:10'); - expect(getFormatter('PT1H')(date)).toBe('2020-05-10 11:00'); - expect(getFormatter('P1D')(date)).toBe('2020-05-10'); - expect(getFormatter('P1W')(date)).toBe('2020-05-10'); - expect(getFormatter('P1M')(date)).toBe('2020-05'); - expect(getFormatter('P0.25Y')(date)).toBe('2020 Q2'); - expect(getFormatter('P1Y')(date)).toBe('2020'); - // sunday based week - expect(getFormatter('1969-12-28T00:00:00Z/P1W')(date)).toBe('2020-05-10'); - expect(getFormatter('P1W/1970-01-03T00:00:00Z')(date)).toBe('2020-05-10'); - // monday based week - expect(getFormatter('1969-12-29T00:00:00Z/P1W')(date)).toBe('2020-05-10'); - expect(getFormatter('P1W/1970-01-04T00:00:00Z')(date)).toBe('2020-05-10'); - }); -}); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/test/utils/createTime.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/test/utils/createTime.test.ts new file mode 100644 index 0000000000..c861d7eba9 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/test/utils/createTime.test.ts @@ -0,0 +1,37 @@ +import createTime from '../../src/utils/createTime'; + +describe('createTime(mode, year, month, date, hours, minutes, seconds, milliseconds)', () => { + describe('mode', () => { + it('creates UTC time when mode==="utc"', () => { + const time = createTime('utc', 2020, 5, 15); + expect(time.getUTCFullYear()).toEqual(2020); + expect(time.getUTCMonth()).toEqual(5); + expect(time.getUTCDate()).toEqual(15); + }); + it('creates local time when mode==="local"', () => { + const time = createTime('local', 2020, 5, 15); + expect(time.getFullYear()).toEqual(2020); + expect(time.getMonth()).toEqual(5); + expect(time.getDate()).toEqual(15); + }); + }); + it('sets all the date parts', () => { + const time = createTime('local', 2020, 5, 15, 1, 2, 3, 4); + expect(time.getFullYear()).toEqual(2020); + expect(time.getMonth()).toEqual(5); + expect(time.getDate()).toEqual(15); + expect(time.getHours()).toEqual(1); + expect(time.getMinutes()).toEqual(2); + expect(time.getSeconds()).toEqual(3); + expect(time.getMilliseconds()).toEqual(4); + }); + it('sets default values for date parts', () => { + const time = createTime('utc', 2020); + expect(time.getUTCMonth()).toEqual(0); + expect(time.getUTCDate()).toEqual(1); + expect(time.getUTCHours()).toEqual(0); + expect(time.getUTCMinutes()).toEqual(0); + expect(time.getUTCSeconds()).toEqual(0); + expect(time.getUTCMilliseconds()).toEqual(0); + }); +}); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/test/utils/createTimeRangeFromGranularity.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/test/utils/createTimeRangeFromGranularity.test.ts new file mode 100644 index 0000000000..4a5db79fb7 --- /dev/null +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/test/utils/createTimeRangeFromGranularity.test.ts @@ -0,0 +1,166 @@ +import createTimeRangeFromGranularity from '../../src/utils/createTimeRangeFromGranularity'; +import { TimeGranularity, getTimeRangeFormatter, LOCAL_PREFIX } from '../../src'; + +const formatString = '%Y-%m-%d %H:%M:%S.%L'; +const formatUTCTimeRange = getTimeRangeFormatter(formatString); +const formatLocalTimeRange = getTimeRangeFormatter(`${LOCAL_PREFIX}${formatString}`); + +function testUTC( + granularity: TimeGranularity, + year: number, + month: number = 0, + date: number = 1, + hours: number = 0, + minutes: number = 0, + seconds: number = 0, +) { + return formatUTCTimeRange( + createTimeRangeFromGranularity( + new Date(Date.UTC(year, month, date, hours, minutes, seconds)), + granularity, + ), + ); +} + +function testLocal( + granularity: TimeGranularity, + year: number, + month: number = 0, + date: number = 1, + hours: number = 0, + minutes: number = 0, + seconds: number = 0, +) { + return formatLocalTimeRange( + createTimeRangeFromGranularity( + new Date(year, month, date, hours, minutes, seconds), + granularity, + true, + ), + ); +} + +describe('createTimeRangeFromGranularity(time, granularity, useLocalTime)', () => { + describe('UTC time', () => { + it('creates time range according to specified granularity', () => { + expect(testUTC(TimeGranularity.DATE, 2020, 4, 15)).toEqual( + '2020-05-15 00:00:00.000 — 2020-05-15 23:59:59.999', + ); + expect(testUTC(TimeGranularity.SECOND, 2020, 4, 15)).toEqual( + '2020-05-15 00:00:00.000 — 2020-05-15 00:00:00.999', + ); + expect(testUTC(TimeGranularity.MINUTE, 2020, 4, 15)).toEqual( + '2020-05-15 00:00:00.000 — 2020-05-15 00:00:59.999', + ); + expect(testUTC(TimeGranularity.FIVE_MINUTES, 2020, 4, 15)).toEqual( + '2020-05-15 00:00:00.000 — 2020-05-15 00:04:59.999', + ); + expect(testUTC(TimeGranularity.TEN_MINUTES, 2020, 4, 15)).toEqual( + '2020-05-15 00:00:00.000 — 2020-05-15 00:09:59.999', + ); + expect(testUTC(TimeGranularity.FIFTEEN_MINUTES, 2020, 4, 15)).toEqual( + '2020-05-15 00:00:00.000 — 2020-05-15 00:14:59.999', + ); + expect(testUTC(TimeGranularity.HALF_HOUR, 2020, 4, 15)).toEqual( + '2020-05-15 00:00:00.000 — 2020-05-15 00:29:59.999', + ); + expect(testUTC(TimeGranularity.HOUR, 2020, 4, 15)).toEqual( + '2020-05-15 00:00:00.000 — 2020-05-15 00:59:59.999', + ); + expect(testUTC(TimeGranularity.DAY, 2020, 4, 15)).toEqual( + '2020-05-15 00:00:00.000 — 2020-05-15 23:59:59.999', + ); + expect(testUTC(TimeGranularity.WEEK, 2020, 4, 15)).toEqual( + '2020-05-15 00:00:00.000 — 2020-05-21 23:59:59.999', + ); + expect(testUTC(TimeGranularity.WEEK_STARTING_SUNDAY, 2020, 4, 17)).toEqual( + '2020-05-17 00:00:00.000 — 2020-05-23 23:59:59.999', + ); + expect(testUTC(TimeGranularity.WEEK_STARTING_MONDAY, 2020, 4, 18)).toEqual( + '2020-05-18 00:00:00.000 — 2020-05-24 23:59:59.999', + ); + expect(testUTC(TimeGranularity.WEEK_ENDING_SATURDAY, 2020, 4, 16)).toEqual( + '2020-05-10 00:00:00.000 — 2020-05-16 23:59:59.999', + ); + expect(testUTC(TimeGranularity.WEEK_ENDING_SUNDAY, 2020, 4, 17)).toEqual( + '2020-05-11 00:00:00.000 — 2020-05-17 23:59:59.999', + ); + expect(testUTC(TimeGranularity.MONTH, 2020, 4, 1)).toEqual( + '2020-05-01 00:00:00.000 — 2020-05-31 23:59:59.999', + ); + expect(testUTC(TimeGranularity.MONTH, 2020, 11, 1)).toEqual( + '2020-12-01 00:00:00.000 — 2020-12-31 23:59:59.999', + ); + expect(testUTC(TimeGranularity.QUARTER, 2020, 3, 1)).toEqual( + '2020-04-01 00:00:00.000 — 2020-06-30 23:59:59.999', + ); + expect(testUTC(TimeGranularity.QUARTER, 2020, 9, 1)).toEqual( + '2020-10-01 00:00:00.000 — 2020-12-31 23:59:59.999', + ); + expect(testUTC(TimeGranularity.YEAR, 2020, 0, 1)).toEqual( + '2020-01-01 00:00:00.000 — 2020-12-31 23:59:59.999', + ); + }); + }); + describe('Local time', () => { + it('creates time range according to specified granularity', () => { + expect(testLocal(TimeGranularity.DATE, 2020, 4, 15)).toEqual( + '2020-05-15 00:00:00.000 — 2020-05-15 23:59:59.999', + ); + expect(testLocal(TimeGranularity.SECOND, 2020, 4, 15)).toEqual( + '2020-05-15 00:00:00.000 — 2020-05-15 00:00:00.999', + ); + expect(testLocal(TimeGranularity.MINUTE, 2020, 4, 15)).toEqual( + '2020-05-15 00:00:00.000 — 2020-05-15 00:00:59.999', + ); + expect(testLocal(TimeGranularity.FIVE_MINUTES, 2020, 4, 15)).toEqual( + '2020-05-15 00:00:00.000 — 2020-05-15 00:04:59.999', + ); + expect(testLocal(TimeGranularity.TEN_MINUTES, 2020, 4, 15)).toEqual( + '2020-05-15 00:00:00.000 — 2020-05-15 00:09:59.999', + ); + expect(testLocal(TimeGranularity.FIFTEEN_MINUTES, 2020, 4, 15)).toEqual( + '2020-05-15 00:00:00.000 — 2020-05-15 00:14:59.999', + ); + expect(testLocal(TimeGranularity.HALF_HOUR, 2020, 4, 15)).toEqual( + '2020-05-15 00:00:00.000 — 2020-05-15 00:29:59.999', + ); + expect(testLocal(TimeGranularity.HOUR, 2020, 4, 15)).toEqual( + '2020-05-15 00:00:00.000 — 2020-05-15 00:59:59.999', + ); + expect(testLocal(TimeGranularity.DAY, 2020, 4, 15)).toEqual( + '2020-05-15 00:00:00.000 — 2020-05-15 23:59:59.999', + ); + expect(testLocal(TimeGranularity.WEEK, 2020, 4, 15)).toEqual( + '2020-05-15 00:00:00.000 — 2020-05-21 23:59:59.999', + ); + expect(testLocal(TimeGranularity.WEEK_STARTING_SUNDAY, 2020, 4, 17)).toEqual( + '2020-05-17 00:00:00.000 — 2020-05-23 23:59:59.999', + ); + expect(testLocal(TimeGranularity.WEEK_STARTING_MONDAY, 2020, 4, 18)).toEqual( + '2020-05-18 00:00:00.000 — 2020-05-24 23:59:59.999', + ); + expect(testLocal(TimeGranularity.WEEK_ENDING_SATURDAY, 2020, 4, 16)).toEqual( + '2020-05-10 00:00:00.000 — 2020-05-16 23:59:59.999', + ); + expect(testLocal(TimeGranularity.WEEK_ENDING_SUNDAY, 2020, 4, 17)).toEqual( + '2020-05-11 00:00:00.000 — 2020-05-17 23:59:59.999', + ); + expect(testLocal(TimeGranularity.MONTH, 2020, 4, 1)).toEqual( + '2020-05-01 00:00:00.000 — 2020-05-31 23:59:59.999', + ); + expect(testLocal(TimeGranularity.MONTH, 2020, 11, 1)).toEqual( + '2020-12-01 00:00:00.000 — 2020-12-31 23:59:59.999', + ); + expect(testLocal(TimeGranularity.QUARTER, 2020, 3, 1)).toEqual( + '2020-04-01 00:00:00.000 — 2020-06-30 23:59:59.999', + ); + expect(testLocal(TimeGranularity.QUARTER, 2020, 9, 1)).toEqual( + '2020-10-01 00:00:00.000 — 2020-12-31 23:59:59.999', + ); + expect(testLocal(TimeGranularity.YEAR, 2020, 0, 1)).toEqual( + '2020-01-01 00:00:00.000 — 2020-12-31 23:59:59.999', + ); + }); + }); +}); diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/test/utils.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/test/utils/d3Time.test.ts similarity index 98% rename from superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/test/utils.test.ts rename to superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/test/utils/d3Time.test.ts index fba4bf652c..10e1655edb 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/test/utils.test.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-time-format/test/utils/d3Time.test.ts @@ -1,4 +1,4 @@ -import { utcUtils, localTimeUtils } from '../src/utils'; +import { utcUtils, localTimeUtils } from '../../src/utils/d3Time'; describe('utils', () => { describe('utcUtils', () => {