Date Range Overlap And Merge

Detect overlapping date ranges, compute their intersection duration, and merge touching or overlapping ranges.

TypescriptData Integrity

Code

snippet
export interface DateRange {
  start: Date;
  end: Date;
}

function normalize(range: DateRange): DateRange {
  if (!(range.start instanceof Date) || !(range.end instanceof Date)) {
    throw new TypeError("range bounds must be Date instances");
  }
  if (Number.isNaN(range.start.getTime()) || Number.isNaN(range.end.getTime())) {
    throw new RangeError("range bounds must be valid dates");
  }
  return range.start <= range.end
    ? { start: range.start, end: range.end }
    : { start: range.end, end: range.start };
}

export function rangesOverlap(left: DateRange, right: DateRange): boolean {
  const a = normalize(left);
  const b = normalize(right);
  return a.start.getTime() <= b.end.getTime() && b.start.getTime() <= a.end.getTime();
}

export function overlapDurationMs(left: DateRange, right: DateRange): number {
  const a = normalize(left);
  const b = normalize(right);
  const start = Math.max(a.start.getTime(), b.start.getTime());
  const end = Math.min(a.end.getTime(), b.end.getTime());
  return Math.max(0, end - start);
}

export function mergeDateRanges(ranges: DateRange[]): DateRange[] {
  const sorted = ranges.map(normalize).sort((a, b) => a.start.getTime() - b.start.getTime());
  const merged: DateRange[] = [];
  for (const range of sorted) {
    const last = merged[merged.length - 1];
    if (last && range.start.getTime() <= last.end.getTime()) {
      if (range.end.getTime() > last.end.getTime()) last.end = new Date(range.end);
    } else {
      merged.push({ start: new Date(range.start), end: new Date(range.end) });
    }
  }
  return merged;
}