PHP: Adding months to a date, while not exceeding the last day of the month
You can compare the day of the month before and after you add 1 month. If it's not the same, you exceeded the next month.
function add($date_str, $months)
{
$date = new DateTime($date_str);
// We extract the day of the month as $start_day
$start_day = $date->format('j');
// We add 1 month to the given date
$date->modify("+{$months} month");
// We extract the day of the month again so we can compare
$end_day = $date->format('j');
if ($start_day != $end_day)
{
// The day of the month isn't the same anymore, so we correct the date
$date->modify('last day of last month');
}
return $date;
}
$result = add('2011-01-28', 1); // 2011-02-28
$result = add('2011-01-31', 3); // 2011-04-30
$result = add('2011-01-30', 13); // 2012-02-29
$result = add('2011-10-31', 1); // 2011-11-30
$result = add('2011-12-30', 1); // 2011-02-28
As far as I can tell, this problem is very limited in scope, so you're likely to be best off by testing for one type of error, and fixing it.
All you want to do is make sure that adding "one month" to a late date like the 29th, 30th or 31st does not push you forward to the 1st, 2nd or 3rd of the next-next month.
The way date_modify() works (using it on an example date "2012-01-31" with a string like "+1 months"), is that it first increases the month number by 1, then finds the 31st day from the start of that month. This is why it spills over to March 3rd.
When this is not what you desired, all you have to do is use date_modify() again, now telling it to go back a few days (3 days in this example). Since you only want to go back to the last day of the previous month, the number of days you will want to go back is always the same as the day-of-month in your faulty date.
All that remains is to make sure you don't apply this correction when it is not needed, like when PHP were to improves in future. This is relatively easy, because the scope of the possible problem situations is very limited.
- (1) The problem only occurs when adding months to dates 29, 30 or 31
- (2) When the problem occurs, the resulting date is always 1, 2 or 3.
My code below adds "+1 month", checks if that has caused the day-of-month to change wildly from something high to something low, and adjusts the date if that's the case.
//Create the date, store its day-of-month, and add X months
$myDateTimeISO = "2012-01-31";
$addThese = 1;
$myDateTime = new DateTime($myDateTimeISO);
$myDayOfMonth = date_format($myDateTime,'j');
date_modify($myDateTime,"+$addThese months");
//Find out if the day-of-month has dropped
$myNewDayOfMonth = date_format($myDateTime,'j');
if ($myDayOfMonth > 28 && $myNewDayOfMonth < 4){
//If so, fix by going back the number of days that have spilled over
date_modify($myDateTime,"-$myNewDayOfMonth days");
}
echo date_format($myDateTime,"Y-m-d");
Results in: 2012-02-29 (yes, this was a leap year).
PS: If you want to add years, the problem and the symptoms are nearly identical. Again, you just need to to check if the day-of-month resulting is 1/2/3 and the day-of-month going in is 29/30/31. If so, you need to go back "-X days" using date_modify, where X is the resulting day-of-month.
this seems to work for me and gives yor desired result:
<?php
$date = "2011-01-30";
list($year,$month,$day) = explode("-",$date);
// add month here
$month++;
// to avoid a month-wrap, set the day to the number of days of the new month if it's too high
$day = min($day,date("t",strtotime($year."-".$month."-01")));
$date = $year."-".$month."-".$day;
// 2011-02-28
echo $date;
?>
EDIT:
after reading Crend Kings comemnt, i think we need more information here. whats the desired result in the following cases:
2011-01-30 > 2011-02-28
2011-01-28 > 2011-02-28 or 2011-02-26 ?
2011-02-01 > 2011-03-01 or 2011-03-03 ?
in words: should the method add the number of days of the next month, wich is what Crend King does and what gives results like 2011-02-26 and 2011-03-03 (wich doesn't seem like the desired results to me) or should this add one month and leave the day as is, instead of a day thats "too high" like my code does? i'm confused...