跳轉到

來自PHP的strtotime函數的坑

問題描述

今天,我手邊某個外包的Case收到客戶的友善回報,當用戶希望將某個日期的預約時間移動到下個月或者提前一個月時,有時候會發生跟預期的結果的不同,最終將問題定位在strtotime這個函數上。

問題重現

$reservation_date = "2019-09-05";
var_dump(date("Y-m-d", strtotime("-1 month", strtotime($reservation_date)))); //2019-08-05-如預期

$reservation_date = "2019-09-05";
var_dump(date("Y-m-d", strtotime("+1 month", strtotime($reservation_date)))); //2019-10-05-如預期

$reservation_date = "2019-08-31";
var_dump(date("Y-m-d", strtotime("-1 month", strtotime($reservation_date)))); //2019-07-31-如預期

$reservation_date = "2019-07-31";
var_dump(date("Y-m-d", strtotime("-1 month", strtotime($reservation_date)))); //2019-07-01

$reservation_date = "2019-03-31";
var_dump(date("Y-m-d", strtotime("-1 month", strtotime($reservation_date)))); //2019-03-03

$reservation_date = "2016-03-31";
var_dump(date("Y-m-d", strtotime("-1 month", strtotime($reservation_date)))); //2016-03-02

問題原因

從上面重現的程式碼來看,主要的問題是發生在月底最後一天,且是發生在大小月(月份天數不同)的情境下,會發生這個問題原因主要是strtotime內部時間計算邏輯是以對應日的方式去處理。

以上面問題重現的第四個錯誤的例子來說。

$reservation_date = "2019-07-31";
var_dump(date("Y-m-d", strtotime("-1 month", strtotime($reservation_date)))); //2019-07-01
根據結果,推測PHP strtotime 函數的內部邏輯,strtotime應該是先嘗試找到對應日,例如07-31減去一個月後的對應日是06-31,但是由於6月份沒有31日,所以進行日期規範化後變成07-01

有待驗證的想法

其實我更覺得,可能會像是,會把差距的天數加在下一個月份。 例如,上面的例子由於6月沒有31日只有30日,所以31-30 = 1,然後把這個數字加進下個月的裡,從0開始加,就變得是7月0日+1結果是7月1日

根據上面的想法,看第5個錯誤的例子。

$reservation_date = "2019-03-31";
var_dump(date("Y-m-d", strtotime("-1 month", strtotime($reservation_date)))); //2019-03-03
為什麼會變成3月3日呢,按照原本的邏輯,3月31的前一個月的對應日為2月31日,但是由於2月只有28天,所以31-28 = 3,把數字加入到下個月裡,變成3月0日+3,結果是3月3日

接著,我們繼續看第6個錯誤的例子。

$reservation_date = "2016-03-31";
var_dump(date("Y-m-d", strtotime("-1 month", strtotime($reservation_date)))); //2016-03-02
那為什麼這個錯誤的例子卻是3月2日,這邊主要是閏年的關係,2016年2月有29天。

解決方式

  • 使用修飾短語last day of
    $reservation_date = "2019-07-31";
    var_dump(date("Y-m-d", strtotime("last day of -1 month", strtotime($reservation_date)))); 
    //顯示 string(10) "2019-06-30"
    
  • 用時間戳記處理
    $date = '2019-07-31';
    $timestamp = strtotime($date);
    $timestamp -= 2678400; // 一個月有 2678400 秒
    $new_date = date('Y-m-d', $timestamp);
    
    echo $new_date; // 輸出:2019-06-30