import { z } from "zod"; import parseDuration from "parse-duration"; const DurationString = z.string().refine( (val) => { const ms = parseDuration(val); return ms !== null && ms > 0; }, (val) => ({ message: `Invalid or non-positive duration string: "${val}"` }) ); const BracketSchema = z.object({ max: z.union([z.literal("Infinity"), DurationString]), granularity: DurationString, }); const BracketsSchema = z .array(BracketSchema) .min(1, "TimeGranularity requires at least one bracket"); export type TimeGranularityBracket = z.input; type ParsedBracket = { maxMs: number; granularityMs: number; }; function requireParsedDuration(input: string): number { const ms = parseDuration(input); if (ms === null || ms <= 0) { throw new Error(`Duration must be strictly positive, got "${input}" (${ms}ms)`); } return ms; } export class TimeGranularity { private readonly parsed: ParsedBracket[]; constructor(brackets: TimeGranularityBracket[]) { const validated = BracketsSchema.parse(brackets); this.parsed = validated.map((b) => ({ maxMs: b.max === "Infinity" ? Infinity : requireParsedDuration(b.max), granularityMs: requireParsedDuration(b.granularity), })); } getTimeGranularityMs(from: Date, to: Date): number { if (from.getTime() > to.getTime()) { return this.parsed[this.parsed.length - 1].granularityMs; } const rangeMs = to.getTime() - from.getTime(); for (const bracket of this.parsed) { if (rangeMs <= bracket.maxMs) { return bracket.granularityMs; } } return this.parsed[this.parsed.length - 1].granularityMs; } }