-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathinterval.ts
113 lines (89 loc) · 2.79 KB
/
interval.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
interface PGNamedIntervalObject<T> {
milliseconds: T | undefined;
seconds: T | undefined;
minutes: T | undefined;
hours: T | undefined;
days: T | undefined;
months: T | undefined;
years: T | undefined;
}
interface PGIntervalObject<T> extends Record<string, T | undefined>, PGNamedIntervalObject<T> {}
const NEGATION_INDEX = 8;
const NEGATION_INDICATOR = "-";
const NUMBER = "([+-]?\\d+)";
const YEAR = NUMBER + "\\s+years?";
const MONTH = NUMBER + "\\s+mons?";
const DAY = NUMBER + "\\s+days?";
const TIME = "([+-])?([\\d]*):(\\d\\d):(\\d\\d).?(\\d{1,6})?";
const INTERVAL = new RegExp(
[YEAR, MONTH, DAY, TIME].map(regexString => `(${regexString})?`).join("\\s*"),
);
const propMapToISO: PGIntervalObject<string> = {
days: "D",
hours: "H",
milliseconds: undefined,
minutes: "M",
months: "M",
seconds: "S",
years: "Y",
};
const positionLookup: PGIntervalObject<number> = {
days: 6,
hours: 9,
milliseconds: 12,
minutes: 10,
months: 4,
seconds: 11,
years: 2,
};
const dateProps: string[] = ["years", "months", "days"];
const timeProps: string[] = ["hours", "minutes", "seconds"];
// pad sub-seconds up to microseconds before parsing
const parseSubseconds = (fraction: string): number =>
parseInt(`${fraction}${"000000".slice(fraction.length)}`, 10);
const parse = (raw: string): PGIntervalObject<number> | null => {
if (!raw) {
return null;
}
const matches = INTERVAL.exec(raw);
if (!matches) {
return null;
}
const isNegative = matches[NEGATION_INDEX] === NEGATION_INDICATOR;
return Object.keys(positionLookup).reduce(
(acc, prop: string) => {
const position = positionLookup[prop];
if (!position) {
return acc;
}
const value = matches[position];
if (!value) {
return acc;
}
const parsed = String(prop) === "milliseconds" ? parseSubseconds(value) : parseInt(value, 10);
if (!parsed) {
return acc;
}
return {
...acc,
[prop]: isNegative && timeProps.includes(prop) ? parsed * -1 : parsed,
};
},
{} as PGIntervalObject<number>,
);
};
const formatMilliseconds = (raw: number): string => String(raw * 1000).replace(/[0]+$/g, "");
const buildProperty = (parsed: PGIntervalObject<number> | null) => (prop: string): string => {
const value = parsed == null ? 0 : parsed[prop] || 0;
const isoEquiv = propMapToISO[prop];
if (prop === "seconds" && parsed && parsed.milliseconds) {
return `${value}.${formatMilliseconds(parsed.milliseconds)}${isoEquiv}`;
}
return `${value}${isoEquiv}`;
};
export const parseInterval = (raw: string): string => {
const parsed = parse(raw);
const datePart = dateProps.map(buildProperty(parsed)).join("");
const timePart = timeProps.map(buildProperty(parsed)).join("");
return `P${datePart}T${timePart}`;
};