1. 引言

在预约系统、预订管理或项目时间线等应用中,避免时间冲突至关重要。日期重叠可能导致数据不一致和业务错误。本文将深入探讨日期范围重叠的各种场景,并介绍多种检查重叠的实用方法和公式。

2. 理解日期范围重叠

在深入实现前,我们先明确日期范围重叠的定义。理解不同重叠场景对实现高效准确的检测逻辑至关重要。以下是需要重点考虑的几种典型情况:

2.1. 部分重叠

当一个日期范围与另一个范围部分重叠时发生。两个范围共享部分时间段但未完全包含对方。例如:

  • 项目A:1月1日 - 2月10日
  • 项目B:2月5日 - 3月1日
A                            B
|-----------------------------|
                C                                D
                |--------------------------------|

2.2. 完全重叠

当一个日期范围完全被另一个范围包含时发生。一个范围完全落在另一个范围的边界内。例如:

  • 预订A:7月1日 - 7月31日
  • 预订B:7月15日 - 7月25日
A                                               B
|------------------------------------------------|
                C                      D
                |----------------------|

2.3. 连续范围

一个范围结束紧接另一个范围开始时称为连续范围。常见于项目时间线:

  • 项目X:3月1日 - 3月31日
  • 项目Y:4月1日 - 4月30日
A                    B
|---------------------|
                       C                    D
                       |--------------------|

2.4. 零持续时间与连续范围

零持续时间指范围的起止日期相同,持续时间为零。例如只安排一天的零时长事件。处理这种特殊情况对保证连续范围的检测逻辑正确性至关重要:

A                    B
|---------------------|
                      CD
                      |

3. 重叠检测公式

多种数学公式可封装日期范围重叠的检测逻辑。以下是三种常用公式及其原理说明:

先定义变量:

  • A:第一个范围的开始日期
  • B:第一个范围的结束日期
  • C:第二个范围的开始日期
  • D:第二个范围的结束日期

3.1. 计算重叠持续时间

通过计算较晚开始日期与较早结束日期的差值来判断:

(min(B, D) - max(A, C)) >= 0

⚠️ 结果解读:

  • 负值:无重叠
  • 零值:范围相切或单点重叠(需业务决定是否视为重叠)
  • 正值:存在重叠

3.2. 检查非重叠条件

通过排除非重叠情况来判断:

!(B <= C || A >= D)

✅ 关键点:

  • 使用严格不等式(</>)表示范围不能有任何接触点
  • 使用非严格不等式(<=/>=)允许边界接触
  • 业务需决定是否将边界接触视为重叠

3.3. 查找最小重叠

通过计算四种可能重叠的最小值来判断:

min((B - A), (B - C), (D - A), (D - C)) >= 0

⚠️ 条件解读:

  • 零或负值:无实际重叠(仅接触或分离)
  • 正值:存在非零持续时间的重叠
  • 改用> 0可强制要求必须存在有效重叠时长

4. 实现方案

现在我们用不同方案实现重叠检测逻辑,踩过坑的代码更实用。

4.1. 使用 Calendar 类

利用 Calendar 类管理日期范围并检测重叠。通过getTimeInMillis()获取毫秒值计算。

方案1:计算重叠持续时间

boolean isOverlapUsingCalendarAndDuration(Calendar start1, Calendar end1, Calendar start2, Calendar end2) {
    long overlap = Math.min(end1.getTimeInMillis(), end2.getTimeInMillis()) - 
                   Math.max(start1.getTimeInMillis(), start2.getTimeInMillis());
    return overlap > 0;
}

方案2:检查非重叠条件

boolean isOverlapUsingCalendarAndCondition(Calendar start1, Calendar end1, Calendar start2, Calendar end2) {
    return !(end1.before(start2) || start1.after(end2));
}

方案3:查找最小重叠

boolean isOverlapUsingCalendarAndFindMin(Calendar start1, Calendar end1, Calendar start2, Calendar end2) {
    long overlap1 = Math.min(end1.getTimeInMillis() - start1.getTimeInMillis(), 
                             end1.getTimeInMillis() - start2.getTimeInMillis());
    long overlap2 = Math.min(end2.getTimeInMillis() - start2.getTimeInMillis(), 
                             end2.getTimeInMillis() - start1.getTimeInMillis());
    return Math.min(overlap1, overlap2) >= 0;
}

测试用例

// 创建基础范围:2024-12-15 到 2024-12-20
Calendar start1 = Calendar.getInstance();
start1.set(2024, 11, 15); // 月份从0开始
Calendar end1 = Calendar.getInstance();
end1.set(2024, 11, 20);

// 场景1:部分重叠 (2024-12-18 到 2024-12-22)
Calendar start2 = Calendar.getInstance();
start2.set(2024, 11, 18);
Calendar end2 = Calendar.getInstance();
end2.set(2024, 11, 22);

assertTrue(isOverlapUsingCalendarAndDuration(start1, end1, start2, end2));
assertTrue(isOverlapUsingCalendarAndCondition(start1, end1, start2, end2));
assertTrue(isOverlapUsingCalendarAndFindMin(start1, end1, start2, end2));

// 场景2:完全重叠 (2024-12-16 到 2024-12-18)
start2.set(2024, 11, 16);
end2.set(2024, 11, 18);

assertTrue(isOverlapUsingCalendarAndDuration(start1, end1, start2, end2));
assertTrue(isOverlapUsingCalendarAndCondition(start1, end1, start2, end2));
assertTrue(isOverlapUsingCalendarAndFindMin(start1, end1, start2, end2));

// 场景3:连续范围 (2024-12-21 到 2024-12-24)
start2.set(2024, 11, 21);
end2.set(2024, 11, 24);

assertFalse(isOverlapUsingCalendarAndDuration(start1, end1, start2, end2));
assertFalse(isOverlapUsingCalendarAndCondition(start1, end1, start2, end2));
assertFalse(isOverlapUsingCalendarAndFindMin(start1, end1, start2, end2));

4.2. 使用 Java 8 的 LocalDate

Java 8 引入的 LocalDate 提供了更现代的日期处理方式。利用toEpochDay()获取天数简化计算。

方案1:计算重叠持续时间

boolean isOverlapUsingLocalDateAndDuration(LocalDate start1, LocalDate end1, LocalDate start2, LocalDate end2) {
    long overlap = Math.min(end1.toEpochDay(), end2.toEpochDay()) - 
                   Math.max(start1.toEpochDay(), start2.toEpochDay());
    return overlap >= 0;
}

方案2:检查非重叠条件

boolean isOverlapUsingLocalDateAndCondition(LocalDate start1, LocalDate end1, LocalDate start2, LocalDate end2) {
    return !(end1.isBefore(start2) || start1.isAfter(end2));
}

方案3:查找最小重叠

boolean isOverlapUsingLocalDateAndFindMin(LocalDate start1, LocalDate end1, LocalDate start2, LocalDate end2) {
    long overlap1 = Math.min(end1.toEpochDay() - start1.toEpochDay(), 
                             end1.toEpochDay() - start2.toEpochDay());
    long overlap2 = Math.min(end2.toEpochDay() - start2.toEpochDay(), 
                             end2.toEpochDay() - start1.toEpochDay());
    return Math.min(overlap1, overlap2) >= 0;
}

测试用例

// 基础范围:2024-12-15 到 2024-12-20
LocalDate start1 = LocalDate.of(2024, 12, 15);
LocalDate end1 = LocalDate.of(2024, 12, 20);

// 场景1:部分重叠 (2024-12-18 到 2024-12-22)
LocalDate start2 = LocalDate.of(2024, 12, 18);
LocalDate end2 = LocalDate.of(2024, 12, 22);

assertTrue(isOverlapUsingLocalDateAndDuration(start1, end1, start2, end2));
assertTrue(isOverlapUsingLocalDateAndCondition(start1, end1, start2, end2));
assertTrue(isOverlapUsingLocalDateAndFindMin(start1, end1, start2, end2));

// 场景2:完全重叠 (2024-12-16 到 2024-12-18)
start2 = LocalDate.of(2024, 12, 16);
end2 = LocalDate.of(2024, 12, 18);

assertTrue(isOverlapUsingLocalDateAndDuration(start1, end1, start2, end2));
assertTrue(isOverlapUsingLocalDateAndCondition(start1, end1, start2, end2));
assertTrue(isOverlapUsingLocalDateAndFindMin(start1, end1, start2, end2));

// 场景3:连续范围 (2024-12-21 到 2024-12-24)
start2 = LocalDate.of(2024, 12, 21);
end2 = LocalDate.of(2024, 12, 24);

assertFalse(isOverlapUsingLocalDateAndDuration(start1, end1, start2, end2));
assertFalse(isOverlapUsingLocalDateAndCondition(start1, end1, start2, end2));
assertFalse(isOverlapUsingLocalDateAndFindMin(start1, end1, start2, end2));

4.3. 使用 Joda-Time 库

Joda-Time 库提供了便捷的 overlaps() 方法,简化了区间重叠检测。

⚠️ 注意:Joda-Time 认为起止点完全相同的区间不重叠,这在需要精确边界的场景中很重要。

实现代码

boolean isOverlapUsingJodaTime(DateTime start1, DateTime end1, DateTime start2, DateTime end2) {
    Interval interval1 = new Interval(start1, end1);
    Interval interval2 = new Interval(start2, end2);
    return interval1.overlaps(interval2);
}

测试用例

// 基础范围:2024-12-15 到 2024-12-20
DateTime startJT1 = new DateTime(2024, 12, 15, 0, 0);
DateTime endJT1 = new DateTime(2024, 12, 20, 0, 0);

// 场景1:部分重叠 (2024-12-18 到 2024-12-22)
DateTime startJT2 = new DateTime(2024, 12, 18, 0, 0);
DateTime endJT2 = new DateTime(2024, 12, 22, 0, 0);

assertTrue(isOverlapUsingJodaTime(startJT1, endJT1, startJT2, endJT2));

// 场景2:完全重叠 (2024-12-16 到 2024-12-18)
startJT2 = new DateTime(2024, 12, 16, 0, 0);
endJT2 = new DateTime(2024, 12, 18, 0, 0);

assertTrue(isOverlapUsingJodaTime(startJT1, endJT1, startJT2, endJT2));

// 场景3:连续范围 (2024-12-21 到 2024-12-24)
startJT2 = new DateTime(2024, 12, 21, 0, 0);
endJT2 = new DateTime(2024, 12, 24, 0, 0);

assertFalse(isOverlapUsingJodaTime(startJT1, endJT1, startJT2, endJT2));

5. 总结

本文系统探讨了日期范围重叠检测的多种场景、数学公式和实现方案。通过理解部分重叠、完全重叠、连续范围和零持续时间等概念,我们建立了坚实的理论基础。针对不同技术栈(Calendar、Java 8 LocalDate、Joda-Time)提供了可直接使用的实现方案,并附带了完整的测试用例。

✅ 关键收获:

  1. 公式选择:根据业务需求选择严格或宽松的重叠判定条件
  2. API对比:Java 8 LocalDate 提供最简洁的实现,Joda-Time 适合需要精确边界控制的场景
  3. 边界处理:明确零持续时间和边界接触的业务定义,避免逻辑漏洞

所有示例代码可在 GitHub 获取。


原始标题:Check if Two Date Ranges Overlap