diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 42278c960c43..6d3e7d6bb98d 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -749,6 +749,39 @@ public function addMonths(int $months) return $time->add(DateInterval::createFromDateString("{$months} months")); } + /** + * Returns a new Time instance with $months calendar months added to the time. + */ + public function addCalendarMonths(int $months): static + { + $time = clone $this; + + $year = (int) $time->getYear(); + $month = (int) $time->getMonth(); + $day = (int) $time->getDay(); + + // Adjust total months since year 0 + $totalMonths = ($year * 12 + $month - 1) + $months; + + // Recalculate year and month + $newYear = intdiv($totalMonths, 12); + $newMonth = $totalMonths % 12 + 1; + + // Get last day of new month + $lastDayOfMonth = cal_days_in_month(CAL_GREGORIAN, $newMonth, $newYear); + $correctedDay = min($day, $lastDayOfMonth); + + return static::create($newYear, $newMonth, $correctedDay, (int) $this->getHour(), (int) $this->getMinute(), (int) $this->getSecond(), $this->getTimezone(), $this->locale); + } + + /** + * Returns a new Time instance with $months calendar months subtracted from the time + */ + public function subCalendarMonths(int $months): static + { + return $this->addCalendarMonths(-$months); + } + /** * Returns a new Time instance with $years added to the time. * diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index af45abcbe4b8..4f76636ac7eb 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -812,6 +812,22 @@ public function testCanAddMonthsOverYearBoundary(): void $this->assertSame('2018-02-10 13:20:33', $newTime->toDateTimeString()); } + public function testCanAddCalendarMonths(): void + { + $time = Time::parse('January 31, 2017 13:20:33', 'America/Chicago'); + $newTime = $time->addCalendarMonths(1); + $this->assertSame('2017-01-31 13:20:33', $time->toDateTimeString()); + $this->assertSame('2017-02-28 13:20:33', $newTime->toDateTimeString()); + } + + public function testCanAddCalendarMonthsOverYearBoundary(): void + { + $time = Time::parse('January 31, 2017 13:20:33', 'America/Chicago'); + $newTime = $time->addCalendarMonths(13); + $this->assertSame('2017-01-31 13:20:33', $time->toDateTimeString()); + $this->assertSame('2018-02-28 13:20:33', $newTime->toDateTimeString()); + } + public function testCanAddYears(): void { $time = Time::parse('January 10, 2017 13:20:33', 'America/Chicago'); @@ -860,6 +876,22 @@ public function testCanSubtractMonths(): void $this->assertSame('2016-10-10 13:20:33', $newTime->toDateTimeString()); } + public function testCanSubtractCalendarMonths(): void + { + $time = Time::parse('March 31, 2017 13:20:33', 'America/Chicago'); + $newTime = $time->subCalendarMonths(1); + $this->assertSame('2017-03-31 13:20:33', $time->toDateTimeString()); + $this->assertSame('2017-02-28 13:20:33', $newTime->toDateTimeString()); + } + + public function testCanSubtractCalendarMonthsOverYearBoundary(): void + { + $time = Time::parse('March 31, 2017 13:20:33', 'America/Chicago'); + $newTime = $time->subCalendarMonths(13); + $this->assertSame('2017-03-31 13:20:33', $time->toDateTimeString()); + $this->assertSame('2016-02-29 13:20:33', $newTime->toDateTimeString()); + } + public function testCanSubtractYears(): void { $time = Time::parse('January 10, 2017 13:20:33', 'America/Chicago'); diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst index ae6797af7884..30bf101711e6 100644 --- a/user_guide_src/source/changelogs/v4.7.0.rst +++ b/user_guide_src/source/changelogs/v4.7.0.rst @@ -33,6 +33,11 @@ Method Signature Changes Enhancements ************ +Libraries +========= + +- **Time:** added methods ``Time::addCalendarMonths()`` and ``Time::subCalendarMonths()`` + Commands ======== diff --git a/user_guide_src/source/libraries/time.rst b/user_guide_src/source/libraries/time.rst index 17af069c8c5f..c3ddb3c16d39 100644 --- a/user_guide_src/source/libraries/time.rst +++ b/user_guide_src/source/libraries/time.rst @@ -325,6 +325,24 @@ modify the existing Time instance, but will return a new instance. .. literalinclude:: time/031.php +addCalendarMonths() / subCalendarMonths() +----------------------------------------- + +Modifies the current Time by adding or subtracting whole calendar months. These methods can be useful if you +require no calendar months are skipped in recurring dates. Refer to the table below for a comparison between +``addMonths()`` and ``addCalendarMonths()`` for an initial date of ``2025-01-31``. + +======= =========== =================== +$months addMonths() addCalendarMonths() +======= =========== =================== +1 2025-03-03 2025-02-28 +2 2025-03-31 2025-03-31 +3 2025-05-01 2025-04-30 +4 2025-05-31 2025-05-31 +5 2025-07-01 2025-06-30 +6 2025-07-31 2025-07-31 +======= =========== =================== + Comparing Two Times =================== diff --git a/user_guide_src/source/libraries/time/031.php b/user_guide_src/source/libraries/time/031.php index 3737ae67a3c1..914ff279871d 100644 --- a/user_guide_src/source/libraries/time/031.php +++ b/user_guide_src/source/libraries/time/031.php @@ -5,6 +5,7 @@ $time = $time->addHours(12); $time = $time->addDays(21); $time = $time->addMonths(14); +$time = $time->addCalendarMonths(2); $time = $time->addYears(5); $time = $time->subSeconds(23); @@ -12,4 +13,5 @@ $time = $time->subHours(12); $time = $time->subDays(21); $time = $time->subMonths(14); +$time = $time->subCalendarMonths(2); $time = $time->subYears(5);