← 中文资源

简体中文版本

日期区间重叠与合并

判断两个日期区间是否重叠,计算交集时长,并合并重叠或相接的区间集合。

阅读英文原文

功能说明

rangesOverlap 判断两个日期区间是否有交集,overlapDurationMs 返回交集毫秒数,mergeDateRanges 把一组区间排序并合并重叠或首尾相接的部分。适合会议室预订冲突检测、促销时间段合并、日志窗口统计和排班系统。区间边界按闭区间处理,恰好相接的区间会被视为重叠并合并。

关键代码

normalize 会校验两个边界都是有效 Date,并在起点晚于终点时自动交换,容错调用方的顺序。重叠判断等价于各自起点不晚于对方终点。交集时长先取两个起点的较大值、两个终点的较小值,再用 Math.max 截断负值。合并时先按起点排序,再逐个与结果末尾比较,若相接或重叠就扩展终点,否则复制成新区间。

使用注意

闭区间语义意味着 10 点到 11 点与 11 点到 12 点会被合并,若业务要求半开区间应调整比较。函数只处理时间点,不涉及时区规则,跨时区数据应先统一到 UTC 或同一时区。合并结果中的日期是新对象,但 normalize 返回的原始区间仍被引用,调用方不应在合并过程中修改输入。空数组返回空数组。

验证方式

测试完全分离、部分重叠、包含、完全相同、恰好相接与逆序边界六种情况,断言重叠判断与交集时长。合并测试乱序输入、多个重叠链、相接区间、互不重叠与空数组,确认结果有序且边界正确。测试非法 Date、非 Date 边界与无效时间值会抛出错误,并验证输入数组与对象未被修改。

代码示例

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;
}