Time String in Relative Formats

如果今天是 2017-3-31,那麼
date('Y-m-d H:i:s', strtotime('-1 month'))
的結果會是 2017-03-03

在網路上看到別人提起這件事,我就去翻了一下官方文件

Also observe that the “of” in “ordinal space dayname space ‘of’ “ and “‘last’ space dayname space ‘of’ “ does something special.

  1. It sets the day-of-month to 1.
  2. “ordinal dayname ‘of’ “ does not advance to another day. (Example: “first tuesday of july 2008” means “2008-07-01”).
  3. “ordinal dayname “ does advance to another day. (Example: “first tuesday july 2008” means “2008-07-08”, see also point 4 in the list above).
  4. “‘last’ dayname ‘of’ “ takes the last dayname of the current month. (Example: “last wed of july 2008” means “2008-07-30”)
  5. “‘last’ dayname” takes the last dayname from the current day. (Example: “last wed july 2008” means “2008-06-25”; “july 2008” first sets the current date to “2008-07-01” and then “last wed” moves to the previous Wednesday which is “2008-06-25”).

Relative month values are calculated based on the length of months that they pass through. An example would be “+2 month 2011-11-30”, which would produce “2012-01-30”. This is due to November being 30 days in length, and December being 31 days in length, producing a total of 61 days.

還真是有點複雜……

比起 firstlast 反而更要注意 of 的有無

而且我測了一下,改用 DateTime 並不會比較好


重現問題:

// 執行環境是 Psy Shell v0.8.3 (PHP 5.6.30 — cli)
// 在 2017-04-06 時執行下列程式碼

// 得到 "2017-03-03 00:00:00"
date('Y-m-d H:i:s', strtotime('-1 month 2017-03-31'))
// 得到 "2017-05-01 00:00:00"
date('Y-m-d H:i:s', strtotime('+1 month 2017-03-31'))

// 看起來是因為二月只有 28 天,所以 PHP 就直接把 2017-03-31 減掉 28 天,得到的日期還是三月
// 同理,四月有 30 天,直接把 2017-03-31 加上 30 天,就跳到五月了

// 修正
date('Y-m-d H:i:s', strtotime('-1 month Mar 2017'))
date('Y-m-d H:i:s', strtotime('+1 month Mar 2017'))

// 得到 "2016-04-01"
date('Y-m-d', strtotime('first day of last year'))

// 根據文件說明,'first day of' 只會傳回「當月第一天的日期」,所以後面要接上月份名稱才能得到預期的結果

// 修正
date('Y-m-d', strtotime('first day of Jan last year'))

2018-08-08 edit:

今天看到鳥哥發的一篇新文章也提到了這件事:令人困惑的strtotime | 风雪之隅

  1. 先做-1 month, 那么当前是07-31, 减去一以后就是06-31.
  2. 再做日期规范化, 因为6月没有31号, 所以就好像2点60等于3点一样, 6月31就等于了7月1