3

I am displaying month titles 3 month into the future as well as getting the 1st and last day of each of those months.

for($i = 1; $i < 4; $i++) { // For each month for 3 months
    $monthTitle = date('F Y', strtotime('+'.$i.' month'));
    $begin_date = date('Y-m-01', strtotime('+'.$i.' month')); // First day of calendar month in future.
    $end_date = date('Y-m-t', strtotime('+'.$i.' month')); // Last day of calendar months in future.
};

Nov. 29, 2015 output is:

December 2015
2015-12-01
2015-12-31
January 2016
2016-01-01
2016-01-31
February 2016
2016-02-01 
2016-02-29

This was working great right up until yesterday, Nov. 29, 2015 but today Nov. 30, 2015 it skips February.

Nov. 30, 2015 output is:

December 2015
2015-12-01
2015-12-31
January 2016
2016-01-01
2016-01-31
March 2016
2016-03-01 
2016-03-31

I'm guessing a bug but does anybody know of a work around?

Angie
  • 871
  • 2
  • 10
  • 18
  • 1
    Maybe it's because februari only has 28 or 29 days ;-) Are you getting any warnings about a malformed date? ([see here to enable them](http://stackoverflow.com/questions/1053424/how-do-i-get-php-errors-to-display)). – Kenney Nov 30 '15 at 18:15
  • 1
    See here: http://stackoverflow.com/questions/14584115/php-strtotime-1-month-adding-an-extra-month – devlin carnate Nov 30 '15 at 18:19
  • 1
    It isn't a bug that PHP is aware that February 2016 doesn't have 29 days; it's a bug that you don't seem to be aware of the fact.... if you run it on 31st December, you'll get even more differences, because February, April, June, September and November all don't have 31 days – Mark Baker Nov 30 '15 at 18:24
  • The solution is to use the first day of the month for your "get each month" loop, because every month has at least 1 day.... then use `t` to get the last day of that month – Mark Baker Nov 30 '15 at 18:25
  • @Mark Baker Is that not what I'm doing in this line: $begin_date = date('Y-m-01', strtotime('+'.$i.' month')); I'm calling the first day of the month +1 month and I still get Mar. 1st. – Angie Nov 30 '15 at 21:39
  • If it was, then you'd be getting the correct result.... no, you're only saying to get begin date from the +1 month date that you've calculated; but PHP has added 1 month to 30th January to get 30th February (or actually 1st March) and only then are you saying to get the 1st of that month (ie March) – Mark Baker Nov 30 '15 at 22:01
  • @Mark Baker yes, I just realized that as well. Thanks. – Angie Nov 30 '15 at 22:31

4 Answers4

5

Thanks to @devlin carnate for pointing me in the right direction.

for($i = 1; $i < 4; $i++) { # for each month
    $tmp = date('Y-m-15'); // Get the middle of the month to avoid PHP date bug.
    $begin_date = date('Y-m-01', strtotime($tmp . '+'.$i.' month')); // First day of calendar month in future.
    $end_date = date('Y-m-t', strtotime($begin_date)); // Last day of calendar months in future.
    $monthTitle = date('F Y', strtotime($begin_date));
};

This seems to work very well.

Angie
  • 871
  • 2
  • 10
  • 18
  • While It's good there are solutions here, nothing explains why it happens and it's not a bug. See [my answer](https://stackoverflow.com/a/77014443/). – joeljpa Aug 31 '23 at 09:17
4

You can use DateInterval to add one month to the current date, so you can get the first and the last day of month.

<?php
$date = new DateTime('2015-12-01');
$i = 0;
while($i < 3){
    printf("%s | first day: %s, | last day: %s <br>", $date->format('F Y'), $date->format('d'), $date->format('t'));
    $date->add(new DateInterval('P1M'));
    $i++;
}

Output:

December 2015 - first day: 01, | last day: 31
January 2016 - first day: 01, | last day: 31
February 2016 - first day: 01, | last day: 29 
rray
  • 2,518
  • 1
  • 28
  • 38
0

if last day of next month is needed then you can use this

$d = new DateTime( '2010-01-31' );
$d->modify( 'last day of next month' );
echo $d->format( 'Y-m-d' ), "\n";
0

It's not a bug, and I'm surprised no answer makes any mention of this. It's what happens when we run it on a day like the 31st. It gets the next month and if that doesn't have 31 days, it just skips it.

Just see what I got when I ran your code today (31 Aug 2023):

October 2023 //Skips September because it doesn't have day 31
2023-10-01
2023-10-31
October 2023 //Ditto for November
2023-10-01
2023-10-31
December 2023
2023-12-01
2023-12-31

If you want to skip using DateInterval and still use strtotime(), just replace "+1 month" with "last day of +1 month".

Like so: strtotime('+'.$i.' month') -> strtotime('last day of +' . $i . ' month')

for($i = 1; $i < 4; $i++) { // For each month for 3 months
    $monthTitle = date('F Y', strtotime('last day of +' . $i . ' month'));
    $begin_date = date('Y-m-01', strtotime('last day of +' . $i . ' month')); // First day of calendar month in future.
    $end_date = date('Y-m-t', strtotime('last day of +' . $i . ' month')); // Last day of calendar months in future.
}

Output:

September 2023 //Works as we wanted
2023-09-01
2023-09-30
October 2023
2023-10-01
2023-10-31
November 2023 //Works as we wanted
2023-11-01
2023-11-30

There are these closed bug reports one in 2003 and another in 2008 from php.net. See also SeanDowney's explanation for "How to get previous month and year relative to today, using strtotime and date?"

However, I found that most of us agree that adding some mention of this behaviour in the function would save us the trouble. Worse cases would be when it tries to get the dreaded February on days 29-31 of any month.

A month is an ill-defined unit in the end and I'll leave you with this infamous comment by rasmus from those bug reports, "If I told you on January 30 that I would come back in exactly one month to beat the crap out of you, when would you think I would show up?"

joeljpa
  • 317
  • 2
  • 13