PHP's Confusing strtotime

PHP

People often get confused when combining strtotime with -1 month, +1 month, or next month. They then start feeling that this function is somewhat unreliable and tends to cause problems. Using it becomes nerve-wracking…

And just now, someone asked me on Weibo:

Hey Niao Ge, today is 2018-07-31. Executing this code:

<?PHP
date("Y-m-d",strtotime("-1 month"))
// Why does it output 2018-07-01?

Alright, while this question seems puzzling at first, from an internal logic perspective, it’s actually “correct.” Don’t rush — let me explain:

Let’s simulate how date internally handles this:

  • First, apply -1 month. Currently it’s 07-31, subtract one month to get 06-31.
  • Then, normalize the date. Since June doesn’t have 31 days, just like 2:60 equals 3:00, June 31 equals July 1.

Logical, right? We can manually verify the second step:

<?PHP
var_dump(date("Y-m-d", strtotime("2017-06-31")));
// Outputs 2017-07-01

In other words, whenever you’re dealing with the last day of a month that doesn’t exist in the target month, this confusion can occur. We can easily verify this with other months:

<?PHP
var_dump(date("Y-m-d", strtotime("-1 month", strtotime("2017-03-31"))));
// Outputs 2017-03-03
var_dump(date("Y-m-d", strtotime("+1 month", strtotime("2017-08-31"))));
// Outputs 2017-10-01
var_dump(date("Y-m-d", strtotime("next month", strtotime("2017-01-31"))));
// Outputs 2017-03-03
var_dump(date("Y-m-d", strtotime("last month", strtotime("2017-03-31"))));
// Outputs 2017-03-03

So what’s the solution?

Starting from PHP 5.3, date introduced a series of correction phrases to clarify this issue: “first day of” and “last day of”. You can prevent date from auto-“normalizing”:

<?PHP
var_dump(date("Y-m-d", strtotime("last day of -1 month", strtotime("2017-03-31"))));
// Outputs 2017-02-28
var_dump(date("Y-m-d", strtotime("first day of +1 month", strtotime("2017-08-31"))));
// Outputs 2017-09-01
var_dump(date("Y-m-d", strtotime("first day of next month", strtotime("2017-01-31"))));
// Outputs 2017-02-01
var_dump(date("Y-m-d", strtotime("last day of last month", strtotime("2017-03-31"))));
// Outputs 2017-02-28

For versions before 5.3 (anyone still using those?), you can use mktime and set all days to the 1st of each month, though it’s not as elegant as using “first day”.

Now that you understand the internal logic, feeling less anxious? ;)

Author: Laruence Original post: http://www.laruence.com/2018/07/31/3188.html

To be honest, I hadn’t encountered this confusing issue before (probably because I used it too little). If Niao Ge hadn’t pointed out these details, who knows what bugs it might have caused in future projects. Just noting it here.