首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 計算兩個日期差(年/月/日)精確算法:時間戳轉換、月份天數動態計算與邊界情況處理

     編輯:PHP 綜合知識

PHP 計算兩個日期之間的年、月、日差值,看似簡單實則暗藏「月份天數不固定、閏年、跨月/跨年借位」等邊界陷阱。本文提供一個經過實戰驗證的精確算法函數 DiffDate(),深度解析其數學邏輯、時間戳運算、借位規則及測試用例覆蓋。

核心算法: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];
}

算法邏輯深度解析

步驟 3:總月份差 → 年/月分離

將日期差轉換為「總月份數」,再除以 12 得年、取模得月。避免了「年差×12+月差」的分步累積誤差。

步驟 4:天數差計算的巧妙設計

不直接用 $d2 - $d1,而是構造兩個時間戳:

  • ts1:date2 的年月 + date1 的日
  • ts2:date2 的年月 + date2 的日

兩者差值 ÷ 86400 = 天數差。關鍵優勢:自動處理「date1 日 > date2 日」導致的負天數,統一在步驟 5 借位。

步驟 5:動態獲取月份天數借位

$daysInMonth = date('j', mktime(0, 0, 0, $m2, 0, $y2));

mktime(..., 月, 0, 年) 返回上月最後一天(即本月 0 號),date('j') 取日數。完美解決:

  • 2 月閏年 29 天 / 平年 28 天
  • 大月 31 天、小月 30 天

步驟 6:月份負值借位

借位後月份可能變負(如 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";
}

對比:DateTime::diff() 內置方法

$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。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved