PHP 計算兩個日期之間的年、月、日差值,看似簡單實則暗藏「月份天數不固定、閏年、跨月/跨年借位」等邊界陷阱。本文提供一個經過實戰驗證的精確算法函數 DiffDate(),深度解析其數學邏輯、時間戳運算、借位規則及測試用例覆蓋。
/**
* 計算兩個日期差值(年、月、日)
*
* @param string $date1 起始日期 Y-m-d
* @param string $date2 結束日期 Y-m-d
* @return array [years, months, days]
*/
function DiffDate($date1, $date2) {
// 1. 確保 date1 <= date2
if (strtotime($date1) > strtotime($date2)) {
list($date1, $date2) = [$date2, $date1];
}
// 2. 解析年月日
list($y1, $m1, $d1) = explode('-', $date1);
list($y2, $m2, $d2) = explode('-', $date2);
// 3. 計算總月份差
$totalMonths = ($y2 - $y1) * 12 + ($m2 - $m1);
$years = intval($totalMonths / 12);
$months = $totalMonths % 12;
// 4. 計算天數差(關鍵:以 date2 的年月為基準,比較日)
// 構造「date2 年月 + date1 日」的時間戳
$ts1 = mktime(0, 0, 0, $m2, $d1, $y2);
$ts2 = mktime(0, 0, 0, $m2, $d2, $y2);
$days = intval(($ts2 - $ts1) / 86400); // 秒轉天
// 5. 天數借位處理(date1 日 > date2 日,需向月份借位)
if ($days < 0) {
$months -= 1;
// 獲取 date2 所在月份的總天數(動態處理閏年、大小月)
$daysInMonth = date('j', mktime(0, 0, 0, $m2, 0, $y2)); // 第 0 天 = 上月最後一天
$days += $daysInMonth;
}
// 6. 月份借位處理(月份變負,向年份借位)
if ($months < 0) {
$years -= 1;
$months += 12;
}
return [$years, $months, $days];
}
將日期差轉換為「總月份數」,再除以 12 得年、取模得月。避免了「年差×12+月差」的分步累積誤差。
不直接用 $d2 - $d1,而是構造兩個時間戳:
ts1:date2 的年月 + date1 的日ts2:date2 的年月 + date2 的日兩者差值 ÷ 86400 = 天數差。關鍵優勢:自動處理「date1 日 > date2 日」導致的負天數,統一在步驟 5 借位。
$daysInMonth = date('j', mktime(0, 0, 0, $m2, 0, $y2));
mktime(..., 月, 0, 年) 返回上月最後一天(即本月 0 號),date('j') 取日數。完美解決:
借位後月份可能變負(如 1 月借位 → 0 月),需向年份借 1 年(12 月)。
$tests = [
['2023-01-01', '2024-01-01', [1, 0, 0]], // 淨一年
['2023-01-15', '2023-02-14', [0, 0, 30]], // 跨月,31天月借位
['2023-01-31', '2023-03-01', [0, 1, 1]], // 1月31日到3月1日:1個月1天
['2020-02-28', '2020-03-01', [0, 0, 2]], // 閏年2月28到3月1:2天
['2021-02-28', '2021-03-01', [0, 0, 1]], // 平年2月28到3月1:1天
['2023-12-31', '2024-01-01', [0, 0, 1]], // 跨年
['2024-01-01', '2023-01-01', [1, 0, 0]], // 參數順序反轉自動修正
];
foreach ($tests as [$d1, $d2, $expected]) {
$result = DiffDate($d1, $d2);
$status = ($result === $expected) ? '✅ PASS' : '❌ FAIL';
echo "$status DiffDate('$d1', '$d2') = [" . implode(', ', $result) . "] expected [" . implode(', ', $expected) . "]\n";
}
$d1 = new DateTime('2023-01-15');
$d2 = new DateTime('2023-02-14');
$diff = $d1->diff($d2);
echo "y:{$diff->y} m:{$diff->m} d:{$diff->d}"; // y:0 m:0 d:30
// 注意:DateInterval 不自動進位月份(30天不轉為1月),需自行處理
結論:DateTime::diff() 返回「非規範化」差值(天數可能>30),本文算法輸出「規範化」年/月/日(天數<月天數),更符合人類閱讀習慣。
function DiffDateTime($datetime1, $datetime2) {
$ts1 = strtotime($datetime1);
$ts2 = strtotime($datetime2);
if ($ts1 > $ts2) list($ts1, $ts2) = [$ts2, $ts1];
$diff = $ts2 - $ts1;
return [
'days' => intval($diff / 86400),
'hours' => intval(($diff % 86400) / 3600),
'minutes' => intval(($diff % 3600) / 60),
'seconds' => $diff % 60,
];
}
日期差計算的核心難點在於「月份天數動態、借位鏈式傳遞」。本文算法通過「總月份差分離年月 + 基準月構造時間戳計算天數差 + 動態月天數借位 + 月份負值借位」四步走,以純時間戳運算實現了無依賴、高精度、強兼容的日期差計算。建議將 DiffDate() 封裝至項目工具庫,替代手動減法邏輯,消除邊界 Bug。