Skip to content

Commit

Permalink
Fix period type support for Pandas 2.2.0 (#7988)
Browse files Browse the repository at this point in the history
* Fix period type support with Pandas 2.2.x

* Add more comments
  • Loading branch information
LukasMasuch committed Jan 22, 2024
1 parent ddbc306 commit de8dae4
Showing 1 changed file with 97 additions and 51 deletions.
148 changes: 97 additions & 51 deletions frontend/lib/src/dataframes/Quiver.ts
Expand Up @@ -123,68 +123,114 @@ type IntervalClosed = "left" | "right" | "both" | "neither"
type IntervalType = `interval[${IntervalData}, ${IntervalClosed}]`

// The frequency strings defined in pandas.
// See: https://pandas.pydata.org/docs/user_guide/timeseries.html#dateoffset-objects
type SupportedPandasOffsetType = "W" | "Q" | "D" | "H" | "T" | "S" | "L"
// See: https://pandas.pydata.org/docs/user_guide/timeseries.html#period-aliases
// Not supported: "N" (nanoseconds), "U" & "us" (microseconds), and "B" (business days).
// Reason is that these types are not supported by moment.js, but also they are not
// very commonly used in practice.
type SupportedPandasOffsetType =
// yearly frequency:
| "A" // deprecated alias
| "Y"
// quarterly frequency:
| "Q"
// monthly frequency:
| "M"
// weekly frequency:
| "W"
// calendar day frequency:
| "D"
// hourly frequency:
| "H" // deprecated alias
| "h"
// minutely frequency
| "T" // deprecated alias
| "min"
// secondly frequency:
| "S" // deprecated alias
| "s"
// milliseconds frequency:
| "L" // deprecated alias
| "ms"

type PeriodFrequency =
| SupportedPandasOffsetType
| `${SupportedPandasOffsetType}-${string}`
type PeriodType = `period[${PeriodFrequency}]`

const WEEKDAY_SHORT = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]
const formatMs = (duration: number): string =>
moment("19700101", "YYYYMMDD")
.add(duration, "ms")
.format("YYYY-MM-DD HH:mm:ss.SSS")

const formatSec = (duration: number): string =>
moment("19700101", "YYYYMMDD")
.add(duration, "s")
.format("YYYY-MM-DD HH:mm:ss")

const formatMin = (duration: number): string =>
moment("19700101", "YYYYMMDD").add(duration, "m").format("YYYY-MM-DD HH:mm")

const formatHours = (duration: number): string =>
moment("19700101", "YYYYMMDD").add(duration, "h").format("YYYY-MM-DD HH:mm")

const formatDay = (duration: number): string =>
moment("19700101", "YYYYMMDD").add(duration, "d").format("YYYY-MM-DD")

const formatMonth = (duration: number): string =>
moment("19700101", "YYYYMMDD").add(duration, "M").format("YYYY-MM")

const formatYear = (duration: number): string =>
moment("19700101", "YYYYMMDD").add(duration, "y").format("YYYY")

const formatWeeks = (duration: number, freqParam?: string): string => {
if (!freqParam) {
throw new Error('Frequency "W" requires parameter')
}
const dayIndex = WEEKDAY_SHORT.indexOf(freqParam)
if (dayIndex < 0) {
throw new Error(
`Invalid value: ${freqParam}. Supported values: ${JSON.stringify(
WEEKDAY_SHORT
)}`
)
}
const startDate = moment("19700101", "YYYYMMDD")
.add(duration, "w")
.day(dayIndex - 6)
.format("YYYY-MM-DD")
const endDate = moment("19700101", "YYYYMMDD")
.add(duration, "w")
.day(dayIndex)
.format("YYYY-MM-DD")

return `${startDate}/${endDate}`
}

const formatQuarter = (duration: number): string =>
moment("19700101", "YYYYMMDD")
.add(duration, "Q")
.endOf("quarter")
.format("YYYY[Q]Q")

// TODO: For now, we only support the most commonly used offset types.
// In the future, it is worth adding support for other types as needed.
const PERIOD_TYPE_FORMATTERS: Record<
SupportedPandasOffsetType,
(duration: number, freqParam?: string) => string
> = {
L: duration =>
moment("19700101", "YYYYMMDD")
.add(duration, "ms")
.format("YYYY-MM-DD HH:mm:ss.SSS"),
S: duration =>
moment("19700101", "YYYYMMDD")
.add(duration, "s")
.format("YYYY-MM-DD HH:mm:ss"),
T: duration =>
moment("19700101", "YYYYMMDD")
.add(duration, "m")
.format("YYYY-MM-DD HH:mm"),
H: duration =>
moment("19700101", "YYYYMMDD")
.add(duration, "h")
.format("YYYY-MM-DD HH:mm"),
D: duration =>
moment("19700101", "YYYYMMDD").add(duration, "d").format("YYYY-MM-DD"),
W: (duration, freqParam) => {
if (!freqParam) {
throw new Error('Frequency "W" requires parameter')
}
const dayIndex = WEEKDAY_SHORT.indexOf(freqParam)
if (dayIndex < 0) {
throw new Error(
`Invalid value: ${freqParam}. Supported values: ${JSON.stringify(
WEEKDAY_SHORT
)}`
)
}
const startDate = moment("19700101", "YYYYMMDD")
.add(duration, "w")
.day(dayIndex - 6)
.format("YYYY-MM-DD")
const endDate = moment("19700101", "YYYYMMDD")
.add(duration, "w")
.day(dayIndex)
.format("YYYY-MM-DD")

return `${startDate}/${endDate}`
},
Q: duration => {
return moment("19700101", "YYYYMMDD")
.add(duration, "Q")
.endOf("quarter")
.format("YYYY[Q]Q")
},
L: formatMs,
ms: formatMs,
S: formatSec,
s: formatSec,
T: formatMin,
min: formatMin,
H: formatHours,
h: formatHours,
D: formatDay,
M: formatMonth,
W: formatWeeks,
Q: formatQuarter,
Y: formatYear,
A: formatYear,
}

/** Interval data type. */
Expand Down

0 comments on commit de8dae4

Please sign in to comment.