diff --git a/src/ChronosDatePeriod.php b/src/ChronosDatePeriod.php index fb4e8b7..caafcdc 100644 --- a/src/ChronosDatePeriod.php +++ b/src/ChronosDatePeriod.php @@ -14,7 +14,9 @@ namespace Cake\Chronos; +use DateInterval; use DatePeriod; +use InvalidArgumentException; use Iterator; /** @@ -32,15 +34,39 @@ class ChronosDatePeriod implements Iterator protected Iterator $iterator; /** - * @param \DatePeriod $period + * @param \DatePeriod $period The DatePeriod to wrap. + * @throws \InvalidArgumentException If the period has a zero interval which would cause an infinite loop. */ public function __construct(DatePeriod $period) { + if (static::isZeroInterval($period->getDateInterval())) { + throw new InvalidArgumentException( + 'Cannot create a period with a zero interval. This would cause an infinite loop when iterating.', + ); + } + /** @var \Iterator $iterator */ $iterator = $period->getIterator(); $this->iterator = $iterator; } + /** + * Check if a DateInterval is effectively zero. + * + * @param \DateInterval $interval The interval to check. + * @return bool True if the interval is zero. + */ + protected static function isZeroInterval(DateInterval $interval): bool + { + return $interval->y === 0 + && $interval->m === 0 + && $interval->d === 0 + && $interval->h === 0 + && $interval->i === 0 + && $interval->s === 0 + && (int)($interval->f * 1_000_000) === 0; + } + /** * @return \Cake\Chronos\ChronosDate */ diff --git a/src/ChronosPeriod.php b/src/ChronosPeriod.php index 62e5f9f..23a4df4 100644 --- a/src/ChronosPeriod.php +++ b/src/ChronosPeriod.php @@ -14,7 +14,9 @@ namespace Cake\Chronos; +use DateInterval; use DatePeriod; +use InvalidArgumentException; use Iterator; /** @@ -32,15 +34,39 @@ class ChronosPeriod implements Iterator protected Iterator $iterator; /** - * @param \DatePeriod $period + * @param \DatePeriod $period The DatePeriod to wrap. + * @throws \InvalidArgumentException If the period has a zero interval which would cause an infinite loop. */ public function __construct(DatePeriod $period) { + if (static::isZeroInterval($period->getDateInterval())) { + throw new InvalidArgumentException( + 'Cannot create a period with a zero interval. This would cause an infinite loop when iterating.', + ); + } + /** @var \Iterator $iterator */ $iterator = $period->getIterator(); $this->iterator = $iterator; } + /** + * Check if a DateInterval is effectively zero. + * + * @param \DateInterval $interval The interval to check. + * @return bool True if the interval is zero. + */ + protected static function isZeroInterval(DateInterval $interval): bool + { + return $interval->y === 0 + && $interval->m === 0 + && $interval->d === 0 + && $interval->h === 0 + && $interval->i === 0 + && $interval->s === 0 + && (int)($interval->f * 1_000_000) === 0; + } + /** * @return \Cake\Chronos\Chronos */ diff --git a/tests/TestCase/ChronosDatePeriodTest.php b/tests/TestCase/ChronosDatePeriodTest.php index 4f88e4d..4a7cc7f 100644 --- a/tests/TestCase/ChronosDatePeriodTest.php +++ b/tests/TestCase/ChronosDatePeriodTest.php @@ -19,6 +19,7 @@ use DateInterval; use DatePeriod; use DateTime; +use InvalidArgumentException; class ChronosDatePeriodTest extends TestCase { @@ -30,11 +31,37 @@ public function testChronosPeriod(): void $output[$key] = $value; } $this->assertCount(4, $output); - $this->assertInstanceOf(ChronosDAte::class, $output[0]); + $this->assertInstanceOf(ChronosDate::class, $output[0]); $this->assertSame('2025-01-01 00:00:00', $output[0]->format('Y-m-d H:i:s')); $this->assertInstanceOf(ChronosDate::class, $output[1]); $this->assertSame('2025-01-02 00:00:00', $output[1]->format('Y-m-d H:i:s')); $this->assertInstanceOf(ChronosDate::class, $output[3]); $this->assertSame('2025-01-04 00:00:00', $output[3]->format('Y-m-d H:i:s')); } + + public function testZeroIntervalThrowsException(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Cannot create a period with a zero interval'); + + $period = new DatePeriod( + new DateTime('2025-01-01'), + new DateInterval('PT0S'), + new DateTime('2025-01-02'), + ); + new ChronosDatePeriod($period); + } + + public function testZeroIntervalAllZeroComponents(): void + { + $this->expectException(InvalidArgumentException::class); + + $interval = new DateInterval('P0D'); + $period = new DatePeriod( + new DateTime('2025-01-01'), + $interval, + new DateTime('2025-01-02'), + ); + new ChronosDatePeriod($period); + } } diff --git a/tests/TestCase/ChronosPeriodTest.php b/tests/TestCase/ChronosPeriodTest.php index 8bb832c..179f936 100644 --- a/tests/TestCase/ChronosPeriodTest.php +++ b/tests/TestCase/ChronosPeriodTest.php @@ -19,6 +19,7 @@ use DateInterval; use DatePeriod; use DateTime; +use InvalidArgumentException; class ChronosPeriodTest extends TestCase { @@ -35,4 +36,30 @@ public function testChronosPeriod(): void $this->assertInstanceOf(Chronos::class, $output[1]); $this->assertSame('2025-01-01 01:00:00', $output[1]->format('Y-m-d H:i:s')); } + + public function testZeroIntervalThrowsException(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Cannot create a period with a zero interval'); + + $period = new DatePeriod( + new DateTime('2025-01-01'), + new DateInterval('PT0S'), + new DateTime('2025-01-02'), + ); + new ChronosPeriod($period); + } + + public function testZeroIntervalAllZeroComponents(): void + { + $this->expectException(InvalidArgumentException::class); + + $interval = new DateInterval('P0D'); + $period = new DatePeriod( + new DateTime('2025-01-01'), + $interval, + new DateTime('2025-01-02'), + ); + new ChronosPeriod($period); + } }