Skip to content

Commit 2174d5b

Browse files
committed
Time granularity made more robust
1 parent 29981cd commit 2174d5b

File tree

2 files changed

+40
-10
lines changed

2 files changed

+40
-10
lines changed

apps/webapp/app/utils/timeGranularity.ts

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,54 @@
1+
import { z } from "zod";
12
import parseDuration from "parse-duration";
23

3-
export type TimeGranularityBracket = {
4-
max: string;
5-
granularity: string;
6-
};
4+
const DurationString = z
5+
.string()
6+
.refine(
7+
(val) => parseDuration(val) !== null,
8+
(val) => ({ message: `Invalid duration string: "${val}"` })
9+
);
10+
11+
const BracketSchema = z.object({
12+
max: z.union([z.literal("Infinity"), DurationString]),
13+
granularity: DurationString,
14+
});
15+
16+
const BracketsSchema = z
17+
.array(BracketSchema)
18+
.min(1, "TimeGranularity requires at least one bracket");
19+
20+
export type TimeGranularityBracket = z.input<typeof BracketSchema>;
721

822
type ParsedBracket = {
923
maxMs: number;
1024
granularityMs: number;
1125
};
1226

27+
function requireParsedDuration(input: string): number {
28+
const ms = parseDuration(input);
29+
if (ms === null) {
30+
throw new Error(`Failed to parse duration string: "${input}"`);
31+
}
32+
return ms;
33+
}
34+
1335
export class TimeGranularity {
1436
private readonly parsed: ParsedBracket[];
1537

1638
constructor(brackets: TimeGranularityBracket[]) {
17-
if (brackets.length === 0) {
18-
throw new Error("TimeGranularity requires at least one bracket");
19-
}
39+
const validated = BracketsSchema.parse(brackets);
2040

21-
this.parsed = brackets.map((b) => ({
22-
maxMs: parseDuration(b.max) ?? Infinity,
23-
granularityMs: parseDuration(b.granularity)!,
41+
this.parsed = validated.map((b) => ({
42+
maxMs: b.max === "Infinity" ? Infinity : requireParsedDuration(b.max),
43+
granularityMs: requireParsedDuration(b.granularity),
2444
}));
2545
}
2646

2747
getTimeGranularityMs(from: Date, to: Date): number {
48+
if (from.getTime() > to.getTime()) {
49+
return this.parsed[this.parsed.length - 1].granularityMs;
50+
}
51+
2852
const rangeMs = to.getTime() - from.getTime();
2953
for (const bracket of this.parsed) {
3054
if (rangeMs <= bracket.maxMs) {

apps/webapp/test/timeGranularity.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ describe("TimeGranularity", () => {
4848
expect(granularity.getTimeGranularityMs(date, date)).toBe(10 * SECOND);
4949
});
5050

51+
it("returns the broadest granularity for an inverted range (from > to)", () => {
52+
const from = new Date("2025-01-01T01:00:00Z");
53+
const to = new Date("2025-01-01T00:00:00Z");
54+
expect(granularity.getTimeGranularityMs(from, to)).toBe(10 * MINUTE);
55+
});
56+
5157
it("throws when constructed with an empty array", () => {
5258
expect(() => new TimeGranularity([])).toThrow("at least one bracket");
5359
});

0 commit comments

Comments
 (0)