strtotime() 是 PHP 處理日期時間字串的核心函式,功能強大但行為有時違背直覺(特別是月份運算)。本文系統整理常用用法、底層實現原理,並深入剖析 strtotime("-1 month") 在月末日期的失效原因。
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 保持不變,超出目標月份天數時自動順延至下月。
strtotime("first sunday", strtotime("2011-01-01")) // 2011 年第一個星期日
strtotime("last day", strtotime("2000-02-01")) // 2000 年 2 月最後一天(29號,閏年)
first day:將 day=1last day:將 day=0 並 month++(C 底層 tm_mday=0 表示上月最後一天)strtotime("previous sunday", strtotime("2011-02-01")) // 2011-01-30
strtotime("next monday", strtotime("2011-02-01")) // 2011-02-07
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:15front of H:設為 H-1:45,若 H=0 則前日 23:45PHP 使用 re2c 生成詞法分析器,源碼位於 ext/date/lib/parse_date.re。輸入字串經 Tokenize 後,匹配對應規則執行動作。
// 掃描器狀態
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;
};
PHP_FUNCTION(strtotime) 處理參數,預設第二參數為 time()。timelib_strtotime(),由 re2c 解析器處理。Scanner->time->relative 結構。timelib_update_ts() 將相對量套用到基準時間,產生 timestamp。'yesterday' {
DEBUG_OUTPUT("yesterday");
TIMELIB_INIT;
TIMELIB_HAVE_RELATIVE();
TIMELIB_UNHAVE_TIME();
s->time->relative.d = -1; // 相對天數 -1
TIMELIB_DEINIT;
return TIMELIB_RELATIVE;
}
date("Y-m-d", strtotime("-1 month", strtotime("2011-03-31"))); // 輸出 2011-03-03,預期 2011-02-28
-1 month,正則匹配 relative = relnumber space? reltextunit。amount = -1、unit = TIMELIB_MONTH。s->time->relative.m += amount * multiplier(multiplier=1),即 relative.m = -1。2011-03-31 -> 2011-02-31。2011-02-31 非合法日期,C 標準庫 mktime() 自動正規化為 2011-03-03(2月28天 + 3天)。這是 「月份平移」語義:保持「日」不變,僅調整「月」。若目標月無此日,順延至下月。這符合許多商業邏輯(如:每月 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 機制,才能在日期邏輯中避開陷阱。需求不同時,請自行實作「取月尾」或「取月首」邏輯。