Skip to content
This repository has been archived by the owner on Dec 10, 2021. It is now read-only.

Commit

Permalink
feat(time-format): improve support for formatting with granularity in…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
kristw committed May 22, 2020
1 parent da7d6a5 commit 2d8afa8
Show file tree
Hide file tree
Showing 17 changed files with 576 additions and 124 deletions.
30 changes: 30 additions & 0 deletions packages/superset-ui-time-format/src/TimeFormatsForGranularity.ts
Original file line number Diff line number Diff line change
@@ -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;
7 changes: 2 additions & 5 deletions packages/superset-ui-time-format/src/TimeFormatter.ts
Original file line number Diff line number Diff line change
@@ -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));

Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
44 changes: 44 additions & 0 deletions packages/superset-ui-time-format/src/TimeRangeFormatter.ts
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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<{
Expand Down

This file was deleted.

4 changes: 3 additions & 1 deletion packages/superset-ui-time-format/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
46 changes: 28 additions & 18 deletions packages/superset-ui-time-format/src/types.ts
Original file line number Diff line number Diff line change
@@ -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>;
13 changes: 13 additions & 0 deletions packages/superset-ui-time-format/src/utils/createTime.ts
Original file line number Diff line number Diff line change
@@ -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));
}
Original file line number Diff line number Diff line change
@@ -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];
}
10 changes: 10 additions & 0 deletions packages/superset-ui-time-format/src/utils/stringifyTimeInput.ts
Original file line number Diff line number Diff line change
@@ -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));
}
Loading

1 comment on commit 2d8afa8

@vercel
Copy link

@vercel vercel bot commented on 2d8afa8 May 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.