Good afternoon, I have a strange question:

There is a code:

<?php $date = new DateTime('2016-03-31'); echo $date->format('Ym-d')."\n"; $date->sub(new DateInterval('P1M')); echo $date->format('Ym-d')."\n"; 

As a result, I get:

 2016-03-31 2016-03-02 

Although the logic of things should get:

 2016-03-31 2016-02-29 

For the date 2015-03-31 (last, not a leap year) I get:

 2015-03-31 2015-03-03 

Although the logic of things should be:

 2015-03-31 2015-02-28 

What am I doing wrong? Checked for PHP5.5 and PHP7.0

Just in case: TZ = Europe / Moscow

  • one
  • So is this a bug or a feature, that is, for March 31, I should calculate the offset differently? Is there documentation on "magic dates"? - chernomyrdin
  • This is a feature of the work. For subtracting / adding a month, PHP simply replaces the value of the month and then normalizes what happened. Those. the result is absolutely the same as new DateTime('2016-02-31'); - Alexey Ten
  • 2
    But in general, this is an unsolvable problem. Why is it the same day for March 28 and March 31 “a month ago”? In general, we must look at your task. I would compare the resulting month with the original one and, if coincidental, took the end of the previous month - Alexey Ten
  • I certainly found a workaround for this: $ date-> sub (new DateInterval ('P'. $ Date-> format ('t'). 'D')); But it seems to me extremely strange default behavior - chernomyrdin

2 answers 2

After much deliberation on what @Alexey Ten said, and having analyzed how other products work with dates, I stopped at the fact that I (in my task) are much closer to how it happens in PostgreSQL, namely:

 psql> select '2016-03-31'::date - 'P1M'::interval; ?column? --------------------- 2016-02-29 00:00:00 (1 row) 

Accordingly, I wrote a function for this:

 function subMonth (DateTime $dateTime, $num = 1) { $day = $dateTime->format('j'); $month = $dateTime->format('n'); $year = $dateTime->format('Y'); while ($num > 0) { if (1 == $month) { --$year; $month = 12; } else { --$month; } $days = cal_days_in_month(CAL_GREGORIAN, $month, $year); $dateTime->setDate($year, $month, ($day > $days) ? $days : $day); --$num; } return $dateTime; } 

Accordingly, what should happen:

 echo subMonth(new DateTime('2015-03-31'), 1)->format('Ym-d'), "\n"; # 2015-02-28 echo subMonth(new DateTime('2016-03-31'), 1)->format('Ym-d'), "\n"; # 2016-02-29 echo subMonth(new DateTime('2016-03-30'), 1)->format('Ym-d'), "\n"; # 2016-02-29 echo subMonth(new DateTime('2016-03-29'), 1)->format('Ym-d'), "\n"; # 2016-02-29 echo subMonth(new DateTime('2016-05-31'), 1)->format('Ym-d'), "\n"; # 2016-04-30 echo subMonth(new DateTime('2016-05-31'), 2)->format('Ym-d'), "\n"; # 2016-03-31 echo subMonth(new DateTime('2016-05-31'), 3)->format('Ym-d'), "\n"; # 2016-02-29 

It turned out somewhat not optimal, but it does what I need in a particular case.

    I saw the solution of the problem for +1 month ... can be done by analogy for -1.

     function dateAfterMonth($m=1,$d=1,$y=1970) { if($m == 12){ return strtotime("+1 month",mktime(0,0,0,$m,$d,$y)); } ++$m; while(true){ if(checkdate($m,$d,$y)){ break; } --$d; } return mktime(0,0,0,$m,$d,$y); }