Skip to content

Commit

Permalink
feat(DateTime) Provide a human-like months substract and add system
Browse files Browse the repository at this point in the history
  • Loading branch information
veewee committed Jun 7, 2024
1 parent fbbff2e commit a0ce49f
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 39 deletions.
2 changes: 1 addition & 1 deletion docs/component/date-time.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@

#### `Traits`

- [DateTimeConvenienceMethodsTrait](./../../src/Psl/DateTime/DateTimeConvenienceMethodsTrait.php#L15)
- [DateTimeConvenienceMethodsTrait](./../../src/Psl/DateTime/DateTimeConvenienceMethodsTrait.php#L18)
- [TemporalConvenienceMethodsTrait](./../../src/Psl/DateTime/TemporalConvenienceMethodsTrait.php#L16)

#### `Enums`
Expand Down
75 changes: 37 additions & 38 deletions src/Psl/DateTime/DateTimeConvenienceMethodsTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
use Psl\Locale\Locale;
use Psl\Math;

use function intdiv;
use function min;

/**
* @require-implements DateTimeInterface
*
Expand Down Expand Up @@ -362,8 +365,7 @@ public function isLeapYear(): bool
/**
* Adds the specified years to this date-time object, returning a new instance with the added years.
*
* @throws Exception\UnderflowException If adding the years results in an arithmetic underflow.
* @throws Exception\OverflowException If adding the years results in an arithmetic overflow.
* @throws Exception\UnexpectedValueException If adding the years results in an arithmetic issue.
*
* @psalm-mutation-free
*/
Expand All @@ -375,8 +377,7 @@ public function plusYears(int $years): static
/**
* Subtracts the specified years from this date-time object, returning a new instance with the subtracted years.
*
* @throws Exception\UnderflowException If subtracting the years results in an arithmetic underflow.
* @throws Exception\OverflowException If subtracting the years results in an arithmetic overflow.
* @throws Exception\UnexpectedValueException If subtracting the years results in an arithmetic issue.
*
* @psalm-mutation-free
*/
Expand All @@ -388,8 +389,7 @@ public function minusYears(int $years): static
/**
* Adds the specified months to this date-time object, returning a new instance with the added months.
*
* @throws Exception\UnderflowException If adding the months results in an arithmetic underflow.
* @throws Exception\OverflowException If adding the months results in an arithmetic overflow.
* @throws Exception\UnexpectedValueException If adding the months results in an arithmetic issue.
*
* @psalm-mutation-free
*/
Expand All @@ -403,28 +403,29 @@ public function plusMonths(int $months): static
return $this->minusMonths(-$months);
}

$current_year = $this->getYear();
$current_month = $this->getMonthEnum();
$days_to_add = 0;
for ($i = 0; $i < $months; $i++) {
$total_months = $current_month->value + $i;
$target_year = $current_year + Math\div($total_months - 1, MONTHS_PER_YEAR);
$target_month = $total_months % MONTHS_PER_YEAR;
if ($target_month === 0) {
$target_month = 1;
}

$days_to_add += Month::from($target_month)->getDaysForYear($target_year);
$plus_years = intdiv($months, 12);
$months_left = $months - ($plus_years * 12);
$target_month = $this->getMonth() + $months_left;

if ($target_month > 12) {
$plus_years++;
$target_month = $target_month - 12;
}

return $this->plus(Duration::days($days_to_add));
return $this->withDate(
$target_year = $this->getYear() + $plus_years,
$target_month = Month::from($target_month)->value,
min([
$this->getDay(),
Month::from($target_month)->getDaysForYear($target_year)
])
);
}

/**
* Subtracts the specified months from this date-time object, returning a new instance with the subtracted months.
*
* @throws Exception\UnderflowException If subtracting the months results in an arithmetic underflow.
* @throws Exception\OverflowException If subtracting the months results in an arithmetic overflow.
* @throws Exception\UnexpectedValueException If subtracting the months results in an arithmetic issue.
*
* @psalm-mutation-free
*/
Expand All @@ -438,25 +439,23 @@ public function minusMonths(int $months): static
return $this->plusMonths(-$months);
}

$current_year = $this->getYear();
$current_month = $this->getMonthEnum();
$days_to_subtract = 0;
for ($i = 0; $i < $months; $i++) {
// When subtracting, we need to move the current month back before the calculation
$total_months = $current_month->value - $i;
while ($total_months <= 0) {
$total_months += MONTHS_PER_YEAR; // Adjust month to be within 1-12
$current_year--; // Adjust year when wrapping
}

$target_month = ($total_months % MONTHS_PER_YEAR) ?: MONTHS_PER_YEAR;
$target_year = $current_year + Math\div($total_months - 1, MONTHS_PER_YEAR);

// Subtract days of the month we are moving into
$days_to_subtract += Month::from($target_month)->getDaysForYear($target_year);
$minus_years = intdiv($months, 12);
$months_left = $months - ($minus_years * 12);
$target_month = $this->getMonth() - $months_left;

if ($target_month <= 0) {
$minus_years++;
$target_month = 12 - Math\abs($target_month);
}

return $this->minus(Duration::days($days_to_subtract));
return $this->withDate(
$target_year = $this->getYear() - $minus_years,
$target_month = Month::from($target_month)->value,
min([
$this->getDay(),
Month::from($target_month)->getDaysForYear($target_year)
])
);
}

/**
Expand Down
89 changes: 89 additions & 0 deletions tests/unit/DateTime/DateTimeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,44 @@ public function testPlusMethods(): void
static::assertSame(1, $new->getNanoseconds());
}

public function testPlusMonthsEdgeCases(): void
{
$jan_31th = DateTime::fromParts(Timezone::default(), 2024, Month::January, 31, 14, 0, 0, 0);
$febr_29th = $jan_31th->plusMonths(1);
static::assertSame([2024, 2, 29], $febr_29th->getDate());
static::assertSame([14, 0, 0, 0], $febr_29th->getTime());

$dec_31th = DateTime::fromParts(Timezone::default(), 2023, Month::December, 31, 14, 0, 0, 0);
$march_31th = $dec_31th->plusMonths(3);
static::assertSame([2024, 3, 31], $march_31th->getDate());
static::assertSame([14, 0, 0, 0], $march_31th->getTime());

$april_30th = $march_31th->plusMonths(1);
static::assertSame([2024, 4, 30], $april_30th->getDate());
static::assertSame([14, 0, 0, 0], $april_30th->getTime());

$april_30th_next_year = $april_30th->plusYears(1);
static::assertSame([2025, 4, 30], $april_30th_next_year->getDate());
static::assertSame([14, 0, 0, 0], $april_30th_next_year->getTime());
}

public function testPlusMonthOverflows(): void
{
$jan_31th_2024 = DateTime::fromParts(Timezone::default(), 2024, Month::January, 31, 14, 0, 0, 0);
$previous_month = 1;
for ($i = 1; $i < 24; $i++) {
$res = $jan_31th_2024->plusMonths($i);

$expected_month = ($previous_month + 1) % 12;
$expected_month = $expected_month === 0 ? 12 : $expected_month;

static::assertSame($res->getDay(), $res->getMonthEnum()->getDaysForYear($res->getYear()));
static::assertSame($res->getMonth(), $expected_month);

$previous_month = $expected_month;
}
}

public function testMinusMethods(): void
{
$datetime = DateTime::fromParts(Timezone::default(), 2024, Month::February, 4, 14, 0, 0, 0);
Expand All @@ -220,6 +258,57 @@ public function testMinusMethods(): void
static::assertSame(999999999, $new->getNanoseconds());
}

public function testMinusMonthsEdgeCases(): void
{
$febr_29th = DateTime::fromParts(Timezone::default(), 2024, Month::February, 29, 14, 0, 0, 0);
$jan_29th = $febr_29th->minusMonths(1);
static::assertSame([2024, 1, 29], $jan_29th->getDate());
static::assertSame([14, 0, 0, 0], $jan_29th->getTime());

$febr_28th_previous_year = $febr_29th->minusYears(1);
static::assertSame([2023, 2, 28], $febr_28th_previous_year->getDate());
static::assertSame([14, 0, 0, 0], $febr_28th_previous_year->getTime());

$febr_29th_previous_leap_year = $febr_29th->minusYears(4);
static::assertSame([2020, 2, 29], $febr_29th_previous_leap_year->getDate());
static::assertSame([14, 0, 0, 0], $febr_29th_previous_leap_year->getTime());

$march_31th = DateTime::fromParts(Timezone::default(), 2024, Month::March, 31, 14, 0, 0, 0);
$dec_31th = $march_31th->minusMonths(3);
static::assertSame([2023, 12, 31], $dec_31th->getDate());
static::assertSame([14, 0, 0, 0], $dec_31th->getTime());

$jan_31th = $march_31th->minusMonths(2);
static::assertSame([2024, 1, 31], $jan_31th->getDate());
static::assertSame([14, 0, 0, 0], $jan_31th->getTime());

$may_31th = DateTime::fromParts(Timezone::default(), 2024, Month::May, 31, 14, 0, 0, 0);
$april_30th = $may_31th->minusMonths(1);
static::assertSame([2024, 4, 30], $april_30th->getDate());
static::assertSame([14, 0, 0, 0], $april_30th->getTime());

$april_30th_previous_year = $april_30th->minusYears(1);
static::assertSame([2023, 4, 30], $april_30th_previous_year->getDate());
static::assertSame([14, 0, 0, 0], $april_30th_previous_year->getTime());
}

public function testMinusMonthOverflows(): void
{
$jan_31th_2024 = DateTime::fromParts(Timezone::default(), 2024, Month::January, 31, 14, 0, 0, 0);
$previous_month = 1;
for ($i = 1; $i < 24; $i++) {
$res = $jan_31th_2024->minusMonths($i);

$expected_month = $previous_month - 1;
$expected_month = $expected_month === 0 ? 12 : $expected_month;

static::assertSame($res->getDay(), $res->getMonthEnum()->getDaysForYear($res->getYear()));
static::assertSame($res->getMonth(), $expected_month);

$previous_month = $expected_month;
}
}

public function testIsLeapYear(): void
{
$datetime = DateTime::fromParts(Timezone::default(), 2024, Month::February, 4, 14, 0, 0, 0);
Expand Down

0 comments on commit a0ce49f

Please sign in to comment.