public function modifyDate(int $years, int $months = 0, int $days = 0) : Time
    {
        if ($this->frozen) {
            throw new \RuntimeException(get_class($this) . ' is read-only, frozen.');
        }

        if ($years) {
            $year = (int) $this->format('Y');
            $month = (int) $this->format('m');
            $day = (int) $this->format('d');
            // Validity adjustment when part is year:
            // If current date is February 29th and target year isn't
            // a leap year, then target date becomes February 28th.
            // Target date is February 29th and target year isn't leap year.
            if ($month == 2 && $day == 29 && !date('L', mktime(1, 1, 1, 2, 1, $year + $years))) {
                $day = 28;
            }
            $this->setDate($year + $years, $month, $day);
        }

        if ($months) {
            $target_year = $year = (int) $this->format('Y');
            $month = (int) $this->format('m');
            $day = (int) $this->format('d');
            $target_month = $month + $months;
            if ($target_month > 12) {
                $add_years = (int) floor($target_month / 12);
                $target_month -= $add_years * 12;
                $target_year += $add_years;
            }
            elseif ($target_month < 1) {
                $subtract_years = (int) ceil(-$target_month / 12);
                $target_month += $subtract_years * 12;
                $target_year -= $subtract_years;
            }
            if (!$target_month) {
                $target_month = 12;
                --$target_year;
            }
            // Validity adjustment when part is month:
            // If current month is longer than target month and current
            // day doesn't exist in target month, then target day
            // becomes last day of target month.
            if ($day > 28) {
                $max_day = $target_year == $year ? $this->monthLengthDays($target_month) :
                    $this->monthLengthDays($target_month, $target_year);
                if ($day > $max_day) {
                    $day = $max_day;
                }
            }
            $this->setDate($target_year, $target_month, $day);
        }

        if ($days) {
            $this->modify(($days > 0 ? '+' : '-') . abs($days) . ' ' . (abs($days) > 1 ? 'days' : 'day'));
        }

        return $this;
    }
public function modifyDate(int $years, int $months = 0, int $days = 0) : Time
    {
        if ($this->frozen) {
            throw new \RuntimeException(get_class($this) . ' is read-only, frozen.');
        }

        if ($years) {
            $year = (int) $this->format('Y');
            $month = (int) $this->format('m');
            $day = (int) $this->format('d');
            // Validity adjustment when part is year:
            // If current date is February 29th and target year isn't
            // a leap year, then target date becomes February 28th.
            // Target date is February 29th and target year isn't leap year.
            if ($month == 2 && $day == 29 && !date('L', mktime(1, 1, 1, 2, 1, $year + $years))) {
                $day = 28;
            }
            $this->setDate($year + $years, $month, $day);
        }

        if ($months) {
            $target_year = $year = (int) $this->format('Y');
            $month = (int) $this->format('m');
            $day = (int) $this->format('d');
            $target_month = $month + $months;
            if ($target_month > 12) {
                $add_years = (int) floor($target_month / 12);
                $target_month -= $add_years * 12;
                $target_year += $add_years;
            }
            elseif ($target_month < 1) {
                $subtract_years = (int) ceil(-$target_month / 12);
                $target_month += $subtract_years * 12;
                $target_year -= $subtract_years;
            }
            if (!$target_month) {
                $target_month = 12;
                --$target_year;
            }
            // Validity adjustment when part is month:
            // If current month is longer than target month and current
            // day doesn't exist in target month, then target day
            // becomes last day of target month.
            if ($day > 28) {
                $max_day = $target_year == $year ? $this->monthLengthDays($target_month) :
                    $this->monthLengthDays($target_month, $target_year);
                if ($day > $max_day) {
                    $day = $max_day;
                }
            }
            $this->setDate($target_year, $target_month, $day);
        }

        if ($days) {
            $this->modify(($days > 0 ? '+' : '-') . abs($days) . ' ' . (abs($days) > 1 ? 'days' : 'day'));
        }

        return $this;
    }
public function modifyDate ( int $ years , int $ months = 0 , int $ days = 0 ) : Time { if ( $ this -> frozen ) { throw new \ RuntimeException ( get_class ( $ this ) . ' is read-only, frozen.' ) ; } if ( $ years ) { $ year = ( int ) $ this -> format ( 'Y' ) ; $ month = ( int ) $ this -> format ( 'm' ) ; $ day = ( int ) $ this -> format ( 'd' ) ; // Validity adjustment when part is year: // If current date is February 29th and target year isn't // a leap year, then target date becomes February 28th. // Target date is February 29th and target year isn't leap year. if ( $ month == 2 && $ day == 29 && ! date ( 'L' , mktime ( 1 , 1 , 1 , 2 , 1 , $ year + $ years ) ) ) { $ day = 28 ; } $ this -> setDate ( $ year + $ years , $ month , $ day ) ; } if ( $ months ) { $ target_year = $ year = ( int ) $ this -> format ( 'Y' ) ; $ month = ( int ) $ this -> format ( 'm' ) ; $ day = ( int ) $ this -> format ( 'd' ) ; $ target_month = $ month + $ months ; if ( $ target_month > 12 ) { $ add_years = ( int ) floor ( $ target_month / 12 ) ; $ target_month -= $ add_years * 12 ; $ target_year += $ add_years ; } elseif ( $ target_month < 1 ) { $ subtract_years = ( int ) ceil ( - $ target_month / 12 ) ; $ target_month += $ subtract_years * 12 ; $ target_year -= $ subtract_years ; } if ( ! $ target_month ) { $ target_month = 12 ; -- $ target_year ; } // Validity adjustment when part is month: // If current month is longer than target month and current // day doesn't exist in target month, then target day // becomes last day of target month. if ( $ day > 28 ) { $ max_day = $ target_year == $ year ? $ this -> monthLengthDays ( $ target_month ) : $ this -> monthLengthDays ( $ target_month , $ target_year ) ; if ( $ day > $ max_day ) { $ day = $ max_day ; } } $ this -> setDate ( $ target_year , $ target_month , $ day ) ; } if ( $ days ) { $ this -> modify ( ( $ days > 0 ? '+' : '-' ) . abs ( $ days ) . ' ' . ( abs ( $ days ) > 1 ? 'days' : 'day' ) ) ; } return $ this ; }
Add to or subtract from one or more date parts.

Validity adjustment when arg years:
If current date is February 29th and target year isn't a leap year,
then target date becomes February 28th.

Validity adjustment when arg months:
If current month is longer than target month and current day
doesn't exist in target month, then target day becomes last day
of target month.

These validity adjustments are equivalent with database adjustments
like MySQL::date_add() and MSSQL::dateadd().

Native \DateTime::modify():
- is difficult to use and it's format argument isn't documented
- makes nonsensical year|month addition/subtraction
- doesn't throw exception on failure

@see \DateTime::modify()

@param int $years
Subtracts if negative.
@param int $months
Subtracts if negative.
@param int $days
Subtracts if negative.

@return $this|Time

@throws \RuntimeException
Frozen.
Add to or subtract from one or more date parts .