Skip to content
Open
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
20 changes: 17 additions & 3 deletions src/ChronosDate.php
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ public static function diffFormatter(?DifferenceFormatterInterface $formatter =
*/
public function add(DateInterval $interval): static
{
if ($interval->f > 0 || $interval->s > 0 || $interval->i > 0 || $interval->h > 0) {
if ($interval->f != 0 || $interval->s !== 0 || $interval->i !== 0 || $interval->h !== 0) {
throw new InvalidArgumentException('Cannot add intervals with time components');
}
$new = clone $this;
Expand All @@ -319,7 +319,7 @@ public function add(DateInterval $interval): static
*/
public function sub(DateInterval $interval): static
{
if ($interval->f > 0 || $interval->s > 0 || $interval->i > 0 || $interval->h > 0) {
if ($interval->f != 0 || $interval->s !== 0 || $interval->i !== 0 || $interval->h !== 0) {
throw new InvalidArgumentException('Cannot subtract intervals with time components');
}
$new = clone $this;
Expand All @@ -338,7 +338,21 @@ public function sub(DateInterval $interval): static
*/
public function modify(string $modifier): static
{
if (preg_match('/hour|minute|second/', $modifier)) {
$parsed = date_parse($modifier);
$relative = $parsed['relative'] ?? [];
$timestampSeconds = 0;
if (preg_match('/(?:^|\s)@(-?\d+)(?:\.\d+)?(?:\s|$)/', $modifier, $matches)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be simpler to check native after the modify() call and see if the new value has seconds/milliseconds/microseconds set?

$timestampSeconds = (int)$matches[1];
}
if (
($relative['hour'] ?? 0) !== 0 ||
($relative['minute'] ?? 0) !== 0 ||
($relative['second'] ?? 0) !== $timestampSeconds ||
preg_match(
'/(?<![[:alpha:]])(?:milliseconds?|microseconds?|msecs?|usecs?|µsecs?|ms|µs)(?![[:alpha:]])/iu',
$modifier,
)
) {
throw new InvalidArgumentException('Cannot modify date objects by time values');
}

Expand Down
88 changes: 88 additions & 0 deletions tests/TestCase/Date/AddTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ public function testAddRaiseErrorOnTime(): void
$date->add($interval);
}

public function testAddRaiseErrorOnNegativeTime(): void
{
$interval = DateInterval::createFromDateString('-1 hour');
$date = ChronosDate::create(2001, 1, 1);
$this->expectException(InvalidArgumentException::class);
$date->add($interval);
}

public function testSubFullDay(): void
{
$interval = DateInterval::createFromDateString('1 day');
Expand All @@ -53,6 +61,14 @@ public function testSubIgnoreTime(): void
$date->sub($interval);
}

public function testSubRaiseErrorOnNegativeTime(): void
{
$interval = DateInterval::createFromDateString('-1 hour');
$date = ChronosDate::create(2001, 1, 1);
$this->expectException(InvalidArgumentException::class);
$date->sub($interval);
}

public function testAddDay(): void
{
$this->assertSame(1, ChronosDate::create(1975, 5, 31)->addDays(1)->day);
Expand Down Expand Up @@ -90,4 +106,76 @@ public function testModifyTimeComponentError(): void
$this->expectException(InvalidArgumentException::class);
$date->modify('10 seconds');
}

public function testModifyOrdinalWeekday(): void
{
$date = ChronosDate::create(2026, 9, 1);
$new = $date->modify('second monday of this month');
$this->assertSame('2026-09-14', $new->toDateString());
}

public function testModifyUppercaseTimeComponentError(): void
{
$date = ChronosDate::create(2001, 1, 1);
$this->expectException(InvalidArgumentException::class);
$date->modify('10 SECONDS');
}

public function testModifyClockTimeIgnored(): void
{
$date = ChronosDate::create(2001, 1, 1);
$new = $date->modify('12:00');
$this->assertSame('2001-01-01', $new->toDateString());
}

public function testModifyTimeComponentWrappingDayError(): void
{
$date = ChronosDate::create(2001, 1, 1);
$this->expectException(InvalidArgumentException::class);
$date->modify('+24 hours');
}

public function testModifyFractionalTimeComponentError(): void
{
$date = ChronosDate::create(2001, 1, 1);
$this->expectException(InvalidArgumentException::class);
$date->modify('1 millisecond');
}

public function testModifyAbbreviatedFractionalTimeComponentError(): void
{
$date = ChronosDate::create(2001, 1, 1);
$this->expectException(InvalidArgumentException::class);
$date->modify('-1 msec');
}

public function testModifyTimestamp(): void
{
$date = ChronosDate::create(2001, 1, 1);
$new = $date->modify('@86400');
$this->assertSame('1970-01-02', $new->toDateString());
}

public function testModifyTimestampWithTimezone(): void
{
$date = ChronosDate::create(2001, 1, 1);
$new = $date->modify('@1 UTC');
$this->assertSame('1970-01-01', $new->toDateString());
}

public function testModifyTimestampWithDateComponent(): void
{
$date = ChronosDate::create(2001, 1, 1);
$new = $date->modify('@86400 +1 day');
$this->assertSame('1970-01-03', $new->toDateString());
}

public function testModifyAcrossSkippedMidnight(): void
{
$this->withTimezone('America/Havana', function (): void {
$date = new ChronosDate('2023-03-11');
$new = $date->modify('+1 day');
$this->assertSame('2023-03-12', $new->toDateString());
});
}
}
Loading