From 18fcfb86a6a0dd00e3fc3c96dadbac3815bb6a8b Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Fri, 18 Apr 2025 00:13:02 +0300 Subject: [PATCH 01/11] feat: [I18n\Time] addCalendarMonths() --- system/I18n/TimeTrait.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 42278c960c43..405adc61d2af 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -749,6 +749,29 @@ 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): Time + { + $time = clone $this; + + $year = (int) $time->getYear(); + $month = (int) $time->getMonth() + $months; + $day = (int) $time->getDay(); + + // Adjust year and month for overflow + $year += intdiv($month - 1, 12); + $month = (($month - 1) % 12) + 1; + + // Find the last valid day of the target month + $lastDayOfMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year); + $correctedDay = min($day, $lastDayOfMonth); + + // Return new time instance + return static::create($year, $month, $correctedDay, (int) $this->getHour(), (int) $this->getMinute(), (int) $this->getSecond(), $this->getTimezone(), $this->locale); + } + /** * Returns a new Time instance with $years added to the time. * From 6c0fd1ba16d6748161cd78f84fe5b3f1f3fbfadc Mon Sep 17 00:00:00 2001 From: christianberkman <39840601+christianberkman@users.noreply.github.com> Date: Fri, 18 Apr 2025 00:35:48 +0300 Subject: [PATCH 02/11] Remove return type --- system/I18n/TimeTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 405adc61d2af..f3c30fbb32ce 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -752,7 +752,7 @@ public function addMonths(int $months) /** * Returns a new Time instance with $months calendar months added to the time. */ - public function addCalendarMonths(int $months): Time + public function addCalendarMonths(int $months) { $time = clone $this; From 0ebc0711e33aefde1bfdd5c304ac0778a28e2f55 Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Fri, 18 Apr 2025 13:34:50 +0300 Subject: [PATCH 03/11] fix addCalendarMonths() return type --- system/I18n/TimeTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index f3c30fbb32ce..71a4f83d6f5a 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -752,7 +752,7 @@ public function addMonths(int $months) /** * Returns a new Time instance with $months calendar months added to the time. */ - public function addCalendarMonths(int $months) + public function addCalendarMonths(int $months): static { $time = clone $this; From 7913140aebb752efe38bcc270a1c95781782468f Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Sun, 20 Apr 2025 21:03:05 +0300 Subject: [PATCH 04/11] make addCalendarMonths() bi-directional and add subCalendarMonths() --- system/I18n/TimeTrait.php | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 71a4f83d6f5a..ad3b03ff812c 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -330,11 +330,9 @@ public function toDateTime() * @param DateTimeInterface|self|string|null $datetime * @param DateTimeZone|string|null $timezone * - * @return void - * * @throws Exception */ - public static function setTestNow($datetime = null, $timezone = null, ?string $locale = null) + public static function setTestNow($datetime = null, $timezone = null, ?string $locale = null): void { // Reset the test instance if ($datetime === null) { @@ -757,19 +755,29 @@ public function addCalendarMonths(int $months): static $time = clone $this; $year = (int) $time->getYear(); - $month = (int) $time->getMonth() + $months; + $month = (int) $time->getMonth(); $day = (int) $time->getDay(); - // Adjust year and month for overflow - $year += intdiv($month - 1, 12); - $month = (($month - 1) % 12) + 1; + // Adjust total months since year 0 + $totalMonths = ($year * 12 + $month - 1) + $months; + + // Recalculate year and month + $newYear = intdiv($totalMonths, 12); + $newMonth = $totalMonths % 12 + 1; - // Find the last valid day of the target month - $lastDayOfMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year); + // Get last day of new month + $lastDayOfMonth = cal_days_in_month(CAL_GREGORIAN, $newMonth, $newYear); $correctedDay = min($day, $lastDayOfMonth); - // Return new time instance - return static::create($year, $month, $correctedDay, (int) $this->getHour(), (int) $this->getMinute(), (int) $this->getSecond(), $this->getTimezone(), $this->locale); + 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); } /** From 75b1a7a07ae96c3e68328bc184d47b53854fb30a Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Mon, 21 Apr 2025 10:46:53 +0300 Subject: [PATCH 05/11] update userguide and changelog --- user_guide_src/source/changelogs/v4.7.0.rst | 5 +++++ user_guide_src/source/libraries/time/031.php | 2 ++ 2 files changed, 7 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst index ae6797af7884..ede7d0ea8177 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 functions ``Time::addCalendarMonths()`` and ``Time::subCalendarMonths()`` + Commands ======== 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); From 4ad1f19d86c1b2e39787582d46e5af15cd65691a Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Mon, 21 Apr 2025 10:55:00 +0300 Subject: [PATCH 06/11] add tests for addCalendarMonths() and subCalendarMonths() --- tests/system/I18n/TimeTest.php | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) 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'); From 1f436782107c31384d8f6a3ca3939367453d42bf Mon Sep 17 00:00:00 2001 From: christianberkman <39840601+christianberkman@users.noreply.github.com> Date: Mon, 21 Apr 2025 17:17:31 +0300 Subject: [PATCH 07/11] user guide: functions -> methods Co-authored-by: John Paul E. Balandan, CPA --- user_guide_src/source/changelogs/v4.7.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst index ede7d0ea8177..30bf101711e6 100644 --- a/user_guide_src/source/changelogs/v4.7.0.rst +++ b/user_guide_src/source/changelogs/v4.7.0.rst @@ -36,7 +36,7 @@ Enhancements Libraries ========= -- **Time:** added functions ``Time::addCalendarMonths()`` and ``Time::subCalendarMonths()`` +- **Time:** added methods ``Time::addCalendarMonths()`` and ``Time::subCalendarMonths()`` Commands ======== From 6e7507cff853065665be10897e5bef5515495945 Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Mon, 21 Apr 2025 17:23:23 +0300 Subject: [PATCH 08/11] revert change for TimeTrait::setTimeNow() --- system/I18n/TimeTrait.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index ad3b03ff812c..6d3e7d6bb98d 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -330,9 +330,11 @@ public function toDateTime() * @param DateTimeInterface|self|string|null $datetime * @param DateTimeZone|string|null $timezone * + * @return void + * * @throws Exception */ - public static function setTestNow($datetime = null, $timezone = null, ?string $locale = null): void + public static function setTestNow($datetime = null, $timezone = null, ?string $locale = null) { // Reset the test instance if ($datetime === null) { From 681f2d691b924a64a00d3f64224366dfa34ce54e Mon Sep 17 00:00:00 2001 From: christianberkman <39840601+christianberkman@users.noreply.github.com> Date: Mon, 21 Apr 2025 17:26:03 +0300 Subject: [PATCH 09/11] Update TimeTrait.php revert change to TimeTrait::setTimeNow() --- system/I18n/TimeTrait.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index ad3b03ff812c..6d3e7d6bb98d 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -330,9 +330,11 @@ public function toDateTime() * @param DateTimeInterface|self|string|null $datetime * @param DateTimeZone|string|null $timezone * + * @return void + * * @throws Exception */ - public static function setTestNow($datetime = null, $timezone = null, ?string $locale = null): void + public static function setTestNow($datetime = null, $timezone = null, ?string $locale = null) { // Reset the test instance if ($datetime === null) { From 6492d71be45364275ee9fb8b86106315f77b7945 Mon Sep 17 00:00:00 2001 From: Christian Berkman Date: Mon, 21 Apr 2025 23:48:47 +0300 Subject: [PATCH 10/11] update user guide --- user_guide_src/source/libraries/time.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/user_guide_src/source/libraries/time.rst b/user_guide_src/source/libraries/time.rst index 17af069c8c5f..e4ee6dec0df4 100644 --- a/user_guide_src/source/libraries/time.rst +++ b/user_guide_src/source/libraries/time.rst @@ -325,6 +325,27 @@ 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 =================== From 0325782225448e132e72370c2e3314ecdf73a769 Mon Sep 17 00:00:00 2001 From: christianberkman <39840601+christianberkman@users.noreply.github.com> Date: Tue, 22 Apr 2025 22:42:44 +0300 Subject: [PATCH 11/11] Update user_guide_src/source/libraries/time.rst Co-authored-by: Michal Sniatala --- user_guide_src/source/libraries/time.rst | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/user_guide_src/source/libraries/time.rst b/user_guide_src/source/libraries/time.rst index e4ee6dec0df4..c3ddb3c16d39 100644 --- a/user_guide_src/source/libraries/time.rst +++ b/user_guide_src/source/libraries/time.rst @@ -325,11 +325,8 @@ modify the existing Time instance, but will return a new instance. .. literalinclude:: time/031.php -addCalendarMonths() -------------------- - -subCalendarMonths() -------------------- +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