feat(time-format): improve support for formatting with granularity in mind (#509)

* feat(time-format): add support for granularity

* feat: create time range from granularity

* fix: update format

* wip

* feat: refactor getFormatter

* feat: reconcile api

* test: add unit tests

* refactor: clean up

* refactor: createTime

* refactor: improve end time computation to be daylight saving compatible
This commit is contained in:
Krist Wongsuphasawat 2020-05-22 13:46:42 -07:00 committed by Yongjie Zhao
parent fadf0d61a2
commit 17075ae021
17 changed files with 576 additions and 124 deletions

View File

@ -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, string> = {
[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;

View File

@ -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) {

View File

@ -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);
}

View File

@ -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;

View File

@ -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<{

View File

@ -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;
}

View File

@ -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';

View File

@ -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> = T[keyof T];
export type TimeGranularity = ValueOf<typeof TimeGranularity>;

View File

@ -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));
}

View File

@ -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];
}

View File

@ -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));
}

View File

@ -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',
);
});
});
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});

View File

@ -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',
);
});
});
});

View File

@ -1,4 +1,4 @@
import { utcUtils, localTimeUtils } from '../src/utils';
import { utcUtils, localTimeUtils } from '../../src/utils/d3Time';
describe('utils', () => {
describe('utcUtils', () => {