This repository has been archived by the owner on Dec 10, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 272
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
17 changed files
with
576 additions
and
124 deletions.
There are no files selected for viewing
30 changes: 30 additions & 0 deletions
30
packages/superset-ui-time-format/src/TimeFormatsForGranularity.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
55 changes: 52 additions & 3 deletions
55
packages/superset-ui-time-format/src/TimeFormatterRegistrySingleton.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
44
packages/superset-ui-time-format/src/TimeRangeFormatter.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
2 changes: 1 addition & 1 deletion
2
packages/superset-ui-time-format/src/factories/createMultiFormatter.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
53 changes: 0 additions & 53 deletions
53
packages/superset-ui-time-format/src/factories/getTimeFormatterForGranularity.ts
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} |
81 changes: 81 additions & 0 deletions
81
packages/superset-ui-time-format/src/utils/createTimeRangeFromGranularity.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]; | ||
} |
File renamed without changes.
10 changes: 10 additions & 0 deletions
10
packages/superset-ui-time-format/src/utils/stringifyTimeInput.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} |
Oops, something went wrong.
2d8afa8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to following URLs: