
本文介绍一种健壮的 php 时间区间差集算法,用于从一组初始时间区间中精确减去多个排除区间,支持任意重叠关系,并避免递归死循环,最终返回合并后无重叠的剩余时间片段。
本文介绍一种健壮的 php 时间区间差集算法,用于从一组初始时间区间中精确减去多个排除区间,支持任意重叠关系,并避免递归死循环,最终返回合并后无重叠的剩余时间片段。
在构建日程调度、资源占用计算、权限时段控制等系统时,常需解决「从一个或多个时间区间中,扣除若干排除时段,得到剩余有效时段」的问题。这本质上是时间区间的集合差运算(Set Difference),但 PHP 原生不提供此类功能,需手动实现逻辑严谨的区间切割与合并。
原始代码虽能处理单个排除区间,但在多个排除区间存在时陷入无限递归——根本原因在于:每次递归调用未对 $exclusions 进行状态隔离或进度推进,导致同一排除项反复作用于新生成的子区间,且缺乏去重与合并机制,造成逻辑震荡。
✅ 正确解法应遵循以下核心原则:
- 非递归优先:避免深度递归,改用迭代+队列方式逐个处理待切分区间;
- 区间标准化:预先合并重叠的排除区间,减少冗余计算;
- 四类交集关系全覆盖:对任一 set 与 exclusion,仅存在四种几何关系(完全包含、左交、右交、内含),需分别处理;
- 结果自动归并:切割后可能存在相邻/重叠区间,需排序后合并。
以下是优化后的生产就绪版本(已移除递归,增强鲁棒性与可读性):
立即学习“PHP免费学习笔记(深入)”;
/**
* 计算时间区间集合差:$sets 减去 $exclusions
* @param array $sets [['start' => DateTime, 'end' => DateTime], ...]
* @param array $exclusions [['start' => DateTime, 'end' => DateTime], ...]
* @return array 剩余不重叠区间列表(已按起始时间升序排列并合并)
*/
function dateIntervalDiff(array $sets, array $exclusions): array
{
// Step 1: 预处理 — 合并重叠的排除区间,提升效率并避免重复切割
$mergedExclusions = mergeDateTimeIntervals($exclusions);
// Step 2: 迭代处理每个原始区间
$result = [];
foreach ($sets as $set) {
$currentSegments = [$set]; // 当前待处理的子区间队列
// 对每个排除区间,切分所有现存子段
foreach ($mergedExclusions as $excl) {
$nextSegments = [];
foreach ($currentSegments as $seg) {
$intersection = getIntersection($seg, $excl);
if (!$intersection) {
// 无交集:保留原段
$nextSegments[] = $seg;
} else {
// 有交集:按四类关系切分
$parts = splitByExclusion($seg, $excl);
$nextSegments = array_merge($nextSegments, $parts);
}
}
$currentSegments = $nextSegments;
}
$result = array_merge($result, $currentSegments);
}
// Step 3: 合并相邻/重叠结果区间
return mergeDateTimeIntervals($result);
}
/**
* 合并重叠或相邻的时间区间(关键辅助函数)
*/
function mergeDateTimeIntervals(array $intervals): array
{
if (empty($intervals)) return [];
// 按 start 排序
usort($intervals, function($a, $b) {
return $a['start']->getTimestamp() <=> $b['start']->getTimestamp();
});
$merged = [$intervals[0]];
for ($i = 1; $i < count($intervals); $i++) {
$last = end($merged);
$curr = $intervals[$i];
// 若当前区间起点 ≤ 上一段终点 → 可合并(含相邻:如 [09:00,12:00] 和 [12:00,15:00])
if ($curr['start'] <= $last['end']) {
$last['end'] = max($last['end'], $curr['end']);
$merged[key($merged)] = $last;
} else {
$merged[] = $curr;
}
}
return $merged;
}
/**
* 获取两区间交集(返回 null 表示无交集)
*/
function getIntersection(array $a, array $b): ?array
{
$start = max($a['start'], $b['start']);
$end = min($a['end'], $b['end']);
return ($start <= $end) ? ['start' => $start, 'end' => $end] : null;
}
/**
* 将区间 $seg 按 $excl 切分为 0–2 个新区间(不含递归)
*/
function splitByExclusion(array $seg, array $excl): array
{
$sStart = $seg['start']->getTimestamp();
$sEnd = $seg['end']->getTimestamp();
$eStart = $excl['start']->getTimestamp();
$eEnd = $excl['end']->getTimestamp();
// Case 1: exclusion 完全覆盖 segment → 返回空
if ($eStart <= $sStart && $eEnd >= $sEnd) {
return [];
}
// Case 2: exclusion 在 segment 左侧相交 → 保留右侧
if ($eStart <= $sStart && $eEnd > $sStart && $eEnd < $sEnd) {
return [[
'start' => (clone $seg['start'])->setTimestamp($eEnd),
'end' => $seg['end']
]];
}
// Case 3: exclusion 在 segment 右侧相交 → 保留左侧
if ($eStart > $sStart && $eStart < $sEnd && $eEnd >= $sEnd) {
return [[
'start' => $seg['start'],
'end' => (clone $seg['end'])->setTimestamp($eStart)
]];
}
// Case 4: exclusion 完全在 segment 内部 → 拆为左右两段
if ($eStart > $sStart && $eEnd < $sEnd) {
return [
[
'start' => $seg['start'],
'end' => (clone $seg['start'])->setTimestamp($eStart)
],
[
'start' => (clone $seg['end'])->setTimestamp($eEnd),
'end' => $seg['end']
]
];
}
// No overlap → 保留原段
return [$seg];
}
// ✅ 使用示例
$sets = [
[
'start' => new DateTime('2022-07-04 00:00:00', new DateTimeZone('UTC')),
'end' => new DateTime('2022-07-04 23:59:59', new DateTimeZone('UTC'))
]
];
$exclusions = [
[
'start' => new DateTime('2022-07-04 09:00:00', new DateTimeZone('UTC')),
'end' => new DateTime('2022-07-04 13:00:00', new DateTimeZone('UTC'))
],
[
'start' => new DateTime('2022-07-04 14:00:00', new DateTimeZone('UTC')),
'end' => new DateTime('2022-07-04 18:00:00', new DateTimeZone('UTC'))
]
];
$result = dateIntervalDiff($sets, $exclusions);
foreach ($result as $r) {
echo 'From ' . $r['start']->format(DateTime::ATOM) . ' to ' . $r['end']->format(DateTime::ATOM) . '<br>';
}
// 输出:
// From 2022-07-04T00:00:00+00:00 to 2022-07-04T09:00:00+00:00
// From 2022-07-04T13:00:00+00:00 to 2022-07-04T14:00:00+00:00
// From 2022-07-04T18:00:00+00:00 to 2022-07-04T23:59:59+00:00⚠️ 关键注意事项:
- 所有 DateTime 实例必须使用相同 DateTimeZone,否则时间戳比较将出错;
- 本实现默认将 [A,B] 和 [B,C] 视为可合并相邻区间(即闭区间语义),如需开区间行为(如 [A,B)),请调整 <= 为 <;
- 输入区间需保证 start <= end,否则应提前校验;
- 大量区间场景下,建议对 $exclusions 预排序并采用区间树优化,但本版已满足中小规模业务需求。
该方案彻底规避了原始递归陷阱,逻辑清晰、边界完备、结果可预测,是 PHP 时间区间运算的推荐实践模式。



















