計算兩個日期之間的年、月、周、日差值是常見業務需求(會員天數、租賃計費、專案工期)。PHP `DateTime`/`DateInterval` 雖強大,但直接用 `diff()` 得到的 `y/m/d` 非直觀「總月數/總周數」。本文提供兩個實用函式:dateDiffDetail() 精確分解年月周日、getWeekInfo() 查詢指定日期所屬週數與起迄日。
/**
* 計算兩日期差值,返回 ['years'=>, 'months'=>, 'weeks'=>, 'days'=>, 'total_days'=>]
* @param string $start 起始日期 (Y-m-d)
* @param string $end 結束日期 (Y-m-d)
* @return array
*/
function dateDiffDetail(string $start, string $end): array
<?php
function dateDiffDetail(string $start, string $end): array {
// 確保順序
if (strtotime($start) > strtotime($end)) {
[$start, $end] = [$end, $start];
}
$startTs = strtotime($start);
$endTs = strtotime($end);
$totalDays = ($endTs - $startTs) / 86400;
$result = ['total_days' => (int)$totalDays];
if ($totalDays < 7) {
$result['days'] = (int)$totalDays;
return $result;
}
// 年
$years = 0;
while (strtotime("+{$years} year", $startTs) <= $endTs) $years++;
$years = max(0, $years - 1);
if ($years > 0) {
$result['years'] = $years;
$startTs = strtotime("+{$years} year", $startTs);
$start = date('Y-m-d', $startTs);
}
// 月
$months = 0;
while (strtotime("+{$months} month", $startTs) <= $endTs) $months++;
$months = max(0, $months - 1);
if ($months > 0) {
$result['months'] = $months;
$startTs = strtotime("+{$months} month", $startTs);
}
// 剩餘天數
$remainDays = ($endTs - $startTs) / 86400;
if ($remainDays >= 7) {
$result['weeks'] = (int)($remainDays / 7);
$remainDays = $remainDays % 7;
}
if ($remainDays > 0) {
$result['days'] = (int)$remainDays;
}
return array_filter($result);
}
// 測試
print_r(dateDiffDetail('2012-10-01', '2012-12-15'));
/*
Array
(
[total_days] => 75
[months] => 2
[weeks] => 2
[days] => 1
)
*/
/**
* 取得日期所屬 ISO 8601 週數、週一起始日、週日結束日
* @param string $date 日期 (Y-m-d)
* @return array ['week_num', 'week_start', 'week_end', 'week_start_cn', 'week_end_cn']
*/
function getWeekInfo(string $date): array
function getWeekInfo(string $date): array {
$ts = strtotime($date);
// ISO 8601: 週一為週首,%V 為週數(01-53), %u 為星期(1=Mon..7=Sun)
$weekNum = (int)strftime('%V', $ts);
$weekDay = (int)strftime('%u', $ts); // 1-7
$startTs = $ts - ($weekDay - 1) * 86400; // 週一
$endTs = $ts + (7 - $weekDay) * 86400; // 週日
return [
'week_num' => $weekNum,
'week_start' => date('Y-m-d', $startTs),
'week_end' => date('Y-m-d', $endTs),
'week_start_cn' => strftime('%Y年%m月%d日', $startTs),
'week_end_cn' => strftime('%Y年%m月%d日', $endTs),
];
}
// 測試
print_r(getWeekInfo('2024-03-15'));
/*
Array
(
[week_num] => 11
[week_start] => 2024-03-11
[week_end] => 2024-03-17
[week_start_cn] => 2024年03月11日
[week_end_cn] => 2024年03月17日
)
*/
若需更物件導向、支援時區的實作:
use DateTime;
use DateInterval;
function dateDiffModern(string $start, string $end, string $tz = 'UTC'): array {
$dt1 = new DateTime($start, new DateTimeZone($tz));
$dt2 = new DateTime($end, new DateTimeZone($tz));
if ($dt1 > $dt2) [$dt1, $dt2] = [$dt2, $dt1];
$diff = $dt1->diff($dt2); // DateInterval
// 總天數
$totalDays = (int)$dt1->diff($dt2)->days;
// 年月日直接取得
$result = [
'years' => $diff->y,
'months' => $diff->m,
'days' => $diff->d,
'total_days' => $totalDays,
];
// 週數:總天數扣除年月後的餘天計算
$remain = $totalDays - ($diff->y * 365 + $diff->m * 30); // 粗略
if ($remain >= 7) {
$result['weeks'] = (int)($remain / 7);
$result['days'] = $remain % 7;
}
return array_filter($result);
}
注意: `DateInterval::days` 需 PHP 5.3+ 且由 `DateTime::diff()` 產生,手動 `new DateInterval` 不會填充此屬性。
| 陷阱 | 解決方案 |
|---|---|
| 閏年/大小月導致月長不固定 | 使用 `strtotime('+N month')` 由系統自動處理月末邊界 |
| 時區差異導致跨日錯誤 | 統一使用 UTC 或指定時區,儲存/計算皆一致 |
| 開始/結束日是否包含 | 明確業務定義:`diff` 不包含結束日,需+1 視情況調整 |
| 週數標準不一(ISO vs US) | ISO 8601 週一起始(`%V/%u`),US 週日起始(`%U/%w`) |
| 效能:大量日期計算 | 預編譯時間戳、避免重複 `strtotime`、考慮向量化運算 |
日期間隔計算看似簡單,實則涉及曆法邊界、時區、業務定義等細節。dateDiffDetail() 採用「由大到小遞減+系統曆法微調」策略,精確分解年月周日;getWeekInfo() 依循 ISO 8601 標準輸出週資訊。現代專案建議封裝為 `DateCalculator` 服務類,注入時區配置,統一對外提供 `diff()`、`weekOf()` 等介面,確保全系統計算邏輯一致。