做考勤统计时,一个绕不开的需求是:员工请了3月5日到3月10日的假,但期间有两天被安排了加班,这两天的加班该怎么算?这本质上就是一个日期范围交集的计算问题。很多PHP开发者的第一反应是写一个for循环,从开始日期逐天遍历到结束日期,再判断每一天是否落在另一个区间内。这种写法能用,但代码长、效率低,遇到按月甚至按年汇总的场景,性能问题就会被放大。其实日期区间的交集有更优雅的数学解法,只需要比较几个端点值就能得出结果。

日期区间交集的数学原理
两个日期区间是否存在交集,以及交集是什么,完全由四个端点决定:区间A的起点A1、终点A2,区间B的起点B1、终点B2。交集的起点是两个起点中较大的那个,即max(A1, B1);交集的终点是两个终点中较小的那个,即min(A2, B2)。如果计算出来的交集起点小于等于交集终点,说明两个区间有重叠;反之则没有交集。
这个原理画在数轴上非常直观:两段线段只有当左端点的最大值不超过右端点的最小值时,才会有一段公共部分。对于纯日期(不含时分秒)的场景,还要注意一个细节:日期范围通常是闭区间,包含开始日和结束日当天,所以判断条件是max(A1,B1) <= min(A2,B2)。如果区间是左闭右开的,判断条件则要改成严格小于。搞清楚这一点,很多边界错误就能提前避免。
封装一个通用的交集计算函数
基于上面的原理,可以封装一个函数,输入两个区间,返回交集区间或者null。为了稳妥,建议使用DateTime对象或者标准的YYYY-MM-DD格式字符串,避免3/5/2024这类有歧义的写法。
function dateRangeIntersect($start1, $end1, $start2, $end2) {
// 交集起点:两个起点中较大的
$laterStart = max($start1, $start2);
// 交集终点:两个终点中较小的
$earlierEnd = min($end1, $end2);
// 闭区间判断:起点不晚于终点才有交集
if ($laterStart <= $earlierEnd) {
return [$laterStart, $earlierEnd];
}
return null;
}
// 示例:请假 2024-03-05 至 2024-03-10,加班 2024-03-08 至 2024-03-15
$result = dateRangeIntersect('2024-03-05', '2024-03-10', '2024-03-08', '2024-03-15');
if ($result !== null) {
echo '交集区间:' . $result[0] . ' 到 ' . $result[1]; // 2024-03-08 到 2024-03-10
}这个函数依赖PHP字符串比较的特性:标准格式的日期字符串按字典序比较的结果和按时间比较的结果一致。但如果日期来源不可控(比如用户输入),最好先用strtotime或者DateTime构造做一次校验和规范化,防止脏数据混进来。
交集天数计算与考勤场景实战
拿到交集区间后,计算天数同样简单,用DateTime的diff方法求差值再加一(闭区间包含两端)。把它和交集函数组合起来,就能解决开头提到的考勤问题:请假区间和加班区间的重叠天数,既不能算加班费也不能重复扣请假,需要单独标记出来。
function intersectDays($start1, $end1, $start2, $end2) {
$range = dateRangeIntersect($start1, $end2, $start2, $end2);
if ($range === null) {
return 0;
}
$from = new DateTime($range[0]);
$to = new DateTime($range[1]);
// diff返回的是区间跨度,闭区间需加1天
return $from->diff($to)->days + 1;
}
// 月度考勤汇总:请假10天,其中3天与加班重叠
$leaveStart = '2024-03-01';
$leaveEnd = '2024-03-10';
$otStart = '2024-03-08';
$otEnd = '2024-03-12';
$overlap = intersectDays($leaveStart, $leaveEnd, $otStart, $otEnd);
echo '请假与加班重叠天数:' . $overlap; // 输出 3逐天遍历与端点比较的效率差异
逐天遍历的写法是用一个日期指针从A1走到A2,每一步判断当前日期是否在区间B内。假设统计一年的数据,每次比较最多要循环365次,而考勤汇总往往是对几十上百名员工、每人数条记录做两两比较,循环次数会迅速膨胀。端点比较法则不管区间多长,永远只做两次max和min运算,时间复杂度是常数级的O(1)。
除了性能,可读性和可维护性也是端点比较法的优势。交集的起止日期直接作为返回值输出,方便落库、展示或者参与后续计算;而遍历法通常只统计出天数,想拿到具体区间还得再拼一次。需要注意的是,遍历法并非一无是处,如果需求本身就要逐日明细(比如输出每一天的考勤状态),那遍历反而是自然的表达方式。技术选型要看场景,不必教条。
容易踩坑的边界情况
第一种是相邻区间,比如3月1日至3月5日和3月6日至3月10日。闭区间下它们没有交集,但如果有人误用了左闭右开的判断逻辑,就会错误地算出交集。第二种是完全包含,比如请假整月,某次加班只有两天,交集就是加班区间本身,用max和min的公式天然能正确处理,不需要额外分支。第三种是起点或终点相同的区间,3月1日至5日和3月5日至8日,交集就是3月5日这一天,判断条件里的等于号在这里起了关键作用,写成了严格小于就会漏掉这一天。
还有一个实践中常见的坑:时区与时间部分。如果数据库存的是datetime而业务按日期算,2024-03-10 23:00:00和2024-03-10 00:00:00直接比较会导致结果偏差。建议在比较前统一截断到日期,用format('Y-m-d')归一化,或者全程使用DateTime并显式指定时区。把这些细节处理好,考勤统计这类日期密集型业务才能真正做到算得准、跑得快。