import orderBy from 'lodash-es/orderBy'; interface Range { start: T; end: T; } export type DateRange = Range; export type PartialDateRange = Partial; interface MemoryRangeSetInterface { hasCoverage(range: T): boolean; add(range: T): void; clear(): void; } export class MemoryRangeSet implements MemoryRangeSetInterface { protected _ranges: DateRange[]; constructor(ranges?: DateRange[]) { this._ranges = ranges ?? []; } public hasCoverage(range: DateRange): boolean { return this._ranges.some((cachedRange) => rangeIsEntirelyContained(cachedRange, range), ); } public add(range: DateRange): void { this._ranges.push(range); this._ranges = compressRanges(this._ranges); } public clear(): void { this._ranges = []; } } export interface ExpiringRange extends Range { expires: Date; } export class ExpiringMemoryRangeSet implements MemoryRangeSetInterface> { protected _ranges: ExpiringRange[]; constructor(ranges?: ExpiringRange[]) { this._ranges = ranges ?? []; } public hasCoverage(range: DateRange): boolean { const now = new Date(); return this._ranges.some( (cachedRange) => now < cachedRange.expires && rangeIsEntirelyContained(cachedRange, range), ); } public add(range: ExpiringRange): void { this._ranges.push(range); this._expireOldRanges(); } protected _expireOldRanges(): void { const now = new Date(); this._ranges = this._ranges.filter((range) => now < range.expires); } public clear(): void { this._ranges = []; } } const rangeIsEntirelyContained = (bigger: DateRange, smaller: DateRange): boolean => { return smaller.start >= bigger.start && smaller.end <= bigger.end; }; export const rangesOverlap = (a: DateRange, b: DateRange): boolean => { return ( // a starts within the range of b. (a.start >= b.start && a.start <= b.end) || // a ends within the range of b. (a.end >= b.start && a.end <= b.end) || // a encompasses the entire range of b. (a.start <= b.start && a.end >= b.end) ); }; export const compressRanges = ( ranges: Range[], toleranceSeconds = 0, ): Range[] => { const compressedRanges: Range[] = []; ranges = orderBy(ranges, (range) => range.start, 'asc'); let current: Range | null = null; for (const range of ranges) { const rangeStartSeconds: number = range.start instanceof Date ? range.start.getTime() : range.start; if (!current) { current = { ...range }; continue; } const currentEndSeconds = current.end instanceof Date ? current.end.getTime() : (current.end as number); if (currentEndSeconds + toleranceSeconds * 1000 >= rangeStartSeconds) { if (range.end > current.end) { current.end = range.end; } } else { compressedRanges.push(current); current = { ...range }; } } if (current) { compressedRanges.push(current); } return compressedRanges; };