PHP: Adding months to a date, while not exceeding the last day of the month
Asked Answered
K

7

38

Looking to create a function that will do this in PHP.

I need to add a number of months to a date, but not exceed the last day of the month in doing so.

For example:

Add 1 month to January (1-28th), 2011, should produce February (1-28th), 2011.
Add 1 month to January 30th, 2011, should produce February 28th, 2011.
Add 3 months to January 31st, 2011, should produce April 30th, 2011.
Add 13 months to January 30th, 2011, should produce February 29th, 2012.
Add 1 month to October 31st, 2011, should produce November 30th, 2011.

If I use date addition in PHP, I get overruns:

Adding 1 month to January 30th, 2011, results in March 2nd, 2011.

My specification doesn't allow me to overrun into a new month.

What's the easiest method to accomplish this?

Katelyn answered 22/4, 2011 at 21:31 Comment(0)
A
47

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
Alcuin answered 22/4, 2011 at 21:43 Comment(2)
what about leap-years? what happens if one would like to add a month from the first day of a month? if i'm right: adding a month to Jan 01 would give Jan 30; adding a month to Feb 01 would give Mar 03 with your code - not sure if this is needet, but if it is, this doesn't seem to be right.Bastian
Leap years can be checked by checkdate. Therefore only need 2 different arrays.Alcuin
B
3

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...

Bastian answered 22/4, 2011 at 21:51 Comment(4)
How about Feb 28th to Mar 31th? What if Feb 28th to Apr 30th?Alcuin
I don't think this is part of the specification... i think we need more examples to understand this, please see my edit for more informationBastian
If OP need simply month +1 (e.g. add(Feb 28th) produces Mar 28th), I think your answer is correct for 1 month interval.Alcuin
@Bastian this does not work if the date was in December month.For ex: if date was 2011-12-30 it gives 2011-13-30Sylvan
A
3

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.

Armour answered 31/5, 2012 at 15:51 Comment(0)
G
1

You can use this code. The first line accepts a Date in any format, and the later lines split off the month value, adding a month and returning the final value in Y-M-D format.

$year_month = Date("Y-m", strtotime($date));
$year_month_incremented = Date("Y-m", strtotime($year_month . " +1 Month "));
$month_end_dt =strtotime('last day of this month', strtotime($year_month_incremented));
$date = date('Y-m-d', $month_end_dt );
Gerardogeratology answered 24/9, 2019 at 14:10 Comment(1)
If you set starting date to $date = '2020-01-01' the result will be 2020-02-29?Earthly
G
0

For anyone interested I made a solid approach as to deal with this issue

/**
 * @var \DateInterval
 */
private $remainder;

/**
 * @param \DateTimeImmutable $date
 * @param string $modifier
 * @return \DateTimeImmutable
 */
private function nextInterval(\DateTimeImmutable $date, $modifier)
{
    $dayNumber = (int)$date->format('j');
    $next = $date->modify($modifier);
    if (!is_null($this->remainder)) {
        $next = $next->add($this->remainder);
        $dayNumber += $this->remainder->days;
        $this->remainder = null;
    }

    // This should in general only apply to months which do not have the same daynumber in that month after adding
    if ($dayNumber !== (int)$next->format('j')) {
        $n = $next->modify('last day of last month');
        $this->remainder = $n->diff($next);
        $next = $n;
    }

    return $next;
}

Results:

2014-11-30
2014-12-30
2015-01-30
2015-02-28
2015-03-30
2015-04-30
2015-05-30

and

2015-12-30
2016-01-30
2016-02-29
2016-03-30
2016-04-30
Ganoid answered 10/5, 2017 at 22:27 Comment(0)
C
0

You can also use this code.

<?php
$monthToAdd = 1;

$d1 = DateTime::createFromFormat('Y-m-d H:i:s', '2011-01-30 15:57:57');

$year = $d1->format('Y');
$month = $d1->format('n');
$day = $d1->format('d');

$year += floor($monthToAdd/12);
$monthToAdd = $monthToAdd%12;
$month += $monthToAdd;
if($month > 12) {
    $year ++;
    $month = $month % 12;
    if($month === 0)
        $month = 12;
}

if(!checkdate($month, $day, $year)) {
    $d2 = DateTime::createFromFormat('Y-n-j', $year.'-'.$month.'-1');
    $d2->modify('last day of');
}else {
    $d2 = DateTime::createFromFormat('Y-n-d', $year.'-'.$month.'-'.$day);
}
$d2->setTime($d1->format('H'), $d1->format('i'), $d1->format('s'));
echo $d2->format('Y-m-d H:i:s');
Crossstaff answered 31/8, 2020 at 13:18 Comment(0)
S
0

// I have updated the logics for geting last date of month

function addMonthGetLastDate($date_str, $months)
{
    
   
    list($year,$month,$day) = explode("-",$date_str);
    // add month here
    $month += ($months-1);
    if($month>12)
    {
        $month = $month - 12;
        $year += 1;
    }
    // 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 = date('Y-m-t', strtotime($year."-".$month."-".$day));
    
    return $date;
}
Sangsanger answered 31/3, 2021 at 7:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.