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 274
feat(time-format): improve support for formatting with granularity in mind #509
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
d56fb7d
feat(time-format): add support for granularity
kristw a23b906
feat: create time range from granularity
kristw 7ef1c36
fix: update format
kristw 19b6a54
wip
kristw 9785264
feat: refactor getFormatter
kristw 3eda4d8
feat: reconcile api
kristw 7418488
test: add unit tests
kristw 4065e45
refactor: clean up
kristw 08ac1b5
refactor: createTime
kristw 2150aa8
refactor: improve end time computation to be daylight saving compatible
kristw File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make this also a constant to avoid having to type the awkward granularity string for |
||
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; | ||
kristw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
move and rename file