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.
new DateTime('2016-02-31');
- Alexey Ten