計算日期時間差是常見需求,PHP 提供多種方式:`DateTime` 物件導向介面、`date_diff()` 函式、時間戳運算、資料庫函式。本文比較各方法優缺點並提供最佳實踐範例。
PHP 5.3+ 強烈建議使用 `DateTime` 類別,支援時區、夏令時間、閏年等複雜邏輯。
<?php
$start = new DateTime('2023-01-15 09:30:00');
$end = new DateTime('2023-12-25 18:45:00');
$interval = $start->diff($end);
echo "相差 {$interval->y} 年 {$interval->m} 月 {$interval->d} 天 ";
echo "{$interval->h} 時 {$interval->i} 分 {$interval->s} 秒\n";
// 總天數
echo "總天數:{$interval->days}\n";
// 總秒數
$totalSeconds = $interval->days * 86400 + $interval->h * 3600 + $interval->i * 60 + $interval->s;
echo "總秒數:{$totalSeconds}\n";
?>
<?php
function humanTimeDiff(DateTime $from, DateTime $to = null): string {
$to = $to ?? new DateTime();
$interval = $from->diff($to);
$units = [
'y' => ['年', '年'],
'm' => ['個月', '個月'],
'd' => ['天', '天'],
'h' => ['小時', '小時'],
'i' => ['分鐘', '分鐘'],
's' => ['秒', '秒']
];
foreach ($units as $prop => [$singular, $plural]) {
$value = $interval->$prop;
if ($value > 0) {
return "{$value}{$singular}前";
}
}
return '剛剛';
}
// 使用
$postTime = new DateTime('2023-12-20 14:30:00');
echo humanTimeDiff($postTime); // 輸出:5天前(假設現在是 12/25)
?>
<?php
function businessDaysBetween(DateTime $start, DateTime $end): int {
$start = clone $start;
$end = clone $end;
$start->setTime(0, 0, 0);
$end->setTime(0, 0, 0);
if ($start > $end) {
[$start, $end] = [$end, $start];
}
$days = 0;
while ($start <= $end) {
$dayOfWeek = (int)$start->format('N'); // 1=週一 ... 7=週日
if ($dayOfWeek < 6) { // 週一至週五
$days++;
}
$start->modify('+1 day');
}
return $days;
}
// 進階:排除國定假日
function businessDaysExcludingHolidays(DateTime $start, DateTime $end, array $holidays = []): int {
$holidaySet = array_flip(array_map(fn($d) => $d->format('Y-m-d'), $holidays));
$count = 0;
$current = clone $start;
$current->setTime(0, 0, 0);
$endClone = clone $end;
$endClone->setTime(0, 0, 0);
while ($current <= $endClone) {
$dow = (int)$current->format('N');
$dateStr = $current->format('Y-m-d');
if ($dow < 6 && !isset($holidaySet[$dateStr])) {
$count++;
}
$current->modify('+1 day');
}
return $count;
}
?>
<?php
$start = strtotime('2023-01-15');
$end = strtotime('2023-12-25');
$diffSeconds = $end - $start;
$diffDays = floor($diffSeconds / 86400);
echo "相差 {$diffDays} 天\n";
// 注意:strtotime 受限於 32 位元系統的 2038 年問題
// 64 位元 PHP 無此限制
?>
-- 日期差(天數)
SELECT DATEDIFF('2023-12-25', '2023-01-15') AS days_diff; -- 344
-- 時間差(秒數)
SELECT TIMESTAMPDIFF(SECOND, '2023-01-15 09:30:00', '2023-12-25 18:45:00') AS seconds_diff;
-- 多種單位
SELECT
TIMESTAMPDIFF(YEAR, start_dt, end_dt) AS years,
TIMESTAMPDIFF(MONTH, start_dt, end_dt) % 12 AS months,
TIMESTAMPDIFF(DAY, start_dt, end_dt) % 30 AS days;
-- 間隔類型
SELECT age('2023-12-25 18:45:00'::timestamptz, '2023-01-15 09:30:00'::timestamptz) AS age_interval;
-- 輸出:1 year 11 mons 10 days 09:15:00
-- 總天數
SELECT ('2023-12-25'::date - '2023-01-15'::date) AS days_diff; -- 344
| 問題 | 原因 | 解決 |
|---|---|---|
| 月份長度不固定導致月差不準 | `diff()` 回傳曆月差,非固定 30 天 | 用天數換算或接受曆月定義 |
| 時區導致日期跳躍 | 未指定時區或時區不一致 | 統一用 UTC 儲存,顯示時轉換 |
| 夏令時間導致 23/25 小時天 | DST 切換日 | 用 `diff()` 自動處理,避免手算秒數 |
| 閏年 2/29 計算 | 非閏年無 2/29 | `DateTime` 內建處理 |
| 2038 年問題 | 32 位元 `time_t` 溢位 | 用 64 位元 PHP + `DateTime` |
<?php
// 1. 程式啟動時設定預設時區
date_default_timezone_set('Asia/Taipei');
// 2. 資料庫儲存用 UTC
$utc = new DateTime('now', new DateTimeZone('UTC'));
$stmt = $pdo->prepare('INSERT INTO events (event_time_utc) VALUES (?)');
$stmt->execute([$utc->format('Y-m-d H:i:s')]);
// 3. 讀取時轉換為使用者時區
function toUserTimezone(string $utcDateTime, string $userTz = 'Asia/Taipei'): string {
$dt = new DateTime($utcDateTime, new DateTimeZone('UTC'));
$dt->setTimezone(new DateTimeZone($userTz));
return $dt->format('Y-m-d H:i:s');
}
// 4. 前端傳送 ISO 8601 含時區字串
// "2023-12-25T18:45:00+08:00"
$dt = new DateTime($_POST['event_time']); // 自動解析時區
$utc = clone $dt;
$utc->setTimezone(new DateTimeZone('UTC'));
?>
<?php
// 基準測試
$iterations = 1_000_000;
$start = '2023-01-01';
$end = '2023-12-31';
// DateTime::diff
$t = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
(new DateTime($start))->diff(new DateTime($end));
}
echo "DateTime::diff: " . (microtime(true) - $t) . "s\n";
// strtotime 減法
$t = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
(strtotime($end) - strtotime($start)) / 86400;
}
echo "strtotime: " . (microtime(true) - $t) . "s\n";
// 通常 strtotime 較快但功能有限,DateTime 功能完整且安全
?>
**總結**:現代 PHP 專案應優先使用 `DateTime`/`DateTimeImmutable` 類別配合 `diff()` 方法,功能最完整、時區處理正確、避免 2038 年問題。資料庫層面計算適合報表查詢、大量資料過濾。時間戳運算僅適合簡單天數差且不涉及時區的舊代碼維護。