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)提供了可直接使用的实现方案,并附带了完整的测试用例。
✅ 关键收获:
- 公式选择:根据业务需求选择严格或宽松的重叠判定条件
- API对比:Java 8 LocalDate 提供最简洁的实现,Joda-Time 适合需要精确边界控制的场景
- 边界处理:明确零持续时间和边界接触的业务定义,避免逻辑漏洞
所有示例代码可在 GitHub 获取。