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

PHP strtotime 函數深度解析:用法、原理與「-1 month」陷阱

     編輯:PHP 綜合知識

前言

strtotime() 是 PHP 處理日期時間字串的核心函式,功能強大但行為有時違背直覺(特別是月份運算)。本文系統整理常用用法、底層實現原理,並深入剖析 strtotime("-1 month") 在月末日期的失效原因。

一、 常用用法速查

1. 月份關鍵字

strtotime("JAN")        // 本年 1 月今日(若今日 31 號且 1 月無 31 號,順延至 2 月)
strtotime("January")    // 同上
strtotime("feb", strtotime("2011-03-31")) // 2011-03-03(3月31日往前推一個月,2月無31日,順延3月3日)

機制: 僅修改 month 欄位,day 保持不變,超出目標月份天數時自動順延至下月。

2. first / last 關鍵字

strtotime("first sunday", strtotime("2011-01-01")) // 2011 年第一個星期日
strtotime("last day", strtotime("2000-02-01"))    // 2000 年 2 月最後一天(29號,閏年)
  • first day:將 day=1
  • last day:將 day=0 並 month++(C 底層 tm_mday=0 表示上月最後一天)

3. previous / next 關鍵字

strtotime("previous sunday", strtotime("2011-02-01")) // 2011-01-30
strtotime("next monday", strtotime("2011-02-01"))     // 2011-02-07

4. back of / front of(小時級操作)

strtotime("back of 24", strtotime("2011-02-01"))  // 2011-02-02 00:15:00(24點後15分)
strtotime("front of 24", strtotime("2011-02-01")) // 2011-02-01 23:45:00(24點前45分)
  • back of H:設為 H:15,若 H=24 則翌晨 00:15
  • front of H:設為 H-1:45,若 H=0 則前日 23:45

二、 底層實現原理

1. 詞法解析

PHP 使用 re2c 生成詞法分析器,源碼位於 ext/date/lib/parse_date.re。輸入字串經 Tokenize 後,匹配對應規則執行動作。

2. 核心結構體

// 掃描器狀態
struct Scanner {
    int fd;
    uchar *lim, *str, *ptr, *cur, *tok, *pos;
    unsigned int line, len;
    struct timelib_error_container *errors;
    struct timelib_time *time;      // 解析結果時間結構
    const timelib_tzdb *tzdb;
};

// 時間結構
struct timelib_time {
    timelib_sll y, m, d;            // 年月日
    timelib_sll h, i, s;            // 時分秒
    double f;                       // 微秒
    int z;                          // 時區偏移(分鐘)
    char *tz_abbr;
    timelib_tzinfo *tz_info;
    int dst;
    timelib_rel_time relative;      // 相對時間調整量
    timelib_sll sse;                // Unix timestamp
    unsigned int have_time, have_date, have_zone, have_relative, have_weeknr_day;
    unsigned int sse_uptodate, tim_uptodate;
    unsigned int is_localtime;
    unsigned int zone_type;
};

// 相對時間調整
struct timelib_rel_time {
    timelib_sll y, m, d;            // 年月日差值
    timelib_sll h, i, s;            // 時分秒差值
    int weekday;                    // 星期幾
    int weekday_behavior;
    int first_last_day_of;          // first/last day 標記
    int invert;                     // 是否反向
    timelib_sll days;               // 總天數差
    timelib_special special;
    unsigned int have_weekday_relative, have_special_relative;
};

3. 解析流程

  1. PHP_FUNCTION(strtotime) 處理參數,預設第二參數為 time()。
  2. 字串傳給 timelib_strtotime(),由 re2c 解析器處理。
  3. 關鍵字匹配對應動作,修改 Scanner->time->relative 結構。
  4. 最終呼叫 timelib_update_ts() 將相對量套用到基準時間,產生 timestamp。

4. 範例:「yesterday」規則

'yesterday' {
    DEBUG_OUTPUT("yesterday");
    TIMELIB_INIT;
    TIMELIB_HAVE_RELATIVE();
    TIMELIB_UNHAVE_TIME();
    s->time->relative.d = -1;   // 相對天數 -1
    TIMELIB_DEINIT;
    return TIMELIB_RELATIVE;
}

三、 「-1 month」陷阱深度剖析

問題現象

date("Y-m-d", strtotime("-1 month", strtotime("2011-03-31"))); // 輸出 2011-03-03,預期 2011-02-28

底層執行邏輯

  1. 解析 -1 month,正則匹配 relative = relnumber space? reltextunit。
  2. 識別 amount = -1、unit = TIMELIB_MONTH。
  3. 執行:s->time->relative.m += amount * multiplier(multiplier=1),即 relative.m = -1。
  4. 套用相對時間時,直接將月份減 1,日期保持 31:2011-03-31 -> 2011-02-31。
  5. 2011-02-31 非合法日期,C 標準庫 mktime() 自動正規化為 2011-03-03(2月28天 + 3天)。

為何這不是 Bug?

這是 「月份平移」語義:保持「日」不變,僅調整「月」。若目標月無此日,順延至下月。這符合許多商業邏輯(如:每月 31 日扣款,2 月改為 3 月 3 日)。但若需求是「上個月同一天,不存在則取該月最後一天」,則需自行處理。

四、 正確取得「上個月今天」的解法

function lastMonthSameDay($timestamp) {
    $year  = (int)date('Y', $timestamp);
    $month = (int)date('n', $timestamp);
    $day   = (int)date('j', $timestamp);

    // 計算上個月年月
    if ($month === 1) {
        $year--;
        $month = 12;
    } else {
        $month--;
    }

    // 上個月最後一天
    $lastDay = (int)date('t', mktime(0, 0, 0, $month, 1, $year));

    // 取較小值:原日期 vs 上個月最後一天
    $targetDay = min($day, $lastDay);

    return mktime(
        (int)date('G', $timestamp),
        (int)date('i', $timestamp),
        (int)date('s', $timestamp),
        $month, $targetDay, $year
    );
}

// 測試
echo date('Y-m-d', lastMonthSameDay(strtotime('2011-03-31'))); // 2011-02-28
echo date('Y-m-d', lastMonthSameDay(strtotime('2011-01-15'))); // 2010-12-15

結語

strtotime() 的「相對月份運算」遵循 日不變、月平移、非法日順延 的語義,而非直覺的「上個月同一天」。理解其底層 timelib_rel_time 機制,才能在日期邏輯中避開陷阱。需求不同時,請自行實作「取月尾」或「取月首」邏輯。

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