Skip to content

feat: add Time::addCalendarMonths() and Time::subCalendarMonths() methods #9528

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions system/I18n/TimeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
32 changes: 32 additions & 0 deletions tests/system/I18n/TimeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand Down
5 changes: 5 additions & 0 deletions user_guide_src/source/changelogs/v4.7.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ Method Signature Changes
Enhancements
************

Libraries
=========

- **Time:** added methods ``Time::addCalendarMonths()`` and ``Time::subCalendarMonths()``

Commands
========

Expand Down
18 changes: 18 additions & 0 deletions user_guide_src/source/libraries/time.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
===================

Expand Down
2 changes: 2 additions & 0 deletions user_guide_src/source/libraries/time/031.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
$time = $time->addHours(12);
$time = $time->addDays(21);
$time = $time->addMonths(14);
$time = $time->addCalendarMonths(2);
$time = $time->addYears(5);

$time = $time->subSeconds(23);
$time = $time->subMinutes(15);
$time = $time->subHours(12);
$time = $time->subDays(21);
$time = $time->subMonths(14);
$time = $time->subCalendarMonths(2);
$time = $time->subYears(5);
Loading