diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index f3d8eb0..3a0d1dd 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -360,6 +360,77 @@ public function getMultipleRunDates(int $total, $currentTime = 'now', bool $inve return $matches; } + /** + * Get multiple run dates until a specific end date. + * + * This method calculates and returns execution dates based on the cron expression + * until a given end date, with optional constraints such as a maximum number of occurrences, + * timezone adjustments, and date inclusion rules. + * + * @param string|DateTimeInterface $until The date limit for fetching occurrences. + * If a string is provided, it must be a valid date format. + * @param int $limit The maximum number of occurrences to return. If set to 0, it defaults to `$this->maxIterationCount`. + * @param string|DateTimeInterface $currentTime The reference time to start generating dates. Defaults to 'now'. + * @param bool $allowCurrentDate Whether to include the current date in the results if it matches the cron expression. Defaults to `false`. + * @param string|null $timeZone Optional. The timezone to use for date calculations. + * - If `null`, it will be determined based on `$currentTime` (if a `DateTimeInterface` is provided, its timezone will be used). + * - If `$currentTime` is a string or `null`, the system's default timezone will be used. + * @return DateTimeInterface[] An array of DateTimeInterface objects representing the matching execution dates. + * + * @throws InvalidArgumentException|Exception + */ + public function getRunDatesUntil( + $until, + int $limit = 0, + $currentTime = 'now', + bool $allowCurrentDate = false, + $timeZone = null + ): array + + { + if (is_string($until)) { + try { + $until = new DateTimeImmutable($until); + } catch (Exception $e) { + throw new InvalidArgumentException("Invalid date format: $until"); + } + } elseif (!$until instanceof DateTimeInterface) { + throw new InvalidArgumentException("End date must be a string or an instance of DateTimeInterface."); + } + + if (!is_int($limit)) { + throw new InvalidArgumentException("Limit must be an integer."); + } + + $timeZone = $this->determineTimeZone($currentTime, $timeZone); + + if ($limit === 0) { + $limit = $this->maxIterationCount; + } + + $dates = []; + + $untilTimestamp = $until->getTimestamp(); + for ($i = 0; $i < $limit; $i++) { + try { + $result = $this->getRunDate($currentTime, 0, false, $allowCurrentDate, $timeZone); + } catch (RuntimeException $e) { + break; + } + + $allowCurrentDate = false; + $currentTime = clone $result; + + if ($result->getTimestamp() > $untilTimestamp) { + break; + } + + $dates[] = $result; + } + + return $dates; + } + /** * Get all or part of the CRON expression. * diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index e28f929..4292ad7 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -425,6 +425,89 @@ public function testProvidesMultipleRunDatesForTheFarFuture(): void ], $cron->getMultipleRunDates(9, '2015-04-28 00:00:00', false, true)); } + /** + * @covers \Cron\CronExpression::getRunDatesUntil + */ + + public function testInvalidUntilDate(): void + { + $cron = new CronExpression('* * * * *'); + $this->expectException(InvalidArgumentException::class); + $cron->getRunDatesUntil('invalid until date', 1, '2008-11-09 00:00:00'); + } + + /** + * @covers \Cron\CronExpression::getRunDatesUntil + */ + + public function testInvalidLimit(): void + { + $cron = new CronExpression('* * * * *'); + $this->expectException(InvalidArgumentException::class); + $cron->getRunDatesUntil('2008-11-09 00:00:00', null, '2008-11-09 00:00:00'); + } + /** + * @covers \Cron\CronExpression::getRunDatesUntil + */ + public function testGetRunDatesUntil(): void + { + $cron = new CronExpression('*/2 * * * *'); + + // Test with end date and limit of 4 occurrences + $until = '2008-11-09 00:06:00'; + $limit = 4; + $expectedDates = [ + new DateTime('2008-11-09 00:00:00'), + new DateTime('2008-11-09 00:02:00'), + new DateTime('2008-11-09 00:04:00'), + new DateTime('2008-11-09 00:06:00'), + ]; + + // Test with allowCurrentDate set to false (default) + $result = $cron->getRunDatesUntil($until, $limit, '2008-11-09 00:00:00'); + $this->assertEquals(array_slice($expectedDates, 1), $result); + + // Test with allowCurrentDate set to true + $result = $cron->getRunDatesUntil($until, $limit, '2008-11-09 00:00:00', true); + $this->assertEquals($expectedDates, $result); + + // Test with limit set to 0 (defaults to maxIterationCount) + $limit = 0; + $expectedDates = [ + new DateTime('2008-11-09 00:00:00'), + new DateTime('2008-11-09 00:02:00'), + new DateTime('2008-11-09 00:04:00'), + new DateTime('2008-11-09 00:06:00'), + ]; + + // Test with allowCurrentDate set to false + $result = $cron->getRunDatesUntil($until, $limit, '2008-11-09 00:00:00'); + $this->assertEquals(array_slice($expectedDates, 1), $result); + + // Test with allowCurrentDate set to true + $result = $cron->getRunDatesUntil($until, $limit, '2008-11-09 00:00:00', true); + $this->assertEquals($expectedDates, $result); + + // Test with end date limit that exceeds the limit + $until = '2008-11-09 00:05:00'; + $limit = 10; // A larger limit to ensure iteration stops properly + $expectedDates = [ + new DateTime('2008-11-09 00:00:00'), + new DateTime('2008-11-09 00:02:00'), + new DateTime('2008-11-09 00:04:00'), + ]; + + // Test with allowCurrentDate set to false + $result = $cron->getRunDatesUntil($until, $limit, '2008-11-09 00:00:00'); + $this->assertEquals(array_slice($expectedDates, 1), $result); + + // Test with allowCurrentDate set to true + $result = $cron->getRunDatesUntil($until, $limit, '2008-11-09 00:00:00', true); + $this->assertEquals($expectedDates, $result); + } + + + /** * @covers \Cron\CronExpression */