From a1bddc58afcb419d9dd4c4d6db5e80c62e5f8ff5 Mon Sep 17 00:00:00 2001 From: Marcin Michalski Date: Wed, 21 Aug 2024 00:33:17 +0200 Subject: [PATCH 1/6] feat: add `Date.addMonth` method --- lib/Date.php | 27 +++++++++++++++++++++++++++ tests/unit/DateTest.php | 22 ++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/lib/Date.php b/lib/Date.php index 66ea1a1c..070cc1ff 100644 --- a/lib/Date.php +++ b/lib/Date.php @@ -102,6 +102,33 @@ public function isBetween(self $start, self $end): bool return !$this->isBefore($start) && !$this->isAfter($end); } + /** + * Returns a new instance of Date with 1 month added to current. + * If next month has fewer days than current day, the day will be capped at that value. + */ + public function addMonth(): self + { + $day = 1; + $month = $this->month + 1; + $year = $this->year; + if ($month > 12) { + $month = 1; + $year++; + } + + return new self( + $year, + $month, + \min( + $this->day, + (int) DateTimeFactory::immutableFromFormat( + 'Y-m-d', + \sprintf('%02d-%02d-%02d', $year, $month, $day), + )->format('t'), + ), + ); + } + public function offsetByDays(int $days): self { if ($days === 0) { diff --git a/tests/unit/DateTest.php b/tests/unit/DateTest.php index 08f049cf..197c60ad 100644 --- a/tests/unit/DateTest.php +++ b/tests/unit/DateTest.php @@ -518,4 +518,26 @@ public function it_returns_end_of_day_in_provided_timezone(): void $this->assertSame('2022-05-01 23:59:59.999999', $endOfDay->format('Y-m-d H:i:s.u')); $this->assertSame('Indian/Mauritius', $endOfDay->getTimezone()->getName()); } + + #[Test] + #[DataProvider('provideDatesForMonthIncrement')] + public function it_will_correctly_increment_for_following_month(string $currentDate, string $expectedDate): void + { + $this->assertSame( + $expectedDate, + Date::fromYearMonthDayString($currentDate)->addMonth()->toYearMonthDayString(), + ); + } + + /** + * @return iterable> + */ + public static function provideDatesForMonthIncrement(): iterable + { + yield ['2019-01-05', '2019-02-05']; + yield ['2019-12-31', '2020-01-31']; + yield ['2019-01-31', '2019-02-28']; + yield ['2020-01-31', '2020-02-29']; + yield ['2020-03-31', '2020-04-30']; + } } From d166b70d4948ffc6cf204ec56cdd228d80149c58 Mon Sep 17 00:00:00 2001 From: Marcin Michalski Date: Wed, 21 Aug 2024 01:07:18 +0200 Subject: [PATCH 2/6] change to addMonths --- lib/Date.php | 21 ++++++++++------ tests/unit/DateTest.php | 56 ++++++++++++++++++++++++++++++++++------- 2 files changed, 60 insertions(+), 17 deletions(-) diff --git a/lib/Date.php b/lib/Date.php index 070cc1ff..51115e55 100644 --- a/lib/Date.php +++ b/lib/Date.php @@ -103,16 +103,21 @@ public function isBetween(self $start, self $end): bool } /** - * Returns a new instance of Date with 1 month added to current. - * If next month has fewer days than current day, the day will be capped at that value. + * Returns an instance of {@see Date} incremented by specified number of months. + * If resulting month has fewer days than current day, the day will be capped at that value. + * + * @throws \InvalidArgumentException if $increment is less than 1 */ - public function addMonth(): self + public function addMonths(int $increment): self { - $day = 1; - $month = $this->month + 1; + if ($increment < 1) { + throw new \InvalidArgumentException('Months increment must be greater than 0.'); + } + + $month = $this->month + $increment; $year = $this->year; - if ($month > 12) { - $month = 1; + while ($month > 12) { + $month -= 12; $year++; } @@ -123,7 +128,7 @@ public function addMonth(): self $this->day, (int) DateTimeFactory::immutableFromFormat( 'Y-m-d', - \sprintf('%02d-%02d-%02d', $year, $month, $day), + \sprintf('%02d-%02d-%02d', $year, $month, 1), )->format('t'), ), ); diff --git a/tests/unit/DateTest.php b/tests/unit/DateTest.php index 197c60ad..189e5569 100644 --- a/tests/unit/DateTest.php +++ b/tests/unit/DateTest.php @@ -521,23 +521,61 @@ public function it_returns_end_of_day_in_provided_timezone(): void #[Test] #[DataProvider('provideDatesForMonthIncrement')] - public function it_will_correctly_increment_for_following_month(string $currentDate, string $expectedDate): void - { + public function it_will_correctly_add_months( + string $currentDate, + int $increment, + string $expectedDate, + ): void { $this->assertSame( $expectedDate, - Date::fromYearMonthDayString($currentDate)->addMonth()->toYearMonthDayString(), + Date::fromYearMonthDayString($currentDate)->addMonths($increment)->toYearMonthDayString(), ); } /** - * @return iterable> + * @return iterable */ public static function provideDatesForMonthIncrement(): iterable { - yield ['2019-01-05', '2019-02-05']; - yield ['2019-12-31', '2020-01-31']; - yield ['2019-01-31', '2019-02-28']; - yield ['2020-01-31', '2020-02-29']; - yield ['2020-03-31', '2020-04-30']; + yield ['2019-01-05', 1, '2019-02-05']; + yield ['2019-12-31', 1, '2020-01-31']; + yield ['2019-01-31', 1, '2019-02-28']; + yield ['2020-01-31', 1, '2020-02-29']; + yield ['2020-03-31', 1, '2020-04-30']; + yield ['2020-11-30', 1, '2020-12-30']; + + yield ['2019-01-05', 6, '2019-07-05']; + yield ['2019-10-31', 5, '2020-03-31']; + yield ['2019-08-31', 6, '2020-02-29']; + yield ['2019-10-31', 6, '2020-04-30']; + + yield ['2019-01-05', 12, '2020-01-05']; + yield ['2019-10-31', 12, '2020-10-31']; + yield ['2019-08-31', 12, '2020-08-31']; + yield ['2019-10-31', 12, '2020-10-31']; + + yield ['2019-01-05', 30, '2021-07-05']; + yield ['2019-10-31', 29, '2022-03-31']; + yield ['2019-08-31', 30, '2022-02-28']; + yield ['2019-10-31', 30, '2022-04-30']; + } + + #[Test] + #[DataProvider('provideInvalidDateMonthIncrements')] + public function it_throws_when_incrementing_month_by_less_than_1(int $increment): void + { + $this->expectExceptionObject(new \InvalidArgumentException('Months increment must be greater than 0.')); + + Date::fromYearMonthDay(2018, 10, 10)->addMonths($increment); + } + + /** + * @return iterable + */ + public static function provideInvalidDateMonthIncrements(): iterable + { + yield [0]; + yield [-1]; + yield [-10]; } } From b274406d2d25bf69562d84c6e49f73449bf1f835 Mon Sep 17 00:00:00 2001 From: Marcin Michalski Date: Wed, 21 Aug 2024 01:23:52 +0200 Subject: [PATCH 3/6] ignore mutant :( --- lib/Date.php | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/Date.php b/lib/Date.php index 51115e55..5393d155 100644 --- a/lib/Date.php +++ b/lib/Date.php @@ -121,17 +121,13 @@ public function addMonths(int $increment): self $year++; } - return new self( - $year, - $month, - \min( - $this->day, - (int) DateTimeFactory::immutableFromFormat( - 'Y-m-d', - \sprintf('%02d-%02d-%02d', $year, $month, 1), - )->format('t'), - ), - ); + // @infection-ignore-all (IncrementInteger) + $daysInNewMonth = (int) DateTimeFactory::immutableFromFormat( + 'Y-m-d', + \sprintf('%02d-%02d-%02d', $year, $month, 1), + )->format('t'); + + return new self($year, $month, \min($this->day, $daysInNewMonth)); } public function offsetByDays(int $days): self From bce0d3076ec97eedffdbe92cd15d10e0d2ebb2d6 Mon Sep 17 00:00:00 2001 From: Marcin Michalski Date: Wed, 21 Aug 2024 01:30:36 +0200 Subject: [PATCH 4/6] fix year format --- lib/Date.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Date.php b/lib/Date.php index 5393d155..97520cdd 100644 --- a/lib/Date.php +++ b/lib/Date.php @@ -124,7 +124,7 @@ public function addMonths(int $increment): self // @infection-ignore-all (IncrementInteger) $daysInNewMonth = (int) DateTimeFactory::immutableFromFormat( 'Y-m-d', - \sprintf('%02d-%02d-%02d', $year, $month, 1), + \sprintf('%d-%02d-%02d', $year, $month, 1), )->format('t'); return new self($year, $month, \min($this->day, $daysInNewMonth)); From 0a413d85dea5021ccea294cf258d8b08418cc1d3 Mon Sep 17 00:00:00 2001 From: Martin Georgiev Date: Wed, 21 Aug 2024 11:50:13 +0300 Subject: [PATCH 5/6] Update lib/Date.php Co-authored-by: Ben Challis --- lib/Date.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Date.php b/lib/Date.php index 97520cdd..437fe70c 100644 --- a/lib/Date.php +++ b/lib/Date.php @@ -103,8 +103,8 @@ public function isBetween(self $start, self $end): bool } /** - * Returns an instance of {@see Date} incremented by specified number of months. - * If resulting month has fewer days than current day, the day will be capped at that value. + * Returns an instance of {@see Date} incremented by the specified number of months. + * If the resulting month has fewer days than the current day, the day will be the last day of the month. * * @throws \InvalidArgumentException if $increment is less than 1 */ From 23e28f54094c1fd221f900a2bfc0a4b243b8644e Mon Sep 17 00:00:00 2001 From: Marcin Michalski Date: Wed, 21 Aug 2024 10:54:30 +0200 Subject: [PATCH 6/6] Update lib/Date.php --- lib/Date.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Date.php b/lib/Date.php index 437fe70c..ea6b0f4c 100644 --- a/lib/Date.php +++ b/lib/Date.php @@ -104,7 +104,7 @@ public function isBetween(self $start, self $end): bool /** * Returns an instance of {@see Date} incremented by the specified number of months. - * If the resulting month has fewer days than the current day, the day will be the last day of the month. + * If the resulting month has fewer days than the current day, the day will be the last day of that month. * * @throws \InvalidArgumentException if $increment is less than 1 */