From 8a84aee649c3a3ba03a721c1fb080e08dfbcd68b Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Thu, 12 Oct 2017 11:59:13 -0400 Subject: [PATCH 001/146] Updated README and author information for 2.x --- CHANGELOG.md | 3 ++- LICENSE | 2 +- README.md | 8 ++++---- composer.json | 19 +++++++++++++------ 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 336e69a5..08b736b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Change Log -## [Unreleased] +## [2.0.0] - 2017-10-12 ### Added +- N/A ### Changed - Dropped support for PHP 5.x diff --git a/LICENSE b/LICENSE index c6d88ac6..3e38bbc8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011 Michael Dowling and contributors +Copyright (c) 2011 Michael Dowling , 2016 Chris Tankersley , and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 57cdbd96..7eac9210 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ PHP Cron Expression Parser ========================== -[![Latest Stable Version](https://poser.pugx.org/mtdowling/cron-expression/v/stable.png)](https://packagist.org/packages/mtdowling/cron-expression) [![Total Downloads](https://poser.pugx.org/mtdowling/cron-expression/downloads.png)](https://packagist.org/packages/mtdowling/cron-expression) [![Build Status](https://secure.travis-ci.org/mtdowling/cron-expression.png)](http://travis-ci.org/mtdowling/cron-expression) - -**NOTE** This fork has been deprecated and development moved to [https://github.com/dragonmantank/cron-expression](https://github.com/dragonmantank/cron-expression). More information can be found in the blog post [here](http://ctankersley.com/2017/10/12/cron-expression-update/). tl;dr - v2.0.0 is a major breaking change, and @dragonmantank can better take care of the project in a separate fork. +[![Latest Stable Version](https://poser.pugx.org/dragonmantank/cron-expression/v/stable.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Total Downloads](https://poser.pugx.org/dragonmantank/cron-expression/downloads.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Build Status](https://secure.travis-ci.org/dragonmantank/cron-expression.png)](http://travis-ci.org/dragonmantank/cron-expression) The PHP cron expression parser can parse a CRON expression, determine if it is due to run, calculate the next run date of the expression, and calculate the previous @@ -15,13 +13,15 @@ lists (e.g. 1,2,3), W to find the nearest weekday for a given day of the month, find the last day of the month, L to find the last given weekday of a month, and hash (#) to find the nth weekday of a given month. +More information about this fork can be found in the blog post [here](http://ctankersley.com/2017/10/12/cron-expression-update/). tl;dr - v2.0.0 is a major breaking change, and @dragonmantank can better take care of the project in a separate fork. + Installing ========== Add the dependency to your project: ```bash -composer require mtdowling/cron-expression +composer require dragonmantank/cron-expression ``` Usage diff --git a/composer.json b/composer.json index a8fe85e3..d1fff7a1 100644 --- a/composer.json +++ b/composer.json @@ -1,14 +1,21 @@ { - "name": "mtdowling/cron-expression", + "name": "dragonmantank/cron-expression", "type": "library", "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", "keywords": ["cron", "schedule"], "license": "MIT", - "authors": [{ - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], "require": { "php": ">=7.0.0" }, From 39cf81fc3ceddd91349ffe05f06dc1baed0c47c0 Mon Sep 17 00:00:00 2001 From: Gabriel Caruso Date: Sat, 11 Nov 2017 01:18:50 -0200 Subject: [PATCH 002/146] Update to PHPUnit 6 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d1fff7a1..d9997ead 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "php": ">=7.0.0" }, "require-dev": { - "phpunit/phpunit": "~5.7" + "phpunit/phpunit": "~6.4" }, "autoload": { "psr-4": { From 8bcdd1a462a7b843e01197b18c8240e95f7c697f Mon Sep 17 00:00:00 2001 From: Laurence Date: Sun, 3 Dec 2017 22:51:22 +0000 Subject: [PATCH 003/146] tweak --- src/Cron/CronExpression.php | 89 +++++++++++++++++-------------- tests/Cron/CronExpressionTest.php | 77 +++++++++++++++++++++----- 2 files changed, 112 insertions(+), 54 deletions(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index e8095529..cb021a8c 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -4,7 +4,7 @@ use DateTime; use DateTimeImmutable; -use DateTimeZone; +use DateTimezone; use Exception; use InvalidArgumentException; use RuntimeException; @@ -171,7 +171,7 @@ public function setPart($position, $value) public function setMaxIterationCount($maxIterationCount) { $this->maxIterationCount = $maxIterationCount; - + return $this; } @@ -187,14 +187,14 @@ public function setMaxIterationCount($maxIterationCount) * matches and so on. * @param bool $allowCurrentDate Set to TRUE to return the current date if * it matches the cron expression. - * @param null|string $timeZone Timezone to use instead of the system default + * @param null|string $timezone Timezone to use instead of the system default * * @return \DateTime * @throws \RuntimeException on too many iterations */ - public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timeZone = null) + public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timezone = null) { - return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate, $timeZone); + return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate, $timezone); } /** @@ -204,15 +204,15 @@ public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate * @param int $nth Number of matches to skip before returning * @param bool $allowCurrentDate Set to TRUE to return the * current date if it matches the cron expression - * @param null|string $timeZone Timezone to use instead of the system default + * @param null|string $timezone Timezone to use instead of the system default * * @return \DateTime * @throws \RuntimeException on too many iterations * @see \Cron\CronExpression::getNextRunDate */ - public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timeZone = null) + public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timezone = null) { - return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate, $timeZone); + return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate, $timezone); } /** @@ -223,16 +223,16 @@ public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrent * @param bool $invert Set to TRUE to retrieve previous dates * @param bool $allowCurrentDate Set to TRUE to return the * current date if it matches the cron expression - * @param null|string $timeZone Timezone to use instead of the system default + * @param null|string $timezone Timezone to use instead of the system default * * @return array Returns an array of run dates */ - public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false, $timeZone = null) + public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false, $timezone = null) { $matches = array(); for ($i = 0; $i < max(0, $total); $i++) { try { - $matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate, $timeZone); + $matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate, $timezone); } catch (RuntimeException $e) { break; } @@ -277,39 +277,30 @@ public function __toString() * seconds are irrelevant, and should be called once per minute. * * @param string|\DateTime $currentTime Relative calculation date - * @param null|string $timeZone Timezone to use instead of the system default + * @param null|string $timezone Timezone to use instead of the system default * * @return bool Returns TRUE if the cron is due to run or FALSE if not */ - public function isDue($currentTime = 'now', $timeZone = null) + public function isDue($currentTime = 'now', $timezone = null) { - if (is_null($timeZone)) { - $timeZone = date_default_timezone_get(); - } - + $timezone = $this->determineTimezone($currentTime, $timezone); + if ('now' === $currentTime) { - $currentDate = date('Y-m-d H:i'); - $currentTime = strtotime($currentDate); + $currentTime = new DateTime(); } elseif ($currentTime instanceof DateTime) { - $currentDate = clone $currentTime; - // Ensure time in 'current' timezone is used - $currentDate->setTimezone(new DateTimeZone($timeZone)); - $currentDate = $currentDate->format('Y-m-d H:i'); - $currentTime = strtotime($currentDate); + // } elseif ($currentTime instanceof DateTimeImmutable) { - $currentDate = DateTime::createFromFormat('U', $currentTime->format('U')); - $currentDate->setTimezone(new DateTimeZone($timeZone)); - $currentDate = $currentDate->format('Y-m-d H:i'); - $currentTime = strtotime($currentDate); + $currentTime = DateTime::createFromFormat('U', $currentTime->format('U')); } else { $currentTime = new DateTime($currentTime); - $currentTime->setTime($currentTime->format('H'), $currentTime->format('i'), 0); - $currentDate = $currentTime->format('Y-m-d H:i'); - $currentTime = $currentTime->getTimeStamp(); } + $currentTime->setTimezone(new DateTimezone($timezone)); + + // drop the seconds to 0 + $currentTime = DateTime::createFromFormat('Y-m-d H:i', $currentTime->format('Y-m-d H:i')); try { - return $this->getNextRunDate($currentDate, 0, true)->getTimestamp() == $currentTime; + return $this->getNextRunDate($currentTime, 0, true)->getTimestamp() === $currentTime->getTimestamp(); } catch (Exception $e) { return false; } @@ -323,27 +314,24 @@ public function isDue($currentTime = 'now', $timeZone = null) * @param bool $invert Set to TRUE to go backwards in time * @param bool $allowCurrentDate Set to TRUE to return the * current date if it matches the cron expression - * @param string|null $timeZone Timezone to use instead of the system default + * @param string|null $timezone Timezone to use instead of the system default * * @return \DateTime * @throws \RuntimeException on too many iterations */ - protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $allowCurrentDate = false, $timeZone = null) + protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $allowCurrentDate = false, $timezone = null) { - if (is_null($timeZone)) { - $timeZone = date_default_timezone_get(); - } - + $timezone = $this->determineTimezone($currentTime, $timezone); + if ($currentTime instanceof DateTime) { $currentDate = clone $currentTime; } elseif ($currentTime instanceof DateTimeImmutable) { $currentDate = DateTime::createFromFormat('U', $currentTime->format('U')); - $currentDate->setTimezone($currentTime->getTimezone()); } else { $currentDate = new DateTime($currentTime ?: 'now'); - $currentDate->setTimezone(new DateTimeZone($timeZone)); } + $currentDate->setTimezone(new DateTimezone($timezone)); $currentDate->setTime($currentDate->format('H'), $currentDate->format('i'), 0); $nextRun = clone $currentDate; $nth = (int) $nth; @@ -399,4 +387,25 @@ protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $a throw new RuntimeException('Impossible CRON expression'); // @codeCoverageIgnoreEnd } + + /** + * Workout what timezone should be used. + * + * @param string|\DateTime $currentTime Relative calculation date + * @param string|null $timezone Timezone to use instead of the system default + * + * @return string + */ + protected function determineTimezone($currentTime, $timezone) + { + if (! is_null($timezone)) { + return $timezone; + } + + if ($currentTime instanceOf Datetime) { + return $currentTime->getTimezone()->getName(); + } + + return date_default_timezone_get(); + } } diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 61e5137f..e327bbe7 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -217,28 +217,49 @@ public function testIsDueHandlesDifferentDates() /** * @covers \Cron\CronExpression::isDue */ - public function testIsDueHandlesDifferentTimezones() + public function testIsDueHandlesDifferentDefaultTimezones() { + $originalTimezone = date_default_timezone_get(); $cron = CronExpression::factory('0 15 * * 3'); //Wednesday at 15:00 $date = '2014-01-01 15:00'; //Wednesday - $utc = new DateTimeZone('UTC'); - $amsterdam = new DateTimeZone('Europe/Amsterdam'); - $tokyo = new DateTimeZone('Asia/Tokyo'); date_default_timezone_set('UTC'); - $this->assertTrue($cron->isDue(new DateTime($date, $utc))); - $this->assertFalse($cron->isDue(new DateTime($date, $amsterdam))); - $this->assertFalse($cron->isDue(new DateTime($date, $tokyo))); + $this->assertTrue($cron->isDue(new DateTime($date), 'UTC')); + $this->assertFalse($cron->isDue(new DateTime($date), 'Europe/Amsterdam')); + $this->assertFalse($cron->isDue(new DateTime($date), 'Asia/Tokyo')); date_default_timezone_set('Europe/Amsterdam'); - $this->assertFalse($cron->isDue(new DateTime($date, $utc))); - $this->assertTrue($cron->isDue(new DateTime($date, $amsterdam))); - $this->assertFalse($cron->isDue(new DateTime($date, $tokyo))); + $this->assertFalse($cron->isDue(new DateTime($date), 'UTC')); + $this->assertTrue($cron->isDue(new DateTime($date), 'Europe/Amsterdam')); + $this->assertFalse($cron->isDue(new DateTime($date), 'Asia/Tokyo')); date_default_timezone_set('Asia/Tokyo'); - $this->assertFalse($cron->isDue(new DateTime($date, $utc))); - $this->assertFalse($cron->isDue(new DateTime($date, $amsterdam))); - $this->assertTrue($cron->isDue(new DateTime($date, $tokyo))); + $this->assertFalse($cron->isDue(new DateTime($date), 'UTC')); + $this->assertFalse($cron->isDue(new DateTime($date), 'Europe/Amsterdam')); + $this->assertTrue($cron->isDue(new DateTime($date), 'Asia/Tokyo')); + + date_default_timezone_set($originalTimezone); + } + + /** + * @covers \Cron\CronExpression::isDue + */ + public function testIsDueHandlesDifferentSuppliedTimezones() + { + $cron = CronExpression::factory('0 15 * * 3'); //Wednesday at 15:00 + $date = '2014-01-01 15:00'; //Wednesday + + $this->assertTrue($cron->isDue(new DateTime($date, new DateTimeZone('UTC')), 'UTC')); + $this->assertFalse($cron->isDue(new DateTime($date, new DateTimeZone('UTC')), 'Europe/Amsterdam')); + $this->assertFalse($cron->isDue(new DateTime($date, new DateTimeZone('UTC')), 'Asia/Tokyo')); + + $this->assertFalse($cron->isDue(new DateTime($date, new DateTimeZone('Europe/Amsterdam')), 'UTC')); + $this->assertTrue($cron->isDue(new DateTime($date, new DateTimeZone('Europe/Amsterdam')), 'Europe/Amsterdam')); + $this->assertFalse($cron->isDue(new DateTime($date, new DateTimeZone('Europe/Amsterdam')), 'Asia/Tokyo')); + + $this->assertFalse($cron->isDue(new DateTime($date, new DateTimeZone('Asia/Tokyo')), 'UTC')); + $this->assertFalse($cron->isDue(new DateTime($date, new DateTimeZone('Asia/Tokyo')), 'Europe/Amsterdam')); + $this->assertTrue($cron->isDue(new DateTime($date, new DateTimeZone('Asia/Tokyo')), 'Asia/Tokyo')); } /** @@ -261,7 +282,35 @@ public function testIsDueHandlesDifferentTimezonesAsArgument() $this->assertFalse($cron->isDue(new DateTime($date, $amsterdam), 'Asia/Tokyo')); $this->assertTrue($cron->isDue(new DateTime($date, $tokyo), 'Asia/Tokyo')); } - + + /** + * @covers Cron\CronExpression::isDue + */ + public function testRecognisesTimezonesAsPartOfDateTime() + { + $cron = CronExpression::factory("0 7 * * *"); + $tzCron = "America/New_York"; + $tzServer = new \DateTimeZone("Europe/London"); + + $dtCurrent = \DateTime::createFromFormat("!Y-m-d H:i:s", "2017-10-17 10:00:00", $tzServer); + $dtPrev = $cron->getPreviousRunDate($dtCurrent, 0, true, $tzCron); + $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format("U \: c \: e")); + + $dtCurrent = \DateTimeImmutable::createFromFormat("!Y-m-d H:i:s", "2017-10-17 10:00:00", $tzServer); + $dtPrev = $cron->getPreviousRunDate($dtCurrent, 0, true, $tzCron); + $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format("U \: c \: e")); + + $dtCurrent = \DateTimeImmutable::createFromFormat("!Y-m-d H:i:s", "2017-10-17 10:00:00", $tzServer); + $dtPrev = $cron->getPreviousRunDate($dtCurrent->format("c"), 0, true, $tzCron); + $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format("U \: c \: e")); + + $dtCurrent = \DateTimeImmutable::createFromFormat("!Y-m-d H:i:s", "2017-10-17 10:00:00", $tzServer); + $dtPrev = $cron->getPreviousRunDate($dtCurrent->format("\@U"), 0, true, $tzCron); + $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format("U \: c \: e")); + + } + + /** * @covers \Cron\CronExpression::getPreviousRunDate */ From efa18441de56fd3bb6460eff3e837d795e73ccc8 Mon Sep 17 00:00:00 2001 From: Laurence Date: Sun, 3 Dec 2017 22:55:02 +0000 Subject: [PATCH 004/146] fix capital --- src/Cron/CronExpression.php | 50 ++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index cb021a8c..b77b23a3 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -4,7 +4,7 @@ use DateTime; use DateTimeImmutable; -use DateTimezone; +use DateTimeZone; use Exception; use InvalidArgumentException; use RuntimeException; @@ -187,14 +187,14 @@ public function setMaxIterationCount($maxIterationCount) * matches and so on. * @param bool $allowCurrentDate Set to TRUE to return the current date if * it matches the cron expression. - * @param null|string $timezone Timezone to use instead of the system default + * @param null|string $timeZone TimeZone to use instead of the system default * * @return \DateTime * @throws \RuntimeException on too many iterations */ - public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timezone = null) + public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timeZone = null) { - return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate, $timezone); + return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate, $timeZone); } /** @@ -204,15 +204,15 @@ public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate * @param int $nth Number of matches to skip before returning * @param bool $allowCurrentDate Set to TRUE to return the * current date if it matches the cron expression - * @param null|string $timezone Timezone to use instead of the system default + * @param null|string $timeZone TimeZone to use instead of the system default * * @return \DateTime * @throws \RuntimeException on too many iterations * @see \Cron\CronExpression::getNextRunDate */ - public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timezone = null) + public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timeZone = null) { - return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate, $timezone); + return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate, $timeZone); } /** @@ -223,16 +223,16 @@ public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrent * @param bool $invert Set to TRUE to retrieve previous dates * @param bool $allowCurrentDate Set to TRUE to return the * current date if it matches the cron expression - * @param null|string $timezone Timezone to use instead of the system default + * @param null|string $timeZone TimeZone to use instead of the system default * * @return array Returns an array of run dates */ - public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false, $timezone = null) + public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false, $timeZone = null) { $matches = array(); for ($i = 0; $i < max(0, $total); $i++) { try { - $matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate, $timezone); + $matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate, $timeZone); } catch (RuntimeException $e) { break; } @@ -277,13 +277,13 @@ public function __toString() * seconds are irrelevant, and should be called once per minute. * * @param string|\DateTime $currentTime Relative calculation date - * @param null|string $timezone Timezone to use instead of the system default + * @param null|string $timeZone TimeZone to use instead of the system default * * @return bool Returns TRUE if the cron is due to run or FALSE if not */ - public function isDue($currentTime = 'now', $timezone = null) + public function isDue($currentTime = 'now', $timeZone = null) { - $timezone = $this->determineTimezone($currentTime, $timezone); + $timeZone = $this->determineTimeZone($currentTime, $timeZone); if ('now' === $currentTime) { $currentTime = new DateTime(); @@ -294,7 +294,7 @@ public function isDue($currentTime = 'now', $timezone = null) } else { $currentTime = new DateTime($currentTime); } - $currentTime->setTimezone(new DateTimezone($timezone)); + $currentTime->setTimeZone(new DateTimeZone($timeZone)); // drop the seconds to 0 $currentTime = DateTime::createFromFormat('Y-m-d H:i', $currentTime->format('Y-m-d H:i')); @@ -314,14 +314,14 @@ public function isDue($currentTime = 'now', $timezone = null) * @param bool $invert Set to TRUE to go backwards in time * @param bool $allowCurrentDate Set to TRUE to return the * current date if it matches the cron expression - * @param string|null $timezone Timezone to use instead of the system default + * @param string|null $timeZone TimeZone to use instead of the system default * * @return \DateTime * @throws \RuntimeException on too many iterations */ - protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $allowCurrentDate = false, $timezone = null) + protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $allowCurrentDate = false, $timeZone = null) { - $timezone = $this->determineTimezone($currentTime, $timezone); + $timeZone = $this->determineTimeZone($currentTime, $timeZone); if ($currentTime instanceof DateTime) { $currentDate = clone $currentTime; @@ -331,7 +331,7 @@ protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $a $currentDate = new DateTime($currentTime ?: 'now'); } - $currentDate->setTimezone(new DateTimezone($timezone)); + $currentDate->setTimeZone(new DateTimeZone($timeZone)); $currentDate->setTime($currentDate->format('H'), $currentDate->format('i'), 0); $nextRun = clone $currentDate; $nth = (int) $nth; @@ -389,23 +389,23 @@ protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $a } /** - * Workout what timezone should be used. + * Workout what timeZone should be used. * * @param string|\DateTime $currentTime Relative calculation date - * @param string|null $timezone Timezone to use instead of the system default + * @param string|null $timeZone TimeZone to use instead of the system default * * @return string */ - protected function determineTimezone($currentTime, $timezone) + protected function determineTimeZone($currentTime, $timeZone) { - if (! is_null($timezone)) { - return $timezone; + if (! is_null($timeZone)) { + return $timeZone; } if ($currentTime instanceOf Datetime) { - return $currentTime->getTimezone()->getName(); + return $currentTime->getTimeZone()->getName(); } - return date_default_timezone_get(); + return date_default_timeZone_get(); } } From c40ce648eb708fa009eadec4b8339c02d29fc86e Mon Sep 17 00:00:00 2001 From: Laurence Date: Sun, 3 Dec 2017 22:56:28 +0000 Subject: [PATCH 005/146] tweak --- src/Cron/CronExpression.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index b77b23a3..b7ba7da0 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -406,6 +406,6 @@ protected function determineTimeZone($currentTime, $timeZone) return $currentTime->getTimeZone()->getName(); } - return date_default_timeZone_get(); + return date_default_timezone_get(); } } From 2a88eac648d209199d5d2d660d1f20c53583de5b Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Mon, 8 Jan 2018 13:20:29 -0500 Subject: [PATCH 006/146] Fixed bad example in README (#7) --- README.md | 2 +- tests/Cron/CronExpressionTest.php | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7eac9210..67992c7d 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ echo $cron->getNextRunDate()->format('Y-m-d H:i:s'); echo $cron->getPreviousRunDate()->format('Y-m-d H:i:s'); // Works with complex expressions -$cron = Cron\CronExpression::factory('3-59/15 2,6-12 */15 1 2-5'); +$cron = Cron\CronExpression::factory('3-59/15 6-12 */15 1 2-5'); echo $cron->getNextRunDate()->format('Y-m-d H:i:s'); // Calculate a run date two iterations into the future diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 61e5137f..6bf3b6d2 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -172,6 +172,9 @@ public function scheduleProvider() array('* * * * 5#2', strtotime('2011-07-01 00:00:00'), '2011-07-08 00:00:00', false), array('* * * * 5#1', strtotime('2011-07-01 00:00:00'), '2011-07-01 00:00:00', true), array('* * * * 3#4', strtotime('2011-07-01 00:00:00'), '2011-07-27 00:00:00', false), + + // Issue #7, documented example failed + ['3-59/15 6-12 */15 1 2-5', strtotime('2017-01-08 00:00:00'), '2017-01-31 06:03:00', false], ); } @@ -440,5 +443,8 @@ public function testValidationWorks() // Issue #125, this is just all sorts of wrong $this->assertFalse(CronExpression::isValidExpression('990 14 * * mon-fri0345345')); + + // Issue #7, this was the old documented example that had invalid syntax + $this->assertFalse(CronExpression::isValidExpression('3-59/15 2,6-12 */15 1 2-5')); } } From 9f88567641b7985d09d0236cc76f39592f3b1429 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Thu, 1 Mar 2018 22:14:22 -0500 Subject: [PATCH 007/146] Allow ranges and lists in the same expression (#5) --- src/Cron/AbstractField.php | 37 ++++++++++++++++++------------- tests/Cron/AbstractFieldTest.php | 27 ++++++++++++++++++++++ tests/Cron/CronExpressionTest.php | 4 ++-- 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/Cron/AbstractField.php b/src/Cron/AbstractField.php index 86db3063..4f71d98a 100644 --- a/src/Cron/AbstractField.php +++ b/src/Cron/AbstractField.php @@ -153,6 +153,16 @@ public function getRangeForExpression($expression, $max) { $values = array(); + if (strpos($expression, ',') !== false) { + $ranges = explode(',', $expression); + $values = []; + foreach ($ranges as $range) { + $expanded = $this->getRangeForExpression($range, $this->rangeEnd); + $values = array_merge($values, $expanded); + } + return $values; + } + if ($this->isRange($expression) || $this->isIncrementsOfRanges($expression)) { if (!$this->isIncrementsOfRanges($expression)) { list ($offset, $to) = explode('-', $expression); @@ -168,7 +178,7 @@ public function getRangeForExpression($expression, $max) } $offset = $offset == '*' ? 0 : $offset; for ($i = $offset; $i <= $to; $i += $stepSize) { - $values[] = $i; + $values[] = (int)$i; } sort($values); } @@ -206,16 +216,21 @@ public function validate($value) return true; } - // You cannot have a range and a list at the same time - if (strpos($value, ',') !== false && strpos($value, '-') !== false) { - return false; - } - if (strpos($value, '/') !== false) { list($range, $step) = explode('/', $value); return $this->validate($range) && filter_var($step, FILTER_VALIDATE_INT); } + // Validate each chunk of a list individually + if (strpos($value, ',') !== false) { + foreach (explode(',', $value) as $listItem) { + if (!$this->validate($listItem)) { + return false; + } + } + return true; + } + if (strpos($value, '-') !== false) { if (substr_count($value, '-') > 1) { return false; @@ -232,16 +247,6 @@ public function validate($value) return $this->validate($chunks[0]) && $this->validate($chunks[1]); } - // Validate each chunk of a list individually - if (strpos($value, ',') !== false) { - foreach (explode(',', $value) as $listItem) { - if (!$this->validate($listItem)) { - return false; - } - } - return true; - } - // We should have a numeric by now, so coerce this into an integer if (filter_var($value, FILTER_VALIDATE_INT) !== false) { $value = (int) $value; diff --git a/tests/Cron/AbstractFieldTest.php b/tests/Cron/AbstractFieldTest.php index 8fac8349..c9f9110a 100644 --- a/tests/Cron/AbstractFieldTest.php +++ b/tests/Cron/AbstractFieldTest.php @@ -3,6 +3,7 @@ namespace Cron\Tests; use Cron\DayOfWeekField; +use Cron\HoursField; use Cron\MinutesField; use Cron\MonthField; use PHPUnit\Framework\TestCase; @@ -116,4 +117,30 @@ public function testTestsIfSatisfied() $this->assertFalse($f->isSatisfied('12', '3-7/2')); $this->assertFalse($f->isSatisfied('12', '11')); } + + /** + * Allows ranges and lists to coexist in the same expression + * + * @see https://github.com/dragonmantank/cron-expression/issues/5 + */ + public function testAllowRangesAndLists() + { + $expression = '5-7,11-13'; + $f = new HoursField(); + $this->assertTrue($f->validate($expression)); + } + + /** + * Makes sure that various types of ranges expand out properly + * + * @see https://github.com/dragonmantank/cron-expression/issues/5 + */ + public function testGetRangeForExpressionExpandsCorrectly() + { + $f = new HoursField(); + $this->assertSame([5, 6, 7, 11, 12, 13], $f->getRangeForExpression('5-7,11-13', 23)); + $this->assertSame(['5', '6', '7', '11', '12', '13'], $f->getRangeForExpression('5,6,7,11,12,13', 23)); + $this->assertSame([0, 6, 12, 18], $f->getRangeForExpression('*/6', 23)); + $this->assertSame([5, 11], $f->getRangeForExpression('5-13/6', 23)); + } } diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 6bf3b6d2..30f64896 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -444,7 +444,7 @@ public function testValidationWorks() // Issue #125, this is just all sorts of wrong $this->assertFalse(CronExpression::isValidExpression('990 14 * * mon-fri0345345')); - // Issue #7, this was the old documented example that had invalid syntax - $this->assertFalse(CronExpression::isValidExpression('3-59/15 2,6-12 */15 1 2-5')); + // see https://github.com/dragonmantank/cron-expression/issues/5 + $this->assertTrue(CronExpression::isValidExpression('2,17,35,47 5-7,11-13 * * *')); } } From 2b305e3aba18791c38b3a61e8434adca82d7834c Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Thu, 1 Mar 2018 22:51:58 -0500 Subject: [PATCH 008/146] Fixed regression where literals were not converted to their numerical values --- src/Cron/AbstractField.php | 12 +++++++++++- tests/Cron/CronExpressionTest.php | 14 +++++++++++++- tests/Cron/DayOfWeekFieldTest.php | 10 ++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/Cron/AbstractField.php b/src/Cron/AbstractField.php index 4f71d98a..058c0a95 100644 --- a/src/Cron/AbstractField.php +++ b/src/Cron/AbstractField.php @@ -90,7 +90,14 @@ public function isIncrementsOfRanges($value) */ public function isInRange($dateValue, $value) { - $parts = array_map('trim', explode('-', $value, 2)); + $parts = array_map(function($value) { + $value = trim($value); + $value = $this->convertLiterals($value); + return $value; + }, + explode('-', $value, 2) + ); + return $dateValue >= $parts[0] && $dateValue <= $parts[1]; } @@ -152,6 +159,7 @@ public function isInIncrementsOfRanges($dateValue, $value) public function getRangeForExpression($expression, $max) { $values = array(); + $expression = $this->convertLiterals($expression); if (strpos($expression, ',') !== false) { $ranges = explode(',', $expression); @@ -166,6 +174,8 @@ public function getRangeForExpression($expression, $max) if ($this->isRange($expression) || $this->isIncrementsOfRanges($expression)) { if (!$this->isIncrementsOfRanges($expression)) { list ($offset, $to) = explode('-', $expression); + $offset = $this->convertLiterals($offset); + $to = $this->convertLiterals($to); $stepSize = 1; } else { diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 30f64896..e98dda72 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -175,6 +175,10 @@ public function scheduleProvider() // Issue #7, documented example failed ['3-59/15 6-12 */15 1 2-5', strtotime('2017-01-08 00:00:00'), '2017-01-31 06:03:00', false], + + // https://github.com/laravel/framework/commit/07d160ac3cc9764d5b429734ffce4fa311385403 + ['* * * * MON-FRI', strtotime('2017-01-08 00:00:00'), strtotime('2017-01-09 00:00:00'), false], + ['* * * * TUE', strtotime('2017-01-08 00:00:00'), strtotime('2017-01-10 00:00:00'), false], ); } @@ -200,9 +204,17 @@ public function testDeterminesIfCronIsDue($schedule, $relativeTime, $nextRun, $i } elseif (is_int($relativeTime)) { $relativeTime = date('Y-m-d H:i:s', $relativeTime); } + + if (is_string($nextRun)) { + $nextRunDate = new DateTime($nextRun); + } elseif (is_int($nextRun)) { + $nextRunDate = new DateTime(); + $nextRunDate->setTimestamp($nextRun); + } $this->assertSame($isDue, $cron->isDue($relativeTime)); $next = $cron->getNextRunDate($relativeTime, 0, true); - $this->assertEquals(new DateTime($nextRun), $next); + + $this->assertEquals($nextRunDate, $next); } /** diff --git a/tests/Cron/DayOfWeekFieldTest.php b/tests/Cron/DayOfWeekFieldTest.php index f8b02320..483b206a 100644 --- a/tests/Cron/DayOfWeekFieldTest.php +++ b/tests/Cron/DayOfWeekFieldTest.php @@ -114,4 +114,14 @@ public function testIssue47() { $this->assertFalse($f->validate('*-')); $this->assertFalse($f->validate(',-')); } + + /** + * @see https://github.com/laravel/framework/commit/07d160ac3cc9764d5b429734ffce4fa311385403 + */ + public function testLiteralsExpandProperly() + { + $f = new DayOfWeekField(); + $this->assertTrue($f->validate('MON-FRI')); + $this->assertSame([1,2,3,4,5], $f->getRangeForExpression('MON-FRI', 7)); + } } From 3f00985deec8df53d4cc1e5c33619bda1ee309a5 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Fri, 6 Apr 2018 11:51:55 -0400 Subject: [PATCH 009/146] Updated README for 2.1.0 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08b736b7..18a9639d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log +## [2.1.0] - 2018-04-06 +### Added +- N/A +### Changed +- Upgraded to PHPUnit 6 (#2) +### Fixed +- Refactored timezones to deal with some inconsistent behavior (#3) +- Allow ranges and lists in same expression (#5) +- Fixed regression where literals were not converted to their numerical counterpart (#) + ## [2.0.0] - 2017-10-12 ### Added - N/A From d403cc2a530a1b1c0ef2fdec7299121cec9c6ab6 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Tue, 5 Jun 2018 21:59:01 -0400 Subject: [PATCH 010/146] Resolves #12, added support for numerals with leading 0s --- src/Cron/AbstractField.php | 12 +++++++++--- tests/Cron/CronExpressionTest.php | 24 ++++++++++++++++++++++++ tests/Cron/DayOfMonthFieldTest.php | 1 + tests/Cron/DayOfWeekFieldTest.php | 2 ++ tests/Cron/HoursFieldTest.php | 2 ++ 5 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/Cron/AbstractField.php b/src/Cron/AbstractField.php index 058c0a95..5faeffb6 100644 --- a/src/Cron/AbstractField.php +++ b/src/Cron/AbstractField.php @@ -257,11 +257,17 @@ public function validate($value) return $this->validate($chunks[0]) && $this->validate($chunks[1]); } - // We should have a numeric by now, so coerce this into an integer - if (filter_var($value, FILTER_VALIDATE_INT) !== false) { - $value = (int) $value; + if (!is_numeric($value)) { + return false; } + if (is_float($value) || strpos($value, '.') !== false) { + return false; + } + + // We should have a numeric by now, so coerce this into an integer + $value = (int) $value; + return in_array($value, $this->fullRange, true); } } diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index b9f23f39..03186e1a 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -508,4 +508,28 @@ public function testValidationWorks() // see https://github.com/dragonmantank/cron-expression/issues/5 $this->assertTrue(CronExpression::isValidExpression('2,17,35,47 5-7,11-13 * * *')); } + + /** + * Makes sure that 00 is considered a valid value for 0-based fields + * cronie allows numbers with a leading 0, so adding support for this as well + * + * @see https://github.com/dragonmantank/cron-expression/issues/12 + */ + public function testDoubleZeroIsValid() + { + $this->assertTrue(CronExpression::isValidExpression('00 * * * *')); + $this->assertTrue(CronExpression::isValidExpression('01 * * * *')); + $this->assertTrue(CronExpression::isValidExpression('* 00 * * *')); + $this->assertTrue(CronExpression::isValidExpression('* 01 * * *')); + + $e = CronExpression::factory('00 * * * *'); + $this->assertTrue($e->isDue(new DateTime('2014-04-07 00:00:00'))); + $e = CronExpression::factory('01 * * * *'); + $this->assertTrue($e->isDue(new DateTime('2014-04-07 00:01:00'))); + + $e = CronExpression::factory('* 00 * * *'); + $this->assertTrue($e->isDue(new DateTime('2014-04-07 00:00:00'))); + $e = CronExpression::factory('* 01 * * *'); + $this->assertTrue($e->isDue(new DateTime('2014-04-07 01:00:00'))); + } } diff --git a/tests/Cron/DayOfMonthFieldTest.php b/tests/Cron/DayOfMonthFieldTest.php index b728fe98..0dae4ed6 100644 --- a/tests/Cron/DayOfMonthFieldTest.php +++ b/tests/Cron/DayOfMonthFieldTest.php @@ -21,6 +21,7 @@ public function testValidatesField() $this->assertTrue($f->validate('*')); $this->assertTrue($f->validate('L')); $this->assertTrue($f->validate('5W')); + $this->assertTrue($f->validate('01')); $this->assertFalse($f->validate('5W,L')); $this->assertFalse($f->validate('1.')); } diff --git a/tests/Cron/DayOfWeekFieldTest.php b/tests/Cron/DayOfWeekFieldTest.php index 483b206a..d27c34df 100644 --- a/tests/Cron/DayOfWeekFieldTest.php +++ b/tests/Cron/DayOfWeekFieldTest.php @@ -18,6 +18,8 @@ public function testValidatesField() { $f = new DayOfWeekField(); $this->assertTrue($f->validate('1')); + $this->assertTrue($f->validate('01')); + $this->assertTrue($f->validate('00')); $this->assertTrue($f->validate('*')); $this->assertFalse($f->validate('*/3,1,1-12')); $this->assertTrue($f->validate('SUN-2')); diff --git a/tests/Cron/HoursFieldTest.php b/tests/Cron/HoursFieldTest.php index 03f09091..e936d11a 100644 --- a/tests/Cron/HoursFieldTest.php +++ b/tests/Cron/HoursFieldTest.php @@ -18,6 +18,8 @@ public function testValidatesField() { $f = new HoursField(); $this->assertTrue($f->validate('1')); + $this->assertTrue($f->validate('00')); + $this->assertTrue($f->validate('01')); $this->assertTrue($f->validate('*')); $this->assertFalse($f->validate('*/3,1,1-12')); } From 2ff19fb7c4ad21f593bb541b5b698e266733f50d Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Tue, 5 Jun 2018 22:51:08 -0400 Subject: [PATCH 011/146] Resolves #6, added support for ranges to wrap around with large steps --- src/Cron/AbstractField.php | 19 ++++++++++++------- tests/Cron/AbstractFieldTest.php | 7 ------- tests/Cron/CronExpressionTest.php | 25 +++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/Cron/AbstractField.php b/src/Cron/AbstractField.php index 5faeffb6..262ab63c 100644 --- a/src/Cron/AbstractField.php +++ b/src/Cron/AbstractField.php @@ -139,12 +139,13 @@ public function isInIncrementsOfRanges($dateValue, $value) throw new \OutOfRangeException('Invalid range end requested'); } - if ($step > ($rangeEnd - $rangeStart) + 1) { - throw new \OutOfRangeException('Step cannot be greater than total range'); + // Steps larger than the range need to wrap around and be handled slightly differently than smaller steps + if ($step >= $this->rangeEnd) { + $thisRange = [$this->fullRange[$step % count($this->fullRange)]]; + } else { + $thisRange = range($rangeStart, $rangeEnd, $step); } - $thisRange = range($rangeStart, $rangeEnd, $step); - return in_array($dateValue, $thisRange); } @@ -186,9 +187,13 @@ public function getRangeForExpression($expression, $max) $offset = $range[0]; $to = isset($range[1]) ? $range[1] : $max; } - $offset = $offset == '*' ? 0 : $offset; - for ($i = $offset; $i <= $to; $i += $stepSize) { - $values[] = (int)$i; + $offset = $offset == '*' ? $this->rangeStart : $offset; + if ($stepSize >= $this->rangeEnd) { + $values = [$this->fullRange[$stepSize % count($this->fullRange)]]; + } else { + for ($i = $offset; $i <= $to; $i += $stepSize) { + $values[] = (int)$i; + } } sort($values); } diff --git a/tests/Cron/AbstractFieldTest.php b/tests/Cron/AbstractFieldTest.php index c9f9110a..38114392 100644 --- a/tests/Cron/AbstractFieldTest.php +++ b/tests/Cron/AbstractFieldTest.php @@ -96,13 +96,6 @@ public function testTestsIfInIncrementsOfRangesOnOneStartRange() $this->assertFalse($f->isInIncrementsOfRanges('34', '4/1')); } - public function testStepCannotBeLargerThanRange() - { - $this->expectException(\OutOfRangeException::class); - $f = new MonthField(); - $f->isInIncrementsOfRanges('2', '3-12/13'); - } - /** * @covers \Cron\AbstractField::isSatisfied */ diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 03186e1a..5d46644b 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -3,6 +3,7 @@ namespace Cron\Tests; use Cron\CronExpression; +use Cron\MonthField; use DateTime; use DateTimeZone; use InvalidArgumentException; @@ -532,4 +533,28 @@ public function testDoubleZeroIsValid() $e = CronExpression::factory('* 01 * * *'); $this->assertTrue($e->isDue(new DateTime('2014-04-07 01:00:00'))); } + + + /** + * Ranges with large steps should "wrap around" to the appropriate value + * cronie allows for steps that are larger than the range of a field, with it wrapping around like a ring buffer. We + * should do the same. + * + * @see https://github.com/dragonmantank/cron-expression/issues/6 + */ + public function testRangesWrapAroundWithLargeSteps() + { + $f = new MonthField(); + $this->assertTrue($f->validate('*/123')); + $this->assertSame([4], $f->getRangeForExpression('*/123', 12)); + + $e = CronExpression::factory('* * * */123 *'); + $this->assertTrue($e->isDue(new DateTime('2014-04-07 00:00:00'))); + + $nextRunDate = $e->getNextRunDate(new DateTime('2014-04-07 00:00:00')); + $this->assertSame('2014-04-07 00:01:00', $nextRunDate->format('Y-m-d H:i:s')); + + $nextRunDate = $e->getNextRunDate(new DateTime('2014-05-07 00:00:00')); + $this->assertSame('2015-04-01 00:00:00', $nextRunDate->format('Y-m-d H:i:s')); + } } From 92a2c3768d50e21a1f26a53cb795ce72806266c5 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Tue, 5 Jun 2018 23:12:17 -0400 Subject: [PATCH 012/146] Updated changelog for 2.2 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18a9639d..8cb3a084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## [2.2.0] - 2018-06-05 +### Added +- Added support for steps larger than field ranges (#6) +## Changed +- N/A +### Fixed +- Fixed validation for numbers with leading 0s (#12) + ## [2.1.0] - 2018-04-06 ### Added - N/A From 700b373d6ab934b530979e3ac531485fa00b5c99 Mon Sep 17 00:00:00 2001 From: EntranceJew Date: Thu, 7 Jun 2018 19:59:09 -0400 Subject: [PATCH 013/146] fix symmetry in constructor and factory method if it can be null in the factory, the constructor should have it too also added phpdoc stuff --- src/Cron/CronExpression.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index b7ba7da0..5be00b29 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -62,7 +62,7 @@ class CronExpression * `@weekly` - Run once a week, midnight on Sun - 0 0 * * 0 * `@daily` - Run once a day, midnight - 0 0 * * * * `@hourly` - Run once an hour, first minute - 0 * * * * - * @param FieldFactory $fieldFactory Field factory to use + * @param FieldFactory|null $fieldFactory Field factory to use * * @return CronExpression */ @@ -107,9 +107,9 @@ public static function isValidExpression($expression) * Parse a CRON expression * * @param string $expression CRON expression (e.g. '8 * * * *') - * @param FieldFactory $fieldFactory Factory to create cron fields + * @param FieldFactory|null $fieldFactory Factory to create cron fields */ - public function __construct($expression, FieldFactory $fieldFactory) + public function __construct($expression, FieldFactory $fieldFactory = null) { $this->fieldFactory = $fieldFactory; $this->setExpression($expression); From 55d20c6cd2e0d64e160ba117a6a0c61f3ce02b28 Mon Sep 17 00:00:00 2001 From: Atef Ben Ali Date: Thu, 13 Sep 2018 07:15:20 +0100 Subject: [PATCH 014/146] highlight variables --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 67992c7d..d7b58cf6 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ PHP Cron Expression Parser The PHP cron expression parser can parse a CRON expression, determine if it is due to run, calculate the next run date of the expression, and calculate the previous run date of the expression. You can calculate dates far into the future or past by -skipping n number of matching dates. +skipping **n** number of matching dates. The parser can handle increments of ranges (e.g. */12, 2-59/3), intervals (e.g. 0-9), -lists (e.g. 1,2,3), W to find the nearest weekday for a given day of the month, L to -find the last day of the month, L to find the last given weekday of a month, and hash +lists (e.g. 1,2,3), **W** to find the nearest weekday for a given day of the month, **L** to +find the last day of the month, **L** to find the last given weekday of a month, and hash (#) to find the nth weekday of a given month. More information about this fork can be found in the blog post [here](http://ctankersley.com/2017/10/12/cron-expression-update/). tl;dr - v2.0.0 is a major breaking change, and @dragonmantank can better take care of the project in a separate fork. From 0f844c41164b00fba3143855382a693ea15c2199 Mon Sep 17 00:00:00 2001 From: Javier Spagnoletti Date: Tue, 4 Dec 2018 16:19:42 -0300 Subject: [PATCH 015/146] Improve dependency declaration --- .travis.yml | 34 ++++++++++++++++++++++++---------- composer.json | 9 +++++++-- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2aaaa54b..479b6135 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,31 @@ language: php -php: - - 7.0 - - 7.1 - - hhvm - sudo: false -install: travis_retry composer install --no-interaction --prefer-dist - -script: phpunit --coverage-text - matrix: + include: + - php: 7.0 + - php: 7.0 + env: dependencies=lowest + - php: 7.1 + - php: 7.1 + env: dependencies=lowest + - php: 7.2 + - php: 7.2 + env: dependencies=lowest + - php: nighly + - php: nighly + env: dependencies=lowest allow_failures: - - php: hhvm + - php: nighly fast_finish: true + +install: + - | + if [[ "$dependencies" = "lowest" ]]; then + travis_retry composer update --no-interaction --prefer-lowest --prefer-stable -n + else + travis_retry composer install --no-interaction --prefer-dist + fi + +script: vendor/bin/phpunit --coverage-text diff --git a/composer.json b/composer.json index d9997ead..c0b7903f 100644 --- a/composer.json +++ b/composer.json @@ -17,10 +17,10 @@ } ], "require": { - "php": ">=7.0.0" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "~6.4" + "phpunit/phpunit": "^6.4|^7.0" }, "autoload": { "psr-4": { @@ -31,5 +31,10 @@ "psr-4": { "Tests\\": "tests/Cron/" } + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } } } From c2f2518bcae9f7b6b967e9833ca5f999aad46323 Mon Sep 17 00:00:00 2001 From: Piotr Konieczny Date: Thu, 6 Dec 2018 09:49:16 +0100 Subject: [PATCH 016/146] Get timezone from DateTimeInterface --- src/Cron/CronExpression.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index 5be00b29..82c0ef1c 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -4,6 +4,7 @@ use DateTime; use DateTimeImmutable; +use DateTimeInterface; use DateTimeZone; use Exception; use InvalidArgumentException; @@ -391,8 +392,8 @@ protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $a /** * Workout what timeZone should be used. * - * @param string|\DateTime $currentTime Relative calculation date - * @param string|null $timeZone TimeZone to use instead of the system default + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param string|null $timeZone TimeZone to use instead of the system default * * @return string */ @@ -402,7 +403,7 @@ protected function determineTimeZone($currentTime, $timeZone) return $timeZone; } - if ($currentTime instanceOf Datetime) { + if ($currentTime instanceOf DateTimeInterface) { return $currentTime->getTimeZone()->getName(); } From 73de60d9d47aa6ce252d529f8aee60b101c3f3e7 Mon Sep 17 00:00:00 2001 From: Piotr Konieczny Date: Thu, 6 Dec 2018 11:06:28 +0100 Subject: [PATCH 017/146] Adding missing docblocks --- src/Cron/AbstractField.php | 12 ++++++++++-- src/Cron/DayOfMonthField.php | 6 ++++++ src/Cron/DayOfWeekField.php | 9 +++++++++ src/Cron/HoursField.php | 8 ++++++++ src/Cron/MinutesField.php | 8 ++++++++ src/Cron/MonthField.php | 6 ++++++ 6 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/Cron/AbstractField.php b/src/Cron/AbstractField.php index 262ab63c..8b1072ab 100644 --- a/src/Cron/AbstractField.php +++ b/src/Cron/AbstractField.php @@ -31,7 +31,9 @@ abstract class AbstractField implements FieldInterface */ protected $rangeEnd; - + /** + * Constructor + */ public function __construct() { $this->fullRange = range($this->rangeStart, $this->rangeEnd); @@ -204,12 +206,18 @@ public function getRangeForExpression($expression, $max) return $values; } + /** + * Convert literal + * + * @param string $value + * @return string + */ protected function convertLiterals($value) { if (count($this->literals)) { $key = array_search($value, $this->literals); if ($key !== false) { - return $key; + return (string) $key; } } diff --git a/src/Cron/DayOfMonthField.php b/src/Cron/DayOfMonthField.php index abf59690..d31494f1 100644 --- a/src/Cron/DayOfMonthField.php +++ b/src/Cron/DayOfMonthField.php @@ -59,6 +59,9 @@ private static function getNearestWeekday($currentYear, $currentMonth, $targetDa } } + /** + * @inheritDoc + */ public function isSatisfiedBy(DateTime $date, $value) { // ? states that the field value is to be skipped @@ -88,6 +91,9 @@ public function isSatisfiedBy(DateTime $date, $value) return $this->isSatisfied($date->format('d'), $value); } + /** + * @inheritDoc + */ public function increment(DateTime $date, $invert = false) { if ($invert) { diff --git a/src/Cron/DayOfWeekField.php b/src/Cron/DayOfWeekField.php index e1780134..9154fa39 100644 --- a/src/Cron/DayOfWeekField.php +++ b/src/Cron/DayOfWeekField.php @@ -28,12 +28,18 @@ class DayOfWeekField extends AbstractField protected $literals = [1 => 'MON', 2 => 'TUE', 3 => 'WED', 4 => 'THU', 5 => 'FRI', 6 => 'SAT', 7 => 'SUN']; + /** + * Constructor + */ public function __construct() { $this->nthRange = range(1, 5); parent::__construct(); } + /** + * @inheritDoc + */ public function isSatisfiedBy(DateTime $date, $value) { if ($value == '?') { @@ -127,6 +133,9 @@ public function isSatisfiedBy(DateTime $date, $value) return $this->isSatisfied($fieldValue, $value); } + /** + * @inheritDoc + */ public function increment(DateTime $date, $invert = false) { if ($invert) { diff --git a/src/Cron/HoursField.php b/src/Cron/HoursField.php index 4def9ca1..713a642e 100644 --- a/src/Cron/HoursField.php +++ b/src/Cron/HoursField.php @@ -13,11 +13,19 @@ class HoursField extends AbstractField protected $rangeStart = 0; protected $rangeEnd = 23; + /** + * @inheritDoc + */ public function isSatisfiedBy(DateTime $date, $value) { return $this->isSatisfied($date->format('H'), $value); } + /** + * {@inheritDoc} + * + * @param string|null $parts + */ public function increment(DateTime $date, $invert = false, $parts = null) { // Change timezone to UTC temporarily. This will diff --git a/src/Cron/MinutesField.php b/src/Cron/MinutesField.php index 59bb386f..fdebd321 100644 --- a/src/Cron/MinutesField.php +++ b/src/Cron/MinutesField.php @@ -13,11 +13,19 @@ class MinutesField extends AbstractField protected $rangeStart = 0; protected $rangeEnd = 59; + /** + * @inheritDoc + */ public function isSatisfiedBy(DateTime $date, $value) { return $this->isSatisfied($date->format('i'), $value); } + /** + * {@inheritDoc} + * + * @param string|null $parts + */ public function increment(DateTime $date, $invert = false, $parts = null) { if (is_null($parts)) { diff --git a/src/Cron/MonthField.php b/src/Cron/MonthField.php index 79fdf3cf..c9f1ffb2 100644 --- a/src/Cron/MonthField.php +++ b/src/Cron/MonthField.php @@ -14,6 +14,9 @@ class MonthField extends AbstractField protected $literals = [1 => 'JAN', 2 => 'FEB', 3 => 'MAR', 4 => 'APR', 5 => 'MAY', 6 => 'JUN', 7 => 'JUL', 8 => 'AUG', 9 => 'SEP', 10 => 'OCT', 11 => 'NOV', 12 => 'DEC']; + /** + * @inheritDoc + */ public function isSatisfiedBy(DateTime $date, $value) { $value = $this->convertLiterals($value); @@ -21,6 +24,9 @@ public function isSatisfiedBy(DateTime $date, $value) return $this->isSatisfied($date->format('m'), $value); } + /** + * @inheritDoc + */ public function increment(DateTime $date, $invert = false) { if ($invert) { From 0753135272f7e5e636f1ac42f002a53455ac4528 Mon Sep 17 00:00:00 2001 From: Piotr Konieczny Date: Thu, 6 Dec 2018 11:30:08 +0100 Subject: [PATCH 018/146] Adding missing docblocks for class properties --- src/Cron/DayOfMonthField.php | 7 +++++++ src/Cron/DayOfWeekField.php | 13 +++++++++++++ src/Cron/HoursField.php | 7 +++++++ src/Cron/MinutesField.php | 7 +++++++ src/Cron/MonthField.php | 11 +++++++++++ 5 files changed, 45 insertions(+) diff --git a/src/Cron/DayOfMonthField.php b/src/Cron/DayOfMonthField.php index d31494f1..3920cc9e 100644 --- a/src/Cron/DayOfMonthField.php +++ b/src/Cron/DayOfMonthField.php @@ -24,7 +24,14 @@ */ class DayOfMonthField extends AbstractField { + /** + * @inheritDoc + */ protected $rangeStart = 1; + + /** + * @inheritDoc + */ protected $rangeEnd = 31; /** diff --git a/src/Cron/DayOfWeekField.php b/src/Cron/DayOfWeekField.php index 9154fa39..8f7f6f3c 100644 --- a/src/Cron/DayOfWeekField.php +++ b/src/Cron/DayOfWeekField.php @@ -21,11 +21,24 @@ */ class DayOfWeekField extends AbstractField { + /** + * @inheritDoc + */ protected $rangeStart = 0; + + /** + * @inheritDoc + */ protected $rangeEnd = 7; + /** + * @var array Weekday range + */ protected $nthRange; + /** + * @inheritDoc + */ protected $literals = [1 => 'MON', 2 => 'TUE', 3 => 'WED', 4 => 'THU', 5 => 'FRI', 6 => 'SAT', 7 => 'SUN']; /** diff --git a/src/Cron/HoursField.php b/src/Cron/HoursField.php index 713a642e..4fd98dd0 100644 --- a/src/Cron/HoursField.php +++ b/src/Cron/HoursField.php @@ -10,7 +10,14 @@ */ class HoursField extends AbstractField { + /** + * @inheritDoc + */ protected $rangeStart = 0; + + /** + * @inheritDoc + */ protected $rangeEnd = 23; /** diff --git a/src/Cron/MinutesField.php b/src/Cron/MinutesField.php index fdebd321..fb81a8e6 100644 --- a/src/Cron/MinutesField.php +++ b/src/Cron/MinutesField.php @@ -10,7 +10,14 @@ */ class MinutesField extends AbstractField { + /** + * @inheritDoc + */ protected $rangeStart = 0; + + /** + * @inheritDoc + */ protected $rangeEnd = 59; /** diff --git a/src/Cron/MonthField.php b/src/Cron/MonthField.php index c9f1ffb2..238b0e7c 100644 --- a/src/Cron/MonthField.php +++ b/src/Cron/MonthField.php @@ -9,8 +9,19 @@ */ class MonthField extends AbstractField { + /** + * @inheritDoc + */ protected $rangeStart = 1; + + /** + * @inheritDoc + */ protected $rangeEnd = 12; + + /** + * @inheritDoc + */ protected $literals = [1 => 'JAN', 2 => 'FEB', 3 => 'MAR', 4 => 'APR', 5 => 'MAY', 6 => 'JUN', 7 => 'JUL', 8 => 'AUG', 9 => 'SEP', 10 => 'OCT', 11 => 'NOV', 12 => 'DEC']; From c7b878dd428d7214de8e814aec988e6b0e66e51a Mon Sep 17 00:00:00 2001 From: Piotr Konieczny Date: Thu, 6 Dec 2018 11:30:29 +0100 Subject: [PATCH 019/146] Normalize EOLs --- src/Cron/DayOfWeekField.php | 1 - src/Cron/FieldInterface.php | 1 + src/Cron/HoursField.php | 2 +- src/Cron/MinutesField.php | 1 - 4 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Cron/DayOfWeekField.php b/src/Cron/DayOfWeekField.php index 8f7f6f3c..fa73d9a1 100644 --- a/src/Cron/DayOfWeekField.php +++ b/src/Cron/DayOfWeekField.php @@ -5,7 +5,6 @@ use DateTime; use InvalidArgumentException; - /** * Day of week field. Allows: * / , - ? L # * diff --git a/src/Cron/FieldInterface.php b/src/Cron/FieldInterface.php index be37b938..72d2dd29 100644 --- a/src/Cron/FieldInterface.php +++ b/src/Cron/FieldInterface.php @@ -1,6 +1,7 @@ Date: Wed, 19 Dec 2018 10:55:23 +0200 Subject: [PATCH 020/146] Fixed infinite loop when resolving last weekday of the month from literals --- src/Cron/DayOfWeekField.php | 4 +++- tests/Cron/DayOfWeekFieldTest.php | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Cron/DayOfWeekField.php b/src/Cron/DayOfWeekField.php index e1780134..14ee0d94 100644 --- a/src/Cron/DayOfWeekField.php +++ b/src/Cron/DayOfWeekField.php @@ -49,7 +49,9 @@ public function isSatisfiedBy(DateTime $date, $value) // Find out if this is the last specific weekday of the month if (strpos($value, 'L')) { - $weekday = str_replace('7', '0', substr($value, 0, strpos($value, 'L'))); + $weekday = $this->convertLiterals(substr($value, 0, strpos($value, 'L'))); + $weekday = str_replace('7', '0', $weekday); + $tdate = clone $date; $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth); while ($tdate->format('w') != $weekday) { diff --git a/tests/Cron/DayOfWeekFieldTest.php b/tests/Cron/DayOfWeekFieldTest.php index d27c34df..eaed77ce 100644 --- a/tests/Cron/DayOfWeekFieldTest.php +++ b/tests/Cron/DayOfWeekFieldTest.php @@ -103,6 +103,18 @@ public function testHandlesZeroAndSevenDayOfTheWeekValues() $this->assertTrue($f->isSatisfiedBy(new DateTime('2014-04-20 00:00:00'), '7#3')); } + /** + * @covers \Cron\DayOfWeekField::isSatisfiedBy + */ + public function testHandlesLastWeekdayOfTheMonth() + { + $f = new DayOfWeekField(); + $this->assertTrue($f->isSatisfiedBy(new DateTime('2018-12-28 00:00:00'), 'FRIL')); + $this->assertTrue($f->isSatisfiedBy(new DateTime('2018-12-28 00:00:00'), '5L')); + $this->assertFalse($f->isSatisfiedBy(new DateTime('2018-12-21 00:00:00'), 'FRIL')); + $this->assertFalse($f->isSatisfiedBy(new DateTime('2018-12-21 00:00:00'), '5L')); + } + /** * @see https://github.com/mtdowling/cron-expression/issues/47 */ From a2c60195a4d8841a2a7811e3121c63ebd33aa1b0 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Thu, 14 Feb 2019 10:08:44 -0500 Subject: [PATCH 021/146] Started listing projects that use cron-expression --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 67992c7d..19adcc9c 100644 --- a/README.md +++ b/README.md @@ -71,3 +71,8 @@ Requirements - PHP 7.0+ - PHPUnit is required to run the unit tests - Composer is required to run the unit tests + +Projects that Use cron-expression +================================= +* Part of the [Laravel Framework](https://github.com/laravel/framework/) +* Available as a [Symfony Bundle - setono/cron-expression-bundle](https://github.com/Setono/CronExpressionBundle) \ No newline at end of file From b195d12e3ebe73711c2f97ea651da711ff64cbc9 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Thu, 14 Feb 2019 10:24:06 -0500 Subject: [PATCH 022/146] Positions used higher than 4 now throw a human readable error --- src/Cron/FieldFactory.php | 2 +- tests/Cron/CronExpressionTest.php | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Cron/FieldFactory.php b/src/Cron/FieldFactory.php index fd27352d..545e4b83 100644 --- a/src/Cron/FieldFactory.php +++ b/src/Cron/FieldFactory.php @@ -44,7 +44,7 @@ public function getField($position) break; default: throw new InvalidArgumentException( - $position . ' is not a valid position' + ($position + 1) . ' is not a valid position' ); } } diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 5d46644b..678501be 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -557,4 +557,16 @@ public function testRangesWrapAroundWithLargeSteps() $nextRunDate = $e->getNextRunDate(new DateTime('2014-05-07 00:00:00')); $this->assertSame('2015-04-01 00:00:00', $nextRunDate->format('Y-m-d H:i:s')); } + + /** + * When there is an issue with a field, we should report the human readable position + * + * @see https://github.com/dragonmantank/cron-expression/issues/29 + */ + public function testFieldPositionIsHumanAdjusted() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("6 is not a valid position"); + $e = CronExpression::factory('0 * * * * ? *'); + } } From 714f6a8ab14b1951dcc2141b8e5d21ee96e06474 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Thu, 14 Feb 2019 10:33:13 -0500 Subject: [PATCH 023/146] Added PHP 7.3 support, fixed nightly --- .travis.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 479b6135..83ea1084 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,11 +13,14 @@ matrix: - php: 7.2 - php: 7.2 env: dependencies=lowest - - php: nighly - - php: nighly + - php: 7.3 + - php: 7.3 + env: dependencies=lowest + - php: nightly + - php: nightly env: dependencies=lowest allow_failures: - - php: nighly + - php: nightly fast_finish: true install: From 6e990d827bd6572ebfa1c5c30c207590c4e5cd57 Mon Sep 17 00:00:00 2001 From: Niels Theen Date: Tue, 15 Jan 2019 22:19:16 +0100 Subject: [PATCH 024/146] PHP 7.2 improvement + syntax cleanup + php_cs config update. --- .php_cs | 87 +++++++-- composer.json | 2 +- src/Cron/AbstractField.php | 146 ++++++++------- src/Cron/CronExpression.php | 217 ++++++++++++----------- src/Cron/DayOfMonthField.php | 59 ++++--- src/Cron/DayOfWeekField.php | 75 ++++---- src/Cron/FieldFactory.php | 21 ++- src/Cron/FieldInterface.php | 25 +-- src/Cron/HoursField.php | 44 ++--- src/Cron/MinutesField.php | 41 +++-- src/Cron/MonthField.php | 24 +-- tests/Cron/AbstractFieldTest.php | 106 +++++------ tests/Cron/CronExpressionTest.php | 274 +++++++++++++++-------------- tests/Cron/DayOfMonthFieldTest.php | 6 +- tests/Cron/DayOfWeekFieldTest.php | 11 +- tests/Cron/FieldFactoryTest.php | 10 +- tests/Cron/HoursFieldTest.php | 4 +- tests/Cron/MinutesFieldTest.php | 3 + tests/Cron/MonthFieldTest.php | 3 +- 19 files changed, 649 insertions(+), 509 deletions(-) diff --git a/.php_cs b/.php_cs index 2c39ae5e..fc1237ea 100644 --- a/.php_cs +++ b/.php_cs @@ -1,15 +1,82 @@ in(__DIR__); -$fixers = array( - '-psr0', - 'long_array_syntax', -); +$config = PhpCsFixer\Config::create() + ->setRules([ + '@Symfony' => true, + 'declare_strict_types' => true, + 'native_function_invocation' => [ + 'include' => ['@compiler_optimized'], + 'scope' => 'namespaced', + ], + 'phpdoc_align' => [ + 'align' => 'left', + ], + 'blank_line_before_statement' => true, + 'align_multiline_comment' => false, + 'concat_space' => [ + 'spacing' => 'one', + ], + 'array_syntax' => [ + 'syntax' => 'short', + ], + 'combine_consecutive_issets' => true, + 'combine_consecutive_unsets' => true, + 'array_indentation' => true, + 'no_extra_blank_lines' => [ + 'break', + 'case', + 'continue', + 'curly_brace_block', + 'default', + 'extra', + 'parenthesis_brace_block', + 'return', + 'square_brace_block', + 'switch', + 'throw', + 'use', + 'useTrait', + 'use_trait', + ], + 'compact_nullable_typehint' => true, + 'escape_implicit_backslashes' => true, + 'explicit_indirect_variable' => true, + 'explicit_string_variable' => true, + 'final_internal_class' => true, + 'fully_qualified_strict_types' => true, + 'function_to_constant' => [ + 'functions' => [ + 'get_class', + 'get_called_class', + 'php_sapi_name', + 'phpversion', + 'pi', + ], + ], + 'list_syntax' => [ + 'syntax' => 'short', + ], + 'logical_operators' => true, + 'no_alternative_syntax' => true, + 'no_null_property_initialization' => true, + 'no_short_echo_tag' => true, + 'no_superfluous_elseif' => false, + 'no_unreachable_default_argument_value' => true, + 'no_unset_on_property' => false, + 'no_useless_else' => false, + 'ordered_imports' => true, + 'phpdoc_add_missing_param_annotation' => true, + 'phpdoc_order' => true, + 'phpdoc_trim_consecutive_blank_line_separation' => true, + 'phpdoc_types_order' => true, + 'return_assignment' => true, + 'string_line_ending' => true, + 'strict_param' => true, + 'strict_comparison' => true, + ]) + ->setFinder($finder); -return Symfony\CS\Config\Config::create() - ->finder($finder) - ->fixers($fixers) - ->level(Symfony\CS\FixerInterface::PSR2_LEVEL) - ->setUsingCache(true); +return $config; \ No newline at end of file diff --git a/composer.json b/composer.json index c0b7903f..4339c203 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ } ], "require": { - "php": "^7.0" + "php": "^7.2" }, "require-dev": { "phpunit/phpunit": "^6.4|^7.0" diff --git a/src/Cron/AbstractField.php b/src/Cron/AbstractField.php index 8b1072ab..10acef55 100644 --- a/src/Cron/AbstractField.php +++ b/src/Cron/AbstractField.php @@ -1,38 +1,44 @@ isIncrementsOfRanges($value)) { return $this->isInIncrementsOfRanges($dateValue, $value); - } elseif ($this->isRange($value)) { + } + + if ($this->isRange($value)) { return $this->isInRange($dateValue, $value); } - return $value == '*' || $dateValue == $value; + return '*' === $value || $dateValue === (int) $value; } /** - * Check if a value is a range + * Check if a value is a range. * * @param string $value Value to test * * @return bool */ - public function isRange($value) + public function isRange(string $value): bool { - return strpos($value, '-') !== false; + return false !== strpos($value, '-'); } /** - * Check if a value is an increments of ranges + * Check if a value is an increments of ranges. * * @param string $value Value to test * * @return bool */ - public function isIncrementsOfRanges($value) + public function isIncrementsOfRanges(string $value): bool { - return strpos($value, '/') !== false; + return false !== strpos($value, '/'); } /** - * Test if a value is within a range + * Test if a value is within a range. * - * @param string $dateValue Set date value - * @param string $value Value to test + * @param int $dateValue Set date value + * @param string $value Value to test * * @return bool */ - public function isInRange($dateValue, $value) + public function isInRange(int $dateValue, $value): bool { - $parts = array_map(function($value) { - $value = trim($value); - $value = $this->convertLiterals($value); - return $value; - }, + $parts = array_map(function ($value) { + $value = trim($value); + + return $this->convertLiterals($value); + }, explode('-', $value, 2) ); - return $dateValue >= $parts[0] && $dateValue <= $parts[1]; } /** - * Test if a value is within an increments of ranges (offset[-to]/step size) + * Test if a value is within an increments of ranges (offset[-to]/step size). * * @param string $dateValue Set date value - * @param string $value Value to test + * @param string $value Value to test * * @return bool */ - public function isInIncrementsOfRanges($dateValue, $value) + public function isInIncrementsOfRanges(int $dateValue, string $value): bool { $chunks = array_map('trim', explode('/', $value, 2)); $range = $chunks[0]; - $step = isset($chunks[1]) ? $chunks[1] : 0; + $step = $chunks[1] ?? 0; // No step or 0 steps aren't cool - if (is_null($step) || '0' === $step || 0 === $step) { + if (null === $step || '0' === $step || 0 === $step) { return false; } // Expand the * to a full range - if ('*' == $range) { + if ('*' === $range) { $range = $this->rangeStart . '-' . $this->rangeEnd; } // Generate the requested small range $rangeChunks = explode('-', $range, 2); $rangeStart = $rangeChunks[0]; - $rangeEnd = isset($rangeChunks[1]) ? $rangeChunks[1] : $rangeStart; + $rangeEnd = $rangeChunks[1] ?? $rangeStart; if ($rangeStart < $this->rangeStart || $rangeStart > $this->rangeEnd || $rangeStart > $rangeEnd) { throw new \OutOfRangeException('Invalid range start requested'); @@ -143,80 +150,80 @@ public function isInIncrementsOfRanges($dateValue, $value) // Steps larger than the range need to wrap around and be handled slightly differently than smaller steps if ($step >= $this->rangeEnd) { - $thisRange = [$this->fullRange[$step % count($this->fullRange)]]; + $thisRange = [$this->fullRange[$step % \count($this->fullRange)]]; } else { $thisRange = range($rangeStart, $rangeEnd, $step); } - return in_array($dateValue, $thisRange); + return \in_array($dateValue, $thisRange, true); } /** - * Returns a range of values for the given cron expression + * Returns a range of values for the given cron expression. * * @param string $expression The expression to evaluate - * @param int $max Maximum offset for range + * @param int $max Maximum offset for range * * @return array */ - public function getRangeForExpression($expression, $max) + public function getRangeForExpression(string $expression, int $max): array { - $values = array(); + $values = []; $expression = $this->convertLiterals($expression); - if (strpos($expression, ',') !== false) { + if (false !== strpos($expression, ',')) { $ranges = explode(',', $expression); $values = []; foreach ($ranges as $range) { $expanded = $this->getRangeForExpression($range, $this->rangeEnd); $values = array_merge($values, $expanded); } + return $values; } if ($this->isRange($expression) || $this->isIncrementsOfRanges($expression)) { if (!$this->isIncrementsOfRanges($expression)) { - list ($offset, $to) = explode('-', $expression); + [$offset, $to] = explode('-', $expression); $offset = $this->convertLiterals($offset); $to = $this->convertLiterals($to); $stepSize = 1; - } - else { + } else { $range = array_map('trim', explode('/', $expression, 2)); - $stepSize = isset($range[1]) ? $range[1] : 0; + $stepSize = $range[1] ?? 0; $range = $range[0]; $range = explode('-', $range, 2); $offset = $range[0]; - $to = isset($range[1]) ? $range[1] : $max; + $to = $range[1] ?? $max; } - $offset = $offset == '*' ? $this->rangeStart : $offset; + $offset = '*' === $offset ? $this->rangeStart : $offset; if ($stepSize >= $this->rangeEnd) { - $values = [$this->fullRange[$stepSize % count($this->fullRange)]]; + $values = [$this->fullRange[$stepSize % \count($this->fullRange)]]; } else { for ($i = $offset; $i <= $to; $i += $stepSize) { - $values[] = (int)$i; + $values[] = (int) $i; } } sort($values); - } - else { - $values = array($expression); + } else { + $values = [$expression]; } return $values; } /** - * Convert literal + * Convert literal. * * @param string $value + * * @return string */ - protected function convertLiterals($value) + protected function convertLiterals(string $value): string { - if (count($this->literals)) { - $key = array_search($value, $this->literals); - if ($key !== false) { + if (\count($this->literals)) { + $key = array_search($value, $this->literals, true); + if (false !== $key) { return (string) $key; } } @@ -225,12 +232,13 @@ protected function convertLiterals($value) } /** - * Checks to see if a value is valid for the field + * Checks to see if a value is valid for the field. * * @param string $value + * * @return bool */ - public function validate($value) + public function validate(string $value): bool { $value = $this->convertLiterals($value); @@ -239,22 +247,24 @@ public function validate($value) return true; } - if (strpos($value, '/') !== false) { - list($range, $step) = explode('/', $value); + if (false !== strpos($value, '/')) { + [$range, $step] = explode('/', $value); + return $this->validate($range) && filter_var($step, FILTER_VALIDATE_INT); } // Validate each chunk of a list individually - if (strpos($value, ',') !== false) { + if (false !== strpos($value, ',')) { foreach (explode(',', $value) as $listItem) { if (!$this->validate($listItem)) { return false; } } + return true; } - if (strpos($value, '-') !== false) { + if (false !== strpos($value, '-')) { if (substr_count($value, '-') > 1) { return false; } @@ -263,7 +273,7 @@ public function validate($value) $chunks[0] = $this->convertLiterals($chunks[0]); $chunks[1] = $this->convertLiterals($chunks[1]); - if ('*' == $chunks[0] || '*' == $chunks[1]) { + if ('*' === $chunks[0] || '*' === $chunks[1]) { return false; } @@ -274,13 +284,13 @@ public function validate($value) return false; } - if (is_float($value) || strpos($value, '.') !== false) { + if (\is_float($value) || false !== strpos($value, '.')) { return false; } // We should have a numeric by now, so coerce this into an integer $value = (int) $value; - return in_array($value, $this->fullRange, true); + return \in_array($value, $this->fullRange, true); } } diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index 82c0ef1c..28db302b 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -1,5 +1,7 @@ '0 0 1 1 *', '@annually' => '0 0 1 1 *', '@monthly' => '0 0 1 * *', '@weekly' => '0 0 * * 0', '@daily' => '0 0 * * *', - '@hourly' => '0 * * * *' - ); + '@hourly' => '0 * * * *', + ]; if (isset($mappings[$expression])) { $expression = $mappings[$expression]; @@ -88,12 +90,13 @@ public static function factory($expression, FieldFactory $fieldFactory = null) /** * Validate a CronExpression. * - * @param string $expression The CRON expression to validate. + * @param string $expression the CRON expression to validate * * @return bool True if a valid CRON expression was passed. False if not. + * * @see \Cron\CronExpression::factory */ - public static function isValidExpression($expression) + public static function isValidExpression(string $expression): bool { try { self::factory($expression); @@ -105,29 +108,30 @@ public static function isValidExpression($expression) } /** - * Parse a CRON expression + * Parse a CRON expression. * - * @param string $expression CRON expression (e.g. '8 * * * *') - * @param FieldFactory|null $fieldFactory Factory to create cron fields + * @param string $expression CRON expression (e.g. '8 * * * *') + * @param null|FieldFactory $fieldFactory Factory to create cron fields */ - public function __construct($expression, FieldFactory $fieldFactory = null) + public function __construct(string $expression, FieldFactory $fieldFactory = null) { $this->fieldFactory = $fieldFactory; $this->setExpression($expression); } /** - * Set or change the CRON expression + * Set or change the CRON expression. * * @param string $value CRON expression (e.g. 8 * * * *) * - * @return CronExpression * @throws \InvalidArgumentException if not a valid CRON expression + * + * @return CronExpression */ - public function setExpression($value) + public function setExpression(string $value): CronExpression { $this->cronParts = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY); - if (count($this->cronParts) < 5) { + if (\count($this->cronParts) < 5) { throw new InvalidArgumentException( $value . ' is not a valid CRON expression' ); @@ -141,15 +145,16 @@ public function setExpression($value) } /** - * Set part of the CRON expression + * Set part of the CRON expression. * - * @param int $position The position of the CRON expression to set - * @param string $value The value to set + * @param int $position The position of the CRON expression to set + * @param string $value The value to set * - * @return CronExpression * @throws \InvalidArgumentException if the value is not valid for the part + * + * @return CronExpression */ - public function setPart($position, $value) + public function setPart(int $position, string $value): CronExpression { if (!$this->fieldFactory->getField($position)->validate($value)) { throw new InvalidArgumentException( @@ -163,13 +168,13 @@ public function setPart($position, $value) } /** - * Set max iteration count for searching next run dates + * Set max iteration count for searching next run dates. * * @param int $maxIterationCount Max iteration count when searching for next run date * * @return CronExpression */ - public function setMaxIterationCount($maxIterationCount) + public function setMaxIterationCount(int $maxIterationCount): CronExpression { $this->maxIterationCount = $maxIterationCount; @@ -177,61 +182,69 @@ public function setMaxIterationCount($maxIterationCount) } /** - * Get a next run date relative to the current date or a specific date - * - * @param string|\DateTime $currentTime Relative calculation date - * @param int $nth Number of matches to skip before returning a - * matching next run date. 0, the default, will return the current - * date and time if the next run date falls on the current date and - * time. Setting this value to 1 will skip the first match and go to - * the second match. Setting this value to 2 will skip the first 2 - * matches and so on. - * @param bool $allowCurrentDate Set to TRUE to return the current date if - * it matches the cron expression. - * @param null|string $timeZone TimeZone to use instead of the system default + * Get a next run date relative to the current date or a specific date. + * + * @param \DateTime|string $currentTime Relative calculation date + * @param int $nth Number of matches to skip before returning a + * matching next run date. 0, the default, will return the current + * date and time if the next run date falls on the current date and + * time. Setting this value to 1 will skip the first match and go to + * the second match. Setting this value to 2 will skip the first 2 + * matches and so on. + * @param bool $allowCurrentDate set to TRUE to return the current date if + * it matches the cron expression + * @param null|string $timeZone TimeZone to use instead of the system default * - * @return \DateTime * @throws \RuntimeException on too many iterations + * @throws \Exception + * + * @return \DateTime */ - public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timeZone = null) + public function getNextRunDate($currentTime = 'now', int $nth = 0, bool $allowCurrentDate = false, $timeZone = null): DateTime { return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate, $timeZone); } /** - * Get a previous run date relative to the current date or a specific date + * Get a previous run date relative to the current date or a specific date. * - * @param string|\DateTime $currentTime Relative calculation date - * @param int $nth Number of matches to skip before returning - * @param bool $allowCurrentDate Set to TRUE to return the - * current date if it matches the cron expression - * @param null|string $timeZone TimeZone to use instead of the system default + * @param \DateTime|string $currentTime Relative calculation date + * @param int $nth Number of matches to skip before returning + * @param bool $allowCurrentDate Set to TRUE to return the + * current date if it matches the cron expression + * @param null|string $timeZone TimeZone to use instead of the system default * - * @return \DateTime * @throws \RuntimeException on too many iterations + * @throws \Exception + * + * @return \DateTime + * * @see \Cron\CronExpression::getNextRunDate */ - public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timeZone = null) + public function getPreviousRunDate($currentTime = 'now', int $nth = 0, bool $allowCurrentDate = false, $timeZone = null): DateTime { return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate, $timeZone); } /** - * Get multiple run dates starting at the current date or a specific date + * Get multiple run dates starting at the current date or a specific date. * - * @param int $total Set the total number of dates to calculate - * @param string|\DateTime $currentTime Relative calculation date - * @param bool $invert Set to TRUE to retrieve previous dates - * @param bool $allowCurrentDate Set to TRUE to return the - * current date if it matches the cron expression - * @param null|string $timeZone TimeZone to use instead of the system default + * @param int $total Set the total number of dates to calculate + * @param \DateTime|string $currentTime Relative calculation date + * @param bool $invert Set to TRUE to retrieve previous dates + * @param bool $allowCurrentDate Set to TRUE to return the + * current date if it matches the cron expression + * @param null|string $timeZone TimeZone to use instead of the system default + * + * @throws Exception * * @return array Returns an array of run dates */ - public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false, $timeZone = null) + public function getMultipleRunDates(int $total, $currentTime = 'now', bool $invert = false, bool $allowCurrentDate = false, $timeZone = null): array { - $matches = array(); - for ($i = 0; $i < max(0, $total); $i++) { + $matches = []; + $max = max(0, $total); + for ($i = 0; $i < $max; ++$i) { try { $matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate, $timeZone); } catch (RuntimeException $e) { @@ -243,19 +256,21 @@ public function getMultipleRunDates($total, $currentTime = 'now', $invert = fals } /** - * Get all or part of the CRON expression + * Get all or part of the CRON expression. * - * @param string $part Specify the part to retrieve or NULL to get the full - * cron schedule string. + * @param string $part specify the part to retrieve or NULL to get the full + * cron schedule string * - * @return string|null Returns the CRON expression, a part of the + * @return null|string Returns the CRON expression, a part of the * CRON expression, or NULL if the part was specified but not found */ - public function getExpression($part = null) + public function getExpression($part = null): ?string { if (null === $part) { return implode(' ', $this->cronParts); - } elseif (array_key_exists($part, $this->cronParts)) { + } + + if (array_key_exists($part, $this->cronParts)) { return $this->cronParts[$part]; } @@ -267,9 +282,9 @@ public function getExpression($part = null) * * @return string Full CRON expression */ - public function __toString() + public function __toString(): string { - return $this->getExpression(); + return (string) $this->getExpression(); } /** @@ -277,12 +292,14 @@ public function __toString() * specific date. This method assumes that the current number of * seconds are irrelevant, and should be called once per minute. * - * @param string|\DateTime $currentTime Relative calculation date - * @param null|string $timeZone TimeZone to use instead of the system default + * @param \DateTime|string $currentTime Relative calculation date + * @param null|string $timeZone TimeZone to use instead of the system default + * + * @throws Exception * * @return bool Returns TRUE if the cron is due to run or FALSE if not */ - public function isDue($currentTime = 'now', $timeZone = null) + public function isDue($currentTime = 'now', $timeZone = null): ?bool { $timeZone = $this->determineTimeZone($currentTime, $timeZone); @@ -295,7 +312,7 @@ public function isDue($currentTime = 'now', $timeZone = null) } else { $currentTime = new DateTime($currentTime); } - $currentTime->setTimeZone(new DateTimeZone($timeZone)); + $currentTime->setTimezone(new DateTimeZone($timeZone)); // drop the seconds to 0 $currentTime = DateTime::createFromFormat('Y-m-d H:i', $currentTime->format('Y-m-d H:i')); @@ -308,19 +325,21 @@ public function isDue($currentTime = 'now', $timeZone = null) } /** - * Get the next or previous run date of the expression relative to a date + * Get the next or previous run date of the expression relative to a date. * - * @param string|\DateTime $currentTime Relative calculation date - * @param int $nth Number of matches to skip before returning - * @param bool $invert Set to TRUE to go backwards in time - * @param bool $allowCurrentDate Set to TRUE to return the - * current date if it matches the cron expression - * @param string|null $timeZone TimeZone to use instead of the system default + * @param \DateTime|string $currentTime Relative calculation date + * @param int $nth Number of matches to skip before returning + * @param bool $invert Set to TRUE to go backwards in time + * @param bool $allowCurrentDate Set to TRUE to return the + * current date if it matches the cron expression + * @param null|string $timeZone TimeZone to use instead of the system default * - * @return \DateTime * @throws \RuntimeException on too many iterations + * @throws Exception + * + * @return \DateTime */ - protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $allowCurrentDate = false, $timeZone = null) + protected function getRunDate($currentTime = null, int $nth = 0, bool $invert = false, bool $allowCurrentDate = false, $timeZone = null): DateTime { $timeZone = $this->determineTimeZone($currentTime, $timeZone); @@ -332,14 +351,14 @@ protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $a $currentDate = new DateTime($currentTime ?: 'now'); } - $currentDate->setTimeZone(new DateTimeZone($timeZone)); - $currentDate->setTime($currentDate->format('H'), $currentDate->format('i'), 0); + $currentDate->setTimezone(new DateTimeZone($timeZone)); + $currentDate->setTime((int) $currentDate->format('H'), (int) $currentDate->format('i'), 0); + $nextRun = clone $currentDate; - $nth = (int) $nth; // We don't have to satisfy * or null fields - $parts = array(); - $fields = array(); + $parts = []; + $fields = []; foreach (self::$order as $position) { $part = $this->getExpression($position); if (null === $part || '*' === $part) { @@ -350,19 +369,19 @@ protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $a } // Set a hard limit to bail on an impossible date - for ($i = 0; $i < $this->maxIterationCount; $i++) { - + for ($i = 0; $i < $this->maxIterationCount; ++$i) { foreach ($parts as $position => $part) { $satisfied = false; // Get the field object used to validate this part $field = $fields[$position]; // Check if this is singular or a list - if (strpos($part, ',') === false) { + if (false === strpos($part, ',')) { $satisfied = $field->isSatisfiedBy($nextRun, $part); } else { foreach (array_map('trim', explode(',', $part)) as $listPart) { if ($field->isSatisfiedBy($nextRun, $listPart)) { $satisfied = true; + break; } } @@ -371,13 +390,15 @@ protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $a // If the field is not satisfied, then start over if (!$satisfied) { $field->increment($nextRun, $invert, $part); + continue 2; } } // Skip this match if needed if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) { - $this->fieldFactory->getField(0)->increment($nextRun, $invert, isset($parts[0]) ? $parts[0] : null); + $this->fieldFactory->getField(0)->increment($nextRun, $invert, $parts[0] ?? null); + continue; } @@ -392,19 +413,19 @@ protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $a /** * Workout what timeZone should be used. * - * @param string|\DateTimeInterface $currentTime Relative calculation date - * @param string|null $timeZone TimeZone to use instead of the system default + * @param \DateTimeInterface|string $currentTime Relative calculation date + * @param null|string $timeZone TimeZone to use instead of the system default * * @return string */ - protected function determineTimeZone($currentTime, $timeZone) + protected function determineTimeZone($currentTime, $timeZone): string { - if (! is_null($timeZone)) { + if (null !== $timeZone) { return $timeZone; } - if ($currentTime instanceOf DateTimeInterface) { - return $currentTime->getTimeZone()->getName(); + if ($currentTime instanceof DateTimeInterface) { + return $currentTime->getTimezone()->getName(); } return date_default_timezone_get(); diff --git a/src/Cron/DayOfMonthField.php b/src/Cron/DayOfMonthField.php index 3920cc9e..c6e222b6 100644 --- a/src/Cron/DayOfMonthField.php +++ b/src/Cron/DayOfMonthField.php @@ -1,11 +1,13 @@ format('N'); if ($currentWeekday < 6) { @@ -54,12 +56,12 @@ private static function getNearestWeekday($currentYear, $currentMonth, $targetDa } $lastDayOfMonth = $target->format('t'); - - foreach (array(-1, 1, -2, 2) as $i) { + foreach ([-1, 1, -2, 2] as $i) { $adjusted = $targetDay + $i; if ($adjusted > 0 && $adjusted <= $lastDayOfMonth) { $target->setDate($currentYear, $currentMonth, $adjusted); - if ($target->format('N') < 6 && $target->format('m') == $currentMonth) { + + if ((int) $target->format('N') < 6 && (int) $target->format('m') === $currentMonth) { return $target; } } @@ -67,41 +69,41 @@ private static function getNearestWeekday($currentYear, $currentMonth, $targetDa } /** - * @inheritDoc + * {@inheritdoc} */ - public function isSatisfiedBy(DateTime $date, $value) + public function isSatisfiedBy(DateTime $date, string $value): bool { // ? states that the field value is to be skipped - if ($value == '?') { + if ('?' === $value) { return true; } $fieldValue = $date->format('d'); // Check to see if this is the last day of the month - if ($value == 'L') { - return $fieldValue == $date->format('t'); + if ('L' === $value) { + return $fieldValue === $date->format('t'); } // Check to see if this is the nearest weekday to a particular value if (strpos($value, 'W')) { // Parse the target day - $targetDay = substr($value, 0, strpos($value, 'W')); + $targetDay = (int) substr($value, 0, strpos($value, 'W')); // Find out if the current day is the nearest day of the week - return $date->format('j') == self::getNearestWeekday( - $date->format('Y'), - $date->format('m'), + return $date->format('j') === self::getNearestWeekday( + (int) $date->format('Y'), + (int) $date->format('m'), $targetDay )->format('j'); } - return $this->isSatisfied($date->format('d'), $value); + return $this->isSatisfied((int) $date->format('d'), $value); } /** - * @inheritDoc + * {@inheritdoc} */ - public function increment(DateTime $date, $invert = false) + public function increment(DateTime $date, bool $invert = false, ?string $parts = null): FieldInterface { if ($invert) { $date->modify('previous day'); @@ -115,20 +117,19 @@ public function increment(DateTime $date, $invert = false) } /** - * @inheritDoc + * {@inheritdoc} */ - public function validate($value) + public function validate(string $value): bool { $basicChecks = parent::validate($value); // Validate that a list don't have W or L - if (strpos($value, ',') !== false && (strpos($value, 'W') !== false || strpos($value, 'L') !== false)) { + if (false !== strpos($value, ',') && (false !== strpos($value, 'W') || false !== strpos($value, 'L'))) { return false; } if (!$basicChecks) { - - if ($value === 'L') { + if ('L' === $value) { return true; } diff --git a/src/Cron/DayOfWeekField.php b/src/Cron/DayOfWeekField.php index 96fdea3c..e8fade4e 100644 --- a/src/Cron/DayOfWeekField.php +++ b/src/Cron/DayOfWeekField.php @@ -1,12 +1,14 @@ 'MON', 2 => 'TUE', 3 => 'WED', 4 => 'THU', 5 => 'FRI', 6 => 'SAT', 7 => 'SUN']; /** - * Constructor + * Constructor. */ public function __construct() { @@ -50,41 +52,42 @@ public function __construct() } /** - * @inheritDoc + * {@inheritdoc} */ - public function isSatisfiedBy(DateTime $date, $value) + public function isSatisfiedBy(DateTime $date, string $value): bool { - if ($value == '?') { + if ('?' === $value) { return true; } // Convert text day of the week values to integers $value = $this->convertLiterals($value); - $currentYear = $date->format('Y'); - $currentMonth = $date->format('m'); - $lastDayOfMonth = $date->format('t'); + $currentYear = (int) $date->format('Y'); + $currentMonth = (int) $date->format('m'); + $lastDayOfMonth = (int) $date->format('t'); // Find out if this is the last specific weekday of the month if (strpos($value, 'L')) { - $weekday = $this->convertLiterals(substr($value, 0, strpos($value, 'L'))); - $weekday = str_replace('7', '0', $weekday); + $weekday = (int) $this->convertLiterals(substr($value, 0, strpos($value, 'L'))); + $weekday = (int) str_replace(7, 0, $weekday); $tdate = clone $date; $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth); - while ($tdate->format('w') != $weekday) { + + while ((int) $tdate->format('w') !== $weekday) { $tdateClone = new DateTime(); $tdate = $tdateClone ->setTimezone($tdate->getTimezone()) ->setDate($currentYear, $currentMonth, --$lastDayOfMonth); } - return $date->format('j') == $lastDayOfMonth; + return (int) $date->format('j') === $lastDayOfMonth; } // Handle # hash tokens if (strpos($value, '#')) { - list($weekday, $nth) = explode('#', $value); + [$weekday, $nth] = explode('#', $value); if (!is_numeric($nth)) { throw new InvalidArgumentException("Hashed weekdays must be numeric, {$nth} given"); @@ -93,23 +96,23 @@ public function isSatisfiedBy(DateTime $date, $value) } // 0 and 7 are both Sunday, however 7 matches date('N') format ISO-8601 - if ($weekday === '0') { + if ('0' === $weekday) { $weekday = 7; } - $weekday = $this->convertLiterals($weekday); + $weekday = (int) $this->convertLiterals((string) $weekday); // Validate the hash fields if ($weekday < 0 || $weekday > 7) { throw new InvalidArgumentException("Weekday must be a value between 0 and 7. {$weekday} given"); } - if (!in_array($nth, $this->nthRange)) { + if (!\in_array($nth, $this->nthRange, true)) { throw new InvalidArgumentException("There are never more than 5 or less than 1 of a given weekday in a month, {$nth} given"); } // The current weekday must match the targeted weekday to proceed - if ($date->format('N') != $weekday) { + if ((int) $date->format('N') !== $weekday) { return false; } @@ -118,7 +121,7 @@ public function isSatisfiedBy(DateTime $date, $value) $dayCount = 0; $currentDay = 1; while ($currentDay < $lastDayOfMonth + 1) { - if ($tdate->format('N') == $weekday) { + if ((int) $tdate->format('N') === $weekday) { if (++$dayCount >= $nth) { break; } @@ -126,31 +129,33 @@ public function isSatisfiedBy(DateTime $date, $value) $tdate->setDate($currentYear, $currentMonth, ++$currentDay); } - return $date->format('j') == $currentDay; + return (int) $date->format('j') === $currentDay; } // Handle day of the week values - if (strpos($value, '-')) { + if (false !== strpos($value, '-')) { $parts = explode('-', $value); - if ($parts[0] == '7') { - $parts[0] = '0'; - } elseif ($parts[1] == '0') { - $parts[1] = '7'; + if ('7' === $parts[0]) { + $parts[0] = 0; + } elseif ('0' === $parts[1]) { + $parts[1] = 7; } $value = implode('-', $parts); } // Test to see which Sunday to use -- 0 == 7 == Sunday - $format = in_array(7, str_split($value)) ? 'N' : 'w'; - $fieldValue = $date->format($format); + $format = \in_array(7, array_map(function ($value) { + return (int) $value; + }, str_split($value)), true) ? 'N' : 'w'; + $fieldValue = (int) $date->format($format); return $this->isSatisfied($fieldValue, $value); } /** - * @inheritDoc + * {@inheritdoc} */ - public function increment(DateTime $date, $invert = false) + public function increment(DateTime $date, bool $invert = false, ?string $parts = null): FieldInterface { if ($invert) { $date->modify('-1 day'); @@ -164,19 +169,19 @@ public function increment(DateTime $date, $invert = false) } /** - * @inheritDoc + * {@inheritdoc} */ - public function validate($value) + public function validate(string $value): bool { $basicChecks = parent::validate($value); if (!$basicChecks) { // Handle the # value - if (strpos($value, '#') !== false) { + if (false !== strpos($value, '#')) { $chunks = explode('#', $value); $chunks[0] = $this->convertLiterals($chunks[0]); - if (parent::validate($chunks[0]) && is_numeric($chunks[1]) && in_array($chunks[1], $this->nthRange)) { + if (parent::validate($chunks[0]) && is_numeric($chunks[1]) && \in_array((int) $chunks[1], $this->nthRange, true)) { return true; } } diff --git a/src/Cron/FieldFactory.php b/src/Cron/FieldFactory.php index 545e4b83..f30358d1 100644 --- a/src/Cron/FieldFactory.php +++ b/src/Cron/FieldFactory.php @@ -1,46 +1,55 @@ fields[$position])) { switch ($position) { case 0: $this->fields[$position] = new MinutesField(); + break; case 1: $this->fields[$position] = new HoursField(); + break; case 2: $this->fields[$position] = new DayOfMonthField(); + break; case 3: $this->fields[$position] = new MonthField(); + break; case 4: $this->fields[$position] = new DayOfWeekField(); + break; default: throw new InvalidArgumentException( diff --git a/src/Cron/FieldInterface.php b/src/Cron/FieldInterface.php index 72d2dd29..74e0e3df 100644 --- a/src/Cron/FieldInterface.php +++ b/src/Cron/FieldInterface.php @@ -1,41 +1,44 @@ isSatisfied($date->format('H'), $value); + return $this->isSatisfied((int) $date->format('H'), $value); } /** - * {@inheritDoc} + * {@inheritdoc} * - * @param string|null $parts + * @param null|string $parts */ - public function increment(DateTime $date, $invert = false, $parts = null) + public function increment(DateTime $date, bool $invert = false, ?string $parts = null): FieldInterface { // Change timezone to UTC temporarily. This will // allow us to go back or forwards and hour even // if DST will be changed between the hours. - if (is_null($parts) || $parts == '*') { + if (null === $parts || '*' === $parts) { $timezone = $date->getTimezone(); $date->setTimezone(new DateTimeZone('UTC')); if ($invert) { @@ -48,34 +50,36 @@ public function increment(DateTime $date, $invert = false, $parts = null) } $date->setTimezone($timezone); - $date->setTime($date->format('H'), $invert ? 59 : 0); + $date->setTime((int) $date->format('H'), $invert ? 59 : 0); + return $this; } - $parts = strpos($parts, ',') !== false ? explode(',', $parts) : array($parts); - $hours = array(); + $parts = false !== strpos($parts, ',') ? explode(',', $parts) : [$parts]; + $hours = []; foreach ($parts as $part) { $hours = array_merge($hours, $this->getRangeForExpression($part, 23)); } $current_hour = $date->format('H'); - $position = $invert ? count($hours) - 1 : 0; - if (count($hours) > 1) { - for ($i = 0; $i < count($hours) - 1; $i++) { + $position = $invert ? \count($hours) - 1 : 0; + $countHours = \count($hours); + if ($countHours > 1) { + for ($i = 0; $i < $countHours - 1; ++$i) { if ((!$invert && $current_hour >= $hours[$i] && $current_hour < $hours[$i + 1]) || ($invert && $current_hour > $hours[$i] && $current_hour <= $hours[$i + 1])) { $position = $invert ? $i : $i + 1; + break; } } } - $hour = $hours[$position]; - if ((!$invert && $date->format('H') >= $hour) || ($invert && $date->format('H') <= $hour)) { + $hour = (int) $hours[$position]; + if ((!$invert && (int) $date->format('H') >= $hour) || ($invert && (int) $date->format('H') <= $hour)) { $date->modify(($invert ? '-' : '+') . '1 day'); $date->setTime($invert ? 23 : 0, $invert ? 59 : 0); - } - else { + } else { $date->setTime($hour, $invert ? 59 : 0); } diff --git a/src/Cron/MinutesField.php b/src/Cron/MinutesField.php index e8aea7aa..e4f28e5f 100644 --- a/src/Cron/MinutesField.php +++ b/src/Cron/MinutesField.php @@ -1,61 +1,65 @@ isSatisfied($date->format('i'), $value); + return $this->isSatisfied((int) $date->format('i'), $value); } /** - * {@inheritDoc} + * {@inheritdoc} * - * @param string|null $parts + * @param null|string $parts */ - public function increment(DateTime $date, $invert = false, $parts = null) + public function increment(DateTime $date, bool $invert = false, ?string $parts = null): FieldInterface { - if (is_null($parts)) { + if (null === $parts) { if ($invert) { $date->modify('-1 minute'); } else { $date->modify('+1 minute'); } + return $this; } - $parts = strpos($parts, ',') !== false ? explode(',', $parts) : array($parts); - $minutes = array(); + $parts = false !== strpos($parts, ',') ? explode(',', $parts) : [$parts]; + $minutes = []; foreach ($parts as $part) { $minutes = array_merge($minutes, $this->getRangeForExpression($part, 59)); } $current_minute = $date->format('i'); - $position = $invert ? count($minutes) - 1 : 0; - if (count($minutes) > 1) { - for ($i = 0; $i < count($minutes) - 1; $i++) { + $position = $invert ? \count($minutes) - 1 : 0; + if (\count($minutes) > 1) { + for ($i = 0; $i < \count($minutes) - 1; ++$i) { if ((!$invert && $current_minute >= $minutes[$i] && $current_minute < $minutes[$i + 1]) || ($invert && $current_minute > $minutes[$i] && $current_minute <= $minutes[$i + 1])) { $position = $invert ? $i : $i + 1; + break; } } @@ -63,10 +67,9 @@ public function increment(DateTime $date, $invert = false, $parts = null) if ((!$invert && $current_minute >= $minutes[$position]) || ($invert && $current_minute <= $minutes[$position])) { $date->modify(($invert ? '-' : '+') . '1 hour'); - $date->setTime($date->format('H'), $invert ? 59 : 0); - } - else { - $date->setTime($date->format('H'), $minutes[$position]); + $date->setTime((int) $date->format('H'), $invert ? 59 : 0); + } else { + $date->setTime((int) $date->format('H'), (int) $minutes[$position]); } return $this; diff --git a/src/Cron/MonthField.php b/src/Cron/MonthField.php index 238b0e7c..512758b9 100644 --- a/src/Cron/MonthField.php +++ b/src/Cron/MonthField.php @@ -1,44 +1,46 @@ 'JAN', 2 => 'FEB', 3 => 'MAR', 4 => 'APR', 5 => 'MAY', 6 => 'JUN', 7 => 'JUL', - 8 => 'AUG', 9 => 'SEP', 10 => 'OCT', 11 => 'NOV', 12 => 'DEC']; + 8 => 'AUG', 9 => 'SEP', 10 => 'OCT', 11 => 'NOV', 12 => 'DEC', ]; /** - * @inheritDoc + * {@inheritdoc} */ - public function isSatisfiedBy(DateTime $date, $value) + public function isSatisfiedBy(DateTime $date, string $value): bool { $value = $this->convertLiterals($value); - return $this->isSatisfied($date->format('m'), $value); + return $this->isSatisfied((int) $date->format('m'), $value); } /** - * @inheritDoc + * {@inheritdoc} */ - public function increment(DateTime $date, $invert = false) + public function increment(DateTime $date, bool $invert = false, ?string $parts = null): FieldInterface { if ($invert) { $date->modify('last day of previous month'); @@ -50,6 +52,4 @@ public function increment(DateTime $date, $invert = false) return $this; } - - } diff --git a/tests/Cron/AbstractFieldTest.php b/tests/Cron/AbstractFieldTest.php index 38114392..de0784d8 100644 --- a/tests/Cron/AbstractFieldTest.php +++ b/tests/Cron/AbstractFieldTest.php @@ -1,5 +1,7 @@ assertTrue($f->isRange('1-2')); @@ -26,7 +28,7 @@ public function testTestsIfRange() /** * @covers \Cron\AbstractField::isIncrementsOfRanges */ - public function testTestsIfIncrementsOfRanges() + public function testTestsIfIncrementsOfRanges(): void { $f = new DayOfWeekField(); $this->assertFalse($f->isIncrementsOfRanges('1-2')); @@ -38,85 +40,85 @@ public function testTestsIfIncrementsOfRanges() /** * @covers \Cron\AbstractField::isInRange */ - public function testTestsIfInRange() + public function testTestsIfInRange(): void { $f = new DayOfWeekField(); - $this->assertTrue($f->isInRange('1', '1-2')); - $this->assertTrue($f->isInRange('2', '1-2')); - $this->assertTrue($f->isInRange('5', '4-12')); - $this->assertFalse($f->isInRange('3', '4-12')); - $this->assertFalse($f->isInRange('13', '4-12')); + $this->assertTrue($f->isInRange(1, '1-2')); + $this->assertTrue($f->isInRange(2, '1-2')); + $this->assertTrue($f->isInRange(5, '4-12')); + $this->assertFalse($f->isInRange(3, '4-12')); + $this->assertFalse($f->isInRange(13, '4-12')); } /** * @covers \Cron\AbstractField::isInIncrementsOfRanges */ - public function testTestsIfInIncrementsOfRangesOnZeroStartRange() + public function testTestsIfInIncrementsOfRangesOnZeroStartRange(): void { $f = new MinutesField(); - $this->assertTrue($f->isInIncrementsOfRanges('3', '3-59/2')); - $this->assertTrue($f->isInIncrementsOfRanges('13', '3-59/2')); - $this->assertTrue($f->isInIncrementsOfRanges('15', '3-59/2')); - $this->assertTrue($f->isInIncrementsOfRanges('14', '*/2')); - $this->assertFalse($f->isInIncrementsOfRanges('2', '3-59/13')); - $this->assertFalse($f->isInIncrementsOfRanges('14', '*/13')); - $this->assertFalse($f->isInIncrementsOfRanges('14', '3-59/2')); - $this->assertFalse($f->isInIncrementsOfRanges('3', '2-59')); - $this->assertFalse($f->isInIncrementsOfRanges('3', '2')); - $this->assertFalse($f->isInIncrementsOfRanges('3', '*')); - $this->assertFalse($f->isInIncrementsOfRanges('0', '*/0')); - $this->assertFalse($f->isInIncrementsOfRanges('1', '*/0')); + $this->assertTrue($f->isInIncrementsOfRanges(3, '3-59/2')); + $this->assertTrue($f->isInIncrementsOfRanges(13, '3-59/2')); + $this->assertTrue($f->isInIncrementsOfRanges(15, '3-59/2')); + $this->assertTrue($f->isInIncrementsOfRanges(14, '*/2')); + $this->assertFalse($f->isInIncrementsOfRanges(2, '3-59/13')); + $this->assertFalse($f->isInIncrementsOfRanges(14, '*/13')); + $this->assertFalse($f->isInIncrementsOfRanges(14, '3-59/2')); + $this->assertFalse($f->isInIncrementsOfRanges(3, '2-59')); + $this->assertFalse($f->isInIncrementsOfRanges(3, '2')); + $this->assertFalse($f->isInIncrementsOfRanges(3, '*')); + $this->assertFalse($f->isInIncrementsOfRanges(0, '*/0')); + $this->assertFalse($f->isInIncrementsOfRanges(1, '*/0')); - $this->assertTrue($f->isInIncrementsOfRanges('4', '4/1')); - $this->assertFalse($f->isInIncrementsOfRanges('14', '4/1')); - $this->assertFalse($f->isInIncrementsOfRanges('34', '4/1')); + $this->assertTrue($f->isInIncrementsOfRanges(4, '4/1')); + $this->assertFalse($f->isInIncrementsOfRanges(14, '4/1')); + $this->assertFalse($f->isInIncrementsOfRanges(34, '4/1')); } /** * @covers \Cron\AbstractField::isInIncrementsOfRanges */ - public function testTestsIfInIncrementsOfRangesOnOneStartRange() + public function testTestsIfInIncrementsOfRangesOnOneStartRange(): void { $f = new MonthField(); - $this->assertTrue($f->isInIncrementsOfRanges('3', '3-12/2')); - $this->assertFalse($f->isInIncrementsOfRanges('13', '3-12/2')); - $this->assertFalse($f->isInIncrementsOfRanges('15', '3-12/2')); - $this->assertTrue($f->isInIncrementsOfRanges('3', '*/2')); - $this->assertFalse($f->isInIncrementsOfRanges('3', '*/3')); - $this->assertTrue($f->isInIncrementsOfRanges('7', '*/3')); - $this->assertFalse($f->isInIncrementsOfRanges('14', '3-12/2')); - $this->assertFalse($f->isInIncrementsOfRanges('3', '2-12')); - $this->assertFalse($f->isInIncrementsOfRanges('3', '2')); - $this->assertFalse($f->isInIncrementsOfRanges('3', '*')); - $this->assertFalse($f->isInIncrementsOfRanges('0', '*/0')); - $this->assertFalse($f->isInIncrementsOfRanges('1', '*/0')); + $this->assertTrue($f->isInIncrementsOfRanges(3, '3-12/2')); + $this->assertFalse($f->isInIncrementsOfRanges(13, '3-12/2')); + $this->assertFalse($f->isInIncrementsOfRanges(15, '3-12/2')); + $this->assertTrue($f->isInIncrementsOfRanges(3, '*/2')); + $this->assertFalse($f->isInIncrementsOfRanges(3, '*/3')); + $this->assertTrue($f->isInIncrementsOfRanges(7, '*/3')); + $this->assertFalse($f->isInIncrementsOfRanges(14, '3-12/2')); + $this->assertFalse($f->isInIncrementsOfRanges(3, '2-12')); + $this->assertFalse($f->isInIncrementsOfRanges(3, '2')); + $this->assertFalse($f->isInIncrementsOfRanges(3, '*')); + $this->assertFalse($f->isInIncrementsOfRanges(0, '*/0')); + $this->assertFalse($f->isInIncrementsOfRanges(1, '*/0')); - $this->assertTrue($f->isInIncrementsOfRanges('4', '4/1')); - $this->assertFalse($f->isInIncrementsOfRanges('14', '4/1')); - $this->assertFalse($f->isInIncrementsOfRanges('34', '4/1')); + $this->assertTrue($f->isInIncrementsOfRanges(4, '4/1')); + $this->assertFalse($f->isInIncrementsOfRanges(14, '4/1')); + $this->assertFalse($f->isInIncrementsOfRanges(34, '4/1')); } /** * @covers \Cron\AbstractField::isSatisfied */ - public function testTestsIfSatisfied() + public function testTestsIfSatisfied(): void { $f = new DayOfWeekField(); - $this->assertTrue($f->isSatisfied('12', '3-13')); - $this->assertFalse($f->isSatisfied('15', '3-7/2')); - $this->assertTrue($f->isSatisfied('12', '*')); - $this->assertTrue($f->isSatisfied('12', '12')); - $this->assertFalse($f->isSatisfied('12', '3-11')); - $this->assertFalse($f->isSatisfied('12', '3-7/2')); - $this->assertFalse($f->isSatisfied('12', '11')); + $this->assertTrue($f->isSatisfied(12, '3-13')); + $this->assertFalse($f->isSatisfied(15, '3-7/2')); + $this->assertTrue($f->isSatisfied(12, '*')); + $this->assertTrue($f->isSatisfied(12, '12')); + $this->assertFalse($f->isSatisfied(12, '3-11')); + $this->assertFalse($f->isSatisfied(12, '3-7/2')); + $this->assertFalse($f->isSatisfied(12, '11')); } /** - * Allows ranges and lists to coexist in the same expression + * Allows ranges and lists to coexist in the same expression. * * @see https://github.com/dragonmantank/cron-expression/issues/5 */ - public function testAllowRangesAndLists() + public function testAllowRangesAndLists(): void { $expression = '5-7,11-13'; $f = new HoursField(); @@ -124,11 +126,11 @@ public function testAllowRangesAndLists() } /** - * Makes sure that various types of ranges expand out properly + * Makes sure that various types of ranges expand out properly. * * @see https://github.com/dragonmantank/cron-expression/issues/5 */ - public function testGetRangeForExpressionExpandsCorrectly() + public function testGetRangeForExpressionExpandsCorrectly(): void { $f = new HoursField(); $this->assertSame([5, 6, 7, 11, 12, 13], $f->getRangeForExpression('5-7,11-13', 23)); diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 678501be..bc9d4174 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -1,5 +1,7 @@ assertSame('0 0 1 1 *', CronExpression::factory('@annually')->getExpression()); $this->assertSame('0 0 1 1 *', CronExpression::factory('@yearly')->getExpression()); @@ -29,7 +31,7 @@ public function testFactoryRecognizesTemplates() * @covers \Cron\CronExpression::getExpression * @covers \Cron\CronExpression::__toString */ - public function testParsesCronSchedule() + public function testParsesCronSchedule(): void { // '2010-09-10 12:00:00' $cron = CronExpression::factory('1 2-4 * 4,5,6 */3'); @@ -50,7 +52,7 @@ public function testParsesCronSchedule() * @expectedException \InvalidArgumentException * @expectedExceptionMessage Invalid CRON field value A at position 0 */ - public function testParsesCronScheduleThrowsAnException() + public function testParsesCronScheduleThrowsAnException(): void { CronExpression::factory('A 1 2 3 4'); } @@ -59,8 +61,10 @@ public function testParsesCronScheduleThrowsAnException() * @covers \Cron\CronExpression::__construct * @covers \Cron\CronExpression::getExpression * @dataProvider scheduleWithDifferentSeparatorsProvider + * + * @param mixed $schedule */ - public function testParsesCronScheduleWithAnySpaceCharsAsSeparators($schedule, array $expected) + public function testParsesCronScheduleWithAnySpaceCharsAsSeparators($schedule, array $expected): void { $cron = CronExpression::factory($schedule); $this->assertSame($expected[0], $cron->getExpression(CronExpression::MINUTE)); @@ -71,27 +75,27 @@ public function testParsesCronScheduleWithAnySpaceCharsAsSeparators($schedule, a } /** - * Data provider for testParsesCronScheduleWithAnySpaceCharsAsSeparators + * Data provider for testParsesCronScheduleWithAnySpaceCharsAsSeparators. * * @return array */ - public static function scheduleWithDifferentSeparatorsProvider() + public static function scheduleWithDifferentSeparatorsProvider(): array { - return array( - array("*\t*\t*\t*\t*\t", array('*', '*', '*', '*', '*', '*')), - array("* * * * * ", array('*', '*', '*', '*', '*', '*')), - array("* \t * \t * \t * \t * \t", array('*', '*', '*', '*', '*', '*')), - array("*\t \t*\t \t*\t \t*\t \t*\t \t", array('*', '*', '*', '*', '*', '*')), - ); + return [ + ["*\t*\t*\t*\t*\t", ['*', '*', '*', '*', '*', '*']], + ['* * * * * ', ['*', '*', '*', '*', '*', '*']], + ["* \t * \t * \t * \t * \t", ['*', '*', '*', '*', '*', '*']], + ["*\t \t*\t \t*\t \t*\t \t*\t \t", ['*', '*', '*', '*', '*', '*']], + ]; } /** * @covers \Cron\CronExpression::__construct * @covers \Cron\CronExpression::setExpression * @covers \Cron\CronExpression::setPart - * @expectedException InvalidArgumentException + * @expectedException \InvalidArgumentException */ - public function testInvalidCronsWillFail() + public function testInvalidCronsWillFail(): void { // Only four values $cron = CronExpression::factory('* * * 1'); @@ -99,9 +103,9 @@ public function testInvalidCronsWillFail() /** * @covers \Cron\CronExpression::setPart - * @expectedException InvalidArgumentException + * @expectedException \InvalidArgumentException */ - public function testInvalidPartsWillFail() + public function testInvalidPartsWillFail(): void { // Only four values $cron = CronExpression::factory('* * * * *'); @@ -109,70 +113,70 @@ public function testInvalidPartsWillFail() } /** - * Data provider for cron schedule + * Data provider for cron schedule. * * @return array */ - public function scheduleProvider() + public function scheduleProvider(): array { - return array( - array('*/2 */2 * * *', '2015-08-10 21:47:27', '2015-08-10 22:00:00', false), - array('* * * * *', '2015-08-10 21:50:37', '2015-08-10 21:50:00', true), - array('* 20,21,22 * * *', '2015-08-10 21:50:00', '2015-08-10 21:50:00', true), + return [ + ['*/2 */2 * * *', '2015-08-10 21:47:27', '2015-08-10 22:00:00', false], + ['* * * * *', '2015-08-10 21:50:37', '2015-08-10 21:50:00', true], + ['* 20,21,22 * * *', '2015-08-10 21:50:00', '2015-08-10 21:50:00', true], // Handles CSV values - array('* 20,22 * * *', '2015-08-10 21:50:00', '2015-08-10 22:00:00', false), + ['* 20,22 * * *', '2015-08-10 21:50:00', '2015-08-10 22:00:00', false], // CSV values can be complex - array('7-9 * */9 * *', '2015-08-10 22:02:33', '2015-08-10 22:07:00', false), + ['7-9 * */9 * *', '2015-08-10 22:02:33', '2015-08-10 22:07:00', false], // 15th minute, of the second hour, every 15 days, in January, every Friday - array('1 * * * 7', '2015-08-10 21:47:27', '2015-08-16 00:01:00', false), + ['1 * * * 7', '2015-08-10 21:47:27', '2015-08-16 00:01:00', false], // Test with exact times - array('47 21 * * *', strtotime('2015-08-10 21:47:30'), '2015-08-10 21:47:00', true), + ['47 21 * * *', strtotime('2015-08-10 21:47:30'), '2015-08-10 21:47:00', true], // Test Day of the week (issue #1) // According cron implementation, 0|7 = sunday, 1 => monday, etc - array('* * * * 0', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false), - array('* * * * 7', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false), - array('* * * * 1', strtotime('2011-06-15 23:09:00'), '2011-06-20 00:00:00', false), + ['* * * * 0', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false], + ['* * * * 7', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false], + ['* * * * 1', strtotime('2011-06-15 23:09:00'), '2011-06-20 00:00:00', false], // Should return the sunday date as 7 equals 0 - array('0 0 * * MON,SUN', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false), - array('0 0 * * 1,7', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false), - array('0 0 * * 0-4', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false), - array('0 0 * * 7-4', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false), - array('0 0 * * 4-7', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false), - array('0 0 * * 7-3', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false), - array('0 0 * * 3-7', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false), - array('0 0 * * 3-7', strtotime('2011-06-18 23:09:00'), '2011-06-19 00:00:00', false), + ['0 0 * * MON,SUN', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false], + ['0 0 * * 1,7', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false], + ['0 0 * * 0-4', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false], + ['0 0 * * 7-4', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false], + ['0 0 * * 4-7', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false], + ['0 0 * * 7-3', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false], + ['0 0 * * 3-7', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false], + ['0 0 * * 3-7', strtotime('2011-06-18 23:09:00'), '2011-06-19 00:00:00', false], // Test lists of values and ranges (Abhoryo) - array('0 0 * * 2-7', strtotime('2011-06-20 23:09:00'), '2011-06-21 00:00:00', false), - array('0 0 * * 2-7', strtotime('2011-06-18 23:09:00'), '2011-06-19 00:00:00', false), - array('0 0 * * 4-7', strtotime('2011-07-19 00:00:00'), '2011-07-21 00:00:00', false), + ['0 0 * * 2-7', strtotime('2011-06-20 23:09:00'), '2011-06-21 00:00:00', false], + ['0 0 * * 2-7', strtotime('2011-06-18 23:09:00'), '2011-06-19 00:00:00', false], + ['0 0 * * 4-7', strtotime('2011-07-19 00:00:00'), '2011-07-21 00:00:00', false], // Test increments of ranges - array('0-12/4 * * * *', strtotime('2011-06-20 12:04:00'), '2011-06-20 12:04:00', true), - array('4-59/2 * * * *', strtotime('2011-06-20 12:04:00'), '2011-06-20 12:04:00', true), - array('4-59/2 * * * *', strtotime('2011-06-20 12:06:00'), '2011-06-20 12:06:00', true), - array('4-59/3 * * * *', strtotime('2011-06-20 12:06:00'), '2011-06-20 12:07:00', false), + ['0-12/4 * * * *', strtotime('2011-06-20 12:04:00'), '2011-06-20 12:04:00', true], + ['4-59/2 * * * *', strtotime('2011-06-20 12:04:00'), '2011-06-20 12:04:00', true], + ['4-59/2 * * * *', strtotime('2011-06-20 12:06:00'), '2011-06-20 12:06:00', true], + ['4-59/3 * * * *', strtotime('2011-06-20 12:06:00'), '2011-06-20 12:07:00', false], // Test Day of the Week and the Day of the Month (issue #1) - array('0 0 1 1 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false), - array('0 0 1 JAN 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false), - array('0 0 1 * 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false), + ['0 0 1 1 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false], + ['0 0 1 JAN 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false], + ['0 0 1 * 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false], // Test the W day of the week modifier for day of the month field - array('0 0 2W * *', strtotime('2011-07-01 00:00:00'), '2011-07-01 00:00:00', true), - array('0 0 1W * *', strtotime('2011-05-01 00:00:00'), '2011-05-02 00:00:00', false), - array('0 0 1W * *', strtotime('2011-07-01 00:00:00'), '2011-07-01 00:00:00', true), - array('0 0 3W * *', strtotime('2011-07-01 00:00:00'), '2011-07-04 00:00:00', false), - array('0 0 16W * *', strtotime('2011-07-01 00:00:00'), '2011-07-15 00:00:00', false), - array('0 0 28W * *', strtotime('2011-07-01 00:00:00'), '2011-07-28 00:00:00', false), - array('0 0 30W * *', strtotime('2011-07-01 00:00:00'), '2011-07-29 00:00:00', false), - array('0 0 31W * *', strtotime('2011-07-01 00:00:00'), '2011-07-29 00:00:00', false), + ['0 0 2W * *', strtotime('2011-07-01 00:00:00'), '2011-07-01 00:00:00', true], + ['0 0 1W * *', strtotime('2011-05-01 00:00:00'), '2011-05-02 00:00:00', false], + ['0 0 1W * *', strtotime('2011-07-01 00:00:00'), '2011-07-01 00:00:00', true], + ['0 0 3W * *', strtotime('2011-07-01 00:00:00'), '2011-07-04 00:00:00', false], + ['0 0 16W * *', strtotime('2011-07-01 00:00:00'), '2011-07-15 00:00:00', false], + ['0 0 28W * *', strtotime('2011-07-01 00:00:00'), '2011-07-28 00:00:00', false], + ['0 0 30W * *', strtotime('2011-07-01 00:00:00'), '2011-07-29 00:00:00', false], + ['0 0 31W * *', strtotime('2011-07-01 00:00:00'), '2011-07-29 00:00:00', false], // Test the last weekday of a month - array('* * * * 5L', strtotime('2011-07-01 00:00:00'), '2011-07-29 00:00:00', false), - array('* * * * 6L', strtotime('2011-07-01 00:00:00'), '2011-07-30 00:00:00', false), - array('* * * * 7L', strtotime('2011-07-01 00:00:00'), '2011-07-31 00:00:00', false), - array('* * * * 1L', strtotime('2011-07-24 00:00:00'), '2011-07-25 00:00:00', false), - array('* * * 1 5L', strtotime('2011-12-25 00:00:00'), '2012-01-27 00:00:00', false), + ['* * * * 5L', strtotime('2011-07-01 00:00:00'), '2011-07-29 00:00:00', false], + ['* * * * 6L', strtotime('2011-07-01 00:00:00'), '2011-07-30 00:00:00', false], + ['* * * * 7L', strtotime('2011-07-01 00:00:00'), '2011-07-31 00:00:00', false], + ['* * * * 1L', strtotime('2011-07-24 00:00:00'), '2011-07-25 00:00:00', false], + ['* * * 1 5L', strtotime('2011-12-25 00:00:00'), '2012-01-27 00:00:00', false], // Test the hash symbol for the nth weekday of a given month - array('* * * * 5#2', strtotime('2011-07-01 00:00:00'), '2011-07-08 00:00:00', false), - array('* * * * 5#1', strtotime('2011-07-01 00:00:00'), '2011-07-01 00:00:00', true), - array('* * * * 3#4', strtotime('2011-07-01 00:00:00'), '2011-07-27 00:00:00', false), + ['* * * * 5#2', strtotime('2011-07-01 00:00:00'), '2011-07-08 00:00:00', false], + ['* * * * 5#1', strtotime('2011-07-01 00:00:00'), '2011-07-01 00:00:00', true], + ['* * * * 3#4', strtotime('2011-07-01 00:00:00'), '2011-07-27 00:00:00', false], // Issue #7, documented example failed ['3-59/15 6-12 */15 1 2-5', strtotime('2017-01-08 00:00:00'), '2017-01-31 06:03:00', false], @@ -180,7 +184,7 @@ public function scheduleProvider() // https://github.com/laravel/framework/commit/07d160ac3cc9764d5b429734ffce4fa311385403 ['* * * * MON-FRI', strtotime('2017-01-08 00:00:00'), strtotime('2017-01-09 00:00:00'), false], ['* * * * TUE', strtotime('2017-01-08 00:00:00'), strtotime('2017-01-10 00:00:00'), false], - ); + ]; } /** @@ -193,22 +197,25 @@ public function scheduleProvider() * @covers \Cron\MonthField * @covers \Cron\CronExpression::getRunDate * @dataProvider scheduleProvider + * + * @param mixed $schedule + * @param mixed $relativeTime + * @param mixed $nextRun + * @param mixed $isDue */ - public function testDeterminesIfCronIsDue($schedule, $relativeTime, $nextRun, $isDue) + public function testDeterminesIfCronIsDue($schedule, $relativeTime, $nextRun, $isDue): void { - $relativeTimeString = is_int($relativeTime) ? date('Y-m-d H:i:s', $relativeTime) : $relativeTime; - // Test next run date $cron = CronExpression::factory($schedule); - if (is_string($relativeTime)) { + if (\is_string($relativeTime)) { $relativeTime = new DateTime($relativeTime); - } elseif (is_int($relativeTime)) { + } elseif (\is_int($relativeTime)) { $relativeTime = date('Y-m-d H:i:s', $relativeTime); } - if (is_string($nextRun)) { + if (\is_string($nextRun)) { $nextRunDate = new DateTime($nextRun); - } elseif (is_int($nextRun)) { + } elseif (\is_int($nextRun)) { $nextRunDate = new DateTime(); $nextRunDate->setTimestamp($nextRun); } @@ -221,7 +228,7 @@ public function testDeterminesIfCronIsDue($schedule, $relativeTime, $nextRun, $i /** * @covers \Cron\CronExpression::isDue */ - public function testIsDueHandlesDifferentDates() + public function testIsDueHandlesDifferentDates(): void { $cron = CronExpression::factory('* * * * *'); $this->assertTrue($cron->isDue()); @@ -233,7 +240,7 @@ public function testIsDueHandlesDifferentDates() /** * @covers \Cron\CronExpression::isDue */ - public function testIsDueHandlesDifferentDefaultTimezones() + public function testIsDueHandlesDifferentDefaultTimezones(): void { $originalTimezone = date_default_timezone_get(); $cron = CronExpression::factory('0 15 * * 3'); //Wednesday at 15:00 @@ -260,7 +267,7 @@ public function testIsDueHandlesDifferentDefaultTimezones() /** * @covers \Cron\CronExpression::isDue */ - public function testIsDueHandlesDifferentSuppliedTimezones() + public function testIsDueHandlesDifferentSuppliedTimezones(): void { $cron = CronExpression::factory('0 15 * * 3'); //Wednesday at 15:00 $date = '2014-01-01 15:00'; //Wednesday @@ -278,16 +285,16 @@ public function testIsDueHandlesDifferentSuppliedTimezones() $this->assertTrue($cron->isDue(new DateTime($date, new DateTimeZone('Asia/Tokyo')), 'Asia/Tokyo')); } - /** - * @covers Cron\CronExpression::isDue + /** + * @covers \Cron\CronExpression::isDue */ - public function testIsDueHandlesDifferentTimezonesAsArgument() + public function testIsDueHandlesDifferentTimezonesAsArgument(): void { - $cron = CronExpression::factory('0 15 * * 3'); //Wednesday at 15:00 - $date = '2014-01-01 15:00'; //Wednesday - $utc = new \DateTimeZone('UTC'); + $cron = CronExpression::factory('0 15 * * 3'); //Wednesday at 15:00 + $date = '2014-01-01 15:00'; //Wednesday + $utc = new \DateTimeZone('UTC'); $amsterdam = new \DateTimeZone('Europe/Amsterdam'); - $tokyo = new \DateTimeZone('Asia/Tokyo'); + $tokyo = new \DateTimeZone('Asia/Tokyo'); $this->assertTrue($cron->isDue(new DateTime($date, $utc), 'UTC')); $this->assertFalse($cron->isDue(new DateTime($date, $amsterdam), 'UTC')); $this->assertFalse($cron->isDue(new DateTime($date, $tokyo), 'UTC')); @@ -300,37 +307,35 @@ public function testIsDueHandlesDifferentTimezonesAsArgument() } /** - * @covers Cron\CronExpression::isDue + * @covers \Cron\CronExpression::isDue */ - public function testRecognisesTimezonesAsPartOfDateTime() + public function testRecognisesTimezonesAsPartOfDateTime(): void { - $cron = CronExpression::factory("0 7 * * *"); - $tzCron = "America/New_York"; - $tzServer = new \DateTimeZone("Europe/London"); + $cron = CronExpression::factory('0 7 * * *'); + $tzCron = 'America/New_York'; + $tzServer = new \DateTimeZone('Europe/London'); - $dtCurrent = \DateTime::createFromFormat("!Y-m-d H:i:s", "2017-10-17 10:00:00", $tzServer); + $dtCurrent = \DateTime::createFromFormat('!Y-m-d H:i:s', '2017-10-17 10:00:00', $tzServer); $dtPrev = $cron->getPreviousRunDate($dtCurrent, 0, true, $tzCron); - $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format("U \: c \: e")); + $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format('U \\: c \\: e')); - $dtCurrent = \DateTimeImmutable::createFromFormat("!Y-m-d H:i:s", "2017-10-17 10:00:00", $tzServer); + $dtCurrent = \DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', '2017-10-17 10:00:00', $tzServer); $dtPrev = $cron->getPreviousRunDate($dtCurrent, 0, true, $tzCron); - $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format("U \: c \: e")); + $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format('U \\: c \\: e')); - $dtCurrent = \DateTimeImmutable::createFromFormat("!Y-m-d H:i:s", "2017-10-17 10:00:00", $tzServer); - $dtPrev = $cron->getPreviousRunDate($dtCurrent->format("c"), 0, true, $tzCron); - $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format("U \: c \: e")); - - $dtCurrent = \DateTimeImmutable::createFromFormat("!Y-m-d H:i:s", "2017-10-17 10:00:00", $tzServer); - $dtPrev = $cron->getPreviousRunDate($dtCurrent->format("\@U"), 0, true, $tzCron); - $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format("U \: c \: e")); + $dtCurrent = \DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', '2017-10-17 10:00:00', $tzServer); + $dtPrev = $cron->getPreviousRunDate($dtCurrent->format('c'), 0, true, $tzCron); + $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format('U \\: c \\: e')); + $dtCurrent = \DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', '2017-10-17 10:00:00', $tzServer); + $dtPrev = $cron->getPreviousRunDate($dtCurrent->format('\\@U'), 0, true, $tzCron); + $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format('U \\: c \\: e')); } - /** * @covers \Cron\CronExpression::getPreviousRunDate */ - public function testCanGetPreviousRunDates() + public function testCanGetPreviousRunDates(): void { $cron = CronExpression::factory('* * * * *'); $next = $cron->getNextRunDate('now'); @@ -351,26 +356,27 @@ public function testCanGetPreviousRunDates() /** * @covers \Cron\CronExpression::getMultipleRunDates */ - public function testProvidesMultipleRunDates() + public function testProvidesMultipleRunDates(): void { $cron = CronExpression::factory('*/2 * * * *'); - $this->assertEquals(array( + $this->assertEquals([ 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') - ), $cron->getMultipleRunDates(4, '2008-11-09 00:00:00', false, true)); + new DateTime('2008-11-09 00:06:00'), + ], $cron->getMultipleRunDates(4, '2008-11-09 00:00:00', false, true)); } /** * @covers \Cron\CronExpression::getMultipleRunDates * @covers \Cron\CronExpression::setMaxIterationCount */ - public function testProvidesMultipleRunDatesForTheFarFuture() { + public function testProvidesMultipleRunDatesForTheFarFuture(): void + { // Fails with the default 1000 iteration limit $cron = CronExpression::factory('0 0 12 1 *'); $cron->setMaxIterationCount(2000); - $this->assertEquals(array( + $this->assertEquals([ new DateTime('2016-01-12 00:00:00'), new DateTime('2017-01-12 00:00:00'), new DateTime('2018-01-12 00:00:00'), @@ -380,37 +386,33 @@ public function testProvidesMultipleRunDatesForTheFarFuture() { new DateTime('2022-01-12 00:00:00'), new DateTime('2023-01-12 00:00:00'), new DateTime('2024-01-12 00:00:00'), - ), $cron->getMultipleRunDates(9, '2015-04-28 00:00:00', false, true)); + ], $cron->getMultipleRunDates(9, '2015-04-28 00:00:00', false, true)); } /** * @covers \Cron\CronExpression */ - public function testCanIterateOverNextRuns() + public function testCanIterateOverNextRuns(): void { $cron = CronExpression::factory('@weekly'); - $nextRun = $cron->getNextRunDate("2008-11-09 08:00:00"); - $this->assertEquals($nextRun, new DateTime("2008-11-16 00:00:00")); - - // true is cast to 1 - $nextRun = $cron->getNextRunDate("2008-11-09 00:00:00", true, true); - $this->assertEquals($nextRun, new DateTime("2008-11-16 00:00:00")); + $nextRun = $cron->getNextRunDate('2008-11-09 08:00:00'); + $this->assertEquals($nextRun, new DateTime('2008-11-16 00:00:00')); // You can iterate over them - $nextRun = $cron->getNextRunDate($cron->getNextRunDate("2008-11-09 00:00:00", 1, true), 1, true); - $this->assertEquals($nextRun, new DateTime("2008-11-23 00:00:00")); + $nextRun = $cron->getNextRunDate($cron->getNextRunDate('2008-11-09 00:00:00', 1, true), 1, true); + $this->assertEquals($nextRun, new DateTime('2008-11-23 00:00:00')); // You can skip more than one - $nextRun = $cron->getNextRunDate("2008-11-09 00:00:00", 2, true); - $this->assertEquals($nextRun, new DateTime("2008-11-23 00:00:00")); - $nextRun = $cron->getNextRunDate("2008-11-09 00:00:00", 3, true); - $this->assertEquals($nextRun, new DateTime("2008-11-30 00:00:00")); + $nextRun = $cron->getNextRunDate('2008-11-09 00:00:00', 2, true); + $this->assertEquals($nextRun, new DateTime('2008-11-23 00:00:00')); + $nextRun = $cron->getNextRunDate('2008-11-09 00:00:00', 3, true); + $this->assertEquals($nextRun, new DateTime('2008-11-30 00:00:00')); } /** * @covers \Cron\CronExpression::getRunDate */ - public function testSkipsCurrentDateByDefault() + public function testSkipsCurrentDateByDefault(): void { $cron = CronExpression::factory('* * * * *'); $current = new DateTime('now'); @@ -423,7 +425,7 @@ public function testSkipsCurrentDateByDefault() * @covers \Cron\CronExpression::getRunDate * @ticket 7 */ - public function testStripsForSeconds() + public function testStripsForSeconds(): void { $cron = CronExpression::factory('* * * * *'); $current = new DateTime('2011-09-27 10:10:54'); @@ -433,13 +435,13 @@ public function testStripsForSeconds() /** * @covers \Cron\CronExpression::getRunDate */ - public function testFixesPhpBugInDateIntervalMonth() + public function testFixesPhpBugInDateIntervalMonth(): void { $cron = CronExpression::factory('0 0 27 JAN *'); $this->assertSame('2011-01-27 00:00:00', $cron->getPreviousRunDate('2011-08-22 00:00:00')->format('Y-m-d H:i:s')); } - public function testIssue29() + public function testIssue29(): void { $cron = CronExpression::factory('@weekly'); $this->assertSame( @@ -451,7 +453,8 @@ public function testIssue29() /** * @see https://github.com/mtdowling/cron-expression/issues/20 */ - public function testIssue20() { + public function testIssue20(): void + { $e = CronExpression::factory('* * * * MON#1'); $this->assertTrue($e->isDue(new DateTime('2014-04-07 00:00:00'))); $this->assertFalse($e->isDue(new DateTime('2014-04-14 00:00:00'))); @@ -471,9 +474,9 @@ public function testIssue20() { /** * @covers \Cron\CronExpression::getRunDate */ - public function testKeepOriginalTime() + public function testKeepOriginalTime(): void { - $now = new \DateTime; + $now = new \DateTime(); $strNow = $now->format(DateTime::ISO8601); $cron = CronExpression::factory('0 0 * * *'); $cron->getPreviousRunDate($now); @@ -487,7 +490,7 @@ public function testKeepOriginalTime() * @covers \Cron\CronExpression::setExpression * @covers \Cron\CronExpression::setPart */ - public function testValidationWorks() + public function testValidationWorks(): void { // Invalid. Only four values $this->assertFalse(CronExpression::isValidExpression('* * * 1')); @@ -495,13 +498,13 @@ public function testValidationWorks() $this->assertTrue(CronExpression::isValidExpression('* * * * 1')); // Issue #156, 13 is an invalid month - $this->assertFalse(CronExpression::isValidExpression("* * * 13 * ")); + $this->assertFalse(CronExpression::isValidExpression('* * * 13 * ')); // Issue #155, 90 is an invalid second $this->assertFalse(CronExpression::isValidExpression('90 * * * *')); // Issue #154, 24 is an invalid hour - $this->assertFalse(CronExpression::isValidExpression("0 24 1 12 0")); + $this->assertFalse(CronExpression::isValidExpression('0 24 1 12 0')); // Issue #125, this is just all sorts of wrong $this->assertFalse(CronExpression::isValidExpression('990 14 * * mon-fri0345345')); @@ -512,11 +515,11 @@ public function testValidationWorks() /** * Makes sure that 00 is considered a valid value for 0-based fields - * cronie allows numbers with a leading 0, so adding support for this as well + * cronie allows numbers with a leading 0, so adding support for this as well. * * @see https://github.com/dragonmantank/cron-expression/issues/12 */ - public function testDoubleZeroIsValid() + public function testDoubleZeroIsValid(): void { $this->assertTrue(CronExpression::isValidExpression('00 * * * *')); $this->assertTrue(CronExpression::isValidExpression('01 * * * *')); @@ -534,7 +537,6 @@ public function testDoubleZeroIsValid() $this->assertTrue($e->isDue(new DateTime('2014-04-07 01:00:00'))); } - /** * Ranges with large steps should "wrap around" to the appropriate value * cronie allows for steps that are larger than the range of a field, with it wrapping around like a ring buffer. We @@ -542,7 +544,7 @@ public function testDoubleZeroIsValid() * * @see https://github.com/dragonmantank/cron-expression/issues/6 */ - public function testRangesWrapAroundWithLargeSteps() + public function testRangesWrapAroundWithLargeSteps(): void { $f = new MonthField(); $this->assertTrue($f->validate('*/123')); @@ -559,14 +561,14 @@ public function testRangesWrapAroundWithLargeSteps() } /** - * When there is an issue with a field, we should report the human readable position + * When there is an issue with a field, we should report the human readable position. * * @see https://github.com/dragonmantank/cron-expression/issues/29 */ - public function testFieldPositionIsHumanAdjusted() + public function testFieldPositionIsHumanAdjusted(): void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage("6 is not a valid position"); + $this->expectExceptionMessage('6 is not a valid position'); $e = CronExpression::factory('0 * * * * ? *'); } } diff --git a/tests/Cron/DayOfMonthFieldTest.php b/tests/Cron/DayOfMonthFieldTest.php index 0dae4ed6..6808ac0e 100644 --- a/tests/Cron/DayOfMonthFieldTest.php +++ b/tests/Cron/DayOfMonthFieldTest.php @@ -1,5 +1,7 @@ assertFalse($f->validate(0)); + $this->assertFalse($f->validate('0')); } } diff --git a/tests/Cron/DayOfWeekFieldTest.php b/tests/Cron/DayOfWeekFieldTest.php index eaed77ce..5847afea 100644 --- a/tests/Cron/DayOfWeekFieldTest.php +++ b/tests/Cron/DayOfWeekFieldTest.php @@ -1,5 +1,7 @@ assertFalse($f->validate('mon,')); $this->assertFalse($f->validate('mon-')); @@ -136,6 +139,6 @@ public function testLiteralsExpandProperly() { $f = new DayOfWeekField(); $this->assertTrue($f->validate('MON-FRI')); - $this->assertSame([1,2,3,4,5], $f->getRangeForExpression('MON-FRI', 7)); + $this->assertSame([1, 2, 3, 4, 5], $f->getRangeForExpression('MON-FRI', 7)); } } diff --git a/tests/Cron/FieldFactoryTest.php b/tests/Cron/FieldFactoryTest.php index a6e66b0e..8d320813 100644 --- a/tests/Cron/FieldFactoryTest.php +++ b/tests/Cron/FieldFactoryTest.php @@ -1,5 +1,7 @@ 'Cron\MinutesField', 1 => 'Cron\HoursField', 2 => 'Cron\DayOfMonthField', 3 => 'Cron\MonthField', 4 => 'Cron\DayOfWeekField', - ); + ]; $f = new FieldFactory(); foreach ($mappings as $position => $class) { - $this->assertSame($class, get_class($f->getField($position))); + $this->assertSame($class, \get_class($f->getField($position))); } } /** * @covers \Cron\FieldFactory::getField - * @expectedException InvalidArgumentException + * @expectedException \InvalidArgumentException */ public function testValidatesFieldPosition() { diff --git a/tests/Cron/HoursFieldTest.php b/tests/Cron/HoursFieldTest.php index e936d11a..63b1fa3e 100644 --- a/tests/Cron/HoursFieldTest.php +++ b/tests/Cron/HoursFieldTest.php @@ -1,5 +1,7 @@ assertTrue($f->validate('01')); $this->assertTrue($f->validate('*')); $this->assertFalse($f->validate('*/3,1,1-12')); - } + } /** * @covers \Cron\HoursField::increment diff --git a/tests/Cron/MinutesFieldTest.php b/tests/Cron/MinutesFieldTest.php index b91bffac..b66a8de4 100644 --- a/tests/Cron/MinutesFieldTest.php +++ b/tests/Cron/MinutesFieldTest.php @@ -1,5 +1,7 @@ Date: Fri, 15 Feb 2019 17:27:02 -0500 Subject: [PATCH 025/146] remove php <= 7.1 --- .travis.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 83ea1084..474bbced 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,12 +4,6 @@ sudo: false matrix: include: - - php: 7.0 - - php: 7.0 - env: dependencies=lowest - - php: 7.1 - - php: 7.1 - env: dependencies=lowest - php: 7.2 - php: 7.2 env: dependencies=lowest From 5d2b25632a4f89f343efb21bb25bc7f70c60e763 Mon Sep 17 00:00:00 2001 From: Menno Holtkamp Date: Sun, 24 Feb 2019 17:36:54 +0100 Subject: [PATCH 026/146] Use null coalescing operator --- src/Cron/FieldFactory.php | 42 ++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/src/Cron/FieldFactory.php b/src/Cron/FieldFactory.php index 545e4b83..e4413ff5 100644 --- a/src/Cron/FieldFactory.php +++ b/src/Cron/FieldFactory.php @@ -25,30 +25,26 @@ class FieldFactory */ public function getField($position) { - if (!isset($this->fields[$position])) { - switch ($position) { - case 0: - $this->fields[$position] = new MinutesField(); - break; - case 1: - $this->fields[$position] = new HoursField(); - break; - case 2: - $this->fields[$position] = new DayOfMonthField(); - break; - case 3: - $this->fields[$position] = new MonthField(); - break; - case 4: - $this->fields[$position] = new DayOfWeekField(); - break; - default: - throw new InvalidArgumentException( - ($position + 1) . ' is not a valid position' - ); - } + return $this->fields[$position] ?? $this->fields[$position] = $this->instantiateField($position); + } + + private function instantiateField($position): FieldInterface + { + switch ($position) { + case CronExpression::MINUTE: + return new MinutesField(); + case CronExpression::HOUR: + return new HoursField(); + case CronExpression::DAY: + return new DayOfMonthField(); + case CronExpression::MONTH: + return new MonthField(); + case CronExpression::WEEKDAY: + return new DayOfWeekField(); } - return $this->fields[$position]; + throw new InvalidArgumentException( + ($position + 1) . ' is not a valid position' + ); } } From d92943588f8ab3ba20e0e8cc3ae76cc392fd3f28 Mon Sep 17 00:00:00 2001 From: Menno Holtkamp Date: Sun, 24 Feb 2019 17:41:27 +0100 Subject: [PATCH 027/146] Introduce FieldFactoryInterface --- src/Cron/FieldFactory.php | 2 +- src/Cron/FieldFactoryInterface.php | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 src/Cron/FieldFactoryInterface.php diff --git a/src/Cron/FieldFactory.php b/src/Cron/FieldFactory.php index 545e4b83..15304bd7 100644 --- a/src/Cron/FieldFactory.php +++ b/src/Cron/FieldFactory.php @@ -8,7 +8,7 @@ * CRON field factory implementing a flyweight factory * @link http://en.wikipedia.org/wiki/Cron */ -class FieldFactory +class FieldFactory implements FieldFactoryInterface { /** * @var array Cache of instantiated fields diff --git a/src/Cron/FieldFactoryInterface.php b/src/Cron/FieldFactoryInterface.php new file mode 100644 index 00000000..ab2f451a --- /dev/null +++ b/src/Cron/FieldFactoryInterface.php @@ -0,0 +1,11 @@ + Date: Sun, 24 Feb 2019 17:46:51 +0100 Subject: [PATCH 028/146] Introduce PHPStan --- composer.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c0b7903f..5b7e6585 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ } ], "require": { - "php": "^7.0" + "php": "^7.0", + "phpstan/phpstan": "^0.11" }, "require-dev": { "phpunit/phpunit": "^6.4|^7.0" @@ -36,5 +37,8 @@ "branch-alias": { "dev-master": "2.3-dev" } + }, + "scripts": { + "phpstan": "./vendor/bin/phpstan analyse -l 1 src" } } From 30e6633998f2c78dc864de1e8873c4a78d7a870a Mon Sep 17 00:00:00 2001 From: Menno Holtkamp Date: Sun, 24 Feb 2019 18:02:33 +0100 Subject: [PATCH 029/146] Reduce strictness level to add tests to analysis --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 5b7e6585..36a789f4 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ }, "autoload-dev": { "psr-4": { - "Tests\\": "tests/Cron/" + "Cron\\Tests\\": "tests/Cron/" } }, "extra": { @@ -39,6 +39,6 @@ } }, "scripts": { - "phpstan": "./vendor/bin/phpstan analyse -l 1 src" + "phpstan": "./vendor/bin/phpstan analyse -l 0 src tests" } } From c38f254abf675115e60c3510be23e150a261c824 Mon Sep 17 00:00:00 2001 From: Chris Morbitzer Date: Wed, 6 Mar 2019 13:27:19 -0600 Subject: [PATCH 030/146] Support DateTimeImmutable --- src/Cron/CronExpression.php | 61 +++++++++++++++--------------- src/Cron/DayOfMonthField.php | 13 ++++--- src/Cron/DayOfWeekField.php | 21 +++++----- src/Cron/FieldInterface.php | 14 +++---- src/Cron/HoursField.php | 31 +++++++-------- src/Cron/MinutesField.php | 25 ++++++------ src/Cron/MonthField.php | 18 +++++---- tests/Cron/CronExpressionTest.php | 14 +++++++ tests/Cron/DayOfMonthFieldTest.php | 13 +++++++ tests/Cron/DayOfWeekFieldTest.php | 13 +++++++ tests/Cron/HoursFieldTest.php | 24 +++++++++++- tests/Cron/MinutesFieldTest.php | 22 +++++++++++ tests/Cron/MonthFieldTest.php | 22 +++++++++++ 13 files changed, 204 insertions(+), 87 deletions(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index 82c0ef1c..594b4358 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -179,16 +179,17 @@ public function setMaxIterationCount($maxIterationCount) /** * Get a next run date relative to the current date or a specific date * - * @param string|\DateTime $currentTime Relative calculation date - * @param int $nth Number of matches to skip before returning a - * matching next run date. 0, the default, will return the current - * date and time if the next run date falls on the current date and - * time. Setting this value to 1 will skip the first match and go to - * the second match. Setting this value to 2 will skip the first 2 - * matches and so on. - * @param bool $allowCurrentDate Set to TRUE to return the current date if - * it matches the cron expression. - * @param null|string $timeZone TimeZone to use instead of the system default + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param int $nth Number of matches to skip before returning a + * matching next run date. 0, the default, will return the + * current date and time if the next run date falls on the + * current date and time. Setting this value to 1 will + * skip the first match and go to the second match. + * Setting this value to 2 will skip the first 2 + * matches and so on. + * @param bool $allowCurrentDate Set to TRUE to return the current date if + * it matches the cron expression. + * @param null|string $timeZone TimeZone to use instead of the system default * * @return \DateTime * @throws \RuntimeException on too many iterations @@ -201,11 +202,11 @@ public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate /** * Get a previous run date relative to the current date or a specific date * - * @param string|\DateTime $currentTime Relative calculation date - * @param int $nth Number of matches to skip before returning - * @param bool $allowCurrentDate Set to TRUE to return the - * current date if it matches the cron expression - * @param null|string $timeZone TimeZone to use instead of the system default + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param int $nth Number of matches to skip before returning + * @param bool $allowCurrentDate Set to TRUE to return the + * current date if it matches the cron expression + * @param null|string $timeZone TimeZone to use instead of the system default * * @return \DateTime * @throws \RuntimeException on too many iterations @@ -219,14 +220,14 @@ public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrent /** * Get multiple run dates starting at the current date or a specific date * - * @param int $total Set the total number of dates to calculate - * @param string|\DateTime $currentTime Relative calculation date - * @param bool $invert Set to TRUE to retrieve previous dates - * @param bool $allowCurrentDate Set to TRUE to return the - * current date if it matches the cron expression - * @param null|string $timeZone TimeZone to use instead of the system default + * @param int $total Set the total number of dates to calculate + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param bool $invert Set to TRUE to retrieve previous dates + * @param bool $allowCurrentDate Set to TRUE to return the + * current date if it matches the cron expression + * @param null|string $timeZone TimeZone to use instead of the system default * - * @return array Returns an array of run dates + * @return \DateTime[] Returns an array of run dates */ public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false, $timeZone = null) { @@ -277,8 +278,8 @@ public function __toString() * specific date. This method assumes that the current number of * seconds are irrelevant, and should be called once per minute. * - * @param string|\DateTime $currentTime Relative calculation date - * @param null|string $timeZone TimeZone to use instead of the system default + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param null|string $timeZone TimeZone to use instead of the system default * * @return bool Returns TRUE if the cron is due to run or FALSE if not */ @@ -310,12 +311,12 @@ public function isDue($currentTime = 'now', $timeZone = null) /** * Get the next or previous run date of the expression relative to a date * - * @param string|\DateTime $currentTime Relative calculation date - * @param int $nth Number of matches to skip before returning - * @param bool $invert Set to TRUE to go backwards in time - * @param bool $allowCurrentDate Set to TRUE to return the - * current date if it matches the cron expression - * @param string|null $timeZone TimeZone to use instead of the system default + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param int $nth Number of matches to skip before returning + * @param bool $invert Set to TRUE to go backwards in time + * @param bool $allowCurrentDate Set to TRUE to return the + * current date if it matches the cron expression + * @param string|null $timeZone TimeZone to use instead of the system default * * @return \DateTime * @throws \RuntimeException on too many iterations diff --git a/src/Cron/DayOfMonthField.php b/src/Cron/DayOfMonthField.php index 3920cc9e..d4552e06 100644 --- a/src/Cron/DayOfMonthField.php +++ b/src/Cron/DayOfMonthField.php @@ -3,6 +3,7 @@ namespace Cron; use DateTime; +use DateTimeInterface; /** * Day of month field. Allows: * , / - ? L W @@ -69,7 +70,7 @@ private static function getNearestWeekday($currentYear, $currentMonth, $targetDa /** * @inheritDoc */ - public function isSatisfiedBy(DateTime $date, $value) + public function isSatisfiedBy(DateTimeInterface $date, $value) { // ? states that the field value is to be skipped if ($value == '?') { @@ -100,15 +101,15 @@ public function isSatisfiedBy(DateTime $date, $value) /** * @inheritDoc + * + * @param \DateTime|\DateTimeImmutable &$date */ - public function increment(DateTime $date, $invert = false) + public function increment(DateTimeInterface &$date, $invert = false) { if ($invert) { - $date->modify('previous day'); - $date->setTime(23, 59); + $date = $date->modify('previous day')->setTime(23, 59); } else { - $date->modify('next day'); - $date->setTime(0, 0); + $date = $date->modify('next day')->setTime(0, 0); } return $this; diff --git a/src/Cron/DayOfWeekField.php b/src/Cron/DayOfWeekField.php index 96fdea3c..9db9e956 100644 --- a/src/Cron/DayOfWeekField.php +++ b/src/Cron/DayOfWeekField.php @@ -3,6 +3,7 @@ namespace Cron; use DateTime; +use DateTimeInterface; use InvalidArgumentException; /** @@ -51,8 +52,10 @@ public function __construct() /** * @inheritDoc + * + * @param \DateTime|\DateTimeImmutable $date */ - public function isSatisfiedBy(DateTime $date, $value) + public function isSatisfiedBy(DateTimeInterface $date, $value) { if ($value == '?') { return true; @@ -71,7 +74,7 @@ public function isSatisfiedBy(DateTime $date, $value) $weekday = str_replace('7', '0', $weekday); $tdate = clone $date; - $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth); + $tdate = $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth); while ($tdate->format('w') != $weekday) { $tdateClone = new DateTime(); $tdate = $tdateClone @@ -114,7 +117,7 @@ public function isSatisfiedBy(DateTime $date, $value) } $tdate = clone $date; - $tdate->setDate($currentYear, $currentMonth, 1); + $tdate = $tdate->setDate($currentYear, $currentMonth, 1); $dayCount = 0; $currentDay = 1; while ($currentDay < $lastDayOfMonth + 1) { @@ -123,7 +126,7 @@ public function isSatisfiedBy(DateTime $date, $value) break; } } - $tdate->setDate($currentYear, $currentMonth, ++$currentDay); + $tdate = $tdate->setDate($currentYear, $currentMonth, ++$currentDay); } return $date->format('j') == $currentDay; @@ -149,15 +152,15 @@ public function isSatisfiedBy(DateTime $date, $value) /** * @inheritDoc + * + * @param \DateTime|\DateTimeImmutable &$date */ - public function increment(DateTime $date, $invert = false) + public function increment(DateTimeInterface &$date, $invert = false) { if ($invert) { - $date->modify('-1 day'); - $date->setTime(23, 59, 0); + $date = $date->modify('-1 day')->setTime(23, 59, 0); } else { - $date->modify('+1 day'); - $date->setTime(0, 0, 0); + $date = $date->modify('+1 day')->setTime(0, 0, 0); } return $this; diff --git a/src/Cron/FieldInterface.php b/src/Cron/FieldInterface.php index 72d2dd29..f8366eae 100644 --- a/src/Cron/FieldInterface.php +++ b/src/Cron/FieldInterface.php @@ -2,7 +2,7 @@ namespace Cron; -use DateTime; +use DateTimeInterface; /** * CRON field interface @@ -12,23 +12,23 @@ interface FieldInterface /** * Check if the respective value of a DateTime field satisfies a CRON exp * - * @param DateTime $date DateTime object to check - * @param string $value CRON expression to test against + * @param DateTimeInterface $date DateTime object to check + * @param string $value CRON expression to test against * * @return bool Returns TRUE if satisfied, FALSE otherwise */ - public function isSatisfiedBy(DateTime $date, $value); + public function isSatisfiedBy(DateTimeInterface $date, $value); /** * When a CRON expression is not satisfied, this method is used to increment * or decrement a DateTime object by the unit of the cron field * - * @param DateTime $date DateTime object to change - * @param bool $invert (optional) Set to TRUE to decrement + * @param DateTimeInterface &$date DateTime object to change + * @param bool $invert (optional) Set to TRUE to decrement * * @return FieldInterface */ - public function increment(DateTime $date, $invert = false); + public function increment(DateTimeInterface &$date, $invert = false); /** * Validates a CRON expression for a given field diff --git a/src/Cron/HoursField.php b/src/Cron/HoursField.php index 0e6dd74c..628218a6 100644 --- a/src/Cron/HoursField.php +++ b/src/Cron/HoursField.php @@ -2,7 +2,7 @@ namespace Cron; -use DateTime; +use DateTimeInterface; use DateTimeZone; /** @@ -23,32 +23,33 @@ class HoursField extends AbstractField /** * @inheritDoc */ - public function isSatisfiedBy(DateTime $date, $value) + public function isSatisfiedBy(DateTimeInterface $date, $value) { + if ($value == '?') { + return true; + } + return $this->isSatisfied($date->format('H'), $value); } /** * {@inheritDoc} * - * @param string|null $parts + * @param \DateTime|\DateTimeImmutable &$date + * @param string|null $parts */ - public function increment(DateTime $date, $invert = false, $parts = null) + public function increment(DateTimeInterface &$date, $invert = false, $parts = null) { // Change timezone to UTC temporarily. This will // allow us to go back or forwards and hour even // if DST will be changed between the hours. if (is_null($parts) || $parts == '*') { $timezone = $date->getTimezone(); - $date->setTimezone(new DateTimeZone('UTC')); - if ($invert) { - $date->modify('-1 hour'); - } else { - $date->modify('+1 hour'); - } - $date->setTimezone($timezone); + $date = $date->setTimezone(new DateTimeZone('UTC')); + $date = $date->modify(($invert ? '-' : '+') . '1 hour'); + $date = $date->setTimezone($timezone); - $date->setTime($date->format('H'), $invert ? 59 : 0); + $date = $date->setTime($date->format('H'), $invert ? 59 : 0); return $this; } @@ -72,11 +73,11 @@ public function increment(DateTime $date, $invert = false, $parts = null) $hour = $hours[$position]; if ((!$invert && $date->format('H') >= $hour) || ($invert && $date->format('H') <= $hour)) { - $date->modify(($invert ? '-' : '+') . '1 day'); - $date->setTime($invert ? 23 : 0, $invert ? 59 : 0); + $date = $date->modify(($invert ? '-' : '+') . '1 day'); + $date = $date->setTime($invert ? 23 : 0, $invert ? 59 : 0); } else { - $date->setTime($hour, $invert ? 59 : 0); + $date = $date->setTime($hour, $invert ? 59 : 0); } return $this; diff --git a/src/Cron/MinutesField.php b/src/Cron/MinutesField.php index e8aea7aa..fecc9b6d 100644 --- a/src/Cron/MinutesField.php +++ b/src/Cron/MinutesField.php @@ -2,7 +2,7 @@ namespace Cron; -use DateTime; +use DateTimeInterface; /** * Minutes field. Allows: * , / - @@ -22,24 +22,25 @@ class MinutesField extends AbstractField /** * @inheritDoc */ - public function isSatisfiedBy(DateTime $date, $value) + public function isSatisfiedBy(DateTimeInterface $date, $value) { + if ($value == '?') { + return true; + } + return $this->isSatisfied($date->format('i'), $value); } /** * {@inheritDoc} * - * @param string|null $parts + * @param \DateTime|\DateTimeImmutable &$date + * @param string|null $parts */ - public function increment(DateTime $date, $invert = false, $parts = null) + public function increment(DateTimeInterface &$date, $invert = false, $parts = null) { if (is_null($parts)) { - if ($invert) { - $date->modify('-1 minute'); - } else { - $date->modify('+1 minute'); - } + $date = $date->modify(($invert ? '-' : '+') . '1 minute'); return $this; } @@ -62,11 +63,11 @@ public function increment(DateTime $date, $invert = false, $parts = null) } if ((!$invert && $current_minute >= $minutes[$position]) || ($invert && $current_minute <= $minutes[$position])) { - $date->modify(($invert ? '-' : '+') . '1 hour'); - $date->setTime($date->format('H'), $invert ? 59 : 0); + $date = $date->modify(($invert ? '-' : '+') . '1 hour'); + $date = $date->setTime($date->format('H'), $invert ? 59 : 0); } else { - $date->setTime($date->format('H'), $minutes[$position]); + $date = $date->setTime($date->format('H'), $minutes[$position]); } return $this; diff --git a/src/Cron/MonthField.php b/src/Cron/MonthField.php index 238b0e7c..afc9caff 100644 --- a/src/Cron/MonthField.php +++ b/src/Cron/MonthField.php @@ -2,7 +2,7 @@ namespace Cron; -use DateTime; +use DateTimeInterface; /** * Month field. Allows: * , / - @@ -28,8 +28,12 @@ class MonthField extends AbstractField /** * @inheritDoc */ - public function isSatisfiedBy(DateTime $date, $value) + public function isSatisfiedBy(DateTimeInterface $date, $value) { + if ($value == '?') { + return true; + } + $value = $this->convertLiterals($value); return $this->isSatisfied($date->format('m'), $value); @@ -37,15 +41,15 @@ public function isSatisfiedBy(DateTime $date, $value) /** * @inheritDoc + * + * @param \DateTime|\DateTimeImmutable &$date */ - public function increment(DateTime $date, $invert = false) + public function increment(DateTimeInterface &$date, $invert = false) { if ($invert) { - $date->modify('last day of previous month'); - $date->setTime(23, 59); + $date = $date->modify('last day of previous month')->setTime(23, 59); } else { - $date->modify('first day of next month'); - $date->setTime(0, 0); + $date = $date->modify('first day of next month')->setTime(0, 0); } return $this; diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 678501be..9b82ae5b 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -5,6 +5,7 @@ use Cron\CronExpression; use Cron\MonthField; use DateTime; +use DateTimeImmutable; use DateTimeZone; use InvalidArgumentException; use PHPUnit\Framework\TestCase; @@ -228,6 +229,7 @@ public function testIsDueHandlesDifferentDates() $this->assertTrue($cron->isDue('now')); $this->assertTrue($cron->isDue(new DateTime('now'))); $this->assertTrue($cron->isDue(date('Y-m-d H:i'))); + $this->assertTrue($cron->isDue(new DateTimeImmutable('now'))); } /** @@ -407,6 +409,18 @@ public function testCanIterateOverNextRuns() $this->assertEquals($nextRun, new DateTime("2008-11-30 00:00:00")); } + /** + * @covers \Cron\CronExpression::getRunDate + */ + public function testGetRunDateHandlesDifferentDates() + { + $cron = CronExpression::factory('@weekly'); + $date = new DateTime("2019-03-10 00:00:00"); + $this->assertEquals($date, $cron->getNextRunDate("2019-03-03 08:00:00")); + $this->assertEquals($date, $cron->getNextRunDate(new DateTime("2019-03-03 08:00:00"))); + $this->assertEquals($date, $cron->getNextRunDate(new DateTimeImmutable("2019-03-03 08:00:00"))); + } + /** * @covers \Cron\CronExpression::getRunDate */ diff --git a/tests/Cron/DayOfMonthFieldTest.php b/tests/Cron/DayOfMonthFieldTest.php index 0dae4ed6..2191b6bf 100644 --- a/tests/Cron/DayOfMonthFieldTest.php +++ b/tests/Cron/DayOfMonthFieldTest.php @@ -4,6 +4,7 @@ use Cron\DayOfMonthField; use DateTime; +use DateTimeImmutable; use PHPUnit\Framework\TestCase; /** @@ -33,6 +34,7 @@ public function testChecksIfSatisfied() { $f = new DayOfMonthField(); $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?')); + $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?')); } /** @@ -50,6 +52,17 @@ public function testIncrementsDate() $this->assertSame('2011-03-14 23:59:00', $d->format('Y-m-d H:i:s')); } + /** + * @covers \Cron\DayOfMonthField::increment + */ + public function testIncrementsDateTimeImmutable() + { + $d = new DateTimeImmutable('2011-03-15 11:15:00'); + $f = new DayOfMonthField(); + $f->increment($d); + $this->assertSame('2011-03-16 00:00:00', $d->format('Y-m-d H:i:s')); + } + /** * Day of the month cannot accept a 0 value, it must be between 1 and 31 * See Github issue #120 diff --git a/tests/Cron/DayOfWeekFieldTest.php b/tests/Cron/DayOfWeekFieldTest.php index eaed77ce..ef89b47f 100644 --- a/tests/Cron/DayOfWeekFieldTest.php +++ b/tests/Cron/DayOfWeekFieldTest.php @@ -4,6 +4,7 @@ use Cron\DayOfWeekField; use DateTime; +use DateTimeImmutable; use PHPUnit\Framework\TestCase; /** @@ -33,6 +34,7 @@ public function testChecksIfSatisfied() { $f = new DayOfWeekField(); $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?')); + $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?')); } /** @@ -50,6 +52,17 @@ public function testIncrementsDate() $this->assertSame('2011-03-14 23:59:00', $d->format('Y-m-d H:i:s')); } + /** + * @covers \Cron\DayOfWeekField::increment + */ + public function testIncrementsDateTimeImmutable() + { + $d = new DateTimeImmutable('2011-03-15 11:15:00'); + $f = new DayOfWeekField(); + $f->increment($d); + $this->assertSame('2011-03-16 00:00:00', $d->format('Y-m-d H:i:s')); + } + /** * @covers \Cron\DayOfWeekField::isSatisfiedBy * @expectedException InvalidArgumentException diff --git a/tests/Cron/HoursFieldTest.php b/tests/Cron/HoursFieldTest.php index e936d11a..1849f28b 100644 --- a/tests/Cron/HoursFieldTest.php +++ b/tests/Cron/HoursFieldTest.php @@ -4,6 +4,7 @@ use Cron\HoursField; use DateTime; +use DateTimeImmutable; use PHPUnit\Framework\TestCase; /** @@ -22,7 +23,17 @@ public function testValidatesField() $this->assertTrue($f->validate('01')); $this->assertTrue($f->validate('*')); $this->assertFalse($f->validate('*/3,1,1-12')); - } + } + + /** + * @covers \Cron\HoursField::isSatisfiedBy + */ + public function testChecksIfSatisfied() + { + $f = new HoursField(); + $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?')); + $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?')); + } /** * @covers \Cron\HoursField::increment @@ -39,6 +50,17 @@ public function testIncrementsDate() $this->assertSame('2011-03-15 10:59:00', $d->format('Y-m-d H:i:s')); } + /** + * @covers \Cron\HoursField::increment + */ + public function testIncrementsDateTimeImmutable() + { + $d = new DateTimeImmutable('2011-03-15 11:15:00'); + $f = new HoursField(); + $f->increment($d); + $this->assertSame('2011-03-15 12:00:00', $d->format('Y-m-d H:i:s')); + } + /** * @covers \Cron\HoursField::increment */ diff --git a/tests/Cron/MinutesFieldTest.php b/tests/Cron/MinutesFieldTest.php index b91bffac..41a536d6 100644 --- a/tests/Cron/MinutesFieldTest.php +++ b/tests/Cron/MinutesFieldTest.php @@ -4,6 +4,7 @@ use Cron\MinutesField; use DateTime; +use DateTimeImmutable; use PHPUnit\Framework\TestCase; /** @@ -22,6 +23,16 @@ public function testValidatesField() $this->assertFalse($f->validate('*/3,1,1-12')); } + /** + * @covers \Cron\MinutesField::isSatisfiedBy + */ + public function testChecksIfSatisfied() + { + $f = new MinutesField(); + $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?')); + $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?')); + } + /** * @covers \Cron\MinutesField::increment */ @@ -35,6 +46,17 @@ public function testIncrementsDate() $this->assertSame('2011-03-15 11:15:00', $d->format('Y-m-d H:i:s')); } + /** + * @covers \Cron\MinutesField::increment + */ + public function testIncrementsDateTimeImmutable() + { + $d = new DateTimeImmutable('2011-03-15 11:15:00'); + $f = new MinutesField(); + $f->increment($d); + $this->assertSame('2011-03-15 11:16:00', $d->format('Y-m-d H:i:s')); + } + /** * Various bad syntaxes that are reported to work, but shouldn't. * diff --git a/tests/Cron/MonthFieldTest.php b/tests/Cron/MonthFieldTest.php index 83f0f164..f329f4c1 100644 --- a/tests/Cron/MonthFieldTest.php +++ b/tests/Cron/MonthFieldTest.php @@ -4,6 +4,7 @@ use Cron\MonthField; use DateTime; +use DateTimeImmutable; use PHPUnit\Framework\TestCase; /** @@ -23,6 +24,16 @@ public function testValidatesField() $this->assertFalse($f->validate('1.fix-regexp')); } + /** + * @covers \Cron\MonthField::isSatisfiedBy + */ + public function testChecksIfSatisfied() + { + $f = new MonthField(); + $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?')); + $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?')); + } + /** * @covers \Cron\MonthField::increment */ @@ -38,6 +49,17 @@ public function testIncrementsDate() $this->assertSame('2011-02-28 23:59:00', $d->format('Y-m-d H:i:s')); } + /** + * @covers \Cron\MonthField::increment + */ + public function testIncrementsDateTimeImmutable() + { + $d = new DateTimeImmutable('2011-03-15 11:15:00'); + $f = new MonthField(); + $f->increment($d); + $this->assertSame('2011-04-01 00:00:00', $d->format('Y-m-d H:i:s')); + } + /** * @covers \Cron\MonthField::increment */ From c8d107f7146979aa1724420280b227b4732a3665 Mon Sep 17 00:00:00 2001 From: "Eimantas.Morkunas" Date: Wed, 19 Dec 2018 10:55:23 +0200 Subject: [PATCH 031/146] Fixed infinite loop when resolving last weekday of the month from literals --- src/Cron/DayOfWeekField.php | 4 +++- tests/Cron/DayOfWeekFieldTest.php | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Cron/DayOfWeekField.php b/src/Cron/DayOfWeekField.php index e1780134..14ee0d94 100644 --- a/src/Cron/DayOfWeekField.php +++ b/src/Cron/DayOfWeekField.php @@ -49,7 +49,9 @@ public function isSatisfiedBy(DateTime $date, $value) // Find out if this is the last specific weekday of the month if (strpos($value, 'L')) { - $weekday = str_replace('7', '0', substr($value, 0, strpos($value, 'L'))); + $weekday = $this->convertLiterals(substr($value, 0, strpos($value, 'L'))); + $weekday = str_replace('7', '0', $weekday); + $tdate = clone $date; $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth); while ($tdate->format('w') != $weekday) { diff --git a/tests/Cron/DayOfWeekFieldTest.php b/tests/Cron/DayOfWeekFieldTest.php index d27c34df..eaed77ce 100644 --- a/tests/Cron/DayOfWeekFieldTest.php +++ b/tests/Cron/DayOfWeekFieldTest.php @@ -103,6 +103,18 @@ public function testHandlesZeroAndSevenDayOfTheWeekValues() $this->assertTrue($f->isSatisfiedBy(new DateTime('2014-04-20 00:00:00'), '7#3')); } + /** + * @covers \Cron\DayOfWeekField::isSatisfiedBy + */ + public function testHandlesLastWeekdayOfTheMonth() + { + $f = new DayOfWeekField(); + $this->assertTrue($f->isSatisfiedBy(new DateTime('2018-12-28 00:00:00'), 'FRIL')); + $this->assertTrue($f->isSatisfiedBy(new DateTime('2018-12-28 00:00:00'), '5L')); + $this->assertFalse($f->isSatisfiedBy(new DateTime('2018-12-21 00:00:00'), 'FRIL')); + $this->assertFalse($f->isSatisfiedBy(new DateTime('2018-12-21 00:00:00'), '5L')); + } + /** * @see https://github.com/mtdowling/cron-expression/issues/47 */ From 5b530336641ce26f4e7c5a5e586f74a25344d44e Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Thu, 14 Feb 2019 10:08:44 -0500 Subject: [PATCH 032/146] Started listing projects that use cron-expression --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 67992c7d..19adcc9c 100644 --- a/README.md +++ b/README.md @@ -71,3 +71,8 @@ Requirements - PHP 7.0+ - PHPUnit is required to run the unit tests - Composer is required to run the unit tests + +Projects that Use cron-expression +================================= +* Part of the [Laravel Framework](https://github.com/laravel/framework/) +* Available as a [Symfony Bundle - setono/cron-expression-bundle](https://github.com/Setono/CronExpressionBundle) \ No newline at end of file From 362cb119b7616e37bcf881da3e9a2f33041d9513 Mon Sep 17 00:00:00 2001 From: Javier Spagnoletti Date: Tue, 4 Dec 2018 16:19:42 -0300 Subject: [PATCH 033/146] Improve dependency declaration --- .travis.yml | 34 ++++++++++++++++++++++++---------- composer.json | 9 +++++++-- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2aaaa54b..479b6135 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,31 @@ language: php -php: - - 7.0 - - 7.1 - - hhvm - sudo: false -install: travis_retry composer install --no-interaction --prefer-dist - -script: phpunit --coverage-text - matrix: + include: + - php: 7.0 + - php: 7.0 + env: dependencies=lowest + - php: 7.1 + - php: 7.1 + env: dependencies=lowest + - php: 7.2 + - php: 7.2 + env: dependencies=lowest + - php: nighly + - php: nighly + env: dependencies=lowest allow_failures: - - php: hhvm + - php: nighly fast_finish: true + +install: + - | + if [[ "$dependencies" = "lowest" ]]; then + travis_retry composer update --no-interaction --prefer-lowest --prefer-stable -n + else + travis_retry composer install --no-interaction --prefer-dist + fi + +script: vendor/bin/phpunit --coverage-text diff --git a/composer.json b/composer.json index d9997ead..c0b7903f 100644 --- a/composer.json +++ b/composer.json @@ -17,10 +17,10 @@ } ], "require": { - "php": ">=7.0.0" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "~6.4" + "phpunit/phpunit": "^6.4|^7.0" }, "autoload": { "psr-4": { @@ -31,5 +31,10 @@ "psr-4": { "Tests\\": "tests/Cron/" } + }, + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } } } From 899ea1ccdc572ddd1098779451e954dc0e9f42cc Mon Sep 17 00:00:00 2001 From: Piotr Konieczny Date: Thu, 6 Dec 2018 09:49:16 +0100 Subject: [PATCH 034/146] Get timezone from DateTimeInterface --- src/Cron/CronExpression.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index 5be00b29..82c0ef1c 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -4,6 +4,7 @@ use DateTime; use DateTimeImmutable; +use DateTimeInterface; use DateTimeZone; use Exception; use InvalidArgumentException; @@ -391,8 +392,8 @@ protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $a /** * Workout what timeZone should be used. * - * @param string|\DateTime $currentTime Relative calculation date - * @param string|null $timeZone TimeZone to use instead of the system default + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param string|null $timeZone TimeZone to use instead of the system default * * @return string */ @@ -402,7 +403,7 @@ protected function determineTimeZone($currentTime, $timeZone) return $timeZone; } - if ($currentTime instanceOf Datetime) { + if ($currentTime instanceOf DateTimeInterface) { return $currentTime->getTimeZone()->getName(); } From 02b94aa7f200217154857dce5aa5df4637fdf514 Mon Sep 17 00:00:00 2001 From: Piotr Konieczny Date: Thu, 6 Dec 2018 11:06:28 +0100 Subject: [PATCH 035/146] Adding missing docblocks --- src/Cron/AbstractField.php | 12 ++++++++++-- src/Cron/DayOfMonthField.php | 6 ++++++ src/Cron/DayOfWeekField.php | 9 +++++++++ src/Cron/HoursField.php | 8 ++++++++ src/Cron/MinutesField.php | 8 ++++++++ src/Cron/MonthField.php | 6 ++++++ 6 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/Cron/AbstractField.php b/src/Cron/AbstractField.php index 262ab63c..8b1072ab 100644 --- a/src/Cron/AbstractField.php +++ b/src/Cron/AbstractField.php @@ -31,7 +31,9 @@ abstract class AbstractField implements FieldInterface */ protected $rangeEnd; - + /** + * Constructor + */ public function __construct() { $this->fullRange = range($this->rangeStart, $this->rangeEnd); @@ -204,12 +206,18 @@ public function getRangeForExpression($expression, $max) return $values; } + /** + * Convert literal + * + * @param string $value + * @return string + */ protected function convertLiterals($value) { if (count($this->literals)) { $key = array_search($value, $this->literals); if ($key !== false) { - return $key; + return (string) $key; } } diff --git a/src/Cron/DayOfMonthField.php b/src/Cron/DayOfMonthField.php index abf59690..d31494f1 100644 --- a/src/Cron/DayOfMonthField.php +++ b/src/Cron/DayOfMonthField.php @@ -59,6 +59,9 @@ private static function getNearestWeekday($currentYear, $currentMonth, $targetDa } } + /** + * @inheritDoc + */ public function isSatisfiedBy(DateTime $date, $value) { // ? states that the field value is to be skipped @@ -88,6 +91,9 @@ public function isSatisfiedBy(DateTime $date, $value) return $this->isSatisfied($date->format('d'), $value); } + /** + * @inheritDoc + */ public function increment(DateTime $date, $invert = false) { if ($invert) { diff --git a/src/Cron/DayOfWeekField.php b/src/Cron/DayOfWeekField.php index 14ee0d94..728bfc87 100644 --- a/src/Cron/DayOfWeekField.php +++ b/src/Cron/DayOfWeekField.php @@ -28,12 +28,18 @@ class DayOfWeekField extends AbstractField protected $literals = [1 => 'MON', 2 => 'TUE', 3 => 'WED', 4 => 'THU', 5 => 'FRI', 6 => 'SAT', 7 => 'SUN']; + /** + * Constructor + */ public function __construct() { $this->nthRange = range(1, 5); parent::__construct(); } + /** + * @inheritDoc + */ public function isSatisfiedBy(DateTime $date, $value) { if ($value == '?') { @@ -129,6 +135,9 @@ public function isSatisfiedBy(DateTime $date, $value) return $this->isSatisfied($fieldValue, $value); } + /** + * @inheritDoc + */ public function increment(DateTime $date, $invert = false) { if ($invert) { diff --git a/src/Cron/HoursField.php b/src/Cron/HoursField.php index 4def9ca1..713a642e 100644 --- a/src/Cron/HoursField.php +++ b/src/Cron/HoursField.php @@ -13,11 +13,19 @@ class HoursField extends AbstractField protected $rangeStart = 0; protected $rangeEnd = 23; + /** + * @inheritDoc + */ public function isSatisfiedBy(DateTime $date, $value) { return $this->isSatisfied($date->format('H'), $value); } + /** + * {@inheritDoc} + * + * @param string|null $parts + */ public function increment(DateTime $date, $invert = false, $parts = null) { // Change timezone to UTC temporarily. This will diff --git a/src/Cron/MinutesField.php b/src/Cron/MinutesField.php index 59bb386f..fdebd321 100644 --- a/src/Cron/MinutesField.php +++ b/src/Cron/MinutesField.php @@ -13,11 +13,19 @@ class MinutesField extends AbstractField protected $rangeStart = 0; protected $rangeEnd = 59; + /** + * @inheritDoc + */ public function isSatisfiedBy(DateTime $date, $value) { return $this->isSatisfied($date->format('i'), $value); } + /** + * {@inheritDoc} + * + * @param string|null $parts + */ public function increment(DateTime $date, $invert = false, $parts = null) { if (is_null($parts)) { diff --git a/src/Cron/MonthField.php b/src/Cron/MonthField.php index 79fdf3cf..c9f1ffb2 100644 --- a/src/Cron/MonthField.php +++ b/src/Cron/MonthField.php @@ -14,6 +14,9 @@ class MonthField extends AbstractField protected $literals = [1 => 'JAN', 2 => 'FEB', 3 => 'MAR', 4 => 'APR', 5 => 'MAY', 6 => 'JUN', 7 => 'JUL', 8 => 'AUG', 9 => 'SEP', 10 => 'OCT', 11 => 'NOV', 12 => 'DEC']; + /** + * @inheritDoc + */ public function isSatisfiedBy(DateTime $date, $value) { $value = $this->convertLiterals($value); @@ -21,6 +24,9 @@ public function isSatisfiedBy(DateTime $date, $value) return $this->isSatisfied($date->format('m'), $value); } + /** + * @inheritDoc + */ public function increment(DateTime $date, $invert = false) { if ($invert) { From 63d388f65660738d729aa131776645c0996f035a Mon Sep 17 00:00:00 2001 From: Piotr Konieczny Date: Thu, 6 Dec 2018 11:30:08 +0100 Subject: [PATCH 036/146] Adding missing docblocks for class properties --- src/Cron/DayOfMonthField.php | 7 +++++++ src/Cron/DayOfWeekField.php | 13 +++++++++++++ src/Cron/HoursField.php | 7 +++++++ src/Cron/MinutesField.php | 7 +++++++ src/Cron/MonthField.php | 11 +++++++++++ 5 files changed, 45 insertions(+) diff --git a/src/Cron/DayOfMonthField.php b/src/Cron/DayOfMonthField.php index d31494f1..3920cc9e 100644 --- a/src/Cron/DayOfMonthField.php +++ b/src/Cron/DayOfMonthField.php @@ -24,7 +24,14 @@ */ class DayOfMonthField extends AbstractField { + /** + * @inheritDoc + */ protected $rangeStart = 1; + + /** + * @inheritDoc + */ protected $rangeEnd = 31; /** diff --git a/src/Cron/DayOfWeekField.php b/src/Cron/DayOfWeekField.php index 728bfc87..2391a790 100644 --- a/src/Cron/DayOfWeekField.php +++ b/src/Cron/DayOfWeekField.php @@ -21,11 +21,24 @@ */ class DayOfWeekField extends AbstractField { + /** + * @inheritDoc + */ protected $rangeStart = 0; + + /** + * @inheritDoc + */ protected $rangeEnd = 7; + /** + * @var array Weekday range + */ protected $nthRange; + /** + * @inheritDoc + */ protected $literals = [1 => 'MON', 2 => 'TUE', 3 => 'WED', 4 => 'THU', 5 => 'FRI', 6 => 'SAT', 7 => 'SUN']; /** diff --git a/src/Cron/HoursField.php b/src/Cron/HoursField.php index 713a642e..4fd98dd0 100644 --- a/src/Cron/HoursField.php +++ b/src/Cron/HoursField.php @@ -10,7 +10,14 @@ */ class HoursField extends AbstractField { + /** + * @inheritDoc + */ protected $rangeStart = 0; + + /** + * @inheritDoc + */ protected $rangeEnd = 23; /** diff --git a/src/Cron/MinutesField.php b/src/Cron/MinutesField.php index fdebd321..fb81a8e6 100644 --- a/src/Cron/MinutesField.php +++ b/src/Cron/MinutesField.php @@ -10,7 +10,14 @@ */ class MinutesField extends AbstractField { + /** + * @inheritDoc + */ protected $rangeStart = 0; + + /** + * @inheritDoc + */ protected $rangeEnd = 59; /** diff --git a/src/Cron/MonthField.php b/src/Cron/MonthField.php index c9f1ffb2..238b0e7c 100644 --- a/src/Cron/MonthField.php +++ b/src/Cron/MonthField.php @@ -9,8 +9,19 @@ */ class MonthField extends AbstractField { + /** + * @inheritDoc + */ protected $rangeStart = 1; + + /** + * @inheritDoc + */ protected $rangeEnd = 12; + + /** + * @inheritDoc + */ protected $literals = [1 => 'JAN', 2 => 'FEB', 3 => 'MAR', 4 => 'APR', 5 => 'MAY', 6 => 'JUN', 7 => 'JUL', 8 => 'AUG', 9 => 'SEP', 10 => 'OCT', 11 => 'NOV', 12 => 'DEC']; From 9b67b7eb99b1067db9ef7e7b4023f9bbbffd67e0 Mon Sep 17 00:00:00 2001 From: Piotr Konieczny Date: Thu, 6 Dec 2018 11:30:29 +0100 Subject: [PATCH 037/146] Normalize EOLs --- src/Cron/DayOfWeekField.php | 1 - src/Cron/FieldInterface.php | 1 + src/Cron/HoursField.php | 2 +- src/Cron/MinutesField.php | 1 - 4 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Cron/DayOfWeekField.php b/src/Cron/DayOfWeekField.php index 2391a790..96fdea3c 100644 --- a/src/Cron/DayOfWeekField.php +++ b/src/Cron/DayOfWeekField.php @@ -5,7 +5,6 @@ use DateTime; use InvalidArgumentException; - /** * Day of week field. Allows: * / , - ? L # * diff --git a/src/Cron/FieldInterface.php b/src/Cron/FieldInterface.php index be37b938..72d2dd29 100644 --- a/src/Cron/FieldInterface.php +++ b/src/Cron/FieldInterface.php @@ -1,6 +1,7 @@ Date: Thu, 14 Feb 2019 10:24:06 -0500 Subject: [PATCH 038/146] Positions used higher than 4 now throw a human readable error --- src/Cron/FieldFactory.php | 2 +- tests/Cron/CronExpressionTest.php | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Cron/FieldFactory.php b/src/Cron/FieldFactory.php index fd27352d..545e4b83 100644 --- a/src/Cron/FieldFactory.php +++ b/src/Cron/FieldFactory.php @@ -44,7 +44,7 @@ public function getField($position) break; default: throw new InvalidArgumentException( - $position . ' is not a valid position' + ($position + 1) . ' is not a valid position' ); } } diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 5d46644b..678501be 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -557,4 +557,16 @@ public function testRangesWrapAroundWithLargeSteps() $nextRunDate = $e->getNextRunDate(new DateTime('2014-05-07 00:00:00')); $this->assertSame('2015-04-01 00:00:00', $nextRunDate->format('Y-m-d H:i:s')); } + + /** + * When there is an issue with a field, we should report the human readable position + * + * @see https://github.com/dragonmantank/cron-expression/issues/29 + */ + public function testFieldPositionIsHumanAdjusted() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("6 is not a valid position"); + $e = CronExpression::factory('0 * * * * ? *'); + } } From ab9f57a902d4ae351e061e0d3229160c58f76748 Mon Sep 17 00:00:00 2001 From: Atef Ben Ali Date: Thu, 13 Sep 2018 07:15:20 +0100 Subject: [PATCH 039/146] highlight variables --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 19adcc9c..8e8021b2 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ PHP Cron Expression Parser The PHP cron expression parser can parse a CRON expression, determine if it is due to run, calculate the next run date of the expression, and calculate the previous run date of the expression. You can calculate dates far into the future or past by -skipping n number of matching dates. +skipping **n** number of matching dates. The parser can handle increments of ranges (e.g. */12, 2-59/3), intervals (e.g. 0-9), -lists (e.g. 1,2,3), W to find the nearest weekday for a given day of the month, L to -find the last day of the month, L to find the last given weekday of a month, and hash +lists (e.g. 1,2,3), **W** to find the nearest weekday for a given day of the month, **L** to +find the last day of the month, **L** to find the last given weekday of a month, and hash (#) to find the nth weekday of a given month. More information about this fork can be found in the blog post [here](http://ctankersley.com/2017/10/12/cron-expression-update/). tl;dr - v2.0.0 is a major breaking change, and @dragonmantank can better take care of the project in a separate fork. From e54f124620592816ef85f3c5aedc6299f2d9985f Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Thu, 14 Feb 2019 10:33:13 -0500 Subject: [PATCH 040/146] Added PHP 7.3 support, fixed nightly --- .travis.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 479b6135..83ea1084 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,11 +13,14 @@ matrix: - php: 7.2 - php: 7.2 env: dependencies=lowest - - php: nighly - - php: nighly + - php: 7.3 + - php: 7.3 + env: dependencies=lowest + - php: nightly + - php: nightly env: dependencies=lowest allow_failures: - - php: nighly + - php: nightly fast_finish: true install: From 72b6fbf76adb3cf5bc0db68559b33d41219aba27 Mon Sep 17 00:00:00 2001 From: Chris Morbitzer Date: Wed, 6 Mar 2019 13:27:19 -0600 Subject: [PATCH 041/146] Support DateTimeImmutable --- CHANGELOG.md | 10 +++++ src/Cron/CronExpression.php | 61 +++++++++++++++--------------- src/Cron/DayOfMonthField.php | 13 ++++--- src/Cron/DayOfWeekField.php | 21 +++++----- src/Cron/FieldInterface.php | 14 +++---- src/Cron/HoursField.php | 31 +++++++-------- src/Cron/MinutesField.php | 25 ++++++------ src/Cron/MonthField.php | 18 +++++---- tests/Cron/CronExpressionTest.php | 14 +++++++ tests/Cron/DayOfMonthFieldTest.php | 13 +++++++ tests/Cron/DayOfWeekFieldTest.php | 13 +++++++ tests/Cron/HoursFieldTest.php | 24 +++++++++++- tests/Cron/MinutesFieldTest.php | 22 +++++++++++ tests/Cron/MonthFieldTest.php | 22 +++++++++++ 14 files changed, 214 insertions(+), 87 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cb3a084..e3939df5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log +## [2.3.0] - 2019-03-30 +### Added +- Added support for DateTimeImmutable via DateTimeInterface +- Added support for PHP 7.3 +- Started listing projects that use the library +### Changed +- Errors should now report a human readable position in the cron expression, instead of starting at 0 +### Fixed +- N/A + ## [2.2.0] - 2018-06-05 ### Added - Added support for steps larger than field ranges (#6) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index 82c0ef1c..594b4358 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -179,16 +179,17 @@ public function setMaxIterationCount($maxIterationCount) /** * Get a next run date relative to the current date or a specific date * - * @param string|\DateTime $currentTime Relative calculation date - * @param int $nth Number of matches to skip before returning a - * matching next run date. 0, the default, will return the current - * date and time if the next run date falls on the current date and - * time. Setting this value to 1 will skip the first match and go to - * the second match. Setting this value to 2 will skip the first 2 - * matches and so on. - * @param bool $allowCurrentDate Set to TRUE to return the current date if - * it matches the cron expression. - * @param null|string $timeZone TimeZone to use instead of the system default + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param int $nth Number of matches to skip before returning a + * matching next run date. 0, the default, will return the + * current date and time if the next run date falls on the + * current date and time. Setting this value to 1 will + * skip the first match and go to the second match. + * Setting this value to 2 will skip the first 2 + * matches and so on. + * @param bool $allowCurrentDate Set to TRUE to return the current date if + * it matches the cron expression. + * @param null|string $timeZone TimeZone to use instead of the system default * * @return \DateTime * @throws \RuntimeException on too many iterations @@ -201,11 +202,11 @@ public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate /** * Get a previous run date relative to the current date or a specific date * - * @param string|\DateTime $currentTime Relative calculation date - * @param int $nth Number of matches to skip before returning - * @param bool $allowCurrentDate Set to TRUE to return the - * current date if it matches the cron expression - * @param null|string $timeZone TimeZone to use instead of the system default + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param int $nth Number of matches to skip before returning + * @param bool $allowCurrentDate Set to TRUE to return the + * current date if it matches the cron expression + * @param null|string $timeZone TimeZone to use instead of the system default * * @return \DateTime * @throws \RuntimeException on too many iterations @@ -219,14 +220,14 @@ public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrent /** * Get multiple run dates starting at the current date or a specific date * - * @param int $total Set the total number of dates to calculate - * @param string|\DateTime $currentTime Relative calculation date - * @param bool $invert Set to TRUE to retrieve previous dates - * @param bool $allowCurrentDate Set to TRUE to return the - * current date if it matches the cron expression - * @param null|string $timeZone TimeZone to use instead of the system default + * @param int $total Set the total number of dates to calculate + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param bool $invert Set to TRUE to retrieve previous dates + * @param bool $allowCurrentDate Set to TRUE to return the + * current date if it matches the cron expression + * @param null|string $timeZone TimeZone to use instead of the system default * - * @return array Returns an array of run dates + * @return \DateTime[] Returns an array of run dates */ public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false, $timeZone = null) { @@ -277,8 +278,8 @@ public function __toString() * specific date. This method assumes that the current number of * seconds are irrelevant, and should be called once per minute. * - * @param string|\DateTime $currentTime Relative calculation date - * @param null|string $timeZone TimeZone to use instead of the system default + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param null|string $timeZone TimeZone to use instead of the system default * * @return bool Returns TRUE if the cron is due to run or FALSE if not */ @@ -310,12 +311,12 @@ public function isDue($currentTime = 'now', $timeZone = null) /** * Get the next or previous run date of the expression relative to a date * - * @param string|\DateTime $currentTime Relative calculation date - * @param int $nth Number of matches to skip before returning - * @param bool $invert Set to TRUE to go backwards in time - * @param bool $allowCurrentDate Set to TRUE to return the - * current date if it matches the cron expression - * @param string|null $timeZone TimeZone to use instead of the system default + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param int $nth Number of matches to skip before returning + * @param bool $invert Set to TRUE to go backwards in time + * @param bool $allowCurrentDate Set to TRUE to return the + * current date if it matches the cron expression + * @param string|null $timeZone TimeZone to use instead of the system default * * @return \DateTime * @throws \RuntimeException on too many iterations diff --git a/src/Cron/DayOfMonthField.php b/src/Cron/DayOfMonthField.php index 3920cc9e..d4552e06 100644 --- a/src/Cron/DayOfMonthField.php +++ b/src/Cron/DayOfMonthField.php @@ -3,6 +3,7 @@ namespace Cron; use DateTime; +use DateTimeInterface; /** * Day of month field. Allows: * , / - ? L W @@ -69,7 +70,7 @@ private static function getNearestWeekday($currentYear, $currentMonth, $targetDa /** * @inheritDoc */ - public function isSatisfiedBy(DateTime $date, $value) + public function isSatisfiedBy(DateTimeInterface $date, $value) { // ? states that the field value is to be skipped if ($value == '?') { @@ -100,15 +101,15 @@ public function isSatisfiedBy(DateTime $date, $value) /** * @inheritDoc + * + * @param \DateTime|\DateTimeImmutable &$date */ - public function increment(DateTime $date, $invert = false) + public function increment(DateTimeInterface &$date, $invert = false) { if ($invert) { - $date->modify('previous day'); - $date->setTime(23, 59); + $date = $date->modify('previous day')->setTime(23, 59); } else { - $date->modify('next day'); - $date->setTime(0, 0); + $date = $date->modify('next day')->setTime(0, 0); } return $this; diff --git a/src/Cron/DayOfWeekField.php b/src/Cron/DayOfWeekField.php index 96fdea3c..9db9e956 100644 --- a/src/Cron/DayOfWeekField.php +++ b/src/Cron/DayOfWeekField.php @@ -3,6 +3,7 @@ namespace Cron; use DateTime; +use DateTimeInterface; use InvalidArgumentException; /** @@ -51,8 +52,10 @@ public function __construct() /** * @inheritDoc + * + * @param \DateTime|\DateTimeImmutable $date */ - public function isSatisfiedBy(DateTime $date, $value) + public function isSatisfiedBy(DateTimeInterface $date, $value) { if ($value == '?') { return true; @@ -71,7 +74,7 @@ public function isSatisfiedBy(DateTime $date, $value) $weekday = str_replace('7', '0', $weekday); $tdate = clone $date; - $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth); + $tdate = $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth); while ($tdate->format('w') != $weekday) { $tdateClone = new DateTime(); $tdate = $tdateClone @@ -114,7 +117,7 @@ public function isSatisfiedBy(DateTime $date, $value) } $tdate = clone $date; - $tdate->setDate($currentYear, $currentMonth, 1); + $tdate = $tdate->setDate($currentYear, $currentMonth, 1); $dayCount = 0; $currentDay = 1; while ($currentDay < $lastDayOfMonth + 1) { @@ -123,7 +126,7 @@ public function isSatisfiedBy(DateTime $date, $value) break; } } - $tdate->setDate($currentYear, $currentMonth, ++$currentDay); + $tdate = $tdate->setDate($currentYear, $currentMonth, ++$currentDay); } return $date->format('j') == $currentDay; @@ -149,15 +152,15 @@ public function isSatisfiedBy(DateTime $date, $value) /** * @inheritDoc + * + * @param \DateTime|\DateTimeImmutable &$date */ - public function increment(DateTime $date, $invert = false) + public function increment(DateTimeInterface &$date, $invert = false) { if ($invert) { - $date->modify('-1 day'); - $date->setTime(23, 59, 0); + $date = $date->modify('-1 day')->setTime(23, 59, 0); } else { - $date->modify('+1 day'); - $date->setTime(0, 0, 0); + $date = $date->modify('+1 day')->setTime(0, 0, 0); } return $this; diff --git a/src/Cron/FieldInterface.php b/src/Cron/FieldInterface.php index 72d2dd29..f8366eae 100644 --- a/src/Cron/FieldInterface.php +++ b/src/Cron/FieldInterface.php @@ -2,7 +2,7 @@ namespace Cron; -use DateTime; +use DateTimeInterface; /** * CRON field interface @@ -12,23 +12,23 @@ interface FieldInterface /** * Check if the respective value of a DateTime field satisfies a CRON exp * - * @param DateTime $date DateTime object to check - * @param string $value CRON expression to test against + * @param DateTimeInterface $date DateTime object to check + * @param string $value CRON expression to test against * * @return bool Returns TRUE if satisfied, FALSE otherwise */ - public function isSatisfiedBy(DateTime $date, $value); + public function isSatisfiedBy(DateTimeInterface $date, $value); /** * When a CRON expression is not satisfied, this method is used to increment * or decrement a DateTime object by the unit of the cron field * - * @param DateTime $date DateTime object to change - * @param bool $invert (optional) Set to TRUE to decrement + * @param DateTimeInterface &$date DateTime object to change + * @param bool $invert (optional) Set to TRUE to decrement * * @return FieldInterface */ - public function increment(DateTime $date, $invert = false); + public function increment(DateTimeInterface &$date, $invert = false); /** * Validates a CRON expression for a given field diff --git a/src/Cron/HoursField.php b/src/Cron/HoursField.php index 0e6dd74c..628218a6 100644 --- a/src/Cron/HoursField.php +++ b/src/Cron/HoursField.php @@ -2,7 +2,7 @@ namespace Cron; -use DateTime; +use DateTimeInterface; use DateTimeZone; /** @@ -23,32 +23,33 @@ class HoursField extends AbstractField /** * @inheritDoc */ - public function isSatisfiedBy(DateTime $date, $value) + public function isSatisfiedBy(DateTimeInterface $date, $value) { + if ($value == '?') { + return true; + } + return $this->isSatisfied($date->format('H'), $value); } /** * {@inheritDoc} * - * @param string|null $parts + * @param \DateTime|\DateTimeImmutable &$date + * @param string|null $parts */ - public function increment(DateTime $date, $invert = false, $parts = null) + public function increment(DateTimeInterface &$date, $invert = false, $parts = null) { // Change timezone to UTC temporarily. This will // allow us to go back or forwards and hour even // if DST will be changed between the hours. if (is_null($parts) || $parts == '*') { $timezone = $date->getTimezone(); - $date->setTimezone(new DateTimeZone('UTC')); - if ($invert) { - $date->modify('-1 hour'); - } else { - $date->modify('+1 hour'); - } - $date->setTimezone($timezone); + $date = $date->setTimezone(new DateTimeZone('UTC')); + $date = $date->modify(($invert ? '-' : '+') . '1 hour'); + $date = $date->setTimezone($timezone); - $date->setTime($date->format('H'), $invert ? 59 : 0); + $date = $date->setTime($date->format('H'), $invert ? 59 : 0); return $this; } @@ -72,11 +73,11 @@ public function increment(DateTime $date, $invert = false, $parts = null) $hour = $hours[$position]; if ((!$invert && $date->format('H') >= $hour) || ($invert && $date->format('H') <= $hour)) { - $date->modify(($invert ? '-' : '+') . '1 day'); - $date->setTime($invert ? 23 : 0, $invert ? 59 : 0); + $date = $date->modify(($invert ? '-' : '+') . '1 day'); + $date = $date->setTime($invert ? 23 : 0, $invert ? 59 : 0); } else { - $date->setTime($hour, $invert ? 59 : 0); + $date = $date->setTime($hour, $invert ? 59 : 0); } return $this; diff --git a/src/Cron/MinutesField.php b/src/Cron/MinutesField.php index e8aea7aa..fecc9b6d 100644 --- a/src/Cron/MinutesField.php +++ b/src/Cron/MinutesField.php @@ -2,7 +2,7 @@ namespace Cron; -use DateTime; +use DateTimeInterface; /** * Minutes field. Allows: * , / - @@ -22,24 +22,25 @@ class MinutesField extends AbstractField /** * @inheritDoc */ - public function isSatisfiedBy(DateTime $date, $value) + public function isSatisfiedBy(DateTimeInterface $date, $value) { + if ($value == '?') { + return true; + } + return $this->isSatisfied($date->format('i'), $value); } /** * {@inheritDoc} * - * @param string|null $parts + * @param \DateTime|\DateTimeImmutable &$date + * @param string|null $parts */ - public function increment(DateTime $date, $invert = false, $parts = null) + public function increment(DateTimeInterface &$date, $invert = false, $parts = null) { if (is_null($parts)) { - if ($invert) { - $date->modify('-1 minute'); - } else { - $date->modify('+1 minute'); - } + $date = $date->modify(($invert ? '-' : '+') . '1 minute'); return $this; } @@ -62,11 +63,11 @@ public function increment(DateTime $date, $invert = false, $parts = null) } if ((!$invert && $current_minute >= $minutes[$position]) || ($invert && $current_minute <= $minutes[$position])) { - $date->modify(($invert ? '-' : '+') . '1 hour'); - $date->setTime($date->format('H'), $invert ? 59 : 0); + $date = $date->modify(($invert ? '-' : '+') . '1 hour'); + $date = $date->setTime($date->format('H'), $invert ? 59 : 0); } else { - $date->setTime($date->format('H'), $minutes[$position]); + $date = $date->setTime($date->format('H'), $minutes[$position]); } return $this; diff --git a/src/Cron/MonthField.php b/src/Cron/MonthField.php index 238b0e7c..afc9caff 100644 --- a/src/Cron/MonthField.php +++ b/src/Cron/MonthField.php @@ -2,7 +2,7 @@ namespace Cron; -use DateTime; +use DateTimeInterface; /** * Month field. Allows: * , / - @@ -28,8 +28,12 @@ class MonthField extends AbstractField /** * @inheritDoc */ - public function isSatisfiedBy(DateTime $date, $value) + public function isSatisfiedBy(DateTimeInterface $date, $value) { + if ($value == '?') { + return true; + } + $value = $this->convertLiterals($value); return $this->isSatisfied($date->format('m'), $value); @@ -37,15 +41,15 @@ public function isSatisfiedBy(DateTime $date, $value) /** * @inheritDoc + * + * @param \DateTime|\DateTimeImmutable &$date */ - public function increment(DateTime $date, $invert = false) + public function increment(DateTimeInterface &$date, $invert = false) { if ($invert) { - $date->modify('last day of previous month'); - $date->setTime(23, 59); + $date = $date->modify('last day of previous month')->setTime(23, 59); } else { - $date->modify('first day of next month'); - $date->setTime(0, 0); + $date = $date->modify('first day of next month')->setTime(0, 0); } return $this; diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 678501be..9b82ae5b 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -5,6 +5,7 @@ use Cron\CronExpression; use Cron\MonthField; use DateTime; +use DateTimeImmutable; use DateTimeZone; use InvalidArgumentException; use PHPUnit\Framework\TestCase; @@ -228,6 +229,7 @@ public function testIsDueHandlesDifferentDates() $this->assertTrue($cron->isDue('now')); $this->assertTrue($cron->isDue(new DateTime('now'))); $this->assertTrue($cron->isDue(date('Y-m-d H:i'))); + $this->assertTrue($cron->isDue(new DateTimeImmutable('now'))); } /** @@ -407,6 +409,18 @@ public function testCanIterateOverNextRuns() $this->assertEquals($nextRun, new DateTime("2008-11-30 00:00:00")); } + /** + * @covers \Cron\CronExpression::getRunDate + */ + public function testGetRunDateHandlesDifferentDates() + { + $cron = CronExpression::factory('@weekly'); + $date = new DateTime("2019-03-10 00:00:00"); + $this->assertEquals($date, $cron->getNextRunDate("2019-03-03 08:00:00")); + $this->assertEquals($date, $cron->getNextRunDate(new DateTime("2019-03-03 08:00:00"))); + $this->assertEquals($date, $cron->getNextRunDate(new DateTimeImmutable("2019-03-03 08:00:00"))); + } + /** * @covers \Cron\CronExpression::getRunDate */ diff --git a/tests/Cron/DayOfMonthFieldTest.php b/tests/Cron/DayOfMonthFieldTest.php index 0dae4ed6..2191b6bf 100644 --- a/tests/Cron/DayOfMonthFieldTest.php +++ b/tests/Cron/DayOfMonthFieldTest.php @@ -4,6 +4,7 @@ use Cron\DayOfMonthField; use DateTime; +use DateTimeImmutable; use PHPUnit\Framework\TestCase; /** @@ -33,6 +34,7 @@ public function testChecksIfSatisfied() { $f = new DayOfMonthField(); $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?')); + $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?')); } /** @@ -50,6 +52,17 @@ public function testIncrementsDate() $this->assertSame('2011-03-14 23:59:00', $d->format('Y-m-d H:i:s')); } + /** + * @covers \Cron\DayOfMonthField::increment + */ + public function testIncrementsDateTimeImmutable() + { + $d = new DateTimeImmutable('2011-03-15 11:15:00'); + $f = new DayOfMonthField(); + $f->increment($d); + $this->assertSame('2011-03-16 00:00:00', $d->format('Y-m-d H:i:s')); + } + /** * Day of the month cannot accept a 0 value, it must be between 1 and 31 * See Github issue #120 diff --git a/tests/Cron/DayOfWeekFieldTest.php b/tests/Cron/DayOfWeekFieldTest.php index eaed77ce..ef89b47f 100644 --- a/tests/Cron/DayOfWeekFieldTest.php +++ b/tests/Cron/DayOfWeekFieldTest.php @@ -4,6 +4,7 @@ use Cron\DayOfWeekField; use DateTime; +use DateTimeImmutable; use PHPUnit\Framework\TestCase; /** @@ -33,6 +34,7 @@ public function testChecksIfSatisfied() { $f = new DayOfWeekField(); $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?')); + $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?')); } /** @@ -50,6 +52,17 @@ public function testIncrementsDate() $this->assertSame('2011-03-14 23:59:00', $d->format('Y-m-d H:i:s')); } + /** + * @covers \Cron\DayOfWeekField::increment + */ + public function testIncrementsDateTimeImmutable() + { + $d = new DateTimeImmutable('2011-03-15 11:15:00'); + $f = new DayOfWeekField(); + $f->increment($d); + $this->assertSame('2011-03-16 00:00:00', $d->format('Y-m-d H:i:s')); + } + /** * @covers \Cron\DayOfWeekField::isSatisfiedBy * @expectedException InvalidArgumentException diff --git a/tests/Cron/HoursFieldTest.php b/tests/Cron/HoursFieldTest.php index e936d11a..1849f28b 100644 --- a/tests/Cron/HoursFieldTest.php +++ b/tests/Cron/HoursFieldTest.php @@ -4,6 +4,7 @@ use Cron\HoursField; use DateTime; +use DateTimeImmutable; use PHPUnit\Framework\TestCase; /** @@ -22,7 +23,17 @@ public function testValidatesField() $this->assertTrue($f->validate('01')); $this->assertTrue($f->validate('*')); $this->assertFalse($f->validate('*/3,1,1-12')); - } + } + + /** + * @covers \Cron\HoursField::isSatisfiedBy + */ + public function testChecksIfSatisfied() + { + $f = new HoursField(); + $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?')); + $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?')); + } /** * @covers \Cron\HoursField::increment @@ -39,6 +50,17 @@ public function testIncrementsDate() $this->assertSame('2011-03-15 10:59:00', $d->format('Y-m-d H:i:s')); } + /** + * @covers \Cron\HoursField::increment + */ + public function testIncrementsDateTimeImmutable() + { + $d = new DateTimeImmutable('2011-03-15 11:15:00'); + $f = new HoursField(); + $f->increment($d); + $this->assertSame('2011-03-15 12:00:00', $d->format('Y-m-d H:i:s')); + } + /** * @covers \Cron\HoursField::increment */ diff --git a/tests/Cron/MinutesFieldTest.php b/tests/Cron/MinutesFieldTest.php index b91bffac..41a536d6 100644 --- a/tests/Cron/MinutesFieldTest.php +++ b/tests/Cron/MinutesFieldTest.php @@ -4,6 +4,7 @@ use Cron\MinutesField; use DateTime; +use DateTimeImmutable; use PHPUnit\Framework\TestCase; /** @@ -22,6 +23,16 @@ public function testValidatesField() $this->assertFalse($f->validate('*/3,1,1-12')); } + /** + * @covers \Cron\MinutesField::isSatisfiedBy + */ + public function testChecksIfSatisfied() + { + $f = new MinutesField(); + $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?')); + $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?')); + } + /** * @covers \Cron\MinutesField::increment */ @@ -35,6 +46,17 @@ public function testIncrementsDate() $this->assertSame('2011-03-15 11:15:00', $d->format('Y-m-d H:i:s')); } + /** + * @covers \Cron\MinutesField::increment + */ + public function testIncrementsDateTimeImmutable() + { + $d = new DateTimeImmutable('2011-03-15 11:15:00'); + $f = new MinutesField(); + $f->increment($d); + $this->assertSame('2011-03-15 11:16:00', $d->format('Y-m-d H:i:s')); + } + /** * Various bad syntaxes that are reported to work, but shouldn't. * diff --git a/tests/Cron/MonthFieldTest.php b/tests/Cron/MonthFieldTest.php index 83f0f164..f329f4c1 100644 --- a/tests/Cron/MonthFieldTest.php +++ b/tests/Cron/MonthFieldTest.php @@ -4,6 +4,7 @@ use Cron\MonthField; use DateTime; +use DateTimeImmutable; use PHPUnit\Framework\TestCase; /** @@ -23,6 +24,16 @@ public function testValidatesField() $this->assertFalse($f->validate('1.fix-regexp')); } + /** + * @covers \Cron\MonthField::isSatisfiedBy + */ + public function testChecksIfSatisfied() + { + $f = new MonthField(); + $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?')); + $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?')); + } + /** * @covers \Cron\MonthField::increment */ @@ -38,6 +49,17 @@ public function testIncrementsDate() $this->assertSame('2011-02-28 23:59:00', $d->format('Y-m-d H:i:s')); } + /** + * @covers \Cron\MonthField::increment + */ + public function testIncrementsDateTimeImmutable() + { + $d = new DateTimeImmutable('2011-03-15 11:15:00'); + $f = new MonthField(); + $f->increment($d); + $this->assertSame('2011-04-01 00:00:00', $d->format('Y-m-d H:i:s')); + } + /** * @covers \Cron\MonthField::increment */ From be02d48f73fb4260361c2e4670146c1c9f60c2f3 Mon Sep 17 00:00:00 2001 From: Menno Holtkamp Date: Sun, 24 Feb 2019 17:46:51 +0100 Subject: [PATCH 042/146] Introduce PHPStan --- composer.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c0b7903f..5b7e6585 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ } ], "require": { - "php": "^7.0" + "php": "^7.0", + "phpstan/phpstan": "^0.11" }, "require-dev": { "phpunit/phpunit": "^6.4|^7.0" @@ -36,5 +37,8 @@ "branch-alias": { "dev-master": "2.3-dev" } + }, + "scripts": { + "phpstan": "./vendor/bin/phpstan analyse -l 1 src" } } From df735eff428ee3357b20a98424b16f9065af0803 Mon Sep 17 00:00:00 2001 From: Menno Holtkamp Date: Sun, 24 Feb 2019 18:02:33 +0100 Subject: [PATCH 043/146] Reduce strictness level to add tests to analysis --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 5b7e6585..36a789f4 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ }, "autoload-dev": { "psr-4": { - "Tests\\": "tests/Cron/" + "Cron\\Tests\\": "tests/Cron/" } }, "extra": { @@ -39,6 +39,6 @@ } }, "scripts": { - "phpstan": "./vendor/bin/phpstan analyse -l 1 src" + "phpstan": "./vendor/bin/phpstan analyse -l 0 src tests" } } From 7bee65e57f1554b0bc2f708ed1493716b0d30c16 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Sat, 30 Mar 2019 20:44:29 -0400 Subject: [PATCH 044/146] Upped PHP to 7.2 --- .travis.yml | 6 ------ composer.json | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 83ea1084..474bbced 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,12 +4,6 @@ sudo: false matrix: include: - - php: 7.0 - - php: 7.0 - env: dependencies=lowest - - php: 7.1 - - php: 7.1 - env: dependencies=lowest - php: 7.2 - php: 7.2 env: dependencies=lowest diff --git a/composer.json b/composer.json index 36a789f4..9762b7d4 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ } ], "require": { - "php": "^7.0", + "php": "^7.2", "phpstan/phpstan": "^0.11" }, "require-dev": { From 548ad7eeb0a259395d64798f61a30ee7905115f5 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Sat, 30 Mar 2019 20:58:51 -0400 Subject: [PATCH 045/146] Cleaned up some issues from rebasing --- src/Cron/HoursField.php | 2 +- src/Cron/MinutesField.php | 2 +- tests/Cron/CronExpressionTest.php | 12 ------------ 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/Cron/HoursField.php b/src/Cron/HoursField.php index 0d80958a..f3d2f47c 100644 --- a/src/Cron/HoursField.php +++ b/src/Cron/HoursField.php @@ -47,7 +47,7 @@ public function increment(DateTimeInterface &$date, $invert = false, $parts = nu $date = $date->modify(($invert ? '-' : '+') . '1 hour'); $date = $date->setTimezone($timezone); - $date = $date->setTime($date->format('H'), $invert ? 59 : 0); + $date = $date->setTime((int)$date->format('H'), $invert ? 59 : 0); return $this; } diff --git a/src/Cron/MinutesField.php b/src/Cron/MinutesField.php index c52d2eb2..a32e9a65 100644 --- a/src/Cron/MinutesField.php +++ b/src/Cron/MinutesField.php @@ -30,7 +30,7 @@ public function isSatisfiedBy(DateTimeInterface $date, $value):bool return true; } - return $this->isSatisfied($date->format('i'), $value); + return $this->isSatisfied((int)$date->format('i'), $value); } /** diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 8c8fcbd4..5de02037 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -423,18 +423,6 @@ public function testGetRunDateHandlesDifferentDates() $this->assertEquals($date, $cron->getNextRunDate(new DateTimeImmutable("2019-03-03 08:00:00"))); } - /** - * @covers \Cron\CronExpression::getRunDate - */ - public function testGetRunDateHandlesDifferentDates() - { - $cron = CronExpression::factory('@weekly'); - $date = new DateTime("2019-03-10 00:00:00"); - $this->assertEquals($date, $cron->getNextRunDate("2019-03-03 08:00:00")); - $this->assertEquals($date, $cron->getNextRunDate(new DateTime("2019-03-03 08:00:00"))); - $this->assertEquals($date, $cron->getNextRunDate(new DateTimeImmutable("2019-03-03 08:00:00"))); - } - /** * @covers \Cron\CronExpression::getRunDate */ From 1aba9b92960c86e606aa9acf370817a23797179e Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Sat, 30 Mar 2019 21:04:45 -0400 Subject: [PATCH 046/146] Removed test that checked for invalid character --- tests/Cron/HoursFieldTest.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/tests/Cron/HoursFieldTest.php b/tests/Cron/HoursFieldTest.php index 86847cbd..7cd4baeb 100644 --- a/tests/Cron/HoursFieldTest.php +++ b/tests/Cron/HoursFieldTest.php @@ -27,17 +27,7 @@ public function testValidatesField() $this->assertFalse($f->validate('*/3,1,1-12')); } - /** - * @covers \Cron\HoursField::isSatisfiedBy - */ - public function testChecksIfSatisfied() - { - $f = new HoursField(); - $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?')); - $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?')); - } - - /** + /** * @covers \Cron\HoursField::increment */ public function testIncrementsDate() From 0bea225b5c298b0a05a3b437552f609c59d0c052 Mon Sep 17 00:00:00 2001 From: Menno Holtkamp Date: Mon, 15 Apr 2019 08:26:36 +0200 Subject: [PATCH 047/146] Move PHPStan to require-dev --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 9762b7d4..c641c330 100644 --- a/composer.json +++ b/composer.json @@ -17,10 +17,10 @@ } ], "require": { - "php": "^7.2", - "phpstan/phpstan": "^0.11" + "php": "^7.2" }, "require-dev": { + "phpstan/phpstan": "^0.11", "phpunit/phpunit": "^6.4|^7.0" }, "autoload": { From d48a515b5de08bd0f957aaa0874c0a32d27fbe96 Mon Sep 17 00:00:00 2001 From: Piotr Konieczny Date: Thu, 25 Apr 2019 10:44:10 +0200 Subject: [PATCH 048/146] Invalid numeric ranges --- src/Cron/AbstractField.php | 5 +++++ tests/Cron/HoursFieldTest.php | 1 + tests/Cron/MinutesFieldTest.php | 1 + tests/Cron/MonthFieldTest.php | 1 + 4 files changed, 8 insertions(+) diff --git a/src/Cron/AbstractField.php b/src/Cron/AbstractField.php index 0b5e6f4d..263ce986 100644 --- a/src/Cron/AbstractField.php +++ b/src/Cron/AbstractField.php @@ -250,6 +250,11 @@ public function validate(string $value): bool if (false !== strpos($value, '/')) { [$range, $step] = explode('/', $value); + // Don't allow numeric ranges + if (is_numeric($range)) { + return false; + } + return $this->validate($range) && filter_var($step, FILTER_VALIDATE_INT); } diff --git a/tests/Cron/HoursFieldTest.php b/tests/Cron/HoursFieldTest.php index 7cd4baeb..9b05a4ae 100644 --- a/tests/Cron/HoursFieldTest.php +++ b/tests/Cron/HoursFieldTest.php @@ -25,6 +25,7 @@ public function testValidatesField() $this->assertTrue($f->validate('01')); $this->assertTrue($f->validate('*')); $this->assertFalse($f->validate('*/3,1,1-12')); + $this->assertFalse($f->validate('1/10')); } /** diff --git a/tests/Cron/MinutesFieldTest.php b/tests/Cron/MinutesFieldTest.php index ebf0c27c..6d38a565 100644 --- a/tests/Cron/MinutesFieldTest.php +++ b/tests/Cron/MinutesFieldTest.php @@ -23,6 +23,7 @@ public function testValidatesField() $this->assertTrue($f->validate('1')); $this->assertTrue($f->validate('*')); $this->assertFalse($f->validate('*/3,1,1-12')); + $this->assertFalse($f->validate('1/10')); } /** diff --git a/tests/Cron/MonthFieldTest.php b/tests/Cron/MonthFieldTest.php index 633a3f5d..05777a84 100644 --- a/tests/Cron/MonthFieldTest.php +++ b/tests/Cron/MonthFieldTest.php @@ -24,6 +24,7 @@ public function testValidatesField() $this->assertTrue($f->validate('*')); $this->assertFalse($f->validate('*/10,2,1-12')); $this->assertFalse($f->validate('1.fix-regexp')); + $this->assertFalse($f->validate('1/10')); } /** From d75fc51906d587d001bbf82245f931cf06aac379 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Thu, 27 Jun 2019 11:19:26 +0100 Subject: [PATCH 049/146] Switch to short array syntax --- .styleci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.styleci.yml b/.styleci.yml index 9b04de91..db567493 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -1,5 +1,5 @@ preset: psr2 enabled: - - long_array_syntax + - short_array_syntax - method_separation From c6fee8cd00763288a8229e4fdf063625b1cd6209 Mon Sep 17 00:00:00 2001 From: dragonmantank Date: Mon, 29 Jul 2019 21:06:33 +0000 Subject: [PATCH 050/146] Apply fixes from StyleCI --- src/Cron/CronExpression.php | 2 +- tests/Cron/HoursFieldTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index b00f51d4..01307393 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -421,7 +421,7 @@ protected function determineTimeZone($currentTime, $timeZone): string return $timeZone; } - if ($currentTime instanceOf DateTimeInterface) { + if ($currentTime instanceof DateTimeInterface) { return $currentTime->getTimeZone()->getName(); } diff --git a/tests/Cron/HoursFieldTest.php b/tests/Cron/HoursFieldTest.php index 7cd4baeb..0eebca6c 100644 --- a/tests/Cron/HoursFieldTest.php +++ b/tests/Cron/HoursFieldTest.php @@ -27,7 +27,7 @@ public function testValidatesField() $this->assertFalse($f->validate('*/3,1,1-12')); } - /** + /** * @covers \Cron\HoursField::increment */ public function testIncrementsDate() From 1fcc224a7e95aeffb4dfa6f06abd4f254c844d92 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Mon, 29 Jul 2019 23:26:05 -0400 Subject: [PATCH 051/146] Dropped support back to 7.1 from 7.2 --- .travis.yml | 3 +++ composer.json | 7 +------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 474bbced..e621bdf7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,9 @@ sudo: false matrix: include: + - php: 7.1 + - php: 7.1 + env: dependencies=lowest - php: 7.2 - php: 7.2 env: dependencies=lowest diff --git a/composer.json b/composer.json index c641c330..a1b503ee 100644 --- a/composer.json +++ b/composer.json @@ -5,11 +5,6 @@ "keywords": ["cron", "schedule"], "license": "MIT", "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, { "name": "Chris Tankersley", "email": "chris@ctankersley.com", @@ -17,7 +12,7 @@ } ], "require": { - "php": "^7.2" + "php": "^7.1" }, "require-dev": { "phpstan/phpstan": "^0.11", From b80b95b094636e7661f60fbf85e5188c273f72b8 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Mon, 29 Jul 2019 23:30:25 -0400 Subject: [PATCH 052/146] Added macros we support --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e8021b2..9f786017 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ PHP Cron Expression Parser ========================== -[![Latest Stable Version](https://poser.pugx.org/dragonmantank/cron-expression/v/stable.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Total Downloads](https://poser.pugx.org/dragonmantank/cron-expression/downloads.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Build Status](https://secure.travis-ci.org/dragonmantank/cron-expression.png)](http://travis-ci.org/dragonmantank/cron-expression) +[![Latest Stable Version](https://poser.pugx.org/dragonmantank/cron-expression/v/stable.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Total Downloads](https://poser.pugx.org/dragonmantank/cron-expression/downloads.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Build Status](https://secure.travis-ci.org/dragonmantank/cron-expression.png)](http://travis-ci.org/dragonmantank/cron-expression) [![StyleCI](https://github.styleci.io/repos/103715337/shield?branch=master)](https://github.styleci.io/repos/103715337) The PHP cron expression parser can parse a CRON expression, determine if it is due to run, calculate the next run date of the expression, and calculate the previous @@ -65,6 +65,14 @@ A CRON expression is a string representing the schedule for a particular command | +-------------------- hour (0 - 23) +------------------------- min (0 - 59) +This library also supports a few macros: + +* `@yearly`, `@annually` - Run once a year, midnight, Jan. 1 - 0 0 1 1 * +* `@monthly` - Run once a month, midnight, first of month - 0 0 1 * * +* `@weekly` - Run once a week, midnight on Sun - 0 0 * * 0 +* `@daily` - Run once a day, midnight - 0 0 * * * +* `@hourly` - Run once an hour, first minute - 0 * * * * + Requirements ============ From 9420e43c8b375bad1211e20e20055f6da70681d4 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Mon, 29 Jul 2019 23:33:24 -0400 Subject: [PATCH 053/146] Cleaned up some README formatting, and typo on PHP version --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9f786017..86f8c9ad 100644 --- a/README.md +++ b/README.md @@ -67,16 +67,16 @@ A CRON expression is a string representing the schedule for a particular command This library also supports a few macros: -* `@yearly`, `@annually` - Run once a year, midnight, Jan. 1 - 0 0 1 1 * -* `@monthly` - Run once a month, midnight, first of month - 0 0 1 * * -* `@weekly` - Run once a week, midnight on Sun - 0 0 * * 0 -* `@daily` - Run once a day, midnight - 0 0 * * * -* `@hourly` - Run once an hour, first minute - 0 * * * * +* `@yearly`, `@annually` - Run once a year, midnight, Jan. 1 - `0 0 1 1 *` +* `@monthly` - Run once a month, midnight, first of month - `0 0 1 * *` +* `@weekly` - Run once a week, midnight on Sun - `0 0 * * 0` +* `@daily` - Run once a day, midnight - `0 0 * * *` +* `@hourly` - Run once an hour, first minute - `0 * * * *` Requirements ============ -- PHP 7.0+ +- PHP 7.1+ - PHPUnit is required to run the unit tests - Composer is required to run the unit tests From b8d6872a13f02f4a7712acb30bfcd7289d2ba3ee Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Mon, 29 Jul 2019 23:38:50 -0400 Subject: [PATCH 054/146] Verified that 0/5 should now fail --- tests/Cron/MinutesFieldTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/Cron/MinutesFieldTest.php b/tests/Cron/MinutesFieldTest.php index 6d38a565..49aa67b5 100644 --- a/tests/Cron/MinutesFieldTest.php +++ b/tests/Cron/MinutesFieldTest.php @@ -74,4 +74,18 @@ public function testBadSyntaxesShouldNotValidate() $this->assertFalse($f->validate('1-2-3')); $this->assertFalse($f->validate('-1')); } + + /** + * Ranges that are invalid should not validate. + * In this case `0/5` would be invalid because `0` is not part of the minute range. + * + * @author Chris Tankersley + * @since 2019-07-29 + * @see https://github.com/dragonmantank/cron-expression/issues/18 + */ + public function testInvalidRangeShouldNotValidate() + { + $f = new MinutesField(); + $this->assertFalse($f->validate('0/5')); + } } From c9d0f452bcc19c3f788132f14de2920b958b00f1 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Mon, 29 Jul 2019 23:46:44 -0400 Subject: [PATCH 055/146] Fixed #24, incoming literals are automatically converted to uppercase --- src/Cron/AbstractField.php | 2 +- tests/Cron/DayOfWeekFieldTest.php | 15 +++++++++++++++ tests/Cron/MonthFieldTest.php | 15 +++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/Cron/AbstractField.php b/src/Cron/AbstractField.php index 263ce986..5784189a 100644 --- a/src/Cron/AbstractField.php +++ b/src/Cron/AbstractField.php @@ -222,7 +222,7 @@ public function getRangeForExpression(string $expression, int $max): array protected function convertLiterals(string $value): string { if (\count($this->literals)) { - $key = array_search($value, $this->literals, true); + $key = array_search(strtoupper($value), $this->literals, true); if (false !== $key) { return (string) $key; } diff --git a/tests/Cron/DayOfWeekFieldTest.php b/tests/Cron/DayOfWeekFieldTest.php index 72fc0814..26c56ccc 100644 --- a/tests/Cron/DayOfWeekFieldTest.php +++ b/tests/Cron/DayOfWeekFieldTest.php @@ -154,4 +154,19 @@ public function testLiteralsExpandProperly() $this->assertTrue($f->validate('MON-FRI')); $this->assertSame([1, 2, 3, 4, 5], $f->getRangeForExpression('MON-FRI', 7)); } + + /** + * Incoming literals should ignore case + * + * @author Chris Tankersley assertTrue($f->validate('MON')); + $this->assertTrue($f->validate('Mon')); + $this->assertTrue($f->validate('mon')); + } } diff --git a/tests/Cron/MonthFieldTest.php b/tests/Cron/MonthFieldTest.php index 05777a84..fe20e2cc 100644 --- a/tests/Cron/MonthFieldTest.php +++ b/tests/Cron/MonthFieldTest.php @@ -102,4 +102,19 @@ public function testDecrementsYearAsNeeded() $f->increment($d, true); $this->assertSame('2010-12-31 23:59:00', $d->format('Y-m-d H:i:s')); } + + /** + * Incoming literals should ignore case + * + * @author Chris Tankersley assertTrue($f->validate('JAN')); + $this->assertTrue($f->validate('Jan')); + $this->assertTrue($f->validate('jan')); + } } From d7ca62d064619fb4b459d9f2e1a93463cdce2c1b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 1 Aug 2019 13:11:55 +0200 Subject: [PATCH 056/146] fixed constructor optional argument --- src/Cron/CronExpression.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index 01307393..b01a35de 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -115,7 +115,7 @@ public static function isValidExpression(string $expression): bool */ public function __construct(string $expression, FieldFactory $fieldFactory = null) { - $this->fieldFactory = $fieldFactory; + $this->fieldFactory = $fieldFactory ?: new FieldFactory(); $this->setExpression($expression); } From df10c806c00b238d7d7a46327fd7966cd53a4087 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 1 Aug 2019 14:08:58 +0200 Subject: [PATCH 057/146] fixed issue with timezones --- src/Cron/CronExpression.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index 01307393..8f2b19b5 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -312,7 +312,7 @@ public function isDue($currentTime = 'now', $timeZone = null): ?bool $currentTime->setTimezone(new DateTimeZone($timeZone)); // drop the seconds to 0 - $currentTime = DateTime::createFromFormat('Y-m-d H:i', $currentTime->format('Y-m-d H:i')); + $currentTime->setTime((int) $currentTime->format('H'), (int) $currentTime->format('i'), 0); try { return $this->getNextRunDate($currentTime, 0, true)->getTimestamp() === $currentTime->getTimestamp(); From e444523aabbb63071f5a611122a960b450389e4c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 1 Aug 2019 14:15:22 +0200 Subject: [PATCH 058/146] fixed cloning dates in isDue() --- src/Cron/CronExpression.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index 01307393..5e5c76be 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -303,7 +303,7 @@ public function isDue($currentTime = 'now', $timeZone = null): ?bool if ('now' === $currentTime) { $currentTime = new DateTime(); } elseif ($currentTime instanceof DateTime) { - // + $currentTime = clone $currentTime; } elseif ($currentTime instanceof DateTimeImmutable) { $currentTime = DateTime::createFromFormat('U', $currentTime->format('U')); } else { From 832f1bd42e593a0c59a1c3665b6e367e0d5fe4e9 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Fri, 11 Oct 2019 00:42:46 -0400 Subject: [PATCH 059/146] Initial fix for and/or problem --- src/Cron/CronExpression.php | 18 ++++++++++++++++++ tests/Cron/CronExpressionTest.php | 19 +++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index b5ba264d..a6871e05 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -365,6 +365,24 @@ protected function getRunDate($currentTime = null, int $nth = 0, bool $invert = $fields[$position] = $this->fieldFactory->getField($position); } + if (isset($parts[2]) && isset($parts[4])) { + $domExpression = sprintf('%s %s %s %s *', $this->getExpression(0), $this->getExpression(1), $this->getExpression(2), $this->getExpression(3)); + $dowExpression = sprintf('%s %s * %s %s', $this->getExpression(0), $this->getExpression(1), $this->getExpression(3), $this->getExpression(4)); + + $domExpression = new self($domExpression); + $dowExpression = new self($dowExpression); + + $domRunDates = $domExpression->getMultipleRunDates($nth + 1, $currentTime, $invert, $allowCurrentDate, $timeZone); + $dowRunDates = $dowExpression->getMultipleRunDates($nth + 1, $currentTime, $invert, $allowCurrentDate, $timeZone); + + $combined = array_merge($domRunDates, $dowRunDates); + usort($combined, function($a, $b) { + return $a->format('Y-m-d H:i:s') <=> $b->format('Y-m-d H:i:s'); + }); + + return $combined[$nth]; + } + // Set a hard limit to bail on an impossible date for ($i = 0; $i < $this->maxIterationCount; ++$i) { foreach ($parts as $position => $part) { diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 5de02037..1bb1278c 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -158,7 +158,7 @@ public function scheduleProvider(): array // Test Day of the Week and the Day of the Month (issue #1) ['0 0 1 1 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false], ['0 0 1 JAN 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false], - ['0 0 1 * 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false], + ['0 0 1 * 0', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false], // Test the W day of the week modifier for day of the month field ['0 0 2W * *', strtotime('2011-07-01 00:00:00'), '2011-07-01 00:00:00', true], ['0 0 1W * *', strtotime('2011-05-01 00:00:00'), '2011-05-02 00:00:00', false], @@ -180,7 +180,7 @@ public function scheduleProvider(): array ['* * * * 3#4', strtotime('2011-07-01 00:00:00'), '2011-07-27 00:00:00', false], // Issue #7, documented example failed - ['3-59/15 6-12 */15 1 2-5', strtotime('2017-01-08 00:00:00'), '2017-01-31 06:03:00', false], + ['3-59/15 6-12 */15 1 2-5', strtotime('2017-01-08 00:00:00'), '2017-01-10 06:03:00', false], // https://github.com/laravel/framework/commit/07d160ac3cc9764d5b429734ffce4fa311385403 ['* * * * MON-FRI', strtotime('2017-01-08 00:00:00'), strtotime('2017-01-09 00:00:00'), false], @@ -586,4 +586,19 @@ public function testFieldPositionIsHumanAdjusted(): void $e = CronExpression::factory('0 * * * * ? *'); } + + /** + * @see https://github.com/dragonmantank/cron-expression/issues/35 + */ + public function testMakeDayOfWeekAnOrSometimes() + { + $cron = CronExpression::factory('30 0 1 * 1'); + $runs = $cron->getMultipleRunDates(5, date("2019-10-10 23:20:00"), false, true); + + $this->assertSame("2019-10-14 00:30:00", $runs[0]->format('Y-m-d H:i:s')); + $this->assertSame("2019-10-21 00:30:00", $runs[1]->format('Y-m-d H:i:s')); + $this->assertSame("2019-10-28 00:30:00", $runs[2]->format('Y-m-d H:i:s')); + $this->assertSame("2019-11-01 00:30:00", $runs[3]->format('Y-m-d H:i:s')); + $this->assertSame("2019-11-04 00:30:00", $runs[4]->format('Y-m-d H:i:s')); + } } From 49c8b8aa358cfb67cf4558be0e2d7118d5b6c249 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Thu, 14 Nov 2019 12:25:39 -0500 Subject: [PATCH 060/146] Make sure that shortcuts are not case sensitive --- src/Cron/CronExpression.php | 5 +++-- tests/Cron/CronExpressionTest.php | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index b5ba264d..e49cce19 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -80,8 +80,9 @@ public static function factory(string $expression, FieldFactory $fieldFactory = '@hourly' => '0 * * * *', ]; - if (isset($mappings[$expression])) { - $expression = $mappings[$expression]; + $shortcut = strtolower($expression); + if (isset($mappings[$shortcut])) { + $expression = $mappings[$shortcut]; } return new static($expression, $fieldFactory ?: new FieldFactory()); diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 5de02037..76340986 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -185,6 +185,11 @@ public function scheduleProvider(): array // https://github.com/laravel/framework/commit/07d160ac3cc9764d5b429734ffce4fa311385403 ['* * * * MON-FRI', strtotime('2017-01-08 00:00:00'), strtotime('2017-01-09 00:00:00'), false], ['* * * * TUE', strtotime('2017-01-08 00:00:00'), strtotime('2017-01-10 00:00:00'), false], + + // Issue #60, make sure that casing is less relevant for shortcuts, months, and days + ['0 1 15 JUL mon,Wed,FRi', strtotime('2019-11-14 00:00:00'), strtotime('2020-07-15 01:00:00'), false], + ['0 1 15 jul mon,Wed,FRi', strtotime('2019-11-14 00:00:00'), strtotime('2020-07-15 01:00:00'), false], + ['@Weekly', strtotime('2019-11-14 00:00:00'), strtotime('2019-11-17 00:00:00'), false], ]; } From 228b7e03ed2bfbf58b3c01670723d447885323d4 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Thu, 14 Nov 2019 12:29:15 -0500 Subject: [PATCH 061/146] Removed branch alias, as actual branches should be used not aliases --- composer.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/composer.json b/composer.json index a1b503ee..687b5e97 100644 --- a/composer.json +++ b/composer.json @@ -28,11 +28,6 @@ "Cron\\Tests\\": "tests/Cron/" } }, - "extra": { - "branch-alias": { - "dev-master": "2.3-dev" - } - }, "scripts": { "phpstan": "./vendor/bin/phpstan analyse -l 0 src tests" } From 2820a7ed81301a445690a080d58dedcc4cc5e14f Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Thu, 5 Dec 2019 15:51:58 -0500 Subject: [PATCH 062/146] Created a FUNDING.yml --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..f61deb96 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [dragonmantank] From e57d8bfacadb2212473942a1d0d60f044e741ecb Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Thu, 19 Dec 2019 23:59:34 +0000 Subject: [PATCH 063/146] Update .travis.yml --- .travis.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index e621bdf7..4062177d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: php -sudo: false +dist: bionic matrix: include: @@ -13,12 +13,9 @@ matrix: - php: 7.3 - php: 7.3 env: dependencies=lowest - - php: nightly - - php: nightly + - php: 7.4 + - php: 7.4 env: dependencies=lowest - allow_failures: - - php: nightly - fast_finish: true install: - | From 507b2ffd279969b1d59e33905caebcdf9de06873 Mon Sep 17 00:00:00 2001 From: PabloKowalczyk <11366345+PabloKowalczyk@users.noreply.github.com> Date: Sat, 4 Jan 2020 07:28:26 +0100 Subject: [PATCH 064/146] Add Crunz to "Projects that Use cron-expression" section --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 86f8c9ad..a3d5bb99 100644 --- a/README.md +++ b/README.md @@ -83,4 +83,5 @@ Requirements Projects that Use cron-expression ================================= * Part of the [Laravel Framework](https://github.com/laravel/framework/) -* Available as a [Symfony Bundle - setono/cron-expression-bundle](https://github.com/Setono/CronExpressionBundle) \ No newline at end of file +* Available as a [Symfony Bundle - setono/cron-expression-bundle](https://github.com/Setono/CronExpressionBundle) +* Framework agnostic, PHP-based job scheduler - [Crunz](https://github.com/lavary/crunz) From a2b682909c0cd43dceed44c17153401a25a78e10 Mon Sep 17 00:00:00 2001 From: Alexander Schranz Date: Thu, 23 Jan 2020 17:12:24 +0100 Subject: [PATCH 065/146] Mark this package as replace for mtdowling/cron-expression --- composer.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/composer.json b/composer.json index 687b5e97..bc6f8bd6 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,9 @@ "Cron\\Tests\\": "tests/Cron/" } }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, "scripts": { "phpstan": "./vendor/bin/phpstan analyse -l 0 src tests" } From 4181db530f332992c46c369f41594b0bc10b482a Mon Sep 17 00:00:00 2001 From: Robin Gustafsson Date: Mon, 10 Feb 2020 22:47:22 +0100 Subject: [PATCH 066/146] Replace deprecated expectedException annotations From PHPUnit 8: > The @expectedException, @expectedExceptionCode, > @expectedExceptionMessage, and @expectedExceptionMessageRegExp > annotations are deprecated. They will be removed in PHPUnit 9. > Refactor your test to use expectException(), expectExceptionCode(), > expectExceptionMessage(), or expectExceptionMessageRegExp() instead. --- tests/Cron/CronExpressionTest.php | 8 ++++---- tests/Cron/DayOfWeekFieldTest.php | 9 +++++---- tests/Cron/FieldFactoryTest.php | 3 ++- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 76340986..e0010e90 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -50,11 +50,11 @@ public function testParsesCronSchedule(): void * @covers \Cron\CronExpression::__construct * @covers \Cron\CronExpression::getExpression * @covers \Cron\CronExpression::__toString - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Invalid CRON field value A at position 0 */ public function testParsesCronScheduleThrowsAnException(): void { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid CRON field value A at position 0'); CronExpression::factory('A 1 2 3 4'); } @@ -94,20 +94,20 @@ public static function scheduleWithDifferentSeparatorsProvider(): array * @covers \Cron\CronExpression::__construct * @covers \Cron\CronExpression::setExpression * @covers \Cron\CronExpression::setPart - * @expectedException \InvalidArgumentException */ public function testInvalidCronsWillFail(): void { + $this->expectException(InvalidArgumentException::class); // Only four values $cron = CronExpression::factory('* * * 1'); } /** * @covers \Cron\CronExpression::setPart - * @expectedException \InvalidArgumentException */ public function testInvalidPartsWillFail(): void { + $this->expectException(InvalidArgumentException::class); // Only four values $cron = CronExpression::factory('* * * * *'); $cron->setPart(1, 'abc'); diff --git a/tests/Cron/DayOfWeekFieldTest.php b/tests/Cron/DayOfWeekFieldTest.php index 26c56ccc..6293c4b6 100644 --- a/tests/Cron/DayOfWeekFieldTest.php +++ b/tests/Cron/DayOfWeekFieldTest.php @@ -7,6 +7,7 @@ use Cron\DayOfWeekField; use DateTime; use DateTimeImmutable; +use InvalidArgumentException; use PHPUnit\Framework\TestCase; /** @@ -67,22 +68,22 @@ public function testIncrementsDateTimeImmutable() /** * @covers \Cron\DayOfWeekField::isSatisfiedBy - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Weekday must be a value between 0 and 7. 12 given */ public function testValidatesHashValueWeekday() { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Weekday must be a value between 0 and 7. 12 given'); $f = new DayOfWeekField(); $this->assertTrue($f->isSatisfiedBy(new DateTime(), '12#1')); } /** * @covers \Cron\DayOfWeekField::isSatisfiedBy - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage There are never more than 5 or less than 1 of a given weekday in a month */ public function testValidatesHashValueNth() { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('There are never more than 5 or less than 1 of a given weekday in a month'); $f = new DayOfWeekField(); $this->assertTrue($f->isSatisfiedBy(new DateTime(), '3#6')); } diff --git a/tests/Cron/FieldFactoryTest.php b/tests/Cron/FieldFactoryTest.php index 8d320813..ee7fb583 100644 --- a/tests/Cron/FieldFactoryTest.php +++ b/tests/Cron/FieldFactoryTest.php @@ -5,6 +5,7 @@ namespace Cron\Tests; use Cron\FieldFactory; +use InvalidArgumentException; use PHPUnit\Framework\TestCase; /** @@ -34,10 +35,10 @@ public function testRetrievesFieldInstances() /** * @covers \Cron\FieldFactory::getField - * @expectedException \InvalidArgumentException */ public function testValidatesFieldPosition() { + $this->expectException(InvalidArgumentException::class); $f = new FieldFactory(); $f->getField(-1); } From a97cd66dd4f3b85b6adef0751f7a877b8d669f31 Mon Sep 17 00:00:00 2001 From: Menno Holtkamp Date: Sat, 29 Feb 2020 18:44:23 +0100 Subject: [PATCH 067/146] Fix CI issues --- src/Cron/FieldFactoryInterface.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Cron/FieldFactoryInterface.php b/src/Cron/FieldFactoryInterface.php index ab2f451a..9ea5fefd 100644 --- a/src/Cron/FieldFactoryInterface.php +++ b/src/Cron/FieldFactoryInterface.php @@ -2,10 +2,7 @@ namespace Cron; - interface FieldFactoryInterface { - public function getField($position); - } From 33e9c654da644400398d7f3a8eefed2a10ca4cc3 Mon Sep 17 00:00:00 2001 From: Menno Holtkamp Date: Sat, 29 Feb 2020 18:47:39 +0100 Subject: [PATCH 068/146] Sync with master --- src/Cron/FieldFactoryInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cron/FieldFactoryInterface.php b/src/Cron/FieldFactoryInterface.php index 9ea5fefd..870a425a 100644 --- a/src/Cron/FieldFactoryInterface.php +++ b/src/Cron/FieldFactoryInterface.php @@ -4,5 +4,5 @@ interface FieldFactoryInterface { - public function getField($position); + public function getField(int $position); } From 1942ec17c802b45621e70531f4cddd14eb90a8be Mon Sep 17 00:00:00 2001 From: Menno Holtkamp Date: Sat, 29 Feb 2020 18:47:54 +0100 Subject: [PATCH 069/146] Actually use FieldFactoryInterface --- src/Cron/CronExpression.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index e49cce19..e91faa44 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -39,7 +39,7 @@ class CronExpression private $cronParts; /** - * @var FieldFactory CRON field factory + * @var FieldFactoryInterface CRON field factory */ private $fieldFactory; @@ -65,11 +65,11 @@ class CronExpression * `@weekly` - Run once a week, midnight on Sun - 0 0 * * 0 * `@daily` - Run once a day, midnight - 0 0 * * * * `@hourly` - Run once an hour, first minute - 0 * * * * - * @param null|FieldFactory $fieldFactory Field factory to use + * @param null|FieldFactoryInterface $fieldFactory Field factory to use * * @return CronExpression */ - public static function factory(string $expression, FieldFactory $fieldFactory = null): CronExpression + public static function factory(string $expression, FieldFactoryInterface $fieldFactory = null): CronExpression { $mappings = [ '@yearly' => '0 0 1 1 *', From 86abbdabbf2dc91d7df634340f9f3940ef325d87 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Thu, 14 Nov 2019 13:01:57 -0500 Subject: [PATCH 070/146] Updated changelog for 2.4.0 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3939df5..78ba8a92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Change Log +## [2.4.0] - 2019-11-14 +### Added +- Additional docblocks for IDE and documentation +- Added phpstan as a development dependency +### Changed +- Changed some DI testing during TravisCI runs +- `\Cron\CronExpression::determineTimezone()` now checks for `\DateTimeInterface` instead of just `\DateTime` +- Errors with fields now report a more human-understandable error and are 1-based instead of 0-based +- Better support for `\DateTimeImmutable` across the library by typehinting for `\DateTimeInterface` now +- Literals should now be less case-sensative across the board +### Fixed +- Fixed infinite loop when determining last day of week from literals +- Fixed bug where single number ranges were allowed (ex: `1/10`) +- Fixed nullable FieldFactory in CronExpression where no factory could be supplied +- Fixed issue where logic for dropping seconds to 0 could lead to a timezone change + ## [2.3.0] - 2019-03-30 ### Added - Added support for DateTimeImmutable via DateTimeInterface From 2815ad8c61a906240091a6d4a4cf97170106d8c2 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Wed, 25 Mar 2020 14:11:41 -0400 Subject: [PATCH 071/146] Added regression test and cleaned up some ?CHANGELOG language --- CHANGELOG.md | 10 +++++++++- tests/Cron/CronExpressionTest.php | 13 +++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78ba8a92..231cc28b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # Change Log -## [2.4.0] - 2019-11-14 +## [3.0.0] - 2020-03-25 + +**MAJOR CHANGE** - In previous versions of this library, setting both a "Day of Month" and a "Day of Week" would be interpreted as an `AND` statement, not an `OR` statement. For example: + +`30 0 1 * 1` + +would evaluate to "Run 30 minutes after the 0 hour when the Day Of Month is 1 AND a Monday" instead of "Run 30 minutes after the 0 hour on Day Of Month 1 OR a Monday", where the latter is more inline with most cron systems. This means that if your cron expression has both of these fields set, you may see your expression fire more often starting with v3.0.0. + ### Added - Additional docblocks for IDE and documentation - Added phpstan as a development dependency @@ -10,6 +17,7 @@ - Errors with fields now report a more human-understandable error and are 1-based instead of 0-based - Better support for `\DateTimeImmutable` across the library by typehinting for `\DateTimeInterface` now - Literals should now be less case-sensative across the board +- Changed logic for when both a Day of Week and a Day of Month are supplied to now be an OR statement, not an AND ### Fixed - Fixed infinite loop when determining last day of week from literals - Fixed bug where single number ranges were allowed (ex: `1/10`) diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index f35212c4..1ec5a690 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -606,4 +606,17 @@ public function testMakeDayOfWeekAnOrSometimes() $this->assertSame("2019-11-01 00:30:00", $runs[3]->format('Y-m-d H:i:s')); $this->assertSame("2019-11-04 00:30:00", $runs[4]->format('Y-m-d H:i:s')); } + + /** + * Make sure that getNextRunDate() does not add arbitrary minutes + * + * @see https://github.com/mtdowling/cron-expression/issues/152 + */ + public function testNextRunDateShouldNotAddMinutes() + { + $e = CronExpression::factory('* 19 * * *'); + $nextRunDate = $e->getNextRunDate(); + + $this->assertSame("00", $nextRunDate->format("i")); + } } From 5e2fce69cfbb03dd173544b176ad7ab56e933984 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Wed, 25 Mar 2020 14:20:26 -0400 Subject: [PATCH 072/146] Expanded tests for casing --- tests/Cron/CronExpressionTest.php | 6 ++++-- tests/Cron/DayOfWeekFieldTest.php | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 1ec5a690..d5b89f63 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -187,9 +187,11 @@ public function scheduleProvider(): array ['* * * * TUE', strtotime('2017-01-08 00:00:00'), strtotime('2017-01-10 00:00:00'), false], // Issue #60, make sure that casing is less relevant for shortcuts, months, and days - ['0 1 15 JUL mon,Wed,FRi', strtotime('2019-11-14 00:00:00'), strtotime('2020-07-15 01:00:00'), false], - ['0 1 15 jul mon,Wed,FRi', strtotime('2019-11-14 00:00:00'), strtotime('2020-07-15 01:00:00'), false], + ['0 1 15 JUL mon,Wed,FRi', strtotime('2019-11-14 00:00:00'), strtotime('2020-07-01 01:00:00'), false], + ['0 1 15 jul mon,Wed,FRi', strtotime('2019-11-14 00:00:00'), strtotime('2020-07-01 01:00:00'), false], ['@Weekly', strtotime('2019-11-14 00:00:00'), strtotime('2019-11-17 00:00:00'), false], + ['@WEEKLY', strtotime('2019-11-14 00:00:00'), strtotime('2019-11-17 00:00:00'), false], + ['@WeeklY', strtotime('2019-11-14 00:00:00'), strtotime('2019-11-17 00:00:00'), false], ]; } diff --git a/tests/Cron/DayOfWeekFieldTest.php b/tests/Cron/DayOfWeekFieldTest.php index 6293c4b6..35b40157 100644 --- a/tests/Cron/DayOfWeekFieldTest.php +++ b/tests/Cron/DayOfWeekFieldTest.php @@ -169,5 +169,6 @@ public function testLiteralsIgnoreCasingProperly() $this->assertTrue($f->validate('MON')); $this->assertTrue($f->validate('Mon')); $this->assertTrue($f->validate('mon')); + $this->assertTrue($f->validate('Mon,Wed,Fri')); } } From 90efed93c8a7aeecba3ec4681ebe97110a0fdaf1 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Wed, 25 Mar 2020 14:30:25 -0400 Subject: [PATCH 073/146] Added CHANGELOG entry for #38 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 231cc28b..6cb05453 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ would evaluate to "Run 30 minutes after the 0 hour when the Day Of Month is 1 AN ### Added - Additional docblocks for IDE and documentation - Added phpstan as a development dependency +- Added a `Cron\FieldFactoryInterface` to make migrations easier (#38) ### Changed - Changed some DI testing during TravisCI runs - `\Cron\CronExpression::determineTimezone()` now checks for `\DateTimeInterface` instead of just `\DateTime` From 70c187e8d95c828fc93a2d09d63179c13381482e Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Wed, 25 Mar 2020 14:33:19 -0400 Subject: [PATCH 074/146] StyleCI fixes --- src/Cron/CronExpression.php | 2 +- tests/Cron/CronExpressionTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index f0b4f847..fd3a8740 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -377,7 +377,7 @@ protected function getRunDate($currentTime = null, int $nth = 0, bool $invert = $dowRunDates = $dowExpression->getMultipleRunDates($nth + 1, $currentTime, $invert, $allowCurrentDate, $timeZone); $combined = array_merge($domRunDates, $dowRunDates); - usort($combined, function($a, $b) { + usort($combined, function ($a, $b) { return $a->format('Y-m-d H:i:s') <=> $b->format('Y-m-d H:i:s'); }); diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index d5b89f63..9df6d379 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -611,7 +611,7 @@ public function testMakeDayOfWeekAnOrSometimes() /** * Make sure that getNextRunDate() does not add arbitrary minutes - * + * * @see https://github.com/mtdowling/cron-expression/issues/152 */ public function testNextRunDateShouldNotAddMinutes() From 2d923cdece5cd71b4db11c1592c817c035121d34 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Thu, 20 Aug 2020 21:58:58 -0400 Subject: [PATCH 075/146] Added missing check for ? in DOM and DOW --- src/Cron/DayOfMonthField.php | 4 ++++ src/Cron/DayOfWeekField.php | 4 ++++ tests/Cron/CronExpressionTest.php | 4 ++++ tests/Cron/DayOfMonthFieldTest.php | 1 + tests/Cron/DayOfWeekFieldTest.php | 1 + 5 files changed, 14 insertions(+) diff --git a/src/Cron/DayOfMonthField.php b/src/Cron/DayOfMonthField.php index 0a351c7e..8c9ae7f3 100644 --- a/src/Cron/DayOfMonthField.php +++ b/src/Cron/DayOfMonthField.php @@ -130,6 +130,10 @@ public function validate(string $value): bool } if (!$basicChecks) { + if ('?' === $value) { + return true; + } + if ('L' === $value) { return true; } diff --git a/src/Cron/DayOfWeekField.php b/src/Cron/DayOfWeekField.php index d0fce181..fa44b63d 100644 --- a/src/Cron/DayOfWeekField.php +++ b/src/Cron/DayOfWeekField.php @@ -178,6 +178,10 @@ public function validate(string $value): bool $basicChecks = parent::validate($value); if (!$basicChecks) { + if ('?' === $value) { + return true; + } + // Handle the # value if (false !== strpos($value, '#')) { $chunks = explode('#', $value); diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 9df6d379..0b10893c 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -192,6 +192,10 @@ public function scheduleProvider(): array ['@Weekly', strtotime('2019-11-14 00:00:00'), strtotime('2019-11-17 00:00:00'), false], ['@WEEKLY', strtotime('2019-11-14 00:00:00'), strtotime('2019-11-17 00:00:00'), false], ['@WeeklY', strtotime('2019-11-14 00:00:00'), strtotime('2019-11-17 00:00:00'), false], + + // Issue #76, DOW and DOM do not support ? + ['0 12 * * ?', strtotime('2020-08-20 00:00:00'), strtotime('2020-08-20 12:00:00'), false], + ['0 12 ? * *', strtotime('2020-08-20 00:00:00'), strtotime('2020-08-20 12:00:00'), false], ]; } diff --git a/tests/Cron/DayOfMonthFieldTest.php b/tests/Cron/DayOfMonthFieldTest.php index ee32e385..defc429a 100644 --- a/tests/Cron/DayOfMonthFieldTest.php +++ b/tests/Cron/DayOfMonthFieldTest.php @@ -24,6 +24,7 @@ public function testValidatesField() $this->assertTrue($f->validate('*')); $this->assertTrue($f->validate('L')); $this->assertTrue($f->validate('5W')); + $this->assertTrue($f->validate('?')); $this->assertTrue($f->validate('01')); $this->assertFalse($f->validate('5W,L')); $this->assertFalse($f->validate('1.')); diff --git a/tests/Cron/DayOfWeekFieldTest.php b/tests/Cron/DayOfWeekFieldTest.php index 35b40157..0d7da202 100644 --- a/tests/Cron/DayOfWeekFieldTest.php +++ b/tests/Cron/DayOfWeekFieldTest.php @@ -25,6 +25,7 @@ public function testValidatesField() $this->assertTrue($f->validate('01')); $this->assertTrue($f->validate('00')); $this->assertTrue($f->validate('*')); + $this->assertTrue($f->validate('?')); $this->assertFalse($f->validate('*/3,1,1-12')); $this->assertTrue($f->validate('SUN-2')); $this->assertFalse($f->validate('1.')); From fa4e95ff5a7f1d62c3fbc05c32729b7f3ca14b52 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Thu, 20 Aug 2020 22:30:13 -0400 Subject: [PATCH 076/146] Cast step to an int as part of calling range(), see #80 --- src/Cron/AbstractField.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cron/AbstractField.php b/src/Cron/AbstractField.php index 5784189a..22723066 100644 --- a/src/Cron/AbstractField.php +++ b/src/Cron/AbstractField.php @@ -152,7 +152,7 @@ public function isInIncrementsOfRanges(int $dateValue, string $value): bool if ($step >= $this->rangeEnd) { $thisRange = [$this->fullRange[$step % \count($this->fullRange)]]; } else { - $thisRange = range($rangeStart, $rangeEnd, $step); + $thisRange = range($rangeStart, $rangeEnd, (int) $step); } return \in_array($dateValue, $thisRange, true); From 0801fd595ec78bf820922cb62cc1ed8b3a10163b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Auswo=CC=88ger?= Date: Wed, 16 Sep 2020 10:49:20 +0200 Subject: [PATCH 077/146] Add compatibility with PHP 8.0 --- .travis.yml | 2 ++ composer.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4062177d..1e5749c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,8 @@ matrix: - php: 7.4 - php: 7.4 env: dependencies=lowest + - php: nightly + install: travis_retry composer install --no-interaction --prefer-dist --ignore-platform-reqs install: - | diff --git a/composer.json b/composer.json index bc6f8bd6..d2bcba1e 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ } ], "require": { - "php": "^7.1" + "php": "^7.1|^8.0" }, "require-dev": { "phpstan/phpstan": "^0.11", From 562588e46bd3a3a1897e2218a165b0ce841a8752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Auswo=CC=88ger?= Date: Wed, 16 Sep 2020 10:59:37 +0200 Subject: [PATCH 078/146] Fix type error --- src/Cron/DayOfWeekField.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cron/DayOfWeekField.php b/src/Cron/DayOfWeekField.php index fa44b63d..022db12f 100644 --- a/src/Cron/DayOfWeekField.php +++ b/src/Cron/DayOfWeekField.php @@ -73,7 +73,7 @@ public function isSatisfiedBy(DateTimeInterface $date, $value): bool // Find out if this is the last specific weekday of the month if (strpos($value, 'L')) { $weekday = (int) $this->convertLiterals(substr($value, 0, strpos($value, 'L'))); - $weekday = (int) str_replace(7, 0, $weekday); + $weekday %= 7; $tdate = clone $date; $tdate = $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth); From 4e77957c34dcb4fb9e09cdfec158ba296ffa4a85 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Mon, 12 Oct 2020 14:37:08 -0400 Subject: [PATCH 079/146] Added support for PHP 8 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cb05453..610d6bc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## [3.0.1] - 2020-10-12 +### Added +- Added support for PHP 8 (#92) +### Changed +- N/A +### Fixed +- N/A + ## [3.0.0] - 2020-03-25 **MAJOR CHANGE** - In previous versions of this library, setting both a "Day of Month" and a "Day of Week" would be interpreted as an `AND` statement, not an `OR` statement. For example: @@ -25,6 +33,14 @@ would evaluate to "Run 30 minutes after the 0 hour when the Day Of Month is 1 AN - Fixed nullable FieldFactory in CronExpression where no factory could be supplied - Fixed issue where logic for dropping seconds to 0 could lead to a timezone change +## [2.3.1] - 2020-10-12 +### Added +- Added support for PHP 8 (#92) +### Changed +- N/A +### Fixed +- N/A + ## [2.3.0] - 2019-03-30 ### Added - Added support for DateTimeImmutable via DateTimeInterface From 48212cdc0a79051d50d7fc2f0645c5a321caf926 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Mon, 12 Oct 2020 21:19:55 -0400 Subject: [PATCH 080/146] Fixed phpstan and phpunit dependencies for v3 line --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index d2bcba1e..aee57b7d 100644 --- a/composer.json +++ b/composer.json @@ -15,8 +15,8 @@ "php": "^7.1|^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.11", - "phpunit/phpunit": "^6.4|^7.0" + "phpstan/phpstan": "^0.11|^0.12", + "phpunit/phpunit": "^7.0|^8.0|^9.0" }, "autoload": { "psr-4": { From ecb44fe6c97407e4ada892378fbfad8583137893 Mon Sep 17 00:00:00 2001 From: Olivier ALLAIN Date: Tue, 6 Oct 2020 00:12:15 +0200 Subject: [PATCH 081/146] set phpstan to level max and fix issues --- composer.json | 8 +++-- phpstan.neon | 2 ++ src/Cron/AbstractField.php | 7 ++-- src/Cron/CronExpression.php | 55 +++++++++++++++++------------- src/Cron/DayOfMonthField.php | 16 +++++++-- src/Cron/DayOfWeekField.php | 7 ++-- src/Cron/FieldFactory.php | 2 +- src/Cron/FieldFactoryInterface.php | 2 +- src/Cron/FieldInterface.php | 7 ++-- src/Cron/HoursField.php | 2 +- src/Cron/MinutesField.php | 2 +- src/Cron/MonthField.php | 4 +-- tests/Cron/CronExpressionTest.php | 14 +++++--- tests/Cron/DayOfMonthFieldTest.php | 10 +++--- tests/Cron/DayOfWeekFieldTest.php | 24 ++++++------- tests/Cron/FieldFactoryTest.php | 4 +-- tests/Cron/HoursFieldTest.php | 10 +++--- tests/Cron/MinutesFieldTest.php | 12 +++---- tests/Cron/MonthFieldTest.php | 16 ++++----- 19 files changed, 118 insertions(+), 86 deletions(-) create mode 100644 phpstan.neon diff --git a/composer.json b/composer.json index aee57b7d..ce5ccdc8 100644 --- a/composer.json +++ b/composer.json @@ -15,8 +15,10 @@ "php": "^7.1|^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.11|^0.12", - "phpunit/phpunit": "^7.0|^8.0|^9.0" + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.0|^8.0|^9.0", + "phpstan/phpstan-webmozart-assert": "^0.12.7", + "phpstan/extension-installer": "^1.0" }, "autoload": { "psr-4": { @@ -32,6 +34,6 @@ "mtdowling/cron-expression": "^1.0" }, "scripts": { - "phpstan": "./vendor/bin/phpstan analyse -l 0 src tests" + "phpstan": "./vendor/bin/phpstan analyse -l max src tests" } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..9d52fd98 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,2 @@ +parameters: + checkMissingIterableValueType: false diff --git a/src/Cron/AbstractField.php b/src/Cron/AbstractField.php index 22723066..48d11bd2 100644 --- a/src/Cron/AbstractField.php +++ b/src/Cron/AbstractField.php @@ -48,7 +48,7 @@ public function __construct() /** * Check to see if a field is satisfied by a value. * - * @param string $dateValue Date value to check + * @param int $dateValue Date value to check * @param string $value Value to test * * @return bool @@ -114,7 +114,7 @@ public function isInRange(int $dateValue, $value): bool /** * Test if a value is within an increments of ranges (offset[-to]/step size). * - * @param string $dateValue Set date value + * @param int $dateValue Set date value * @param string $value Value to test * * @return bool @@ -126,6 +126,7 @@ public function isInIncrementsOfRanges(int $dateValue, string $value): bool $step = $chunks[1] ?? 0; // No step or 0 steps aren't cool + /** @phpstan-ignore-next-line */ if (null === $step || '0' === $step || 0 === $step) { return false; } @@ -289,7 +290,7 @@ public function validate(string $value): bool return false; } - if (\is_float($value) || false !== strpos($value, '.')) { + if (false !== strpos($value, '.')) { return false; } diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index fd3a8740..867d0075 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -11,6 +11,7 @@ use Exception; use InvalidArgumentException; use RuntimeException; +use Webmozart\Assert\Assert; /** * CRON expression parser that can determine whether or not a CRON expression is @@ -85,7 +86,7 @@ public static function factory(string $expression, FieldFactoryInterface $fieldF $expression = $mappings[$shortcut]; } - return new static($expression, $fieldFactory ?: new FieldFactory()); + return new self($expression, $fieldFactory ?: new FieldFactory()); } /** @@ -112,9 +113,9 @@ public static function isValidExpression(string $expression): bool * Parse a CRON expression. * * @param string $expression CRON expression (e.g. '8 * * * *') - * @param null|FieldFactory $fieldFactory Factory to create cron fields + * @param null|FieldFactoryInterface $fieldFactory Factory to create cron fields */ - public function __construct(string $expression, FieldFactory $fieldFactory = null) + public function __construct(string $expression, FieldFactoryInterface $fieldFactory = null) { $this->fieldFactory = $fieldFactory ?: new FieldFactory(); $this->setExpression($expression); @@ -131,7 +132,10 @@ public function __construct(string $expression, FieldFactory $fieldFactory = nul */ public function setExpression(string $value): CronExpression { - $this->cronParts = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY); + $split = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY); + Assert::isArray($split); + + $this->cronParts = $split; if (\count($this->cronParts) < 5) { throw new InvalidArgumentException( $value . ' is not a valid CRON expression' @@ -231,12 +235,12 @@ public function getPreviousRunDate($currentTime = 'now', int $nth = 0, bool $all /** * Get multiple run dates starting at the current date or a specific date. * - * @param int $total Set the total number of dates to calculate - * @param string|\DateTimeInterface $currentTime Relative calculation date - * @param bool $invert Set to TRUE to retrieve previous dates - * @param bool $allowCurrentDate Set to TRUE to return the - * current date if it matches the cron expression - * @param null|string $timeZone TimeZone to use instead of the system default + * @param int $total Set the total number of dates to calculate + * @param string|\DateTimeInterface|null $currentTime Relative calculation date + * @param bool $invert Set to TRUE to retrieve previous dates + * @param bool $allowCurrentDate Set to TRUE to return the + * current date if it matches the cron expression + * @param null|string $timeZone TimeZone to use instead of the system default * * @return \DateTime[] Returns an array of run dates */ @@ -258,7 +262,7 @@ public function getMultipleRunDates(int $total, $currentTime = 'now', bool $inve /** * Get all or part of the CRON expression. * - * @param string $part specify the part to retrieve or NULL to get the full + * @param int|string|null $part specify the part to retrieve or NULL to get the full * cron schedule string * * @return null|string Returns the CRON expression, a part of the @@ -297,7 +301,7 @@ public function __toString(): string * * @return bool Returns TRUE if the cron is due to run or FALSE if not */ - public function isDue($currentTime = 'now', $timeZone = null): ?bool + public function isDue($currentTime = 'now', $timeZone = null): bool { $timeZone = $this->determineTimeZone($currentTime, $timeZone); @@ -307,9 +311,11 @@ public function isDue($currentTime = 'now', $timeZone = null): ?bool $currentTime = clone $currentTime; } elseif ($currentTime instanceof DateTimeImmutable) { $currentTime = DateTime::createFromFormat('U', $currentTime->format('U')); - } else { + } elseif (\is_string($currentTime)) { $currentTime = new DateTime($currentTime); } + + Assert::isInstanceOf($currentTime, DateTime::class); $currentTime->setTimezone(new DateTimeZone($timeZone)); // drop the seconds to 0 @@ -325,12 +331,12 @@ public function isDue($currentTime = 'now', $timeZone = null): ?bool /** * Get the next or previous run date of the expression relative to a date. * - * @param string|\DateTimeInterface $currentTime Relative calculation date - * @param int $nth Number of matches to skip before returning - * @param bool $invert Set to TRUE to go backwards in time - * @param bool $allowCurrentDate Set to TRUE to return the - * current date if it matches the cron expression - * @param string|null $timeZone TimeZone to use instead of the system default + * @param string|\DateTimeInterface|null $currentTime Relative calculation date + * @param int $nth Number of matches to skip before returning + * @param bool $invert Set to TRUE to go backwards in time + * @param bool $allowCurrentDate Set to TRUE to return the + * current date if it matches the cron expression + * @param string|null $timeZone TimeZone to use instead of the system default * * @throws \RuntimeException on too many iterations * @throws Exception @@ -345,10 +351,13 @@ protected function getRunDate($currentTime = null, int $nth = 0, bool $invert = $currentDate = clone $currentTime; } elseif ($currentTime instanceof DateTimeImmutable) { $currentDate = DateTime::createFromFormat('U', $currentTime->format('U')); + } elseif (\is_string($currentTime)) { + $currentDate = new DateTime($currentTime); } else { - $currentDate = new DateTime($currentTime ?: 'now'); + $currentDate = new DateTime('now'); } + Assert::isInstanceOf($currentDate, DateTime::class); $currentDate->setTimezone(new DateTimeZone($timeZone)); $currentDate->setTime((int) $currentDate->format('H'), (int) $currentDate->format('i'), 0); @@ -429,12 +438,12 @@ protected function getRunDate($currentTime = null, int $nth = 0, bool $invert = /** * Workout what timeZone should be used. * - * @param string|\DateTimeInterface $currentTime Relative calculation date - * @param string|null $timeZone TimeZone to use instead of the system default + * @param string|\DateTimeInterface|null $currentTime Relative calculation date + * @param string|null $timeZone TimeZone to use instead of the system default * * @return string */ - protected function determineTimeZone($currentTime, $timeZone): string + protected function determineTimeZone($currentTime, ?string $timeZone): string { if (null !== $timeZone) { return $timeZone; diff --git a/src/Cron/DayOfMonthField.php b/src/Cron/DayOfMonthField.php index 8c9ae7f3..389ac78e 100644 --- a/src/Cron/DayOfMonthField.php +++ b/src/Cron/DayOfMonthField.php @@ -6,6 +6,7 @@ use DateTime; use DateTimeInterface; +use Webmozart\Assert\Assert; /** * Day of month field. Allows: * , / - ? L W. @@ -44,12 +45,17 @@ class DayOfMonthField extends AbstractField * @param int $currentMonth Current month * @param int $targetDay Target day of the month * - * @return \DateTime Returns the nearest date + * @return \DateTime|null Returns the nearest date */ private static function getNearestWeekday(int $currentYear, int $currentMonth, int $targetDay): ?DateTime { $tday = str_pad((string) $targetDay, 2, '0', STR_PAD_LEFT); $target = DateTime::createFromFormat('Y-m-d', "${currentYear}-${currentMonth}-${tday}"); + + if ($target === false) { + return null; + } + $currentWeekday = (int) $target->format('N'); if ($currentWeekday < 6) { @@ -67,6 +73,8 @@ private static function getNearestWeekday(int $currentYear, int $currentMonth, i } } } + + return null; } /** @@ -89,8 +97,10 @@ public function isSatisfiedBy(DateTimeInterface $date, $value): bool // Check to see if this is the nearest weekday to a particular value if (strpos($value, 'W')) { // Parse the target day + /** @phpstan-ignore-next-line */ $targetDay = (int) substr($value, 0, strpos($value, 'W')); // Find out if the current day is the nearest day of the week + /** @phpstan-ignore-next-line */ return $date->format('j') === self::getNearestWeekday( (int) $date->format('Y'), (int) $date->format('m'), @@ -104,9 +114,9 @@ public function isSatisfiedBy(DateTimeInterface $date, $value): bool /** * @inheritDoc * - * @param \DateTime|\DateTimeImmutable &$date + * @param \DateTime|\DateTimeImmutable $date */ - public function increment(DateTimeInterface &$date, $invert = false): FieldInterface + public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface { if ($invert) { $date = $date->modify('previous day')->setTime(23, 59); diff --git a/src/Cron/DayOfWeekField.php b/src/Cron/DayOfWeekField.php index 022db12f..a62e6074 100644 --- a/src/Cron/DayOfWeekField.php +++ b/src/Cron/DayOfWeekField.php @@ -72,7 +72,8 @@ public function isSatisfiedBy(DateTimeInterface $date, $value): bool // Find out if this is the last specific weekday of the month if (strpos($value, 'L')) { - $weekday = (int) $this->convertLiterals(substr($value, 0, strpos($value, 'L'))); + /** @phpstan-ignore-next-line */ + $weekday = $this->convertLiterals(substr($value, 0, strpos($value, 'L'))); $weekday %= 7; $tdate = clone $date; @@ -157,9 +158,9 @@ public function isSatisfiedBy(DateTimeInterface $date, $value): bool /** * @inheritDoc * - * @param \DateTime|\DateTimeImmutable &$date + * @param \DateTime|\DateTimeImmutable $date */ - public function increment(DateTimeInterface &$date, $invert = false): FieldInterface + public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface { if ($invert) { $date = $date->modify('-1 day')->setTime(23, 59, 0); diff --git a/src/Cron/FieldFactory.php b/src/Cron/FieldFactory.php index d6f61d11..839b2757 100644 --- a/src/Cron/FieldFactory.php +++ b/src/Cron/FieldFactory.php @@ -30,7 +30,7 @@ public function getField(int $position): FieldInterface return $this->fields[$position] ?? $this->fields[$position] = $this->instantiateField($position); } - private function instantiateField($position): FieldInterface + private function instantiateField(int $position): FieldInterface { switch ($position) { case CronExpression::MINUTE: diff --git a/src/Cron/FieldFactoryInterface.php b/src/Cron/FieldFactoryInterface.php index 870a425a..8bd3c658 100644 --- a/src/Cron/FieldFactoryInterface.php +++ b/src/Cron/FieldFactoryInterface.php @@ -4,5 +4,5 @@ interface FieldFactoryInterface { - public function getField(int $position); + public function getField(int $position): FieldInterface; } diff --git a/src/Cron/FieldInterface.php b/src/Cron/FieldInterface.php index d5fc34ac..eba0558f 100644 --- a/src/Cron/FieldInterface.php +++ b/src/Cron/FieldInterface.php @@ -25,12 +25,13 @@ public function isSatisfiedBy(DateTimeInterface $date, $value): bool; * When a CRON expression is not satisfied, this method is used to increment * or decrement a DateTime object by the unit of the cron field. * - * @param DateTimeInterface &$date DateTime object to change - * @param bool $invert (optional) Set to TRUE to decrement + * @param DateTimeInterface $date DateTime object to change + * @param bool $invert (optional) Set to TRUE to decrement + * @param string|null $parts (optional) Set parts to use * * @return FieldInterface */ - public function increment(DateTimeInterface &$date, $invert = false): FieldInterface; + public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface; /** * Validates a CRON expression for a given field. diff --git a/src/Cron/HoursField.php b/src/Cron/HoursField.php index f3d2f47c..78a5af42 100644 --- a/src/Cron/HoursField.php +++ b/src/Cron/HoursField.php @@ -33,7 +33,7 @@ public function isSatisfiedBy(DateTimeInterface $date, $value): bool /** * {@inheritdoc} * - * @param \DateTime|\DateTimeImmutable &$date + * @param \DateTime|\DateTimeImmutable $date * @param string|null $parts */ public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface diff --git a/src/Cron/MinutesField.php b/src/Cron/MinutesField.php index a32e9a65..d95ba749 100644 --- a/src/Cron/MinutesField.php +++ b/src/Cron/MinutesField.php @@ -37,7 +37,7 @@ public function isSatisfiedBy(DateTimeInterface $date, $value):bool * {@inheritdoc} * {@inheritDoc} * - * @param \DateTime|\DateTimeImmutable &$date + * @param \DateTime|\DateTimeImmutable $date * @param string|null $parts */ public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface diff --git a/src/Cron/MonthField.php b/src/Cron/MonthField.php index 98141bd7..06bdbf46 100644 --- a/src/Cron/MonthField.php +++ b/src/Cron/MonthField.php @@ -44,9 +44,9 @@ public function isSatisfiedBy(DateTimeInterface $date, $value): bool /** * @inheritDoc * - * @param \DateTime|\DateTimeImmutable &$date + * @param \DateTime|\DateTimeImmutable $date */ - public function increment(DateTimeInterface &$date, $invert = false): FieldInterface + public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface { if ($invert) { $date = $date->modify('last day of previous month')->setTime(23, 59); diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 0b10893c..a55b6673 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -11,6 +11,7 @@ use DateTimeZone; use InvalidArgumentException; use PHPUnit\Framework\TestCase; +use Webmozart\Assert\Assert; /** * @author Michael Dowling @@ -225,6 +226,7 @@ public function testDeterminesIfCronIsDue($schedule, $relativeTime, $nextRun, $i $relativeTime = date('Y-m-d H:i:s', $relativeTime); } + $nextRunDate = new DateTime(); if (\is_string($nextRun)) { $nextRunDate = new DateTime($nextRun); } elseif (\is_int($nextRun)) { @@ -329,18 +331,22 @@ public function testRecognisesTimezonesAsPartOfDateTime(): void $tzServer = new \DateTimeZone('Europe/London'); $dtCurrent = \DateTime::createFromFormat('!Y-m-d H:i:s', '2017-10-17 10:00:00', $tzServer); + Assert::isInstanceOf($dtCurrent, DateTime::class); $dtPrev = $cron->getPreviousRunDate($dtCurrent, 0, true, $tzCron); $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format('U \\: c \\: e')); $dtCurrent = \DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', '2017-10-17 10:00:00', $tzServer); + Assert::isInstanceOf($dtCurrent, \DateTimeImmutable::class); $dtPrev = $cron->getPreviousRunDate($dtCurrent, 0, true, $tzCron); $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format('U \\: c \\: e')); $dtCurrent = \DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', '2017-10-17 10:00:00', $tzServer); + Assert::isInstanceOf($dtCurrent, \DateTimeImmutable::class); $dtPrev = $cron->getPreviousRunDate($dtCurrent->format('c'), 0, true, $tzCron); $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format('U \\: c \\: e')); $dtCurrent = \DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', '2017-10-17 10:00:00', $tzServer); + Assert::isInstanceOf($dtCurrent, \DateTimeImmutable::class); $dtPrev = $cron->getPreviousRunDate($dtCurrent->format('\\@U'), 0, true, $tzCron); $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format('U \\: c \\: e')); } @@ -425,7 +431,7 @@ public function testCanIterateOverNextRuns(): void /** * @covers \Cron\CronExpression::getRunDate */ - public function testGetRunDateHandlesDifferentDates() + public function testGetRunDateHandlesDifferentDates(): void { $cron = CronExpression::factory('@weekly'); $date = new DateTime("2019-03-10 00:00:00"); @@ -437,7 +443,7 @@ public function testGetRunDateHandlesDifferentDates() /** * @covers \Cron\CronExpression::getRunDate */ - public function testSkipsCurrentDateByDefault() + public function testSkipsCurrentDateByDefault(): void { $cron = CronExpression::factory('* * * * *'); $current = new DateTime('now'); @@ -601,7 +607,7 @@ public function testFieldPositionIsHumanAdjusted(): void /** * @see https://github.com/dragonmantank/cron-expression/issues/35 */ - public function testMakeDayOfWeekAnOrSometimes() + public function testMakeDayOfWeekAnOrSometimes(): void { $cron = CronExpression::factory('30 0 1 * 1'); $runs = $cron->getMultipleRunDates(5, date("2019-10-10 23:20:00"), false, true); @@ -618,7 +624,7 @@ public function testMakeDayOfWeekAnOrSometimes() * * @see https://github.com/mtdowling/cron-expression/issues/152 */ - public function testNextRunDateShouldNotAddMinutes() + public function testNextRunDateShouldNotAddMinutes(): void { $e = CronExpression::factory('* 19 * * *'); $nextRunDate = $e->getNextRunDate(); diff --git a/tests/Cron/DayOfMonthFieldTest.php b/tests/Cron/DayOfMonthFieldTest.php index defc429a..15396b8e 100644 --- a/tests/Cron/DayOfMonthFieldTest.php +++ b/tests/Cron/DayOfMonthFieldTest.php @@ -17,7 +17,7 @@ class DayOfMonthFieldTest extends TestCase /** * @covers \Cron\DayOfMonthField::validate */ - public function testValidatesField() + public function testValidatesField(): void { $f = new DayOfMonthField(); $this->assertTrue($f->validate('1')); @@ -33,7 +33,7 @@ public function testValidatesField() /** * @covers \Cron\DayOfMonthField::isSatisfiedBy */ - public function testChecksIfSatisfied() + public function testChecksIfSatisfied(): void { $f = new DayOfMonthField(); $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?')); @@ -43,7 +43,7 @@ public function testChecksIfSatisfied() /** * @covers \Cron\DayOfMonthField::increment */ - public function testIncrementsDate() + public function testIncrementsDate(): void { $d = new DateTime('2011-03-15 11:15:00'); $f = new DayOfMonthField(); @@ -58,7 +58,7 @@ public function testIncrementsDate() /** * @covers \Cron\DayOfMonthField::increment */ - public function testIncrementsDateTimeImmutable() + public function testIncrementsDateTimeImmutable(): void { $d = new DateTimeImmutable('2011-03-15 11:15:00'); $f = new DayOfMonthField(); @@ -72,7 +72,7 @@ public function testIncrementsDateTimeImmutable() * * @since 2017-01-22 */ - public function testDoesNotAccept0Date() + public function testDoesNotAccept0Date(): void { $f = new DayOfMonthField(); $this->assertFalse($f->validate('0')); diff --git a/tests/Cron/DayOfWeekFieldTest.php b/tests/Cron/DayOfWeekFieldTest.php index 0d7da202..6a414313 100644 --- a/tests/Cron/DayOfWeekFieldTest.php +++ b/tests/Cron/DayOfWeekFieldTest.php @@ -18,7 +18,7 @@ class DayOfWeekFieldTest extends TestCase /** * @covers \Cron\DayOfWeekField::validate */ - public function testValidatesField() + public function testValidatesField(): void { $f = new DayOfWeekField(); $this->assertTrue($f->validate('1')); @@ -34,7 +34,7 @@ public function testValidatesField() /** * @covers \Cron\DayOfWeekField::isSatisfiedBy */ - public function testChecksIfSatisfied() + public function testChecksIfSatisfied(): void { $f = new DayOfWeekField(); $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?')); @@ -44,7 +44,7 @@ public function testChecksIfSatisfied() /** * @covers \Cron\DayOfWeekField::increment */ - public function testIncrementsDate() + public function testIncrementsDate(): void { $d = new DateTime('2011-03-15 11:15:00'); $f = new DayOfWeekField(); @@ -59,7 +59,7 @@ public function testIncrementsDate() /** * @covers \Cron\DayOfWeekField::increment */ - public function testIncrementsDateTimeImmutable() + public function testIncrementsDateTimeImmutable(): void { $d = new DateTimeImmutable('2011-03-15 11:15:00'); $f = new DayOfWeekField(); @@ -70,7 +70,7 @@ public function testIncrementsDateTimeImmutable() /** * @covers \Cron\DayOfWeekField::isSatisfiedBy */ - public function testValidatesHashValueWeekday() + public function testValidatesHashValueWeekday(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Weekday must be a value between 0 and 7. 12 given'); @@ -81,7 +81,7 @@ public function testValidatesHashValueWeekday() /** * @covers \Cron\DayOfWeekField::isSatisfiedBy */ - public function testValidatesHashValueNth() + public function testValidatesHashValueNth(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('There are never more than 5 or less than 1 of a given weekday in a month'); @@ -92,7 +92,7 @@ public function testValidatesHashValueNth() /** * @covers \Cron\DayOfWeekField::validate */ - public function testValidateWeekendHash() + public function testValidateWeekendHash(): void { $f = new DayOfWeekField(); $this->assertTrue($f->validate('MON#1')); @@ -108,7 +108,7 @@ public function testValidateWeekendHash() /** * @covers \Cron\DayOfWeekField::isSatisfiedBy */ - public function testHandlesZeroAndSevenDayOfTheWeekValues() + public function testHandlesZeroAndSevenDayOfTheWeekValues(): void { $f = new DayOfWeekField(); $this->assertTrue($f->isSatisfiedBy(new DateTime('2011-09-04 00:00:00'), '0-2')); @@ -123,7 +123,7 @@ public function testHandlesZeroAndSevenDayOfTheWeekValues() /** * @covers \Cron\DayOfWeekField::isSatisfiedBy */ - public function testHandlesLastWeekdayOfTheMonth() + public function testHandlesLastWeekdayOfTheMonth(): void { $f = new DayOfWeekField(); $this->assertTrue($f->isSatisfiedBy(new DateTime('2018-12-28 00:00:00'), 'FRIL')); @@ -135,7 +135,7 @@ public function testHandlesLastWeekdayOfTheMonth() /** * @see https://github.com/mtdowling/cron-expression/issues/47 */ - public function testIssue47() + public function testIssue47(): void { $f = new DayOfWeekField(); $this->assertFalse($f->validate('mon,')); @@ -150,7 +150,7 @@ public function testIssue47() /** * @see https://github.com/laravel/framework/commit/07d160ac3cc9764d5b429734ffce4fa311385403 */ - public function testLiteralsExpandProperly() + public function testLiteralsExpandProperly(): void { $f = new DayOfWeekField(); $this->assertTrue($f->validate('MON-FRI')); @@ -164,7 +164,7 @@ public function testLiteralsExpandProperly() * @since 2019-07-29 * @see https://github.com/dragonmantank/cron-expression/issues/24 */ - public function testLiteralsIgnoreCasingProperly() + public function testLiteralsIgnoreCasingProperly(): void { $f = new DayOfWeekField(); $this->assertTrue($f->validate('MON')); diff --git a/tests/Cron/FieldFactoryTest.php b/tests/Cron/FieldFactoryTest.php index ee7fb583..357c0896 100644 --- a/tests/Cron/FieldFactoryTest.php +++ b/tests/Cron/FieldFactoryTest.php @@ -16,7 +16,7 @@ class FieldFactoryTest extends TestCase /** * @covers \Cron\FieldFactory::getField */ - public function testRetrievesFieldInstances() + public function testRetrievesFieldInstances(): void { $mappings = [ 0 => 'Cron\MinutesField', @@ -36,7 +36,7 @@ public function testRetrievesFieldInstances() /** * @covers \Cron\FieldFactory::getField */ - public function testValidatesFieldPosition() + public function testValidatesFieldPosition(): void { $this->expectException(InvalidArgumentException::class); $f = new FieldFactory(); diff --git a/tests/Cron/HoursFieldTest.php b/tests/Cron/HoursFieldTest.php index 2c6ca9af..837dc3b5 100644 --- a/tests/Cron/HoursFieldTest.php +++ b/tests/Cron/HoursFieldTest.php @@ -17,7 +17,7 @@ class HoursFieldTest extends TestCase /** * @covers \Cron\HoursField::validate */ - public function testValidatesField() + public function testValidatesField(): void { $f = new HoursField(); $this->assertTrue($f->validate('1')); @@ -31,7 +31,7 @@ public function testValidatesField() /** * @covers \Cron\HoursField::increment */ - public function testIncrementsDate() + public function testIncrementsDate(): void { $d = new DateTime('2011-03-15 11:15:00'); $f = new HoursField(); @@ -46,7 +46,7 @@ public function testIncrementsDate() /** * @covers \Cron\HoursField::increment */ - public function testIncrementsDateTimeImmutable() + public function testIncrementsDateTimeImmutable(): void { $d = new DateTimeImmutable('2011-03-15 11:15:00'); $f = new HoursField(); @@ -57,7 +57,7 @@ public function testIncrementsDateTimeImmutable() /** * @covers \Cron\HoursField::increment */ - public function testIncrementsDateWithThirtyMinuteOffsetTimezone() + public function testIncrementsDateWithThirtyMinuteOffsetTimezone(): void { $tz = date_default_timezone_get(); date_default_timezone_set('America/St_Johns'); @@ -75,7 +75,7 @@ public function testIncrementsDateWithThirtyMinuteOffsetTimezone() /** * @covers \Cron\HoursField::increment */ - public function testIncrementDateWithFifteenMinuteOffsetTimezone() + public function testIncrementDateWithFifteenMinuteOffsetTimezone(): void { $tz = date_default_timezone_get(); date_default_timezone_set('Asia/Kathmandu'); diff --git a/tests/Cron/MinutesFieldTest.php b/tests/Cron/MinutesFieldTest.php index 49aa67b5..489548ef 100644 --- a/tests/Cron/MinutesFieldTest.php +++ b/tests/Cron/MinutesFieldTest.php @@ -17,7 +17,7 @@ class MinutesFieldTest extends TestCase /** * @covers \Cron\MinutesField::validate */ - public function testValidatesField() + public function testValidatesField(): void { $f = new MinutesField(); $this->assertTrue($f->validate('1')); @@ -29,7 +29,7 @@ public function testValidatesField() /** * @covers \Cron\MinutesField::isSatisfiedBy */ - public function testChecksIfSatisfied() + public function testChecksIfSatisfied(): void { $f = new MinutesField(); $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?')); @@ -39,7 +39,7 @@ public function testChecksIfSatisfied() /** * @covers \Cron\MinutesField::increment */ - public function testIncrementsDate() + public function testIncrementsDate(): void { $d = new DateTime('2011-03-15 11:15:00'); $f = new MinutesField(); @@ -52,7 +52,7 @@ public function testIncrementsDate() /** * @covers \Cron\MinutesField::increment */ - public function testIncrementsDateTimeImmutable() + public function testIncrementsDateTimeImmutable(): void { $d = new DateTimeImmutable('2011-03-15 11:15:00'); $f = new MinutesField(); @@ -67,7 +67,7 @@ public function testIncrementsDateTimeImmutable() * * @since 2017-08-18 */ - public function testBadSyntaxesShouldNotValidate() + public function testBadSyntaxesShouldNotValidate(): void { $f = new MinutesField(); $this->assertFalse($f->validate('*-1')); @@ -83,7 +83,7 @@ public function testBadSyntaxesShouldNotValidate() * @since 2019-07-29 * @see https://github.com/dragonmantank/cron-expression/issues/18 */ - public function testInvalidRangeShouldNotValidate() + public function testInvalidRangeShouldNotValidate(): void { $f = new MinutesField(); $this->assertFalse($f->validate('0/5')); diff --git a/tests/Cron/MonthFieldTest.php b/tests/Cron/MonthFieldTest.php index fe20e2cc..daa8ea5c 100644 --- a/tests/Cron/MonthFieldTest.php +++ b/tests/Cron/MonthFieldTest.php @@ -17,7 +17,7 @@ class MonthFieldTest extends TestCase /** * @covers \Cron\MonthField::validate */ - public function testValidatesField() + public function testValidatesField(): void { $f = new MonthField(); $this->assertTrue($f->validate('12')); @@ -30,7 +30,7 @@ public function testValidatesField() /** * @covers \Cron\MonthField::isSatisfiedBy */ - public function testChecksIfSatisfied() + public function testChecksIfSatisfied(): void { $f = new MonthField(); $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?')); @@ -40,7 +40,7 @@ public function testChecksIfSatisfied() /** * @covers \Cron\MonthField::increment */ - public function testIncrementsDate() + public function testIncrementsDate(): void { $d = new DateTime('2011-03-15 11:15:00'); $f = new MonthField(); @@ -55,7 +55,7 @@ public function testIncrementsDate() /** * @covers \Cron\MonthField::increment */ - public function testIncrementsDateTimeImmutable() + public function testIncrementsDateTimeImmutable(): void { $d = new DateTimeImmutable('2011-03-15 11:15:00'); $f = new MonthField(); @@ -66,7 +66,7 @@ public function testIncrementsDateTimeImmutable() /** * @covers \Cron\MonthField::increment */ - public function testIncrementsDateWithThirtyMinuteTimezone() + public function testIncrementsDateWithThirtyMinuteTimezone(): void { $tz = date_default_timezone_get(); date_default_timezone_set('America/St_Johns'); @@ -84,7 +84,7 @@ public function testIncrementsDateWithThirtyMinuteTimezone() /** * @covers \Cron\MonthField::increment */ - public function testIncrementsYearAsNeeded() + public function testIncrementsYearAsNeeded(): void { $f = new MonthField(); $d = new DateTime('2011-12-15 00:00:00'); @@ -95,7 +95,7 @@ public function testIncrementsYearAsNeeded() /** * @covers \Cron\MonthField::increment */ - public function testDecrementsYearAsNeeded() + public function testDecrementsYearAsNeeded(): void { $f = new MonthField(); $d = new DateTime('2011-01-15 00:00:00'); @@ -110,7 +110,7 @@ public function testDecrementsYearAsNeeded() * @since 2019-07-29 * @see https://github.com/dragonmantank/cron-expression/issues/24 */ - public function testLiteralsIgnoreCasingProperly() + public function testLiteralsIgnoreCasingProperly(): void { $f = new MonthField(); $this->assertTrue($f->validate('JAN')); From 45e740b357c2028e4579a3d307926c778f7f486f Mon Sep 17 00:00:00 2001 From: Olivier ALLAIN Date: Tue, 6 Oct 2020 00:16:25 +0200 Subject: [PATCH 082/146] add phpstan to travis --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1e5749c4..b8e30cec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,4 +27,6 @@ install: travis_retry composer install --no-interaction --prefer-dist fi -script: vendor/bin/phpunit --coverage-text +script: + - vendor/bin/phpunit --coverage-text + - composer phpstan From 658991e99579f3ca543e10c2666908da26bb1333 Mon Sep 17 00:00:00 2001 From: Olivier ALLAIN Date: Tue, 6 Oct 2020 13:21:52 +0200 Subject: [PATCH 083/146] fix StyleCI --- src/Cron/AbstractField.php | 9 +++++---- src/Cron/DayOfMonthField.php | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Cron/AbstractField.php b/src/Cron/AbstractField.php index 48d11bd2..22d5d0cf 100644 --- a/src/Cron/AbstractField.php +++ b/src/Cron/AbstractField.php @@ -100,11 +100,12 @@ public function isIncrementsOfRanges(string $value): bool */ public function isInRange(int $dateValue, $value): bool { - $parts = array_map(function ($value) { - $value = trim($value); + $parts = array_map( + function ($value) { + $value = trim($value); - return $this->convertLiterals($value); - }, + return $this->convertLiterals($value); + }, explode('-', $value, 2) ); diff --git a/src/Cron/DayOfMonthField.php b/src/Cron/DayOfMonthField.php index 389ac78e..afb30efd 100644 --- a/src/Cron/DayOfMonthField.php +++ b/src/Cron/DayOfMonthField.php @@ -102,8 +102,8 @@ public function isSatisfiedBy(DateTimeInterface $date, $value): bool // Find out if the current day is the nearest day of the week /** @phpstan-ignore-next-line */ return $date->format('j') === self::getNearestWeekday( - (int) $date->format('Y'), - (int) $date->format('m'), + (int) $date->format('Y'), + (int) $date->format('m'), $targetDay )->format('j'); } From 7c8900ca7f1b1e10c592373ad82840a142f4f9ea Mon Sep 17 00:00:00 2001 From: Olivier ALLAIN Date: Tue, 6 Oct 2020 13:31:36 +0200 Subject: [PATCH 084/146] fix dependencies for webmozart/assert --- composer.json | 3 ++- src/Cron/DayOfMonthField.php | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index ce5ccdc8..069b8d04 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,8 @@ } ], "require": { - "php": "^7.1|^8.0" + "php": "^7.1|^8.0", + "webmozart/assert": "^1.7.0" }, "require-dev": { "phpstan/phpstan": "^0.12", diff --git a/src/Cron/DayOfMonthField.php b/src/Cron/DayOfMonthField.php index afb30efd..21c2d97e 100644 --- a/src/Cron/DayOfMonthField.php +++ b/src/Cron/DayOfMonthField.php @@ -6,7 +6,6 @@ use DateTime; use DateTimeInterface; -use Webmozart\Assert\Assert; /** * Day of month field. Allows: * , / - ? L W. From da10dc39d3f908503837ae05e7dc67a06c850467 Mon Sep 17 00:00:00 2001 From: DerekCresswell Date: Thu, 5 Nov 2020 22:34:18 -0500 Subject: [PATCH 085/146] Allow proper usage of interfaces The constructor for CronExpression should take a FieldFactoryInterface, not FieldFactory. --- src/Cron/CronExpression.php | 6 +++--- src/Cron/FieldFactoryInterface.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index fd3a8740..e3b5b7e5 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -85,7 +85,7 @@ public static function factory(string $expression, FieldFactoryInterface $fieldF $expression = $mappings[$shortcut]; } - return new static($expression, $fieldFactory ?: new FieldFactory()); + return new static($expression, $fieldFactory); } /** @@ -112,9 +112,9 @@ public static function isValidExpression(string $expression): bool * Parse a CRON expression. * * @param string $expression CRON expression (e.g. '8 * * * *') - * @param null|FieldFactory $fieldFactory Factory to create cron fields + * @param null|FieldFactoryInterface $fieldFactory Factory to create cron fields */ - public function __construct(string $expression, FieldFactory $fieldFactory = null) + public function __construct(string $expression, FieldFactoryInterface $fieldFactory = null) { $this->fieldFactory = $fieldFactory ?: new FieldFactory(); $this->setExpression($expression); diff --git a/src/Cron/FieldFactoryInterface.php b/src/Cron/FieldFactoryInterface.php index 870a425a..8bd3c658 100644 --- a/src/Cron/FieldFactoryInterface.php +++ b/src/Cron/FieldFactoryInterface.php @@ -4,5 +4,5 @@ interface FieldFactoryInterface { - public function getField(int $position); + public function getField(int $position): FieldInterface; } From 4e6d76eb47b0e522f454b55e2052ece35765c7a3 Mon Sep 17 00:00:00 2001 From: Derek Cresswell Date: Tue, 24 Nov 2020 11:37:38 -0500 Subject: [PATCH 086/146] Depreceate CronExpression::factory and remove references (#99) * Depreceate CronExpression::factory and remove references * Update styling Removed a blank line to make StyleCI happy. Co-authored-by: Chris Tankersley --- README.md | 8 ++-- src/Cron/CronExpression.php | 54 +++++++++------------ tests/Cron/CronExpressionTest.php | 79 +++++++++++++++---------------- 3 files changed, 65 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index a3d5bb99..fbdbdea9 100644 --- a/README.md +++ b/README.md @@ -32,21 +32,21 @@ Usage require_once '/vendor/autoload.php'; // Works with predefined scheduling definitions -$cron = Cron\CronExpression::factory('@daily'); +$cron = new Cron\CronExpression('@daily'); $cron->isDue(); echo $cron->getNextRunDate()->format('Y-m-d H:i:s'); echo $cron->getPreviousRunDate()->format('Y-m-d H:i:s'); // Works with complex expressions -$cron = Cron\CronExpression::factory('3-59/15 6-12 */15 1 2-5'); +$cron = new Cron\CronExpression('3-59/15 6-12 */15 1 2-5'); echo $cron->getNextRunDate()->format('Y-m-d H:i:s'); // Calculate a run date two iterations into the future -$cron = Cron\CronExpression::factory('@daily'); +$cron = new Cron\CronExpression('@daily'); echo $cron->getNextRunDate(null, 2)->format('Y-m-d H:i:s'); // Calculate a run date relative to a specific time -$cron = Cron\CronExpression::factory('@monthly'); +$cron = new Cron\CronExpression('@monthly'); echo $cron->getNextRunDate('2010-01-12 00:00:00')->format('Y-m-d H:i:s'); ``` diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index e3b5b7e5..a96d36f3 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -33,6 +33,15 @@ class CronExpression public const WEEKDAY = 4; public const YEAR = 5; + public const MAPPINGS = [ + '@yearly' => '0 0 1 1 *', + '@annually' => '0 0 1 1 *', + '@monthly' => '0 0 1 * *', + '@weekly' => '0 0 * * 0', + '@daily' => '0 0 * * *', + '@hourly' => '0 * * * *', + ]; + /** * @var array CRON expression parts */ @@ -51,40 +60,20 @@ class CronExpression /** * @var array Order in which to test of cron parts */ - private static $order = [self::YEAR, self::MONTH, self::DAY, self::WEEKDAY, self::HOUR, self::MINUTE]; + private static $order = [ + self::YEAR, + self::MONTH, + self::DAY, + self::WEEKDAY, + self::HOUR, + self::MINUTE, + ]; /** - * Factory method to create a new CronExpression. - * - * @param string $expression The CRON expression to create. There are - * several special predefined values which can be used to substitute the - * CRON expression: - * - * `@yearly`, `@annually` - Run once a year, midnight, Jan. 1 - 0 0 1 1 * - * `@monthly` - Run once a month, midnight, first of month - 0 0 1 * * - * `@weekly` - Run once a week, midnight on Sun - 0 0 * * 0 - * `@daily` - Run once a day, midnight - 0 0 * * * - * `@hourly` - Run once an hour, first minute - 0 * * * * - * @param null|FieldFactoryInterface $fieldFactory Field factory to use - * - * @return CronExpression + * @deprecated since version 3.0.2, use __construct instead. */ public static function factory(string $expression, FieldFactoryInterface $fieldFactory = null): CronExpression { - $mappings = [ - '@yearly' => '0 0 1 1 *', - '@annually' => '0 0 1 1 *', - '@monthly' => '0 0 1 * *', - '@weekly' => '0 0 * * 0', - '@daily' => '0 0 * * *', - '@hourly' => '0 * * * *', - ]; - - $shortcut = strtolower($expression); - if (isset($mappings[$shortcut])) { - $expression = $mappings[$shortcut]; - } - return new static($expression, $fieldFactory); } @@ -94,13 +83,11 @@ public static function factory(string $expression, FieldFactoryInterface $fieldF * @param string $expression the CRON expression to validate * * @return bool True if a valid CRON expression was passed. False if not. - * - * @see \Cron\CronExpression::factory */ public static function isValidExpression(string $expression): bool { try { - self::factory($expression); + new CronExpression($expression); } catch (InvalidArgumentException $e) { return false; } @@ -116,6 +103,9 @@ public static function isValidExpression(string $expression): bool */ public function __construct(string $expression, FieldFactoryInterface $fieldFactory = null) { + $shortcut = strtolower($expression); + $expression = self::MAPPINGS[$shortcut] ?? $expression; + $this->fieldFactory = $fieldFactory ?: new FieldFactory(); $this->setExpression($expression); } diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 0b10893c..bafcbb83 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -18,13 +18,13 @@ class CronExpressionTest extends TestCase { /** - * @covers \Cron\CronExpression::factory + * @covers \Cron\CronExpression::__construct */ - public function testFactoryRecognizesTemplates(): void + public function testConstructorRecognizesTemplates(): void { - $this->assertSame('0 0 1 1 *', CronExpression::factory('@annually')->getExpression()); - $this->assertSame('0 0 1 1 *', CronExpression::factory('@yearly')->getExpression()); - $this->assertSame('0 0 * * 0', CronExpression::factory('@weekly')->getExpression()); + $this->assertSame('0 0 1 1 *', (new CronExpression('@annually'))->getExpression()); + $this->assertSame('0 0 1 1 *', (new CronExpression('@yearly'))->getExpression()); + $this->assertSame('0 0 * * 0', (new CronExpression('@weekly'))->getExpression()); } /** @@ -35,7 +35,7 @@ public function testFactoryRecognizesTemplates(): void public function testParsesCronSchedule(): void { // '2010-09-10 12:00:00' - $cron = CronExpression::factory('1 2-4 * 4,5,6 */3'); + $cron = new CronExpression('1 2-4 * 4,5,6 */3'); $this->assertSame('1', $cron->getExpression(CronExpression::MINUTE)); $this->assertSame('2-4', $cron->getExpression(CronExpression::HOUR)); $this->assertSame('*', $cron->getExpression(CronExpression::DAY)); @@ -55,7 +55,7 @@ public function testParsesCronScheduleThrowsAnException(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid CRON field value A at position 0'); - CronExpression::factory('A 1 2 3 4'); + new CronExpression('A 1 2 3 4'); } /** @@ -67,7 +67,7 @@ public function testParsesCronScheduleThrowsAnException(): void */ public function testParsesCronScheduleWithAnySpaceCharsAsSeparators($schedule, array $expected): void { - $cron = CronExpression::factory($schedule); + $cron = new CronExpression($schedule); $this->assertSame($expected[0], $cron->getExpression(CronExpression::MINUTE)); $this->assertSame($expected[1], $cron->getExpression(CronExpression::HOUR)); $this->assertSame($expected[2], $cron->getExpression(CronExpression::DAY)); @@ -99,7 +99,7 @@ public function testInvalidCronsWillFail(): void { $this->expectException(InvalidArgumentException::class); // Only four values - $cron = CronExpression::factory('* * * 1'); + $cron = new CronExpression('* * * 1'); } /** @@ -109,7 +109,7 @@ public function testInvalidPartsWillFail(): void { $this->expectException(InvalidArgumentException::class); // Only four values - $cron = CronExpression::factory('* * * * *'); + $cron = new CronExpression('* * * * *'); $cron->setPart(1, 'abc'); } @@ -218,7 +218,7 @@ public function scheduleProvider(): array public function testDeterminesIfCronIsDue($schedule, $relativeTime, $nextRun, $isDue): void { // Test next run date - $cron = CronExpression::factory($schedule); + $cron = new CronExpression($schedule); if (\is_string($relativeTime)) { $relativeTime = new DateTime($relativeTime); } elseif (\is_int($relativeTime)) { @@ -242,7 +242,7 @@ public function testDeterminesIfCronIsDue($schedule, $relativeTime, $nextRun, $i */ public function testIsDueHandlesDifferentDates(): void { - $cron = CronExpression::factory('* * * * *'); + $cron = new CronExpression('* * * * *'); $this->assertTrue($cron->isDue()); $this->assertTrue($cron->isDue('now')); $this->assertTrue($cron->isDue(new DateTime('now'))); @@ -256,7 +256,7 @@ public function testIsDueHandlesDifferentDates(): void public function testIsDueHandlesDifferentDefaultTimezones(): void { $originalTimezone = date_default_timezone_get(); - $cron = CronExpression::factory('0 15 * * 3'); //Wednesday at 15:00 + $cron = new CronExpression('0 15 * * 3'); //Wednesday at 15:00 $date = '2014-01-01 15:00'; //Wednesday date_default_timezone_set('UTC'); @@ -282,7 +282,7 @@ public function testIsDueHandlesDifferentDefaultTimezones(): void */ public function testIsDueHandlesDifferentSuppliedTimezones(): void { - $cron = CronExpression::factory('0 15 * * 3'); //Wednesday at 15:00 + $cron = new CronExpression('0 15 * * 3'); //Wednesday at 15:00 $date = '2014-01-01 15:00'; //Wednesday $this->assertTrue($cron->isDue(new DateTime($date, new DateTimeZone('UTC')), 'UTC')); @@ -303,7 +303,7 @@ public function testIsDueHandlesDifferentSuppliedTimezones(): void */ public function testIsDueHandlesDifferentTimezonesAsArgument(): void { - $cron = CronExpression::factory('0 15 * * 3'); //Wednesday at 15:00 + $cron = new CronExpression('0 15 * * 3'); //Wednesday at 15:00 $date = '2014-01-01 15:00'; //Wednesday $utc = new \DateTimeZone('UTC'); $amsterdam = new \DateTimeZone('Europe/Amsterdam'); @@ -324,7 +324,7 @@ public function testIsDueHandlesDifferentTimezonesAsArgument(): void */ public function testRecognisesTimezonesAsPartOfDateTime(): void { - $cron = CronExpression::factory('0 7 * * *'); + $cron = new CronExpression('0 7 * * *'); $tzCron = 'America/New_York'; $tzServer = new \DateTimeZone('Europe/London'); @@ -350,17 +350,17 @@ public function testRecognisesTimezonesAsPartOfDateTime(): void */ public function testCanGetPreviousRunDates(): void { - $cron = CronExpression::factory('* * * * *'); + $cron = new CronExpression('* * * * *'); $next = $cron->getNextRunDate('now'); $two = $cron->getNextRunDate('now', 1); $this->assertEquals($next, $cron->getPreviousRunDate($two)); - $cron = CronExpression::factory('* */2 * * *'); + $cron = new CronExpression('* */2 * * *'); $next = $cron->getNextRunDate('now'); $two = $cron->getNextRunDate('now', 1); $this->assertEquals($next, $cron->getPreviousRunDate($two)); - $cron = CronExpression::factory('* * * */2 *'); + $cron = new CronExpression('* * * */2 *'); $next = $cron->getNextRunDate('now'); $two = $cron->getNextRunDate('now', 1); $this->assertEquals($next, $cron->getPreviousRunDate($two)); @@ -371,7 +371,7 @@ public function testCanGetPreviousRunDates(): void */ public function testProvidesMultipleRunDates(): void { - $cron = CronExpression::factory('*/2 * * * *'); + $cron = new CronExpression('*/2 * * * *'); $this->assertEquals([ new DateTime('2008-11-09 00:00:00'), new DateTime('2008-11-09 00:02:00'), @@ -387,7 +387,7 @@ public function testProvidesMultipleRunDates(): void public function testProvidesMultipleRunDatesForTheFarFuture(): void { // Fails with the default 1000 iteration limit - $cron = CronExpression::factory('0 0 12 1 *'); + $cron = new CronExpression('0 0 12 1 *'); $cron->setMaxIterationCount(2000); $this->assertEquals([ new DateTime('2016-01-12 00:00:00'), @@ -407,7 +407,7 @@ public function testProvidesMultipleRunDatesForTheFarFuture(): void */ public function testCanIterateOverNextRuns(): void { - $cron = CronExpression::factory('@weekly'); + $cron = new CronExpression('@weekly'); $nextRun = $cron->getNextRunDate('2008-11-09 08:00:00'); $this->assertEquals($nextRun, new DateTime('2008-11-16 00:00:00')); @@ -427,7 +427,7 @@ public function testCanIterateOverNextRuns(): void */ public function testGetRunDateHandlesDifferentDates() { - $cron = CronExpression::factory('@weekly'); + $cron = new CronExpression('@weekly'); $date = new DateTime("2019-03-10 00:00:00"); $this->assertEquals($date, $cron->getNextRunDate("2019-03-03 08:00:00")); $this->assertEquals($date, $cron->getNextRunDate(new DateTime("2019-03-03 08:00:00"))); @@ -439,7 +439,7 @@ public function testGetRunDateHandlesDifferentDates() */ public function testSkipsCurrentDateByDefault() { - $cron = CronExpression::factory('* * * * *'); + $cron = new CronExpression('* * * * *'); $current = new DateTime('now'); $next = $cron->getNextRunDate($current); $nextPrev = $cron->getPreviousRunDate($next); @@ -452,7 +452,7 @@ public function testSkipsCurrentDateByDefault() */ public function testStripsForSeconds(): void { - $cron = CronExpression::factory('* * * * *'); + $cron = new CronExpression('* * * * *'); $current = new DateTime('2011-09-27 10:10:54'); $this->assertSame('2011-09-27 10:11:00', $cron->getNextRunDate($current)->format('Y-m-d H:i:s')); } @@ -462,13 +462,13 @@ public function testStripsForSeconds(): void */ public function testFixesPhpBugInDateIntervalMonth(): void { - $cron = CronExpression::factory('0 0 27 JAN *'); + $cron = new CronExpression('0 0 27 JAN *'); $this->assertSame('2011-01-27 00:00:00', $cron->getPreviousRunDate('2011-08-22 00:00:00')->format('Y-m-d H:i:s')); } public function testIssue29(): void { - $cron = CronExpression::factory('@weekly'); + $cron = new CronExpression('@weekly'); $this->assertSame( '2013-03-10 00:00:00', $cron->getPreviousRunDate('2013-03-17 00:00:00')->format('Y-m-d H:i:s') @@ -480,17 +480,17 @@ public function testIssue29(): void */ public function testIssue20(): void { - $e = CronExpression::factory('* * * * MON#1'); + $e = new CronExpression('* * * * MON#1'); $this->assertTrue($e->isDue(new DateTime('2014-04-07 00:00:00'))); $this->assertFalse($e->isDue(new DateTime('2014-04-14 00:00:00'))); $this->assertFalse($e->isDue(new DateTime('2014-04-21 00:00:00'))); - $e = CronExpression::factory('* * * * SAT#2'); + $e->setExpression('* * * * SAT#2'); $this->assertFalse($e->isDue(new DateTime('2014-04-05 00:00:00'))); $this->assertTrue($e->isDue(new DateTime('2014-04-12 00:00:00'))); $this->assertFalse($e->isDue(new DateTime('2014-04-19 00:00:00'))); - $e = CronExpression::factory('* * * * SUN#3'); + $e->setExpression('* * * * SUN#3'); $this->assertFalse($e->isDue(new DateTime('2014-04-13 00:00:00'))); $this->assertTrue($e->isDue(new DateTime('2014-04-20 00:00:00'))); $this->assertFalse($e->isDue(new DateTime('2014-04-27 00:00:00'))); @@ -503,14 +503,13 @@ public function testKeepOriginalTime(): void { $now = new \DateTime(); $strNow = $now->format(DateTime::ISO8601); - $cron = CronExpression::factory('0 0 * * *'); + $cron = new CronExpression('0 0 * * *'); $cron->getPreviousRunDate($now); $this->assertSame($strNow, $now->format(DateTime::ISO8601)); } /** * @covers \Cron\CronExpression::__construct - * @covers \Cron\CronExpression::factory * @covers \Cron\CronExpression::isValidExpression * @covers \Cron\CronExpression::setExpression * @covers \Cron\CronExpression::setPart @@ -551,14 +550,14 @@ public function testDoubleZeroIsValid(): void $this->assertTrue(CronExpression::isValidExpression('* 00 * * *')); $this->assertTrue(CronExpression::isValidExpression('* 01 * * *')); - $e = CronExpression::factory('00 * * * *'); + $e = new CronExpression('00 * * * *'); $this->assertTrue($e->isDue(new DateTime('2014-04-07 00:00:00'))); - $e = CronExpression::factory('01 * * * *'); + $e->setExpression('01 * * * *'); $this->assertTrue($e->isDue(new DateTime('2014-04-07 00:01:00'))); - $e = CronExpression::factory('* 00 * * *'); + $e->setExpression('* 00 * * *'); $this->assertTrue($e->isDue(new DateTime('2014-04-07 00:00:00'))); - $e = CronExpression::factory('* 01 * * *'); + $e->setExpression('* 01 * * *'); $this->assertTrue($e->isDue(new DateTime('2014-04-07 01:00:00'))); } @@ -575,7 +574,7 @@ public function testRangesWrapAroundWithLargeSteps(): void $this->assertTrue($f->validate('*/123')); $this->assertSame([4], $f->getRangeForExpression('*/123', 12)); - $e = CronExpression::factory('* * * */123 *'); + $e = new CronExpression('* * * */123 *'); $this->assertTrue($e->isDue(new DateTime('2014-04-07 00:00:00'))); $nextRunDate = $e->getNextRunDate(new DateTime('2014-04-07 00:00:00')); @@ -595,7 +594,7 @@ public function testFieldPositionIsHumanAdjusted(): void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('6 is not a valid position'); - $e = CronExpression::factory('0 * * * * ? *'); + $e = new CronExpression('0 * * * * ? *'); } /** @@ -603,7 +602,7 @@ public function testFieldPositionIsHumanAdjusted(): void */ public function testMakeDayOfWeekAnOrSometimes() { - $cron = CronExpression::factory('30 0 1 * 1'); + $cron = new CronExpression('30 0 1 * 1'); $runs = $cron->getMultipleRunDates(5, date("2019-10-10 23:20:00"), false, true); $this->assertSame("2019-10-14 00:30:00", $runs[0]->format('Y-m-d H:i:s')); @@ -620,7 +619,7 @@ public function testMakeDayOfWeekAnOrSometimes() */ public function testNextRunDateShouldNotAddMinutes() { - $e = CronExpression::factory('* 19 * * *'); + $e = new CronExpression('* 19 * * *'); $nextRunDate = $e->getNextRunDate(); $this->assertSame("00", $nextRunDate->format("i")); From c6738a4be72921c762c14c171f97c503a42885aa Mon Sep 17 00:00:00 2001 From: Derek Cresswell Date: Tue, 24 Nov 2020 11:38:39 -0500 Subject: [PATCH 087/146] Adds getParts to CronExpression (#84) --- src/Cron/CronExpression.php | 11 +++++++++++ tests/Cron/CronExpressionTest.php | 15 +++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index a96d36f3..2022399d 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -267,6 +267,17 @@ public function getExpression($part = null): ?string return null; } + /** + * Gets the parts of the cron expression as an array. + * + * @return string[] + * The array of parts that make up this expression. + */ + public function getParts() + { + return $this->cronParts; + } + /** * Helper method to output the full expression. * diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index bafcbb83..6b7176f4 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -624,4 +624,19 @@ public function testNextRunDateShouldNotAddMinutes() $this->assertSame("00", $nextRunDate->format("i")); } + + /** + * Tests the getParts function. + */ + public function testGetParts() + { + $e = CronExpression::factory('0 22 * * 1-5'); + $parts = $e->getParts(); + + $this->assertSame('0', $parts[0]); + $this->assertSame('22', $parts[1]); + $this->assertSame('*', $parts[2]); + $this->assertSame('*', $parts[3]); + $this->assertSame('1-5', $parts[4]); + } } From 786b314491fa317d8860883e59a85bace45ecbd6 Mon Sep 17 00:00:00 2001 From: Simon Schaufelberger Date: Tue, 24 Nov 2020 18:08:19 +0100 Subject: [PATCH 088/146] Ignore files when exporting package (#78) This commit is part of a campaign to reduce the amount of data transferred to save global bandwidth and reduce the amount of CO2. See https://github.com/Codeception/Codeception/pull/5527 for more info. --- .gitattributes | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.gitattributes b/.gitattributes index 5f220cf3..435f1a8f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,8 +1,11 @@ * text=auto -.gitattributes export-ignore -.gitignore export-ignore -.php_cs export-ignore -.styleci.yml export-ignore -.travis.yml export-ignore -phpunit.xml.dist export-ignore +/.github export-ignore +/tests export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.php_cs export-ignore +/.styleci.yml export-ignore +/.travis.yml export-ignore +/phpunit.xml.dist export-ignore From 908609e2d7909de9eb9f8000d6e3e1e3298b85cb Mon Sep 17 00:00:00 2001 From: Derek Cresswell Date: Tue, 24 Nov 2020 12:11:30 -0500 Subject: [PATCH 089/146] Updates Phpunit configuration (#102) Phpunit had output warnings with the outdated configuration. This runs --migrate-configuration to update it. --- phpunit.xml.dist | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 484b611f..e525edf0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,6 @@ - + + + ./src/Cron + + + ./tests - - - ./src/Cron - - - From 7f960c9fc3d1dc7e8c7d4c944e0a1d5c59045993 Mon Sep 17 00:00:00 2001 From: oallain Date: Tue, 24 Nov 2020 18:19:53 +0100 Subject: [PATCH 090/146] add github action - phpstan and phpunit (#95) --- .github/workflows/tests.yml | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..80ca6673 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,44 @@ +on: + push: + branches: + - master + pull_request: + branches: + - master + +name: Tests + +jobs: + phpstan: + name: PHPStan - PHP ${{ matrix.php }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php: [7.1, 7.2, 7.3, 7.4, 8.0] + + steps: + - name: "Checkout" + uses: actions/checkout@v2 + + - name: PHPStan + uses: docker://oskarstark/phpstan-ga + with: + args: analyse --level=max src tests + + phpunit: + name: PHPUnit - PHP ${{ matrix.php }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php: [7.1, 7.2, 7.3, 7.4, 8.0] + + steps: + - name: "Checkout" + uses: actions/checkout@v2 + + - name: PHPUnit + uses: php-actions/phpunit@v9 From 2fe1b7d3d7027f17d79e76383cdf840f1b1bd8c3 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Tue, 24 Nov 2020 13:43:29 -0500 Subject: [PATCH 091/146] Fixing max run tests for PHP 7.x, excluding 8.0 --- .github/workflows/tests.yml | 1 + composer.json | 2 +- src/Cron/CronExpression.php | 1 + src/Cron/DayOfWeekField.php | 3 +-- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 80ca6673..7562930a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,6 +17,7 @@ jobs: fail-fast: false matrix: php: [7.1, 7.2, 7.3, 7.4, 8.0] + continue-on-error: ${{ matrix.php == '8.0' }} steps: - name: "Checkout" diff --git a/composer.json b/composer.json index 069b8d04..0641bd0d 100644 --- a/composer.json +++ b/composer.json @@ -35,6 +35,6 @@ "mtdowling/cron-expression": "^1.0" }, "scripts": { - "phpstan": "./vendor/bin/phpstan analyse -l max src tests" + "phpstan": "./vendor/bin/phpstan analyse -l max src" } } diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index a4b348ed..e0fb335d 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -75,6 +75,7 @@ class CronExpression */ public static function factory(string $expression, FieldFactoryInterface $fieldFactory = null): CronExpression { + /** @phpstan-ignore-next-line */ return new static($expression, $fieldFactory); } diff --git a/src/Cron/DayOfWeekField.php b/src/Cron/DayOfWeekField.php index a62e6074..98056951 100644 --- a/src/Cron/DayOfWeekField.php +++ b/src/Cron/DayOfWeekField.php @@ -80,8 +80,7 @@ public function isSatisfiedBy(DateTimeInterface $date, $value): bool $tdate = $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth); while ($tdate->format('w') != $weekday) { $tdateClone = new DateTime(); - $tdate = $tdateClone - ->setTimezone($tdate->getTimezone()) + $tdate = $tdateClone->setTimezone($tdate->getTimezone()) ->setDate($currentYear, $currentMonth, --$lastDayOfMonth); } From 323b5635e809464c6a4a6e18c4a80349701441a9 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Tue, 24 Nov 2020 13:58:15 -0500 Subject: [PATCH 092/146] Cleaned up GHA, dropped PHP 7.1 support (#104) * Allow 8.0 to fail cleanly, because there are some slight differences with DateTime that can't be easily reconciled in phpstan * Upped minimum version to 7.2 * Redid some of the GHA syntax --- .github/workflows/tests.yml | 38 +++++++++++++++++++++++++++++++------ composer.json | 5 +++-- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7562930a..cf8055d5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - php: [7.1, 7.2, 7.3, 7.4, 8.0] + php: [7.2, 7.3, 7.4, 8.0] continue-on-error: ${{ matrix.php == '8.0' }} steps: @@ -26,7 +26,7 @@ jobs: - name: PHPStan uses: docker://oskarstark/phpstan-ga with: - args: analyse --level=max src tests + args: analyse --level=max src phpunit: name: PHPUnit - PHP ${{ matrix.php }} @@ -35,11 +35,37 @@ jobs: strategy: fail-fast: false matrix: - php: [7.1, 7.2, 7.3, 7.4, 8.0] + php: [7.2, 7.3, 7.4, 8.0] steps: - - name: "Checkout" + - name: Checkout uses: actions/checkout@v2 - - name: PHPUnit - uses: php-actions/phpunit@v9 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: json, mbstring + coverage: pcov + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup problem matchers for PHPUnit + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Get Composer cache directory + id: composercache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache Composer dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composercache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer update --prefer-dist --no-interaction + + - name: Analyze & test + run: composer test -- -v --coverage-clover=coverage.xml diff --git a/composer.json b/composer.json index 0641bd0d..59439b05 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ } ], "require": { - "php": "^7.1|^8.0", + "php": "^7.2|^8.0", "webmozart/assert": "^1.7.0" }, "require-dev": { @@ -35,6 +35,7 @@ "mtdowling/cron-expression": "^1.0" }, "scripts": { - "phpstan": "./vendor/bin/phpstan analyse -l max src" + "phpstan": "./vendor/bin/phpstan analyse -l max src", + "test": "phpunit" } } From df032c23c1fd45b8b7212271fc777dab36d67ca8 Mon Sep 17 00:00:00 2001 From: Menno Holtkamp Date: Tue, 24 Nov 2020 20:44:35 +0100 Subject: [PATCH 093/146] Mark YEAR constant as deprecated to prevent usage (#87) --- src/Cron/CronExpression.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index e0fb335d..9b342761 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -32,6 +32,8 @@ class CronExpression public const DAY = 2; public const MONTH = 3; public const WEEKDAY = 4; + + /** @deprecated */ public const YEAR = 5; public const MAPPINGS = [ From 7a8c6e56ab3ffcc538d05e8155bb42269abf1a0c Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Tue, 24 Nov 2020 14:55:57 -0500 Subject: [PATCH 094/146] Added changelog for 3.1.0 --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 610d6bc9..fab23757 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Change Log +## [3.1.0] - 2020-11-24 + +### Added +- Added `CronExpression::getParts()` method to get parts of the expression as an array (#83) + +### Changed +- Changed to Interfaces for some type hints (#97, #86) +- Dropped minimum PHP version to 7.2 +- Few syntax changes for phpstan compatibility (#93) + +### Fixed +- N/A + +### Deprecated +- Deprecated `CronExpression::factory` in favor of the constructor (#56) +- Deprecated `CronExpression::YEAR` as a formality, the functionality is already removed (#87) + ## [3.0.1] - 2020-10-12 ### Added - Added support for PHP 8 (#92) From 76ec7333bd29db2fe3c883b311412a12e5022796 Mon Sep 17 00:00:00 2001 From: Derek Cresswell Date: Wed, 17 Mar 2021 14:04:13 -0400 Subject: [PATCH 095/146] Change private class variables to protected (#106) This allows extending classes to access the order for parts the same way as the base class. --- src/Cron/CronExpression.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index 9b342761..994c1147 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -48,22 +48,22 @@ class CronExpression /** * @var array CRON expression parts */ - private $cronParts; + protected $cronParts; /** * @var FieldFactoryInterface CRON field factory */ - private $fieldFactory; + protected $fieldFactory; /** * @var int Max iteration count when searching for next run date */ - private $maxIterationCount = 1000; + protected $maxIterationCount = 1000; /** * @var array Order in which to test of cron parts */ - private static $order = [ + protected static $order = [ self::YEAR, self::MONTH, self::DAY, From 49b883286229072fed5a8fdd4a460dc1c9efc7dd Mon Sep 17 00:00:00 2001 From: Alexey Khrushch Date: Wed, 5 Jan 2022 04:43:09 +0200 Subject: [PATCH 096/146] refactor(optimize): getMultipleRunDates method (#75) --- src/Cron/CronExpression.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index 994c1147..124a7d58 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -240,10 +240,10 @@ public function getPreviousRunDate($currentTime = 'now', int $nth = 0, bool $all public function getMultipleRunDates(int $total, $currentTime = 'now', bool $invert = false, bool $allowCurrentDate = false, $timeZone = null): array { $matches = []; - $max = max(0, $total); - for ($i = 0; $i < $max; ++$i) { + for ($i = 0; $i < $total; ++$i) { try { - $matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate, $timeZone); + $currentTime = $this->getRunDate($currentTime, 0, $invert, $i === 0 ? $allowCurrentDate : false, $timeZone); + $matches[] = $currentTime; } catch (RuntimeException $e) { break; } From 4f6482a8d26e8a6adc780b227ec7a34c3b1e4698 Mon Sep 17 00:00:00 2001 From: "Chun-Sheng, Li" Date: Wed, 5 Jan 2022 10:49:48 +0800 Subject: [PATCH 097/146] Improve instance of assertion (#105) --- tests/Cron/FieldFactoryTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Cron/FieldFactoryTest.php b/tests/Cron/FieldFactoryTest.php index 357c0896..a6412b59 100644 --- a/tests/Cron/FieldFactoryTest.php +++ b/tests/Cron/FieldFactoryTest.php @@ -29,7 +29,7 @@ public function testRetrievesFieldInstances(): void $f = new FieldFactory(); foreach ($mappings as $position => $class) { - $this->assertSame($class, \get_class($f->getField($position))); + $this->assertInstanceOf($class, $f->getField($position)); } } From cc1d62c82336bdbecf2776bd1a2a0fd2ef68fc10 Mon Sep 17 00:00:00 2001 From: Ike Devolder Date: Wed, 5 Jan 2022 03:51:51 +0100 Subject: [PATCH 098/146] midnight is also an allowed shortcut in crontab (#117) @see https://manpages.debian.org/buster/cron/crontab.5.en.html this adds the `@midnight` to the mapping which is the same as `@daily`. But since it is available in some cron implementations it should not be indicated as invalid. Signed-off-by: BlackEagle --- README.md | 2 +- src/Cron/CronExpression.php | 3 ++- tests/Cron/CronExpressionTest.php | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fbdbdea9..0926f9d6 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ This library also supports a few macros: * `@yearly`, `@annually` - Run once a year, midnight, Jan. 1 - `0 0 1 1 *` * `@monthly` - Run once a month, midnight, first of month - `0 0 1 * *` * `@weekly` - Run once a week, midnight on Sun - `0 0 * * 0` -* `@daily` - Run once a day, midnight - `0 0 * * *` +* `@daily`, `@midnight` - Run once a day, midnight - `0 0 * * *` * `@hourly` - Run once an hour, first minute - `0 * * * *` Requirements diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index 124a7d58..ef22a464 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -32,7 +32,7 @@ class CronExpression public const DAY = 2; public const MONTH = 3; public const WEEKDAY = 4; - + /** @deprecated */ public const YEAR = 5; @@ -42,6 +42,7 @@ class CronExpression '@monthly' => '0 0 1 * *', '@weekly' => '0 0 * * 0', '@daily' => '0 0 * * *', + '@midnight' => '0 0 * * *', '@hourly' => '0 * * * *', ]; diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index c03d2464..d9e7b420 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -26,6 +26,8 @@ public function testConstructorRecognizesTemplates(): void $this->assertSame('0 0 1 1 *', (new CronExpression('@annually'))->getExpression()); $this->assertSame('0 0 1 1 *', (new CronExpression('@yearly'))->getExpression()); $this->assertSame('0 0 * * 0', (new CronExpression('@weekly'))->getExpression()); + $this->assertSame('0 0 * * *', (new CronExpression('@daily'))->getExpression()); + $this->assertSame('0 0 * * *', (new CronExpression('@midnight'))->getExpression()); } /** From 570c4f0188ce342ec69afaa244661fdb4faca8d1 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Tue, 4 Jan 2022 21:58:23 -0500 Subject: [PATCH 099/146] F Updated changelog for v3.2.0 release --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fab23757..8d4966f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Change Log +## [3.2.0] - 2022-01-04 + +### Added +- Added alias for `@midnight` (#117) + +### Changed +- Improved testing for instance of field in tests (#105) +- Optimization for determining multiple run dates (#75) +- `CronExpression` properties changed from private to protected (#106) + +### Fixed +- N/A + ## [3.1.0] - 2020-11-24 ### Added From e89a549c996c7c810aab9510881aa16ee11239d3 Mon Sep 17 00:00:00 2001 From: Kita Date: Wed, 5 Jan 2022 12:09:51 +0900 Subject: [PATCH 100/146] Fix "," delimiter precedence (#122) --- src/Cron/AbstractField.php | 22 +++++++++++----------- tests/Cron/AbstractFieldTest.php | 2 ++ tests/Cron/HoursFieldTest.php | 2 +- tests/Cron/MinutesFieldTest.php | 2 +- tests/Cron/MonthFieldTest.php | 2 +- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/Cron/AbstractField.php b/src/Cron/AbstractField.php index 22d5d0cf..8aa5be78 100644 --- a/src/Cron/AbstractField.php +++ b/src/Cron/AbstractField.php @@ -249,17 +249,6 @@ public function validate(string $value): bool return true; } - if (false !== strpos($value, '/')) { - [$range, $step] = explode('/', $value); - - // Don't allow numeric ranges - if (is_numeric($range)) { - return false; - } - - return $this->validate($range) && filter_var($step, FILTER_VALIDATE_INT); - } - // Validate each chunk of a list individually if (false !== strpos($value, ',')) { foreach (explode(',', $value) as $listItem) { @@ -271,6 +260,17 @@ public function validate(string $value): bool return true; } + if (false !== strpos($value, '/')) { + [$range, $step] = explode('/', $value); + + // Don't allow numeric ranges + if (is_numeric($range)) { + return false; + } + + return $this->validate($range) && filter_var($step, FILTER_VALIDATE_INT); + } + if (false !== strpos($value, '-')) { if (substr_count($value, '-') > 1) { return false; diff --git a/tests/Cron/AbstractFieldTest.php b/tests/Cron/AbstractFieldTest.php index de0784d8..e953abfc 100644 --- a/tests/Cron/AbstractFieldTest.php +++ b/tests/Cron/AbstractFieldTest.php @@ -137,5 +137,7 @@ public function testGetRangeForExpressionExpandsCorrectly(): void $this->assertSame(['5', '6', '7', '11', '12', '13'], $f->getRangeForExpression('5,6,7,11,12,13', 23)); $this->assertSame([0, 6, 12, 18], $f->getRangeForExpression('*/6', 23)); $this->assertSame([5, 11], $f->getRangeForExpression('5-13/6', 23)); + $this->assertSame([1, 2, 3, 4, 11, 13, 21, 24, 27, 40, 50], $f->getRangeForExpression('1-4,11-14/2,21-27/3,40-59/10', 59)); + $this->assertSame(['1', '3', 5, 6, 7, 11, 12, 13, 14, 15, 17, 19, 21, '23'], $f->getRangeForExpression('1,3,5-7,11-15/1,17-22/2,23', 23)); } } diff --git a/tests/Cron/HoursFieldTest.php b/tests/Cron/HoursFieldTest.php index 837dc3b5..37a5ce42 100644 --- a/tests/Cron/HoursFieldTest.php +++ b/tests/Cron/HoursFieldTest.php @@ -24,7 +24,7 @@ public function testValidatesField(): void $this->assertTrue($f->validate('00')); $this->assertTrue($f->validate('01')); $this->assertTrue($f->validate('*')); - $this->assertFalse($f->validate('*/3,1,1-12')); + $this->assertTrue($f->validate('*/3,1,1-12')); $this->assertFalse($f->validate('1/10')); } diff --git a/tests/Cron/MinutesFieldTest.php b/tests/Cron/MinutesFieldTest.php index 489548ef..e6446180 100644 --- a/tests/Cron/MinutesFieldTest.php +++ b/tests/Cron/MinutesFieldTest.php @@ -22,7 +22,7 @@ public function testValidatesField(): void $f = new MinutesField(); $this->assertTrue($f->validate('1')); $this->assertTrue($f->validate('*')); - $this->assertFalse($f->validate('*/3,1,1-12')); + $this->assertTrue($f->validate('*/3,1,1-12')); $this->assertFalse($f->validate('1/10')); } diff --git a/tests/Cron/MonthFieldTest.php b/tests/Cron/MonthFieldTest.php index daa8ea5c..7a686a5d 100644 --- a/tests/Cron/MonthFieldTest.php +++ b/tests/Cron/MonthFieldTest.php @@ -22,7 +22,7 @@ public function testValidatesField(): void $f = new MonthField(); $this->assertTrue($f->validate('12')); $this->assertTrue($f->validate('*')); - $this->assertFalse($f->validate('*/10,2,1-12')); + $this->assertTrue($f->validate('*/10,2,1-12')); $this->assertFalse($f->validate('1.fix-regexp')); $this->assertFalse($f->validate('1/10')); } From 2146fe771298356c3300962508d99bf6191cfc9d Mon Sep 17 00:00:00 2001 From: Olaf Kryus <14991562+olafkryus@users.noreply.github.com> Date: Wed, 5 Jan 2022 04:38:01 +0100 Subject: [PATCH 101/146] Fixed getPreviousRunDate return value for cron expression with both day-of-month and day-of-week set (#121) --- src/Cron/CronExpression.php | 3 +++ tests/Cron/CronExpressionTest.php | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index ef22a464..8cdbd97c 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -394,6 +394,9 @@ protected function getRunDate($currentTime = null, int $nth = 0, bool $invert = usort($combined, function ($a, $b) { return $a->format('Y-m-d H:i:s') <=> $b->format('Y-m-d H:i:s'); }); + if ($invert) { + $combined = array_reverse($combined); + } return $combined[$nth]; } diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index d9e7b420..079af8e4 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -442,6 +442,22 @@ public function testGetRunDateHandlesDifferentDates(): void $this->assertEquals($date, $cron->getNextRunDate(new DateTimeImmutable("2019-03-03 08:00:00"))); } + /** + * If both day of month and day of week are set in an expression, + * we have to return a date which among dates matching either of two criteria is closest to the current date. + * + * Previously the earliest of dates was always returned, which was incorrect for previous run date. + * + * @covers \Cron\CronExpression::getRunDate + */ + public function testGetRunDateHandlesSimultaneousDayOfMonthAndDayOfWeek(): void + { + $cron = new CronExpression('0 0 13 * 3'); + $date = new DateTime("2021-07-15 00:00:00"); + $this->assertEquals(new DateTime("2021-07-21 00:00:00"), $cron->getNextRunDate($date)); + $this->assertEquals(new DateTime("2021-07-14 00:00:00"), $cron->getPreviousRunDate($date)); + } + /** * @covers \Cron\CronExpression::getRunDate */ From 6a9fccce201486b25f9ce209366bc55976623e78 Mon Sep 17 00:00:00 2001 From: Sergiy Petrov Date: Wed, 5 Jan 2022 05:39:14 +0200 Subject: [PATCH 102/146] Update tests.yml (#125) --- .github/workflows/tests.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cf8055d5..4fc29dfa 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,8 +16,7 @@ jobs: strategy: fail-fast: false matrix: - php: [7.2, 7.3, 7.4, 8.0] - continue-on-error: ${{ matrix.php == '8.0' }} + php: [7.2, 7.3, 7.4, 8.0, 8.1] steps: - name: "Checkout" @@ -35,7 +34,7 @@ jobs: strategy: fail-fast: false matrix: - php: [7.2, 7.3, 7.4, 8.0] + php: [7.2, 7.3, 7.4, 8.0, 8.1] steps: - name: Checkout From 7a364e7f57018500cc40abf626fe1fb51f6cde68 Mon Sep 17 00:00:00 2001 From: "Chun-Sheng, Li" Date: Wed, 5 Jan 2022 11:47:35 +0800 Subject: [PATCH 103/146] Fix failing PHPStan issue (#130) --- .github/workflows/tests.yml | 13 ++++++++++--- composer.json | 2 +- tests/Cron/CronExpressionTest.php | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4fc29dfa..ac4b8752 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,10 +22,17 @@ jobs: - name: "Checkout" uses: actions/checkout@v2 - - name: PHPStan - uses: docker://oskarstark/phpstan-ga + - name: Setup PHP + uses: shivammathur/setup-php@v2 with: - args: analyse --level=max src + php-version: ${{ matrix.php }} + extensions: json, mbstring + + - name: Install dependencies + run: composer update --prefer-dist --no-interaction + + - name: Start Static Analyze + run: composer phpstan -n phpunit: name: PHPUnit - PHP ${{ matrix.php }} diff --git a/composer.json b/composer.json index 59439b05..54f296e7 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,7 @@ "mtdowling/cron-expression": "^1.0" }, "scripts": { - "phpstan": "./vendor/bin/phpstan analyse -l max src", + "phpstan": "./vendor/bin/phpstan analyse -l max src tests", "test": "phpunit" } } diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 079af8e4..72f60be2 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -652,7 +652,7 @@ public function testNextRunDateShouldNotAddMinutes(): void /** * Tests the getParts function. */ - public function testGetParts() + public function testGetParts(): void { $e = CronExpression::factory('0 22 * * 1-5'); $parts = $e->getParts(); From 5d6c9a7e4e637aa1694db37d2839302245620b34 Mon Sep 17 00:00:00 2001 From: AllenJB Date: Wed, 5 Jan 2022 03:59:05 +0000 Subject: [PATCH 104/146] DST handling fixes (fixes #111, #112) (#115) * DST fix (attempt 1) * More tests and further fixes WIP as I believe the change to HoursField::isSatisfiedBy is going to break expressions with multiple parts - but one thing at a time! * WIP More tests and further fixes; API change: isSatisfiedBy now requires knowledge of which direction we're travelling in to handle checking initial values correctly * WIP Save Point - Trying to fix one thing breaks another, but I have an idea... * WIP Abstracted NextRunDateTime which keeps track of offset changes, regardless of whether they occurred when changing minute or hour (and to persist them until a next change is made) * WIP Fix easy fixes * WIP More test fixes * All current tests pass! * Fix for issue #112; Use a cache of timezone transitions to avoid having to modify date/time objects every single time we want to check if hour is satisfied All tests pass * Cleanup All tests pass * Cleanup All tests pass * Cleanup - backing out the NextRunDateTime abstraction; All tests pass * Cleanup; All tests pass * Cleanup (diff tidy, restoring deleted tests); All tests pass * Cleanup (diff tidy); All tests pass * Fix CI issues * Fix CI issues Co-authored-by: Chris Tankersley --- src/Cron/AbstractField.php | 26 ++ src/Cron/CronExpression.php | 37 ++- src/Cron/DayOfMonthField.php | 10 +- src/Cron/DayOfWeekField.php | 27 +- src/Cron/FieldInterface.php | 2 +- src/Cron/HoursField.php | 150 +++++++++- src/Cron/MinutesField.php | 36 ++- src/Cron/MonthField.php | 12 +- tests/Cron/CronExpressionTest.php | 4 +- tests/Cron/DayOfMonthFieldTest.php | 4 +- tests/Cron/DayOfWeekFieldTest.php | 28 +- tests/Cron/DaylightSavingsTest.php | 438 +++++++++++++++++++++++++++++ tests/Cron/HoursFieldTest.php | 23 ++ tests/Cron/MinutesFieldTest.php | 5 +- tests/Cron/MonthFieldTest.php | 4 +- 15 files changed, 724 insertions(+), 82 deletions(-) create mode 100644 tests/Cron/DaylightSavingsTest.php diff --git a/src/Cron/AbstractField.php b/src/Cron/AbstractField.php index 8aa5be78..f13e59a5 100644 --- a/src/Cron/AbstractField.php +++ b/src/Cron/AbstractField.php @@ -4,6 +4,8 @@ namespace Cron; +use DateTimeInterface; + /** * Abstract CRON expression field. */ @@ -300,4 +302,28 @@ public function validate(string $value): bool return \in_array($value, $this->fullRange, true); } + + protected function timezoneSafeModify(DateTimeInterface $dt, string $modification): DateTimeInterface + { + $timezone = $dt->getTimezone(); + $dt = $dt->setTimezone(new \DateTimeZone("UTC")); + $dt = $dt->modify($modification); + $dt = $dt->setTimezone($timezone); + return $dt; + } + + protected function setTimeHour(DateTimeInterface $date, bool $invert, int $originalTimestamp): DateTimeInterface + { + $date = $date->setTime((int)$date->format('H'), ($invert ? 59 : 0)); + + // setTime caused the offset to change, moving time in the wrong direction + $actualTimestamp = $date->format('U'); + if ((! $invert) && ($actualTimestamp <= $originalTimestamp)) { + $date = $this->timezoneSafeModify($date, "+1 hour"); + } elseif ($invert && ($actualTimestamp >= $originalTimestamp)) { + $date = $this->timezoneSafeModify($date, "-1 hour"); + } + + return $date; + } } diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index 8cdbd97c..f71490e1 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -240,14 +240,32 @@ public function getPreviousRunDate($currentTime = 'now', int $nth = 0, bool $all */ public function getMultipleRunDates(int $total, $currentTime = 'now', bool $invert = false, bool $allowCurrentDate = false, $timeZone = null): array { + $timeZone = $this->determineTimeZone($currentTime, $timeZone); + + if ('now' === $currentTime) { + $currentTime = new DateTime(); + } elseif ($currentTime instanceof DateTime) { + $currentTime = clone $currentTime; + } elseif ($currentTime instanceof DateTimeImmutable) { + $currentTime = DateTime::createFromFormat('U', $currentTime->format('U')); + } elseif (\is_string($currentTime)) { + $currentTime = new DateTime($currentTime); + } + + Assert::isInstanceOf($currentTime, DateTime::class); + $currentTime->setTimezone(new DateTimeZone($timeZone)); + $matches = []; for ($i = 0; $i < $total; ++$i) { try { - $currentTime = $this->getRunDate($currentTime, 0, $invert, $i === 0 ? $allowCurrentDate : false, $timeZone); - $matches[] = $currentTime; + $result = $this->getRunDate($currentTime, 0, $invert, $allowCurrentDate, $timeZone); } catch (RuntimeException $e) { break; } + + $allowCurrentDate = false; + $currentTime = clone $result; + $matches[] = $result; } return $matches; @@ -364,7 +382,9 @@ protected function getRunDate($currentTime = null, int $nth = 0, bool $invert = Assert::isInstanceOf($currentDate, DateTime::class); $currentDate->setTimezone(new DateTimeZone($timeZone)); - $currentDate->setTime((int) $currentDate->format('H'), (int) $currentDate->format('i'), 0); + // Workaround for setTime causing an offset change: https://bugs.php.net/bug.php?id=81074 + $currentDate = DateTime::createFromFormat("!Y-m-d H:iO", $currentDate->format("Y-m-d H:iP"), $currentDate->getTimezone()); + $currentDate->setTimezone(new DateTimeZone($timeZone)); $nextRun = clone $currentDate; @@ -380,7 +400,7 @@ protected function getRunDate($currentTime = null, int $nth = 0, bool $invert = $fields[$position] = $this->fieldFactory->getField($position); } - if (isset($parts[2]) && isset($parts[4])) { + if (isset($parts[self::DAY]) && isset($parts[self::WEEKDAY])) { $domExpression = sprintf('%s %s %s %s *', $this->getExpression(0), $this->getExpression(1), $this->getExpression(2), $this->getExpression(3)); $dowExpression = sprintf('%s %s * %s %s', $this->getExpression(0), $this->getExpression(1), $this->getExpression(3), $this->getExpression(4)); @@ -409,10 +429,10 @@ protected function getRunDate($currentTime = null, int $nth = 0, bool $invert = $field = $fields[$position]; // Check if this is singular or a list if (false === strpos($part, ',')) { - $satisfied = $field->isSatisfiedBy($nextRun, $part); + $satisfied = $field->isSatisfiedBy($nextRun, $part, $invert); } else { foreach (array_map('trim', explode(',', $part)) as $listPart) { - if ($field->isSatisfiedBy($nextRun, $listPart)) { + if ($field->isSatisfiedBy($nextRun, $listPart, $invert)) { $satisfied = true; break; @@ -430,8 +450,7 @@ protected function getRunDate($currentTime = null, int $nth = 0, bool $invert = // Skip this match if needed if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) { - $this->fieldFactory->getField(0)->increment($nextRun, $invert, $parts[0] ?? null); - + $this->fieldFactory->getField(self::MINUTE)->increment($nextRun, $invert, $parts[self::MINUTE] ?? null); continue; } @@ -458,7 +477,7 @@ protected function determineTimeZone($currentTime, ?string $timeZone): string } if ($currentTime instanceof DateTimeInterface) { - return $currentTime->getTimeZone()->getName(); + return $currentTime->getTimezone()->getName(); } return date_default_timezone_get(); diff --git a/src/Cron/DayOfMonthField.php b/src/Cron/DayOfMonthField.php index 21c2d97e..e0871830 100644 --- a/src/Cron/DayOfMonthField.php +++ b/src/Cron/DayOfMonthField.php @@ -79,7 +79,7 @@ private static function getNearestWeekday(int $currentYear, int $currentMonth, i /** * {@inheritdoc} */ - public function isSatisfiedBy(DateTimeInterface $date, $value): bool + public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool { // ? states that the field value is to be skipped if ('?' === $value) { @@ -117,10 +117,12 @@ public function isSatisfiedBy(DateTimeInterface $date, $value): bool */ public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface { - if ($invert) { - $date = $date->modify('previous day')->setTime(23, 59); + if (! $invert) { + $date = $this->timezoneSafeModify($date, '+1 day'); + $date = $date->setTime(0, 0); } else { - $date = $date->modify('next day')->setTime(0, 0); + $date = $this->timezoneSafeModify($date, '-1 day'); + $date = $date->setTime(23, 59); } return $this; diff --git a/src/Cron/DayOfWeekField.php b/src/Cron/DayOfWeekField.php index 98056951..40db9af1 100644 --- a/src/Cron/DayOfWeekField.php +++ b/src/Cron/DayOfWeekField.php @@ -4,7 +4,6 @@ namespace Cron; -use DateTime; use DateTimeInterface; use InvalidArgumentException; @@ -54,10 +53,8 @@ public function __construct() /** * @inheritDoc - * - * @param \DateTime|\DateTimeImmutable $date */ - public function isSatisfiedBy(DateTimeInterface $date, $value): bool + public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool { if ('?' === $value) { return true; @@ -76,15 +73,9 @@ public function isSatisfiedBy(DateTimeInterface $date, $value): bool $weekday = $this->convertLiterals(substr($value, 0, strpos($value, 'L'))); $weekday %= 7; - $tdate = clone $date; - $tdate = $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth); - while ($tdate->format('w') != $weekday) { - $tdateClone = new DateTime(); - $tdate = $tdateClone->setTimezone($tdate->getTimezone()) - ->setDate($currentYear, $currentMonth, --$lastDayOfMonth); - } - - return (int) $date->format('j') === $lastDayOfMonth; + $daysInMonth = (int) $date->format('t'); + $remainingDaysInMonth = $daysInMonth - (int) $date->format('d'); + return (($weekday === (int) $date->format('w')) && ($remainingDaysInMonth < 7)); } // Handle # hash tokens @@ -156,15 +147,15 @@ public function isSatisfiedBy(DateTimeInterface $date, $value): bool /** * @inheritDoc - * - * @param \DateTime|\DateTimeImmutable $date */ public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface { - if ($invert) { - $date = $date->modify('-1 day')->setTime(23, 59, 0); + if (! $invert) { + $date = $this->timezoneSafeModify($date, '+1 day'); + $date = $date->setTime(0, 0); } else { - $date = $date->modify('+1 day')->setTime(0, 0, 0); + $date = $this->timezoneSafeModify($date, '-1 day'); + $date = $date->setTime(23, 59); } return $this; diff --git a/src/Cron/FieldInterface.php b/src/Cron/FieldInterface.php index eba0558f..38c72a8f 100644 --- a/src/Cron/FieldInterface.php +++ b/src/Cron/FieldInterface.php @@ -19,7 +19,7 @@ interface FieldInterface * * @return bool Returns TRUE if satisfied, FALSE otherwise */ - public function isSatisfiedBy(DateTimeInterface $date, $value): bool; + public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool; /** * When a CRON expression is not satisfied, this method is used to increment diff --git a/src/Cron/HoursField.php b/src/Cron/HoursField.php index 78a5af42..a9b756e1 100644 --- a/src/Cron/HoursField.php +++ b/src/Cron/HoursField.php @@ -22,32 +22,111 @@ class HoursField extends AbstractField */ protected $rangeEnd = 23; + /** + * @var array|null Transitions returned by DateTimeZone::getTransitions() + */ + protected $transitions = null; + + /** + * @var int|null Timestamp of the start of the transitions range + */ + protected $transitionsStart = null; + + /** + * @var int|null Timestamp of the end of the transitions range + */ + protected $transitionsEnd = null; + /** * {@inheritdoc} */ - public function isSatisfiedBy(DateTimeInterface $date, $value): bool + public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool { - return $this->isSatisfied((int) $date->format('H'), $value); + $checkValue = (int) $date->format('H'); + $retval = $this->isSatisfied($checkValue, $value); + if ($retval) { + return $retval; + } + + // Are we on the edge of a transition + $lastTransition = $this->getPastTransition($date); + if (($lastTransition !== null) && ($lastTransition["ts"] > ($date->format('U') - 3600))) { + $dtLastOffset = clone $date; + $this->timezoneSafeModify($dtLastOffset, "-1 hour"); + $lastOffset = $dtLastOffset->getOffset(); + + $dtNextOffset = clone $date; + $this->timezoneSafeModify($dtNextOffset, "+1 hour"); + $nextOffset = $dtNextOffset->getOffset(); + + $offsetChange = $nextOffset - $lastOffset; + if ($offsetChange >= 3600) { + $checkValue -= 1; + return $this->isSatisfied($checkValue, $value); + } + if ((! $invert) && ($offsetChange <= -3600)) { + $checkValue += 1; + return $this->isSatisfied($checkValue, $value); + } + } + + return $retval; + } + + public function getPastTransition(DateTimeInterface $date): ?array + { + $currentTimestamp = (int) $date->format('U'); + if ( + ($this->transitions === null) + || ($this->transitionsStart < ($currentTimestamp + 86400)) + || ($this->transitionsEnd > ($currentTimestamp - 86400)) + ) { + // We start a day before current time so we can differentiate between the first transition entry + // and a change that happens now + $dtLimitStart = clone $date; + $dtLimitStart = $dtLimitStart->modify("-12 months"); + $dtLimitEnd = clone $date; + $dtLimitEnd = $dtLimitEnd->modify('+12 months'); + + $this->transitions = $date->getTimezone()->getTransitions( + $dtLimitStart->getTimestamp(), + $dtLimitEnd->getTimestamp() + ); + $this->transitionsStart = $dtLimitStart->getTimestamp(); + $this->transitionsEnd = $dtLimitEnd->getTimestamp(); + } + + $nextTransition = null; + foreach ($this->transitions as $transition) { + if ($transition["ts"] > $currentTimestamp) { + continue; + } + + if (($nextTransition !== null) && ($transition["ts"] < $nextTransition["ts"])) { + continue; + } + + $nextTransition = $transition; + } + + return ($nextTransition ?? null); } /** * {@inheritdoc} * - * @param \DateTime|\DateTimeImmutable $date * @param string|null $parts */ public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface { + $originalTimestamp = (int) $date->format('U'); + // Change timezone to UTC temporarily. This will // allow us to go back or forwards and hour even // if DST will be changed between the hours. if (null === $parts || '*' === $parts) { - $timezone = $date->getTimezone(); - $date = $date->setTimezone(new DateTimeZone('UTC')); - $date = $date->modify(($invert ? '-' : '+') . '1 hour'); - $date = $date->setTimezone($timezone); - - $date = $date->setTime((int)$date->format('H'), $invert ? 59 : 0); + $date = $this->timezoneSafeModify($date, ($invert ? "-" : "+") ."1 hour"); + $date = $this->setTimeHour($date, $invert, $originalTimestamp); return $this; } @@ -57,7 +136,7 @@ public function increment(DateTimeInterface &$date, $invert = false, $parts = nu $hours = array_merge($hours, $this->getRangeForExpression($part, 23)); } - $current_hour = $date->format('H'); + $current_hour = (int) $date->format('H'); $position = $invert ? \count($hours) - 1 : 0; $countHours = \count($hours); if ($countHours > 1) { @@ -71,12 +150,53 @@ public function increment(DateTimeInterface &$date, $invert = false, $parts = nu } } - $hour = (int) $hours[$position]; - if ((!$invert && (int) $date->format('H') >= $hour) || ($invert && (int) $date->format('H') <= $hour)) { - $date = $date->modify(($invert ? '-' : '+') . '1 day'); - $date = $date->setTime($invert ? 23 : 0, $invert ? 59 : 0); + $target = (int) $hours[$position]; + $originalHour = (int)$date->format('H'); + + $originalDay = (int)$date->format('d'); + $previousOffset = $date->getOffset(); + + if (! $invert) { + if ($originalHour >= $target) { + $distance = 24 - $originalHour; + $date = $this->timezoneSafeModify($date, "+{$distance} hours"); + + $actualDay = (int)$date->format('d'); + $actualHour = (int)$date->format('H'); + if (($actualDay !== ($originalDay + 1)) && ($actualHour !== 0)) { + $offsetChange = ($previousOffset - $date->getOffset()); + $date = $this->timezoneSafeModify($date, "+{$offsetChange} seconds"); + } + + $originalHour = (int)$date->format('H'); + } + + $distance = $target - $originalHour; + $date = $this->timezoneSafeModify($date, "+{$distance} hours"); } else { - $date = $date->setTime($hour, $invert ? 59 : 0); + if ($originalHour <= $target) { + $distance = ($originalHour + 1); + $date = $this->timezoneSafeModify($date, "-" . $distance . " hours"); + + $actualDay = (int)$date->format('d'); + $actualHour = (int)$date->format('H'); + if (($actualDay !== ($originalDay - 1)) && ($actualHour !== 23)) { + $offsetChange = ($previousOffset - $date->getOffset()); + $date = $this->timezoneSafeModify($date, "+{$offsetChange} seconds"); + } + + $originalHour = (int)$date->format('H'); + } + + $distance = $originalHour - $target; + $date = $this->timezoneSafeModify($date, "-{$distance} hours"); + } + + $date = $this->setTimeHour($date, $invert, $originalTimestamp); + + $actualHour = (int)$date->format('H'); + if ($invert && ($actualHour === ($target - 1) || (($actualHour === 23) && ($target === 0)))) { + $date = $this->timezoneSafeModify($date, "+1 hour"); } return $this; diff --git a/src/Cron/MinutesField.php b/src/Cron/MinutesField.php index d95ba749..aabd44ab 100644 --- a/src/Cron/MinutesField.php +++ b/src/Cron/MinutesField.php @@ -24,9 +24,9 @@ class MinutesField extends AbstractField /** * {@inheritdoc} */ - public function isSatisfiedBy(DateTimeInterface $date, $value):bool + public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert):bool { - if ($value == '?') { + if ($value === '?') { return true; } @@ -37,23 +37,23 @@ public function isSatisfiedBy(DateTimeInterface $date, $value):bool * {@inheritdoc} * {@inheritDoc} * - * @param \DateTime|\DateTimeImmutable $date * @param string|null $parts */ public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface { if (is_null($parts)) { - $date = $date->modify(($invert ? '-' : '+') . '1 minute'); + $date = $date->modify(($invert ? '-' : '+'). '1 minute'); return $this; } + $current_minute = (int) $date->format('i'); + $parts = false !== strpos($parts, ',') ? explode(',', $parts) : [$parts]; $minutes = []; foreach ($parts as $part) { $minutes = array_merge($minutes, $this->getRangeForExpression($part, 59)); } - $current_minute = $date->format('i'); $position = $invert ? \count($minutes) - 1 : 0; if (\count($minutes) > 1) { for ($i = 0; $i < \count($minutes) - 1; ++$i) { @@ -66,11 +66,29 @@ public function increment(DateTimeInterface &$date, $invert = false, $parts = nu } } - if ((!$invert && $current_minute >= $minutes[$position]) || ($invert && $current_minute <= $minutes[$position])) { - $date = $date->modify(($invert ? '-' : '+') . '1 hour'); - $date = $date->setTime((int) $date->format('H'), $invert ? 59 : 0); + $target = (int) $minutes[$position]; + $originalMinute = (int) $date->format("i"); + + if (! $invert) { + if ($originalMinute >= $target) { + $distance = 60 - $originalMinute; + $date = $this->timezoneSafeModify($date, "+{$distance} minutes"); + + $originalMinute = (int) $date->format("i"); + } + + $distance = $target - $originalMinute; + $date = $this->timezoneSafeModify($date, "+{$distance} minutes"); } else { - $date = $date->setTime((int) $date->format('H'), (int) $minutes[$position]); + if ($originalMinute <= $target) { + $distance = ($originalMinute + 1); + $date = $this->timezoneSafeModify($date, "-{$distance} minutes"); + + $originalMinute = (int) $date->format("i"); + } + + $distance = $originalMinute - $target; + $date = $this->timezoneSafeModify($date, "-{$distance} minutes"); } return $this; diff --git a/src/Cron/MonthField.php b/src/Cron/MonthField.php index 06bdbf46..5a15fbb8 100644 --- a/src/Cron/MonthField.php +++ b/src/Cron/MonthField.php @@ -30,9 +30,9 @@ class MonthField extends AbstractField /** * {@inheritdoc} */ - public function isSatisfiedBy(DateTimeInterface $date, $value): bool + public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool { - if ($value == '?') { + if ($value === '?') { return true; } @@ -48,10 +48,12 @@ public function isSatisfiedBy(DateTimeInterface $date, $value): bool */ public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface { - if ($invert) { - $date = $date->modify('last day of previous month')->setTime(23, 59); + if (! $invert) { + $date = $date->modify('first day of next month'); + $date = $date->setTime(0, 0); } else { - $date = $date->modify('first day of next month')->setTime(0, 0); + $date = $date->modify('last day of previous month'); + $date = $date->setTime(23, 59); } return $this; diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 72f60be2..744c984d 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -644,7 +644,9 @@ public function testMakeDayOfWeekAnOrSometimes(): void public function testNextRunDateShouldNotAddMinutes(): void { $e = new CronExpression('* 19 * * *'); - $nextRunDate = $e->getNextRunDate(); + $tz = new \DateTimeZone("Europe/London"); + $dt = new \DateTimeImmutable("2021-05-31 18:15:00", $tz); + $nextRunDate = $e->getNextRunDate($dt); $this->assertSame("00", $nextRunDate->format("i")); } diff --git a/tests/Cron/DayOfMonthFieldTest.php b/tests/Cron/DayOfMonthFieldTest.php index 15396b8e..0d18c308 100644 --- a/tests/Cron/DayOfMonthFieldTest.php +++ b/tests/Cron/DayOfMonthFieldTest.php @@ -36,8 +36,8 @@ public function testValidatesField(): void public function testChecksIfSatisfied(): void { $f = new DayOfMonthField(); - $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?')); - $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?')); + $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?', false)); + $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?', false)); } /** diff --git a/tests/Cron/DayOfWeekFieldTest.php b/tests/Cron/DayOfWeekFieldTest.php index 6a414313..883c031e 100644 --- a/tests/Cron/DayOfWeekFieldTest.php +++ b/tests/Cron/DayOfWeekFieldTest.php @@ -37,8 +37,8 @@ public function testValidatesField(): void public function testChecksIfSatisfied(): void { $f = new DayOfWeekField(); - $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?')); - $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?')); + $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?', false)); + $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?', false)); } /** @@ -75,7 +75,7 @@ public function testValidatesHashValueWeekday(): void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Weekday must be a value between 0 and 7. 12 given'); $f = new DayOfWeekField(); - $this->assertTrue($f->isSatisfiedBy(new DateTime(), '12#1')); + $this->assertTrue($f->isSatisfiedBy(new DateTime(), '12#1', false)); } /** @@ -86,7 +86,7 @@ public function testValidatesHashValueNth(): void $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('There are never more than 5 or less than 1 of a given weekday in a month'); $f = new DayOfWeekField(); - $this->assertTrue($f->isSatisfiedBy(new DateTime(), '3#6')); + $this->assertTrue($f->isSatisfiedBy(new DateTime(), '3#6', false)); } /** @@ -111,13 +111,13 @@ public function testValidateWeekendHash(): void public function testHandlesZeroAndSevenDayOfTheWeekValues(): void { $f = new DayOfWeekField(); - $this->assertTrue($f->isSatisfiedBy(new DateTime('2011-09-04 00:00:00'), '0-2')); - $this->assertTrue($f->isSatisfiedBy(new DateTime('2011-09-04 00:00:00'), '6-0')); + $this->assertTrue($f->isSatisfiedBy(new DateTime('2011-09-04 00:00:00'), '0-2', false)); + $this->assertTrue($f->isSatisfiedBy(new DateTime('2011-09-04 00:00:00'), '6-0', false)); - $this->assertTrue($f->isSatisfiedBy(new DateTime('2014-04-20 00:00:00'), 'SUN')); - $this->assertTrue($f->isSatisfiedBy(new DateTime('2014-04-20 00:00:00'), 'SUN#3')); - $this->assertTrue($f->isSatisfiedBy(new DateTime('2014-04-20 00:00:00'), '0#3')); - $this->assertTrue($f->isSatisfiedBy(new DateTime('2014-04-20 00:00:00'), '7#3')); + $this->assertTrue($f->isSatisfiedBy(new DateTime('2014-04-20 00:00:00'), 'SUN', false)); + $this->assertTrue($f->isSatisfiedBy(new DateTime('2014-04-20 00:00:00'), 'SUN#3', false)); + $this->assertTrue($f->isSatisfiedBy(new DateTime('2014-04-20 00:00:00'), '0#3', false)); + $this->assertTrue($f->isSatisfiedBy(new DateTime('2014-04-20 00:00:00'), '7#3', false)); } /** @@ -126,10 +126,10 @@ public function testHandlesZeroAndSevenDayOfTheWeekValues(): void public function testHandlesLastWeekdayOfTheMonth(): void { $f = new DayOfWeekField(); - $this->assertTrue($f->isSatisfiedBy(new DateTime('2018-12-28 00:00:00'), 'FRIL')); - $this->assertTrue($f->isSatisfiedBy(new DateTime('2018-12-28 00:00:00'), '5L')); - $this->assertFalse($f->isSatisfiedBy(new DateTime('2018-12-21 00:00:00'), 'FRIL')); - $this->assertFalse($f->isSatisfiedBy(new DateTime('2018-12-21 00:00:00'), '5L')); + $this->assertTrue($f->isSatisfiedBy(new DateTime('2018-12-28 00:00:00'), 'FRIL', false)); + $this->assertTrue($f->isSatisfiedBy(new DateTime('2018-12-28 00:00:00'), '5L', false)); + $this->assertFalse($f->isSatisfiedBy(new DateTime('2018-12-21 00:00:00'), 'FRIL', false)); + $this->assertFalse($f->isSatisfiedBy(new DateTime('2018-12-21 00:00:00'), '5L', false)); } /** diff --git a/tests/Cron/DaylightSavingsTest.php b/tests/Cron/DaylightSavingsTest.php new file mode 100644 index 00000000..82cfe0a8 --- /dev/null +++ b/tests/Cron/DaylightSavingsTest.php @@ -0,0 +1,438 @@ +createDateTimeExactly("2021-03-28 02:00+01:00", $tz); + + $dtCurrent = \DateTimeImmutable::createFromFormat("!Y-m-d H:i:s", "2021-03-28 14:55:03", $tz); + $dtActual = $cron->getPreviousRunDate($dtCurrent, 0, false, $tz->getName()); + $this->assertEquals($dtExpected, $dtActual); + + $dtCurrent = $this->createDateTimeExactly("2021-04-21 00:00+01:00", $tz); + $dtActual = $cron->getPreviousRunDate($dtCurrent, 3, true, $tz->getName()); + $this->assertEquals($dtExpected, $dtActual); + } + + public function testIssue112(): void + { + $expression = "15 2 * * 0"; + $cron = new CronExpression($expression); + $tz = new \DateTimeZone("America/Winnipeg"); + + $dtCurrent = $this->createDateTimeExactly("2021-03-08 08:15-06:00", $tz); + $dtExpected = $this->createDateTimeExactly("2021-03-14 03:15-05:00", $tz); + $this->assertEquals($dtExpected, $cron->getNextRunDate($dtCurrent)); + } + + /** + * Create a DateTimeImmutable that represents the given exact moment in time. + * This is a bit finicky because DateTime likes to override the timezone with the offset even when it's valid + * and in some cases necessary during DST changes. + * Assertions verify no unexpected behavior changes in PHP. + */ + protected function createDateTimeExactly($dtString, \DateTimeZone $timezone) + { + $dt = \DateTimeImmutable::createFromFormat("!Y-m-d H:iO", $dtString, $timezone); + $dt = $dt->setTimezone($timezone); + $this->assertEquals($dtString, $dt->format("Y-m-d H:iP")); + $this->assertEquals($timezone->getName(), $dt->format("e")); + return $dt; + } + + public function testOffsetIncrementsNextRunDate(): void + { + $tz = new \DateTimeZone("Europe/London"); + $cron = new CronExpression("0 1 * * 0"); + + $dtCurrent = $this->createDateTimeExactly("2021-03-21 00:00+00:00", $tz); + $dtExpected = $this->createDateTimeExactly("2021-03-21 01:00+00:00", $tz); + $this->assertEquals($dtExpected, $cron->getNextRunDate($dtCurrent, 0, false, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2021-03-21 02:00+00:00", $tz); + $dtExpected = $this->createDateTimeExactly("2021-03-28 02:00+01:00", $tz); + $this->assertEquals($dtExpected, $cron->getNextRunDate($dtCurrent, 0, false, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2021-03-28 00:00+00:00", $tz); + $dtExpected = $this->createDateTimeExactly("2021-03-28 02:00+01:00", $tz); + $this->assertEquals($dtExpected, $cron->getNextRunDate($dtCurrent, 0, false, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2021-03-28 02:00+01:00", $tz); + $dtExpected = $this->createDateTimeExactly("2021-03-28 02:00+01:00", $tz); + $this->assertEquals($dtExpected, $cron->getNextRunDate($dtCurrent, 0, true, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2021-03-28 02:00+01:00", $tz); + $dtExpected = $this->createDateTimeExactly("2021-04-04 01:00+01:00", $tz); + $this->assertEquals($dtExpected, $cron->getNextRunDate($dtCurrent, 0, false, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2021-03-28 02:05+01:00", $tz); + $dtExpected = $this->createDateTimeExactly("2021-04-04 01:00+01:00", $tz); + $this->assertEquals($dtExpected, $cron->getNextRunDate($dtCurrent, 0, false, $tz->getName())); + } + + public function testOffsetIncrementsPreviousRunDate(): void + { + $tz = new \DateTimeZone("Europe/London"); + $cron = new CronExpression("0 1 * * 0"); + + $dtCurrent = $this->createDateTimeExactly("2021-04-04 02:00+01:00", $tz); + $dtExpected = $this->createDateTimeExactly("2021-04-04 01:00+01:00", $tz); + $this->assertEquals($dtExpected, $cron->getPreviousRunDate($dtCurrent, 0, false, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2021-04-04 00:00+01:00", $tz); + $dtExpected = $this->createDateTimeExactly("2021-03-28 02:00+01:00", $tz); + $this->assertEquals($dtExpected, $cron->getPreviousRunDate($dtCurrent, 0, false, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2021-03-28 03:00+01:00", $tz); + $dtExpected = $this->createDateTimeExactly("2021-03-28 02:00+01:00", $tz); + $this->assertEquals($dtExpected, $cron->getPreviousRunDate($dtCurrent, 0, false, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2021-03-28 02:00+01:00", $tz); + $dtExpected = $this->createDateTimeExactly("2021-03-21 01:00+00:00", $tz); + $this->assertEquals($dtExpected, $cron->getPreviousRunDate($dtCurrent, 0, false, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2021-03-28 03:00+01:00", $tz); + $dtExpected = $this->createDateTimeExactly("2021-03-28 02:00+01:00", $tz); + $this->assertEquals($dtExpected, $cron->getPreviousRunDate($dtCurrent, 0, true, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2021-03-28 00:00+00:00", $tz); + $dtExpected = $this->createDateTimeExactly("2021-03-21 01:00+00:00", $tz); + $this->assertEquals($dtExpected, $cron->getPreviousRunDate($dtCurrent, 0, false, $tz->getName())); + } + + public function testOffsetDecrementsNextRunDateAllowCurrent(): void + { + $tz = new \DateTimeZone("Europe/London"); + $cron = new CronExpression("0 1 * * 0"); + + $dtCurrent = $this->createDateTimeExactly("2020-10-18 00:00+01:00", $tz); + $dtExpected = $this->createDateTimeExactly("2020-10-18 01:00+01:00", $tz); + $this->assertEquals($dtExpected, $cron->getNextRunDate($dtCurrent, 0, true, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2020-10-18 01:05+01:00", $tz); + $dtExpected = $this->createDateTimeExactly("2020-10-25 01:00+00:00", $tz); + $this->assertEquals($dtExpected, $cron->getNextRunDate($dtCurrent, 0, true, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2020-10-25 00:00+01:00", $tz); + $dtExpected = $this->createDateTimeExactly("2020-10-25 01:00+00:00", $tz); + $this->assertEquals($dtExpected, $cron->getNextRunDate($dtCurrent, 0, true, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2020-10-25 01:00+01:00", $tz); + $dtExpected = $this->createDateTimeExactly("2020-10-25 01:00+01:00", $tz); + $this->assertEquals($dtExpected, $cron->getNextRunDate($dtCurrent, 0, true, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2020-10-25 01:00+00:00", $tz); + $dtExpected = $this->createDateTimeExactly("2020-10-25 01:00+00:00", $tz); + $this->assertEquals($dtExpected, $cron->getNextRunDate($dtCurrent, 0, true, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2020-10-25 01:05+00:00", $tz); + $dtExpected = $this->createDateTimeExactly("2020-11-01 01:00+00:00", $tz); + $this->assertEquals($dtExpected, $cron->getNextRunDate($dtCurrent, 0, true, $tz->getName())); + } + + /** + * The fact that crons will run twice using this setup is expected. + * This can be avoided by using disallowing the current date or with additional checks outside this library + */ + public function testOffsetDecrementsNextRunDateDisallowCurrent(): void + { + $tz = new \DateTimeZone("Europe/London"); + $cron = new CronExpression("0 1 * * 0"); + + $dtCurrent = $this->createDateTimeExactly("2020-10-18 00:00+01:00", $tz); + $dtExpected = $this->createDateTimeExactly("2020-10-18 01:00+01:00", $tz); + $this->assertEquals($dtExpected, $cron->getNextRunDate($dtCurrent, 0, false, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2020-10-18 01:05+01:00", $tz); + $dtExpected = $this->createDateTimeExactly("2020-10-25 01:00+00:00", $tz); + $this->assertEquals($dtExpected, $cron->getNextRunDate($dtCurrent, 0, false, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2020-10-25 00:00+01:00", $tz); + $dtExpected = $this->createDateTimeExactly("2020-10-25 01:00+00:00", $tz); + $this->assertEquals($dtExpected, $cron->getNextRunDate($dtCurrent, 0, false, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2020-10-25 01:00+01:00", $tz); + $dtExpected = $this->createDateTimeExactly("2020-10-25 01:00+00:00", $tz); + $this->assertEquals($dtExpected, $cron->getNextRunDate($dtCurrent, 0, false, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2020-10-25 01:05+01:00", $tz); + $dtExpected = $this->createDateTimeExactly("2020-10-25 01:00+00:00", $tz); + $this->assertEquals($dtExpected, $cron->getNextRunDate($dtCurrent, 0, false, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2020-10-25 01:00+00:00", $tz); + $dtExpected = $this->createDateTimeExactly("2020-11-01 01:00+00:00", $tz); + $this->assertEquals($dtExpected, $cron->getNextRunDate($dtCurrent, 0, false, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2020-10-25 01:05+00:00", $tz); + $dtExpected = $this->createDateTimeExactly("2020-11-01 01:00+00:00", $tz); + $this->assertEquals($dtExpected, $cron->getNextRunDate($dtCurrent, 0, false, $tz->getName())); + } + + public function testOffsetDecrementsPreviousRunDate(): void + { + $tz = new \DateTimeZone("Europe/London"); + $cron = new CronExpression("0 1 * * 0"); + + $dtCurrent = $this->createDateTimeExactly("2021-04-04 02:00+01:00", $tz); + $dtExpected = $this->createDateTimeExactly("2021-04-04 01:00+01:00", $tz); + $this->assertEquals($dtExpected, $cron->getPreviousRunDate($dtCurrent, 0, false, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2021-04-04 00:00+01:00", $tz); + $dtExpected = $this->createDateTimeExactly("2021-03-28 02:00+01:00", $tz); + $this->assertEquals($dtExpected, $cron->getPreviousRunDate($dtCurrent, 0, false, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2021-03-28 03:00+01:00", $tz); + $dtExpected = $this->createDateTimeExactly("2021-03-28 02:00+01:00", $tz); + $this->assertEquals($dtExpected, $cron->getPreviousRunDate($dtCurrent, 0, false, $tz->getName())); + + $dtCurrent = $this->createDateTimeExactly("2021-03-28 00:00+00:00", $tz); + $dtExpected = $this->createDateTimeExactly("2021-03-21 01:00+00:00", $tz); + $this->assertEquals($dtExpected, $cron->getPreviousRunDate($dtCurrent, 0, false, $tz->getName())); + } + + public function testOffsetIncrementsMultipleRunDates(): void + { + $expression = "0 1 * * 0"; + $cron = new CronExpression($expression); + $tz = new \DateTimeZone("Europe/London"); + + $expected = [ + $this->createDateTimeExactly("2021-03-14 01:00+00:00", $tz), + $this->createDateTimeExactly("2021-03-21 01:00+00:00", $tz), + $this->createDateTimeExactly("2021-03-28 02:00+01:00", $tz), + $this->createDateTimeExactly("2021-04-04 01:00+01:00", $tz), + $this->createDateTimeExactly("2021-04-11 01:00+01:00", $tz), + ]; + + $dtCurrent = $this->createDateTimeExactly("2021-03-13 00:00+00:00", $tz); + $actual = $cron->getMultipleRunDates(5, $dtCurrent, false, true, $tz->getName()); + foreach ($expected as $dtExpected) { + $this->assertContainsEquals($dtExpected, $actual); + } + + $dtCurrent = $this->createDateTimeExactly("2021-04-12 00:00+01:00", $tz); + $actual = $cron->getMultipleRunDates(5, $dtCurrent, true, true, $tz->getName()); + foreach ($expected as $dtExpected) { + $this->assertContainsEquals($dtExpected, $actual); + } + } + + public function testOffsetDecrementsMultipleRunDates(): void + { + $expression = "0 1 * * 0"; + $cron = new CronExpression($expression); + $tz = new \DateTimeZone("Europe/London"); + + $expected = [ + $this->createDateTimeExactly("2020-10-11 01:00+01:00", $tz), + $this->createDateTimeExactly("2020-10-18 01:00+01:00", $tz), + $this->createDateTimeExactly("2020-10-25 01:00+00:00", $tz), + $this->createDateTimeExactly("2020-11-01 01:00+00:00", $tz), + $this->createDateTimeExactly("2020-11-08 01:00+00:00", $tz), + ]; + + $dtCurrent = $this->createDateTimeExactly("2020-10-10 00:00+01:00", $tz); + $actual = $cron->getMultipleRunDates(5, $dtCurrent, false, true, $tz->getName()); + foreach ($expected as $dtExpected) { + $this->assertContainsEquals($dtExpected, $actual); + } + + $expected = [ + $this->createDateTimeExactly("2020-10-18 01:00+01:00", $tz), + $this->createDateTimeExactly("2020-10-25 01:00+01:00", $tz), + $this->createDateTimeExactly("2020-10-25 01:00+00:00", $tz), + $this->createDateTimeExactly("2020-11-01 01:00+00:00", $tz), + $this->createDateTimeExactly("2020-11-08 01:00+00:00", $tz), + ]; + + $dtCurrent = $this->createDateTimeExactly("2020-11-12 00:00+00:00", $tz); + $actual = $cron->getMultipleRunDates(5, $dtCurrent, true, true, $tz->getName()); + + foreach ($expected as $dtExpected) { + $this->assertContainsEquals($dtExpected, $actual); + } + } + + public function testOffsetIncrementsEveryOtherHour(): void + { + $expression = "0 */2 * * *"; + $cron = new CronExpression($expression); + $tz = new \DateTimeZone("Europe/London"); + + $expected = [ + $this->createDateTimeExactly("2021-03-27 22:00+00:00", $tz), + $this->createDateTimeExactly("2021-03-28 00:00+00:00", $tz), + $this->createDateTimeExactly("2021-03-28 02:00+01:00", $tz), + $this->createDateTimeExactly("2021-03-28 04:00+01:00", $tz), + $this->createDateTimeExactly("2021-03-28 06:00+01:00", $tz), + ]; + + $dtCurrent = $this->createDateTimeExactly("2021-03-27 22:00+00:00", $tz); + $actual = $cron->getMultipleRunDates(5, $dtCurrent, false, true, $tz->getName()); + foreach ($expected as $dtExpected) { + $this->assertContainsEquals($dtExpected, $actual); + } + + $dtCurrent = $this->createDateTimeExactly("2021-03-28 06:00+01:00", $tz); + $actual = $cron->getMultipleRunDates(5, $dtCurrent, true, true, $tz->getName()); + foreach ($expected as $dtExpected) { + $this->assertContainsEquals($dtExpected, $actual); + } + + $expression = "0 1-23/2 * * *"; + $cron = new CronExpression($expression); + $tz = new \DateTimeZone("Europe/London"); + + $expected = [ + $this->createDateTimeExactly("2021-03-27 23:00+00:00", $tz), + $this->createDateTimeExactly("2021-03-28 02:00+01:00", $tz), + $this->createDateTimeExactly("2021-03-28 03:00+01:00", $tz), + $this->createDateTimeExactly("2021-03-28 05:00+01:00", $tz), + $this->createDateTimeExactly("2021-03-28 07:00+01:00", $tz), + ]; + + $dtCurrent = $this->createDateTimeExactly("2021-03-27 23:00+00:00", $tz); + $actual = $cron->getMultipleRunDates(5, $dtCurrent, false, true, $tz->getName()); + foreach ($expected as $dtExpected) { + $this->assertContainsEquals($dtExpected, $actual); + } + + $dtCurrent = $this->createDateTimeExactly("2021-03-28 07:00+01:00", $tz); + $actual = $cron->getMultipleRunDates(5, $dtCurrent, true, true, $tz->getName()); + foreach ($expected as $dtExpected) { + $this->assertContainsEquals($dtExpected, $actual); + } + } + + public function testOffsetDecrementsEveryOtherHour(): void + { + $expression = "0 */2 * * *"; + $cron = new CronExpression($expression); + $tz = new \DateTimeZone("Europe/London"); + + $expected = [ + $this->createDateTimeExactly("2020-10-24 22:00+01:00", $tz), + $this->createDateTimeExactly("2020-10-25 00:00+01:00", $tz), + $this->createDateTimeExactly("2020-10-25 01:00+00:00", $tz), + $this->createDateTimeExactly("2020-10-25 02:00+00:00", $tz), + $this->createDateTimeExactly("2020-10-25 04:00+00:00", $tz), + ]; + + $dtCurrent = $this->createDateTimeExactly("2020-10-24 22:00+01:00", $tz); + $actual = $cron->getMultipleRunDates(5, $dtCurrent, false, true, $tz->getName()); + foreach ($expected as $dtExpected) { + $this->assertContainsEquals($dtExpected, $actual); + } + + $expected = [ + $this->createDateTimeExactly("2020-10-24 20:00+01:00", $tz), + $this->createDateTimeExactly("2020-10-24 22:00+01:00", $tz), + $this->createDateTimeExactly("2020-10-25 00:00+01:00", $tz), + $this->createDateTimeExactly("2020-10-25 02:00+00:00", $tz), + $this->createDateTimeExactly("2020-10-25 04:00+00:00", $tz), + ]; + + $dtCurrent = $this->createDateTimeExactly("2020-10-25 04:00+00:00", $tz); + $actual = $cron->getMultipleRunDates(5, $dtCurrent, true, true, $tz->getName()); + foreach ($expected as $dtExpected) { + $this->assertContainsEquals($dtExpected, $actual); + } + + $expression = "0 1-23/2 * * *"; + $cron = new CronExpression($expression); + $tz = new \DateTimeZone("Europe/London"); + + $expected = [ + $this->createDateTimeExactly("2020-10-24 23:00+01:00", $tz), + $this->createDateTimeExactly("2020-10-25 01:00+00:00", $tz), + $this->createDateTimeExactly("2020-10-25 03:00+00:00", $tz), + $this->createDateTimeExactly("2020-10-25 05:00+00:00", $tz), + $this->createDateTimeExactly("2020-10-25 07:00+00:00", $tz), + ]; + + $dtCurrent = $this->createDateTimeExactly("2020-10-24 23:00+01:00", $tz); + $actual = $cron->getMultipleRunDates(5, $dtCurrent, false, true, $tz->getName()); + foreach ($expected as $dtExpected) { + $this->assertContainsEquals($dtExpected, $actual); + } + + $expected = [ + $this->createDateTimeExactly("2020-10-25 01:00+01:00", $tz), + $this->createDateTimeExactly("2020-10-25 01:00+00:00", $tz), + $this->createDateTimeExactly("2020-10-25 03:00+00:00", $tz), + $this->createDateTimeExactly("2020-10-25 05:00+00:00", $tz), + $this->createDateTimeExactly("2020-10-25 07:00+00:00", $tz), + ]; + + $dtCurrent = $this->createDateTimeExactly("2020-10-25 07:00+00:00", $tz); + $actual = $cron->getMultipleRunDates(5, $dtCurrent, true, true, $tz->getName()); + foreach ($expected as $dtExpected) { + $this->assertContainsEquals($dtExpected, $actual); + } + } + + public function testOffsetIncrementsMidnight(): void + { + $expression = '@hourly'; + $cron = new CronExpression($expression); + $tz = new \DateTimeZone("America/Asuncion"); + + $expected = [ + $this->createDateTimeExactly("2021-03-27 22:00-03:00", $tz), + $this->createDateTimeExactly("2021-03-27 23:00-03:00", $tz), + $this->createDateTimeExactly("2021-03-27 23:00-04:00", $tz), + $this->createDateTimeExactly("2021-03-28 00:00-04:00", $tz), + $this->createDateTimeExactly("2021-03-28 01:00-04:00", $tz), + ]; + + $dtCurrent = $this->createDateTimeExactly("2021-03-27 22:00-03:00", $tz); + $actual = $cron->getMultipleRunDates(5, $dtCurrent, false, true, $tz->getName()); + foreach ($expected as $dtExpected) { + $this->assertContainsEquals($dtExpected, $actual); + } + + $dtCurrent = $this->createDateTimeExactly("2021-03-28 01:00-04:00", $tz); + $actual = $cron->getMultipleRunDates(5, $dtCurrent, true, true, $tz->getName()); + foreach ($expected as $dtExpected) { + $this->assertContainsEquals($dtExpected, $actual); + } + } + + public function testOffsetDecrementsMidnight(): void + { + $expression = '@hourly'; + $cron = new CronExpression($expression); + $tz = new \DateTimeZone("America/Asuncion"); + + $expected = [ + $this->createDateTimeExactly("2020-10-03 22:00-04:00", $tz), + $this->createDateTimeExactly("2020-10-03 23:00-04:00", $tz), + $this->createDateTimeExactly("2020-10-04 01:00-03:00", $tz), + $this->createDateTimeExactly("2020-10-04 02:00-03:00", $tz), + $this->createDateTimeExactly("2020-10-04 03:00-03:00", $tz), + ]; + + $dtCurrent = $this->createDateTimeExactly("2020-10-03 22:00-04:00", $tz); + $actual = $cron->getMultipleRunDates(5, $dtCurrent, false, true, $tz->getName()); + foreach ($expected as $dtExpected) { + $this->assertContainsEquals($dtExpected, $actual); + } + + $dtCurrent = $this->createDateTimeExactly("2020-10-04 03:00-03:00", $tz); + $actual = $cron->getMultipleRunDates(5, $dtCurrent, true, true, $tz->getName()); + foreach ($expected as $dtExpected) { + $this->assertContainsEquals($dtExpected, $actual); + } + } +} diff --git a/tests/Cron/HoursFieldTest.php b/tests/Cron/HoursFieldTest.php index 37a5ce42..649ea531 100644 --- a/tests/Cron/HoursFieldTest.php +++ b/tests/Cron/HoursFieldTest.php @@ -89,4 +89,27 @@ public function testIncrementDateWithFifteenMinuteOffsetTimezone(): void $this->assertSame('2011-03-15 10:59:00', $d->format('Y-m-d H:i:s')); date_default_timezone_set($tz); } + + /** + * @covers \Cron\HoursField::increment + */ + public function testIncrementAcrossDstChange(): void + { + $tz = new \DateTimeZone("Europe/London"); + $d = \DateTimeImmutable::createFromFormat("!Y-m-d H:i:s", "2021-03-27 23:00:00", $tz); + $f = new HoursField(); + $f->increment($d); + $this->assertSame("2021-03-28 00:00:00", $d->format("Y-m-d H:i:s")); + $f->increment($d); + $this->assertSame("2021-03-28 02:00:00", $d->format("Y-m-d H:i:s")); + $f->increment($d); + $this->assertSame("2021-03-28 03:00:00", $d->format("Y-m-d H:i:s")); + + $f->increment($d, true); + $this->assertSame("2021-03-28 02:59:00", $d->format("Y-m-d H:i:s")); + $f->increment($d, true); + $this->assertSame("2021-03-28 00:59:00", $d->format("Y-m-d H:i:s")); + $f->increment($d, true); + $this->assertSame("2021-03-27 23:59:00", $d->format("Y-m-d H:i:s")); + } } diff --git a/tests/Cron/MinutesFieldTest.php b/tests/Cron/MinutesFieldTest.php index e6446180..67cc47c9 100644 --- a/tests/Cron/MinutesFieldTest.php +++ b/tests/Cron/MinutesFieldTest.php @@ -32,8 +32,8 @@ public function testValidatesField(): void public function testChecksIfSatisfied(): void { $f = new MinutesField(); - $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?')); - $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?')); + $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?', false)); + $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?', false)); } /** @@ -45,6 +45,7 @@ public function testIncrementsDate(): void $f = new MinutesField(); $f->increment($d); $this->assertSame('2011-03-15 11:16:00', $d->format('Y-m-d H:i:s')); + $f->increment($d, true); $this->assertSame('2011-03-15 11:15:00', $d->format('Y-m-d H:i:s')); } diff --git a/tests/Cron/MonthFieldTest.php b/tests/Cron/MonthFieldTest.php index 7a686a5d..4016711a 100644 --- a/tests/Cron/MonthFieldTest.php +++ b/tests/Cron/MonthFieldTest.php @@ -33,8 +33,8 @@ public function testValidatesField(): void public function testChecksIfSatisfied(): void { $f = new MonthField(); - $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?')); - $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?')); + $this->assertTrue($f->isSatisfiedBy(new DateTime(), '?', false)); + $this->assertTrue($f->isSatisfiedBy(new DateTimeImmutable(), '?', false)); } /** From cf3bbe954c2b3892f6dadc89faa5b30ee0207994 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Tue, 4 Jan 2022 22:12:08 -0500 Subject: [PATCH 105/146] Added changelog for v3.2.1 --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d4966f9..40b606f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Change Log +## [3.2.1] - 2022-01-04 + +### Added +- N/A + +### Changed +- Added PHP 8.1 to testing (#125) + +### Fixed +- Allow better mixture of ranges, steps, and lists (#122) +- Fixed return order when multiple dates are requested and inverted (#121) +- Better handling over DST (#115) +- Fixed PHPStan tests (#130) + ## [3.2.0] - 2022-01-04 ### Added From ebb006b66e361c91631bdc6cb07c1183bf709cd6 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Tue, 4 Jan 2022 23:40:50 -0500 Subject: [PATCH 106/146] Marked a bunch of field stuff @internal --- src/Cron/AbstractField.php | 5 +++++ src/Cron/FieldInterface.php | 2 ++ 2 files changed, 7 insertions(+) diff --git a/src/Cron/AbstractField.php b/src/Cron/AbstractField.php index f13e59a5..7e2fb8c2 100644 --- a/src/Cron/AbstractField.php +++ b/src/Cron/AbstractField.php @@ -50,6 +50,7 @@ public function __construct() /** * Check to see if a field is satisfied by a value. * + * @internal * @param int $dateValue Date value to check * @param string $value Value to test * @@ -71,6 +72,7 @@ public function isSatisfied(int $dateValue, string $value): bool /** * Check if a value is a range. * + * @internal * @param string $value Value to test * * @return bool @@ -83,6 +85,7 @@ public function isRange(string $value): bool /** * Check if a value is an increments of ranges. * + * @internal * @param string $value Value to test * * @return bool @@ -95,6 +98,7 @@ public function isIncrementsOfRanges(string $value): bool /** * Test if a value is within a range. * + * @internal * @param int $dateValue Set date value * @param string $value Value to test * @@ -117,6 +121,7 @@ function ($value) { /** * Test if a value is within an increments of ranges (offset[-to]/step size). * + * @internal * @param int $dateValue Set date value * @param string $value Value to test * diff --git a/src/Cron/FieldInterface.php b/src/Cron/FieldInterface.php index 38c72a8f..e0367ed7 100644 --- a/src/Cron/FieldInterface.php +++ b/src/Cron/FieldInterface.php @@ -14,6 +14,7 @@ interface FieldInterface /** * Check if the respective value of a DateTime field satisfies a CRON exp. * + * @internal * @param DateTimeInterface $date DateTime object to check * @param string $value CRON expression to test against * @@ -25,6 +26,7 @@ public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bo * When a CRON expression is not satisfied, this method is used to increment * or decrement a DateTime object by the unit of the cron field. * + * @internal * @param DateTimeInterface $date DateTime object to change * @param bool $invert (optional) Set to TRUE to decrement * @param string|null $parts (optional) Set parts to use From 9d513a77c08e528f2675ffefb391145225335242 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Wed, 5 Jan 2022 00:31:05 -0500 Subject: [PATCH 107/146] Fixing #89, wraparound shouldn't check the high value --- src/Cron/AbstractField.php | 11 +++++++++-- tests/Cron/CronExpressionTest.php | 4 ++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Cron/AbstractField.php b/src/Cron/AbstractField.php index 7e2fb8c2..043ae84e 100644 --- a/src/Cron/AbstractField.php +++ b/src/Cron/AbstractField.php @@ -157,8 +157,15 @@ public function isInIncrementsOfRanges(int $dateValue, string $value): bool throw new \OutOfRangeException('Invalid range end requested'); } - // Steps larger than the range need to wrap around and be handled slightly differently than smaller steps - if ($step >= $this->rangeEnd) { + // Steps larger than the range need to wrap around and be handled + // slightly differently than smaller steps + + // UPDATE - This is actually false. The C implementation will allow a + // larger step as valid syntax, it never wraps around. It will stop + // once it hits the end. Unfortunately this means in future versions + // we will not wrap around. However, because the logic exists today + // per the above documentation, fixing the bug from #89 + if ($step > $this->rangeEnd) { $thisRange = [$this->fullRange[$step % \count($this->fullRange)]]; } else { $thisRange = range($rangeStart, $rangeEnd, (int) $step); diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 744c984d..a223c239 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -199,6 +199,10 @@ public function scheduleProvider(): array // Issue #76, DOW and DOM do not support ? ['0 12 * * ?', strtotime('2020-08-20 00:00:00'), strtotime('2020-08-20 12:00:00'), false], ['0 12 ? * *', strtotime('2020-08-20 00:00:00'), strtotime('2020-08-20 12:00:00'), false], + ['0-59/59 10 * * *', strtotime('2021-08-25 10:00:00'), strtotime('2021-08-25 10:00:00'), true], + ['0-59/59 10 * * *', strtotime('2021-08-25 09:00:00'), strtotime('2021-08-25 10:00:00'), false], + ['0-59/59 10 * * *', strtotime('2021-08-25 10:01:00'), strtotime('2021-08-25 10:59:00'), false], + ['0-59/65 10 * * *', strtotime('2021-08-25 10:01:00'), strtotime('2021-08-25 10:05:00'), false], ]; } From 4445c6fdada6ae9f3b868af8350d539bc90b7e79 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Wed, 5 Jan 2022 00:54:07 -0500 Subject: [PATCH 108/146] Fixes #88, if step is larger than small range only set start --- src/Cron/AbstractField.php | 6 +++++- tests/Cron/CronExpressionTest.php | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Cron/AbstractField.php b/src/Cron/AbstractField.php index 043ae84e..b0b28405 100644 --- a/src/Cron/AbstractField.php +++ b/src/Cron/AbstractField.php @@ -168,7 +168,11 @@ public function isInIncrementsOfRanges(int $dateValue, string $value): bool if ($step > $this->rangeEnd) { $thisRange = [$this->fullRange[$step % \count($this->fullRange)]]; } else { - $thisRange = range($rangeStart, $rangeEnd, (int) $step); + if ($step > ($rangeEnd - $rangeStart)) { + $thisRange[$rangeStart] = (int) $rangeStart; + } else { + $thisRange = range($rangeStart, $rangeEnd, (int) $step); + } } return \in_array($dateValue, $thisRange, true); diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index a223c239..0286e8f1 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -203,6 +203,7 @@ public function scheduleProvider(): array ['0-59/59 10 * * *', strtotime('2021-08-25 09:00:00'), strtotime('2021-08-25 10:00:00'), false], ['0-59/59 10 * * *', strtotime('2021-08-25 10:01:00'), strtotime('2021-08-25 10:59:00'), false], ['0-59/65 10 * * *', strtotime('2021-08-25 10:01:00'), strtotime('2021-08-25 10:05:00'), false], + ['41-59/24 5 * * *', strtotime('2021-08-25 10:00:00'), strtotime('2021-08-26 05:41:00'), false], ]; } From c9e208317b0cf679097cf976ffbb0b0eec81d4df Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Wed, 5 Jan 2022 01:05:42 -0500 Subject: [PATCH 109/146] Added changelog for v3.2.2 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40b606f8..5a9407bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Change Log +## [3.2.2] - 2022-01-05 + +### Added +- N/A + +### Changed +- Marked some methods `@internal` (#124) + +### Fixed +- Fixed issue with small ranges and large steps that caused an error with `range()` (#88) +- Fixed issue where wraparound logic incorrectly considered high bound on range (#89) ## [3.2.1] - 2022-01-04 ### Added From 6ba5cb5568a4d8a8f0a7281c07813a05ae871f2b Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Thu, 6 Jan 2022 00:33:35 -0500 Subject: [PATCH 110/146] Changed how hours and minutes increment/decrement for better TZ handling --- src/Cron/HoursField.php | 7 +++++- src/Cron/MinutesField.php | 2 +- tests/Cron/HoursFieldTest.php | 25 +++++++++++++++++++++- tests/Cron/MinutesFieldTest.php | 38 +++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/Cron/HoursField.php b/src/Cron/HoursField.php index a9b756e1..ef930d45 100644 --- a/src/Cron/HoursField.php +++ b/src/Cron/HoursField.php @@ -125,7 +125,12 @@ public function increment(DateTimeInterface &$date, $invert = false, $parts = nu // allow us to go back or forwards and hour even // if DST will be changed between the hours. if (null === $parts || '*' === $parts) { - $date = $this->timezoneSafeModify($date, ($invert ? "-" : "+") ."1 hour"); + if ($invert) { + $date = $date->sub(new \DateInterval('PT1H')); + } else { + $date = $date->add(new \DateInterval('PT1H')); + } + $date = $this->setTimeHour($date, $invert, $originalTimestamp); return $this; } diff --git a/src/Cron/MinutesField.php b/src/Cron/MinutesField.php index aabd44ab..eda91098 100644 --- a/src/Cron/MinutesField.php +++ b/src/Cron/MinutesField.php @@ -42,7 +42,7 @@ public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert):boo public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface { if (is_null($parts)) { - $date = $date->modify(($invert ? '-' : '+'). '1 minute'); + $date = $this->timezoneSafeModify($date, ($invert ? "-" : "+") ."1 minute"); return $this; } diff --git a/tests/Cron/HoursFieldTest.php b/tests/Cron/HoursFieldTest.php index 649ea531..b8f885ce 100644 --- a/tests/Cron/HoursFieldTest.php +++ b/tests/Cron/HoursFieldTest.php @@ -93,7 +93,30 @@ public function testIncrementDateWithFifteenMinuteOffsetTimezone(): void /** * @covers \Cron\HoursField::increment */ - public function testIncrementAcrossDstChange(): void + public function testIncrementAcrossDstChangeBerlin(): void + { + $tz = new \DateTimeZone("Europe/Berlin"); + $d = \DateTimeImmutable::createFromFormat("!Y-m-d H:i:s", "2021-03-27 23:00:00", $tz); + $f = new HoursField(); + $f->increment($d); + $this->assertSame("2021-03-28 00:00:00", $d->format("Y-m-d H:i:s")); + $f->increment($d); + $this->assertSame("2021-03-28 01:00:00", $d->format("Y-m-d H:i:s")); + $f->increment($d); + $this->assertSame("2021-03-28 03:00:00", $d->format("Y-m-d H:i:s")); + + $f->increment($d, true); + $this->assertSame("2021-03-28 01:59:00", $d->format("Y-m-d H:i:s")); + $f->increment($d, true); + $this->assertSame("2021-03-28 00:59:00", $d->format("Y-m-d H:i:s")); + $f->increment($d, true); + $this->assertSame("2021-03-27 23:59:00", $d->format("Y-m-d H:i:s")); + } + + /** + * @covers \Cron\HoursField::increment + */ + public function testIncrementAcrossDstChangeLondon(): void { $tz = new \DateTimeZone("Europe/London"); $d = \DateTimeImmutable::createFromFormat("!Y-m-d H:i:s", "2021-03-27 23:00:00", $tz); diff --git a/tests/Cron/MinutesFieldTest.php b/tests/Cron/MinutesFieldTest.php index 67cc47c9..585d464e 100644 --- a/tests/Cron/MinutesFieldTest.php +++ b/tests/Cron/MinutesFieldTest.php @@ -89,4 +89,42 @@ public function testInvalidRangeShouldNotValidate(): void $f = new MinutesField(); $this->assertFalse($f->validate('0/5')); } + + /** + * @covers \Cron\MinutesField::increment + */ + public function testIncrementAcrossDstChangeBerlin(): void + { + $tz = new \DateTimeZone("Europe/Berlin"); + $d = \DateTimeImmutable::createFromFormat("!Y-m-d H:i:s", "2021-03-28 01:59:00", $tz); + $f = new MinutesField(); + $f->increment($d); + $this->assertSame("2021-03-28 03:00:00", $d->format("Y-m-d H:i:s")); + + $f->increment($d, true); + $this->assertSame("2021-03-28 01:59:00", $d->format("Y-m-d H:i:s")); + $f->increment($d, true); + $this->assertSame("2021-03-28 01:58:00", $d->format("Y-m-d H:i:s")); + } + + /** + * @covers \Cron\MinutesField::increment + */ + public function testIncrementAcrossDstChangeLondon(): void + { + $tz = new \DateTimeZone("Europe/London"); + $d = \DateTimeImmutable::createFromFormat("!Y-m-d H:i:s", "2021-03-28 00:59:00", $tz); + $f = new MinutesField(); + $f->increment($d); + $this->assertSame("2021-03-28 02:00:00", $d->format("Y-m-d H:i:s")); + $f->increment($d); + $this->assertSame("2021-03-28 02:01:00", $d->format("Y-m-d H:i:s")); + + $f->increment($d, true); + $this->assertSame("2021-03-28 02:00:00", $d->format("Y-m-d H:i:s")); + $f->increment($d, true); + $this->assertSame("2021-03-28 00:59:00", $d->format("Y-m-d H:i:s")); + $f->increment($d, true); + $this->assertSame("2021-03-28 00:58:00", $d->format("Y-m-d H:i:s")); + } } From 47c53bbb260d3c398fba9bfa9683dcf67add2579 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Thu, 6 Jan 2022 00:35:07 -0500 Subject: [PATCH 111/146] Added changelog for v3.2.3 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a9407bc..ba51e503 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Change Log +## [3.2.3] - 2022-01-05 + +### Added +- N/A + +### Changed +- Changed how minutes and hours increment/decrement to help with DST changes (#131) + +### Fixed +- N/A + ## [3.2.2] - 2022-01-05 ### Added @@ -11,6 +22,7 @@ ### Fixed - Fixed issue with small ranges and large steps that caused an error with `range()` (#88) - Fixed issue where wraparound logic incorrectly considered high bound on range (#89) + ## [3.2.1] - 2022-01-04 ### Added From ae5123ba658d47192f038dbc2b5e3517a0fd7e06 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Thu, 6 Jan 2022 08:28:52 -0500 Subject: [PATCH 112/146] Changed handling of DOM for DST crossover --- src/Cron/DayOfMonthField.php | 4 ++-- tests/Cron/CronExpressionTest.php | 8 +++++++ tests/Cron/DayOfMonthFieldTest.php | 38 ++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/Cron/DayOfMonthField.php b/src/Cron/DayOfMonthField.php index e0871830..a66a5ab4 100644 --- a/src/Cron/DayOfMonthField.php +++ b/src/Cron/DayOfMonthField.php @@ -118,10 +118,10 @@ public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bo public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface { if (! $invert) { - $date = $this->timezoneSafeModify($date, '+1 day'); + $date = $date->add(new \DateInterval('P1D')); $date = $date->setTime(0, 0); } else { - $date = $this->timezoneSafeModify($date, '-1 day'); + $date = $date->sub(new \DateInterval('P1D')); $date = $date->setTime(23, 59); } diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 0286e8f1..5f45b649 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -670,4 +670,12 @@ public function testGetParts(): void $this->assertSame('*', $parts[3]); $this->assertSame('1-5', $parts[4]); } + + public function testBerlinShouldAdvanceProperlyOverDST() + { + $e = new CronExpression('0 0 1 * *'); + $expected = new \DateTime('2022-11-01 00:00:00', new \DateTimeZone('Europe/Berlin')); + $next = $e->getNextRunDate(new \DateTime('2022-10-30', new \DateTimeZone('Europe/Berlin'))); + $this->assertEquals($expected, $next); + } } diff --git a/tests/Cron/DayOfMonthFieldTest.php b/tests/Cron/DayOfMonthFieldTest.php index 0d18c308..2fea8b1b 100644 --- a/tests/Cron/DayOfMonthFieldTest.php +++ b/tests/Cron/DayOfMonthFieldTest.php @@ -77,4 +77,42 @@ public function testDoesNotAccept0Date(): void $f = new DayOfMonthField(); $this->assertFalse($f->validate('0')); } + + /** + * @covers \Cron\MinutesField::increment + */ + public function testIncrementAcrossDstChangeBerlin(): void + { + $tz = new \DateTimeZone("Europe/Berlin"); + $d = \DateTimeImmutable::createFromFormat("!Y-m-d H:i:s", "2021-03-28 01:59:00", $tz); + $f = new DayOfMonthField(); + $f->increment($d); + $this->assertSame("2021-03-29 00:00:00", $d->format("Y-m-d H:i:s")); + + $f->increment($d, true); + $this->assertSame("2021-03-28 23:59:00", $d->format("Y-m-d H:i:s")); + $f->increment($d, true); + $this->assertSame("2021-03-27 23:59:00", $d->format("Y-m-d H:i:s")); + } + + /** + * @covers \Cron\MinutesField::increment + */ + public function testIncrementAcrossDstChangeLondon(): void + { + $tz = new \DateTimeZone("Europe/London"); + $d = \DateTimeImmutable::createFromFormat("!Y-m-d H:i:s", "2021-03-28 00:59:00", $tz); + $f = new DayOfMonthField(); + $f->increment($d); + $this->assertSame("2021-03-29 00:00:00", $d->format("Y-m-d H:i:s")); + $f->increment($d); + $this->assertSame("2021-03-30 00:00:00", $d->format("Y-m-d H:i:s")); + + $f->increment($d, true); + $this->assertSame("2021-03-29 23:59:00", $d->format("Y-m-d H:i:s")); + $f->increment($d, true); + $this->assertSame("2021-03-28 23:59:00", $d->format("Y-m-d H:i:s")); + $f->increment($d, true); + $this->assertSame("2021-03-27 23:59:00", $d->format("Y-m-d H:i:s")); + } } From 9c6ee744806672c348fc3bd1637ecdca033a77db Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Wed, 12 Jan 2022 22:58:00 -0500 Subject: [PATCH 113/146] Additional tests covering other times that cropped up from v3.2.1 and issue 131 --- tests/Cron/CronExpressionTest.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 5f45b649..1e32df67 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -678,4 +678,30 @@ public function testBerlinShouldAdvanceProperlyOverDST() $next = $e->getNextRunDate(new \DateTime('2022-10-30', new \DateTimeZone('Europe/Berlin'))); $this->assertEquals($expected, $next); } + + /** + * Helps validate additional test cases that were failing as part of #131's fix + * + * @see https://github.com/dragonmantank/cron-expression/issues/131 + */ + public function testIssue131() + { + $e = new CronExpression('* * * * 2'); + $expected = new \DateTime('2020-10-27 00:00:00'); + $next = $e->getNextRunDate(new DateTime('2020-10-23 15:31:45')); + $this->assertEquals($expected, $next); + + $expected = new \DateTime('2020-10-20 23:59:00'); + $prev = $e->getPreviousRunDate(new DateTime('2020-10-23 15:31:45')); + $this->assertEquals($expected, $prev); + + $e = new CronExpression('15 1 1 9,11 *'); + $expected = new \DateTime('2022-09-01 01:15:00'); + $next = $e->getNextRunDate(new \DateTime('2022-08-20 03:44:02')); + $this->assertEquals($expected, $next); + + $expected = new \DateTime('2021-11-01 01:15:00'); + $prev = $e->getPreviousRunDate(new \DateTime('2022-08-20 03:44:02')); + $this->assertEquals($expected, $prev); + } } From 9545dea2a1d92b60c8b3d06f02025c83e999bde0 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Wed, 12 Jan 2022 23:09:37 -0500 Subject: [PATCH 114/146] Fix for DOW DST changes, and updated README --- CHANGELOG.md | 11 +++++++++++ src/Cron/DayOfWeekField.php | 4 ++-- tests/Cron/CronExpressionTest.php | 8 ++++---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba51e503..9c736bb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Change Log +## [3.2.4] - 2022-01-12 + +### Added +- N/A + +### Changed +- Changed how Day of Week increment/decrement to help with DST changes (#131) + +### Fixed +- N/A + ## [3.2.3] - 2022-01-05 ### Added diff --git a/src/Cron/DayOfWeekField.php b/src/Cron/DayOfWeekField.php index 40db9af1..ef2a609d 100644 --- a/src/Cron/DayOfWeekField.php +++ b/src/Cron/DayOfWeekField.php @@ -151,10 +151,10 @@ public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bo public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface { if (! $invert) { - $date = $this->timezoneSafeModify($date, '+1 day'); + $date = $date->add(new \DateInterval('P1D')); $date = $date->setTime(0, 0); } else { - $date = $this->timezoneSafeModify($date, '-1 day'); + $date = $date->sub(new \DateInterval('P1D')); $date = $date->setTime(23, 59); } diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 1e32df67..84b953be 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -687,12 +687,12 @@ public function testBerlinShouldAdvanceProperlyOverDST() public function testIssue131() { $e = new CronExpression('* * * * 2'); - $expected = new \DateTime('2020-10-27 00:00:00'); - $next = $e->getNextRunDate(new DateTime('2020-10-23 15:31:45')); + $expected = new \DateTime('2020-10-27 00:00:00', new \DateTimeZone('Europe/Berlin')); + $next = $e->getNextRunDate(new DateTime('2020-10-23 15:31:45', new \DateTimeZone('Europe/Berlin'))); $this->assertEquals($expected, $next); - $expected = new \DateTime('2020-10-20 23:59:00'); - $prev = $e->getPreviousRunDate(new DateTime('2020-10-23 15:31:45')); + $expected = new \DateTime('2020-10-20 23:59:00', new \DateTimeZone('Europe/Berlin')); + $prev = $e->getPreviousRunDate(new DateTime('2020-10-23 15:31:45', new \DateTimeZone('Europe/Berlin'))); $this->assertEquals($expected, $prev); $e = new CronExpression('15 1 1 9,11 *'); From 6e0003e69ee30c72404d2b63effad652f33cc7dc Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Thu, 13 Jan 2022 00:08:17 -0500 Subject: [PATCH 115/146] Fixed run date calculations when ? is used to skip --- src/Cron/CronExpression.php | 8 ++++++++ tests/Cron/CronExpressionTest.php | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index f71490e1..fa2acf1a 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -410,6 +410,14 @@ protected function getRunDate($currentTime = null, int $nth = 0, bool $invert = $domRunDates = $domExpression->getMultipleRunDates($nth + 1, $currentTime, $invert, $allowCurrentDate, $timeZone); $dowRunDates = $dowExpression->getMultipleRunDates($nth + 1, $currentTime, $invert, $allowCurrentDate, $timeZone); + if ($parts[self::DAY] === '?' || $parts[self::DAY] === '*') { + $domRunDates = []; + } + + if ($parts[self::WEEKDAY] === '?' || $parts[self::WEEKDAY] === '*') { + $dowRunDates = []; + } + $combined = array_merge($domRunDates, $dowRunDates); usort($combined, function ($a, $b) { return $a->format('Y-m-d H:i:s') <=> $b->format('Y-m-d H:i:s'); diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 84b953be..61be75cf 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -704,4 +704,24 @@ public function testIssue131() $prev = $e->getPreviousRunDate(new \DateTime('2022-08-20 03:44:02')); $this->assertEquals($expected, $prev); } + + public function testIssue128() + { + $e = new CronExpression('0 20 L 6,12 ?'); + $expected = new \DateTime('2022-12-31 20:00:00'); + $next = $e->getNextRunDate(new \DateTime('2022-08-20 03:44:02')); + $this->assertEquals($expected, $next); + + $expected = new \DateTime('2023-12-31 20:00:00'); + $next = $e->getNextRunDate(new \DateTime('2022-08-20 03:44:02'), 2); + $this->assertEquals($expected, $next); + + $expected = new \DateTime('2022-06-30 20:00:00'); + $prev = $e->getPreviousRunDate(new \DateTime('2022-08-20 03:44:02')); + $this->assertEquals($expected, $prev); + + $expected = new \DateTime('2021-12-31 20:00:00'); + $prev = $e->getPreviousRunDate(new \DateTime('2022-08-20 03:44:02'), 1); + $this->assertEquals($expected, $prev); + } } From 9b377d541c743d1cf78738a00295773896696ee8 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Fri, 14 Jan 2022 10:29:10 -0500 Subject: [PATCH 116/146] Changed requirements from 7.1+ to 7.2+ --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0926f9d6..e853ad45 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ This library also supports a few macros: Requirements ============ -- PHP 7.1+ +- PHP 7.2+ - PHPUnit is required to run the unit tests - Composer is required to run the unit tests From d10f9694f1b8963c06687bd9ac85cf05953e1bbd Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Fri, 14 Jan 2022 16:33:48 +0100 Subject: [PATCH 117/146] Adding Registering alias feature to the package (#132) --- src/Cron/CronExpression.php | 74 ++++++++++++++++++++++++++++++- tests/Cron/CronExpressionTest.php | 57 ++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index fa2acf1a..b13f3d48 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -10,6 +10,7 @@ use DateTimeZone; use Exception; use InvalidArgumentException; +use LogicException; use RuntimeException; use Webmozart\Assert\Assert; @@ -73,6 +74,77 @@ class CronExpression self::MINUTE, ]; + /** + * @var array + */ + private static $registeredAliases = self::MAPPINGS; + + /** + * Registered a user defined CRON Expression Alias. + * + * @throws LogicException If the expression or the alias name are invalid + * or if the alias is already registered. + */ + public static function registerAlias(string $alias, string $expression): void + { + try { + new self($expression); + } catch (InvalidArgumentException $exception) { + throw new LogicException("The expression `$expression` is invalid", 0, $exception); + } + + $shortcut = strtolower($alias); + if (1 !== preg_match('/^@\w+$/', $shortcut)) { + throw new LogicException("The alias `$alias` is invalid. It must start with an `@` character and contain alphanumeric (letters, numbers, regardless of case) plus underscore (_)."); + } + + if (isset(self::$registeredAliases[$shortcut])) { + throw new LogicException("The alias `$alias` is already registered."); + } + + self::$registeredAliases[$shortcut] = $expression; + } + + /** + * Unregistered a user defined CRON Expression Alias. + * + * @throws LogicException If the user tries to unregister a built-in alias + */ + public static function unregisterAlias(string $alias): bool + { + $shortcut = strtolower($alias); + if (isset(self::MAPPINGS[$shortcut])) { + throw new LogicException("The alias `$alias` is a built-in alias; it can not be unregistered."); + } + + if (!isset(self::$registeredAliases[$shortcut])) { + return false; + } + + unset(self::$registeredAliases[$shortcut]); + + return true; + } + + /** + * Tells whether a CRON Expression alias is registered. + */ + public static function supportsAlias(string $alias): bool + { + return isset(self::$registeredAliases[strtolower($alias)]); + } + + /** + * Returns all registered aliases as an associated array where the aliases are the key + * and their associated expressions are the values. + * + * @return array + */ + public static function getAliases(): array + { + return self::$registeredAliases; + } + /** * @deprecated since version 3.0.2, use __construct instead. */ @@ -109,7 +181,7 @@ public static function isValidExpression(string $expression): bool public function __construct(string $expression, FieldFactoryInterface $fieldFactory = null) { $shortcut = strtolower($expression); - $expression = self::MAPPINGS[$shortcut] ?? $expression; + $expression = self::$registeredAliases[$shortcut] ?? $expression; $this->fieldFactory = $fieldFactory ?: new FieldFactory(); $this->setExpression($expression); diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 61be75cf..6036c1d1 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -10,6 +10,7 @@ use DateTimeImmutable; use DateTimeZone; use InvalidArgumentException; +use LogicException; use PHPUnit\Framework\TestCase; use Webmozart\Assert\Assert; @@ -724,4 +725,60 @@ public function testIssue128() $prev = $e->getPreviousRunDate(new \DateTime('2022-08-20 03:44:02'), 1); $this->assertEquals($expected, $prev); } + + public function testItCanRegisterAnValidExpression(): void + { + CronExpression::registerAlias('@every', '* * * * *'); + + self::assertCount(8, CronExpression::getAliases()); + self::assertArrayHasKey('@every', CronExpression::getAliases()); + self::assertTrue(CronExpression::supportsAlias('@every')); + self::assertEquals(new CronExpression('@every'), new CronExpression('* * * * *')); + + self::assertTrue(CronExpression::unregisterAlias('@every')); + self::assertFalse(CronExpression::unregisterAlias('@every')); + + self::assertCount(7, CronExpression::getAliases()); + self::assertArrayNotHasKey('@every', CronExpression::getAliases()); + self::assertFalse(CronExpression::supportsAlias('@every')); + + $this->expectException(LogicException::class); + new CronExpression('@every'); + } + + public function testItWillFailToRegisterAnInvalidExpression(): void + { + $this->expectException(LogicException::class); + + CronExpression::registerAlias('@every', 'foobar'); + } + + public function testItWillFailToRegisterAnInvalidName(): void + { + $this->expectException(LogicException::class); + + CronExpression::registerAlias('every', '* * * * *'); + } + + public function testItWillFailToRegisterAnInvalidName2(): void + { + $this->expectException(LogicException::class); + + CronExpression::registerAlias('@évery', '* * * * *'); + } + + public function testItWillFailToRegisterAValidNameTwice(): void + { + CronExpression::registerAlias('@Ev_eR_y', '* * * * *'); + + $this->expectException(LogicException::class); + CronExpression::registerAlias('@eV_Er_Y', '2 2 2 2 2'); + } + + public function testItWillFailToUnregisterADefaultExpression(): void + { + $this->expectException(LogicException::class); + + CronExpression::unregisterAlias('@daily'); + } } From 99a8406e1935f4799f4358167b2e7d5c0bc743ee Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Fri, 14 Jan 2022 10:34:59 -0500 Subject: [PATCH 118/146] More tests around DOW and DOM resolutions --- tests/Cron/CronExpressionTest.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 6036c1d1..5ff346d8 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -724,6 +724,31 @@ public function testIssue128() $expected = new \DateTime('2021-12-31 20:00:00'); $prev = $e->getPreviousRunDate(new \DateTime('2022-08-20 03:44:02'), 1); $this->assertEquals($expected, $prev); + + $e = new CronExpression('0 20 L 6,12 0-6'); + $expected = new \DateTime('2022-12-01 20:00:00'); + $next = $e->getNextRunDate(new \DateTime('2022-08-20 03:44:02')); + $this->assertEquals($expected, $next); + + $e = new CronExpression('0 20 L 6,12 0-6'); + $expected = new \DateTime('2022-12-02 20:00:00'); + $next = $e->getNextRunDate(new \DateTime('2022-08-20 03:44:02'), 1); + $this->assertEquals($expected, $next); + + $e = new CronExpression('0 20 L 6,12 0-6'); + $expected = new \DateTime('2022-12-03 20:00:00'); + $next = $e->getNextRunDate(new \DateTime('2022-08-20 03:44:02'), 2); + $this->assertEquals($expected, $next); + + $e = new CronExpression('0 20 * 6,12 *'); + $expected = new \DateTime('2022-12-01 20:00:00'); + $next = $e->getNextRunDate(new \DateTime('2022-08-20 03:44:02')); + $this->assertEquals($expected, $next); + + $e = new CronExpression('0 20 * 6,12 *'); + $expected = new \DateTime('2022-12-06 20:00:00'); + $next = $e->getNextRunDate(new \DateTime('2022-08-20 03:44:02'), 5); + $this->assertEquals($expected, $next); } public function testItCanRegisterAnValidExpression(): void From d81a5674502403b316fdae19e2aefccb1781c5eb Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Fri, 14 Jan 2022 10:59:30 -0500 Subject: [PATCH 119/146] Fixed PHPStan errors --- composer.json | 8 ++++---- phpstan.neon | 13 +++++++++++++ src/Cron/AbstractField.php | 3 ++- src/Cron/CronExpression.php | 3 +++ src/Cron/DayOfMonthField.php | 11 +++++++---- src/Cron/DayOfWeekField.php | 1 - src/Cron/HoursField.php | 2 +- 7 files changed, 30 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index 54f296e7..e512446d 100644 --- a/composer.json +++ b/composer.json @@ -13,12 +13,12 @@ ], "require": { "php": "^7.2|^8.0", - "webmozart/assert": "^1.7.0" + "webmozart/assert": "^1.0" }, "require-dev": { - "phpstan/phpstan": "^0.12", + "phpstan/phpstan": "^1.0", "phpunit/phpunit": "^7.0|^8.0|^9.0", - "phpstan/phpstan-webmozart-assert": "^0.12.7", + "phpstan/phpstan-webmozart-assert": "^1.0", "phpstan/extension-installer": "^1.0" }, "autoload": { @@ -35,7 +35,7 @@ "mtdowling/cron-expression": "^1.0" }, "scripts": { - "phpstan": "./vendor/bin/phpstan analyse -l max src tests", + "phpstan": "./vendor/bin/phpstan analyze", "test": "phpunit" } } diff --git a/phpstan.neon b/phpstan.neon index 9d52fd98..bea9cb0d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,2 +1,15 @@ parameters: checkMissingIterableValueType: false + + ignoreErrors: + - '#Call to an undefined method DateTimeInterface::add\(\)#' + - '#Call to an undefined method DateTimeInterface::modify\(\)#' + - '#Call to an undefined method DateTimeInterface::setDate\(\)#' + - '#Call to an undefined method DateTimeInterface::setTime\(\)#' + - '#Call to an undefined method DateTimeInterface::setTimezone\(\)#' + - '#Call to an undefined method DateTimeInterface::sub\(\)#' + + level: max + + paths: + - src/ diff --git a/src/Cron/AbstractField.php b/src/Cron/AbstractField.php index b0b28405..df2848df 100644 --- a/src/Cron/AbstractField.php +++ b/src/Cron/AbstractField.php @@ -146,8 +146,9 @@ public function isInIncrementsOfRanges(int $dateValue, string $value): bool // Generate the requested small range $rangeChunks = explode('-', $range, 2); - $rangeStart = $rangeChunks[0]; + $rangeStart = (int) $rangeChunks[0]; $rangeEnd = $rangeChunks[1] ?? $rangeStart; + $rangeEnd = (int) $rangeEnd; if ($rangeStart < $this->rangeStart || $rangeStart > $this->rangeEnd || $rangeStart > $rangeEnd) { throw new \OutOfRangeException('Invalid range start requested'); diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index b13f3d48..d5337cc5 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -456,6 +456,9 @@ protected function getRunDate($currentTime = null, int $nth = 0, bool $invert = $currentDate->setTimezone(new DateTimeZone($timeZone)); // Workaround for setTime causing an offset change: https://bugs.php.net/bug.php?id=81074 $currentDate = DateTime::createFromFormat("!Y-m-d H:iO", $currentDate->format("Y-m-d H:iP"), $currentDate->getTimezone()); + if ($currentDate === false) { + throw new \RuntimeException('Unable to create date from format'); + } $currentDate->setTimezone(new DateTimeZone($timeZone)); $nextRun = clone $currentDate; diff --git a/src/Cron/DayOfMonthField.php b/src/Cron/DayOfMonthField.php index a66a5ab4..e08f62ea 100644 --- a/src/Cron/DayOfMonthField.php +++ b/src/Cron/DayOfMonthField.php @@ -96,15 +96,18 @@ public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bo // Check to see if this is the nearest weekday to a particular value if (strpos($value, 'W')) { // Parse the target day - /** @phpstan-ignore-next-line */ $targetDay = (int) substr($value, 0, strpos($value, 'W')); // Find out if the current day is the nearest day of the week - /** @phpstan-ignore-next-line */ - return $date->format('j') === self::getNearestWeekday( + $nearest = self::getNearestWeekday( (int) $date->format('Y'), (int) $date->format('m'), $targetDay - )->format('j'); + ); + if ($nearest) { + return $date->format('j') === $nearest->format('j'); + } + + throw new \RuntimeException('Unable to return nearest weekday'); } return $this->isSatisfied((int) $date->format('d'), $value); diff --git a/src/Cron/DayOfWeekField.php b/src/Cron/DayOfWeekField.php index ef2a609d..5ac003da 100644 --- a/src/Cron/DayOfWeekField.php +++ b/src/Cron/DayOfWeekField.php @@ -69,7 +69,6 @@ public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bo // Find out if this is the last specific weekday of the month if (strpos($value, 'L')) { - /** @phpstan-ignore-next-line */ $weekday = $this->convertLiterals(substr($value, 0, strpos($value, 'L'))); $weekday %= 7; diff --git a/src/Cron/HoursField.php b/src/Cron/HoursField.php index ef930d45..0b809101 100644 --- a/src/Cron/HoursField.php +++ b/src/Cron/HoursField.php @@ -50,7 +50,7 @@ public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bo // Are we on the edge of a transition $lastTransition = $this->getPastTransition($date); - if (($lastTransition !== null) && ($lastTransition["ts"] > ($date->format('U') - 3600))) { + if (($lastTransition !== null) && ($lastTransition["ts"] > ((int) $date->format('U') - 3600))) { $dtLastOffset = clone $date; $this->timezoneSafeModify($dtLastOffset, "-1 hour"); $lastOffset = $dtLastOffset->getOffset(); From 63f2a76a045bac6ec93cc2daf2b534b412aa0313 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Fri, 14 Jan 2022 11:02:05 -0500 Subject: [PATCH 120/146] Added changelog for 3.3.0 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c736bb8..925407f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Change Log +## [3.3.0] - 2022-01-13 + +### Added +- Added ability to register your own expression aliases (#132) + +### Changed +- Changed how Day of Week and Day of Month resolve when one or the other is `*` or `?` + +### Fixed +- PHPStan should no longer error out + ## [3.2.4] - 2022-01-12 ### Added From be85b3f05b46c39bbc0d95f6c071ddff669510fa Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Tue, 18 Jan 2022 10:43:28 -0500 Subject: [PATCH 121/146] Fixed #134, no transitions now immediately returns null --- CHANGELOG.md | 11 +++++++++++ src/Cron/HoursField.php | 3 +++ tests/Cron/CronExpressionTest.php | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 925407f3..99b587be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Change Log +## [3.3.1] - 2022-01-18 + +### Added +- N/A + +### Changed +- N/A + +### Fixed +- Fixed issue when timezones had no transition, which can occur over very short timespans (#134) + ## [3.3.0] - 2022-01-13 ### Added diff --git a/src/Cron/HoursField.php b/src/Cron/HoursField.php index 0b809101..a7f8f33c 100644 --- a/src/Cron/HoursField.php +++ b/src/Cron/HoursField.php @@ -92,6 +92,9 @@ public function getPastTransition(DateTimeInterface $date): ?array $dtLimitStart->getTimestamp(), $dtLimitEnd->getTimestamp() ); + if ($this->transitions === false) { + return null; + } $this->transitionsStart = $dtLimitStart->getTimestamp(); $this->transitionsEnd = $dtLimitEnd->getTimestamp(); } diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 5ff346d8..54e040a2 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -806,4 +806,11 @@ public function testItWillFailToUnregisterADefaultExpression(): void CronExpression::unregisterAlias('@daily'); } + + public function testIssue134ForeachInvalidArgumentOnHours() + { + $cron = new CronExpression('0 0 1 1 *'); + $prev = $cron->getPreviousRunDate(new \DateTimeImmutable('2021-09-07T09:36:00Z')); + $this->assertEquals(new \DateTime('2021-01-01 00:00:00'), $prev); + } } From 75916a443705ea7be2c3a2cf574caf7b72936e4e Mon Sep 17 00:00:00 2001 From: Ayesh Karunaratne Date: Tue, 6 Sep 2022 20:36:15 +0530 Subject: [PATCH 122/146] [PHP 8.2] Fix `${var}` string interpolation deprecation (#142) PHP 8.2 deprecates `"${var}"` string interpolation pattern. This fixes all three of such occurrences in `dragonmantank/cron-expression` package. - [PHP 8.2: `${var}` string interpolation deprecated](https://php.watch/versions/8.2/${var}-string-interpolation-deprecated) - [RFC](https://wiki.php.net/rfc/deprecate_dollar_brace_string_interpolation) --- src/Cron/DayOfMonthField.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cron/DayOfMonthField.php b/src/Cron/DayOfMonthField.php index e08f62ea..60fa4655 100644 --- a/src/Cron/DayOfMonthField.php +++ b/src/Cron/DayOfMonthField.php @@ -49,7 +49,7 @@ class DayOfMonthField extends AbstractField private static function getNearestWeekday(int $currentYear, int $currentMonth, int $targetDay): ?DateTime { $tday = str_pad((string) $targetDay, 2, '0', STR_PAD_LEFT); - $target = DateTime::createFromFormat('Y-m-d', "${currentYear}-${currentMonth}-${tday}"); + $target = DateTime::createFromFormat('Y-m-d', "{$currentYear}-{$currentMonth}-{$tday}"); if ($target === false) { return null; From 4fe4677c5700c916d67418e895d225717fef16c2 Mon Sep 17 00:00:00 2001 From: Athos Ribeiro Date: Tue, 6 Sep 2022 12:11:40 -0300 Subject: [PATCH 123/146] Skip PHP 8.1 unstable daylight savings tests (#146) As discussed in #133, the PHP 8.1's date extension daylight saving APIs have been suffering with instabilities. Let's skip the affected tests until https://github.com/php/php-src/issues/9165 is resolved. Signed-off-by: Athos Ribeiro Signed-off-by: Athos Ribeiro --- tests/Cron/DaylightSavingsTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/Cron/DaylightSavingsTest.php b/tests/Cron/DaylightSavingsTest.php index 82cfe0a8..7a8c6606 100644 --- a/tests/Cron/DaylightSavingsTest.php +++ b/tests/Cron/DaylightSavingsTest.php @@ -111,6 +111,11 @@ public function testOffsetIncrementsPreviousRunDate(): void $this->assertEquals($dtExpected, $cron->getPreviousRunDate($dtCurrent, 0, false, $tz->getName())); } + /** + * Skip due to PHP 8.1 date instabilities. + * For further references, see https://github.com/dragonmantank/cron-expression/issues/133 + * @requires PHP <= 8.0.22 + */ public function testOffsetDecrementsNextRunDateAllowCurrent(): void { $tz = new \DateTimeZone("Europe/London"); @@ -144,6 +149,9 @@ public function testOffsetDecrementsNextRunDateAllowCurrent(): void /** * The fact that crons will run twice using this setup is expected. * This can be avoided by using disallowing the current date or with additional checks outside this library + * Skip due to PHP 8.1 date instabilities. + * For further references, see https://github.com/dragonmantank/cron-expression/issues/133 + * @requires PHP <= 8.0.22 */ public function testOffsetDecrementsNextRunDateDisallowCurrent(): void { @@ -228,6 +236,11 @@ public function testOffsetIncrementsMultipleRunDates(): void } } + /** + * Skip due to PHP 8.1 date instabilities. + * For further references, see https://github.com/dragonmantank/cron-expression/issues/133 + * @requires PHP <= 8.0.22 + */ public function testOffsetDecrementsMultipleRunDates(): void { $expression = "0 1 * * 0"; @@ -315,6 +328,11 @@ public function testOffsetIncrementsEveryOtherHour(): void } } + /** + * Skip due to PHP 8.1 date instabilities. + * For further references, see https://github.com/dragonmantank/cron-expression/issues/133 + * @requires PHP <= 8.0.22 + */ public function testOffsetDecrementsEveryOtherHour(): void { $expression = "0 */2 * * *"; From 7d9700529bb03f1bd894afb674a106f05aec12c8 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Tue, 6 Sep 2022 11:13:21 -0400 Subject: [PATCH 124/146] Allowed some additional composer plugins to run --- composer.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/composer.json b/composer.json index e512446d..657a5b47 100644 --- a/composer.json +++ b/composer.json @@ -37,5 +37,11 @@ "scripts": { "phpstan": "./vendor/bin/phpstan analyze", "test": "phpunit" + }, + "config": { + "allow-plugins": { + "ocramius/package-versions": true, + "phpstan/extension-installer": true + } } } From fc75c83094d6c07190d7e6cadde72cd385d145e9 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Sat, 10 Sep 2022 14:47:49 -0400 Subject: [PATCH 125/146] Fixed some PHPStan errors around comparisons and literal positions --- src/Cron/DayOfMonthField.php | 4 ++-- src/Cron/DayOfWeekField.php | 4 ++-- src/Cron/HoursField.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Cron/DayOfMonthField.php b/src/Cron/DayOfMonthField.php index 60fa4655..39ff5978 100644 --- a/src/Cron/DayOfMonthField.php +++ b/src/Cron/DayOfMonthField.php @@ -94,9 +94,9 @@ public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bo } // Check to see if this is the nearest weekday to a particular value - if (strpos($value, 'W')) { + if ($wPosition = strpos($value, 'W')) { // Parse the target day - $targetDay = (int) substr($value, 0, strpos($value, 'W')); + $targetDay = (int) substr($value, 0, $wPosition); // Find out if the current day is the nearest day of the week $nearest = self::getNearestWeekday( (int) $date->format('Y'), diff --git a/src/Cron/DayOfWeekField.php b/src/Cron/DayOfWeekField.php index 5ac003da..b9bbf48b 100644 --- a/src/Cron/DayOfWeekField.php +++ b/src/Cron/DayOfWeekField.php @@ -68,8 +68,8 @@ public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bo $lastDayOfMonth = (int) $date->format('t'); // Find out if this is the last specific weekday of the month - if (strpos($value, 'L')) { - $weekday = $this->convertLiterals(substr($value, 0, strpos($value, 'L'))); + if ($lPosition = strpos($value, 'L')) { + $weekday = $this->convertLiterals(substr($value, 0, $lPosition)); $weekday %= 7; $daysInMonth = (int) $date->format('t'); diff --git a/src/Cron/HoursField.php b/src/Cron/HoursField.php index a7f8f33c..413d138b 100644 --- a/src/Cron/HoursField.php +++ b/src/Cron/HoursField.php @@ -25,7 +25,7 @@ class HoursField extends AbstractField /** * @var array|null Transitions returned by DateTimeZone::getTransitions() */ - protected $transitions = null; + protected $transitions = []; /** * @var int|null Timestamp of the start of the transitions range @@ -92,7 +92,7 @@ public function getPastTransition(DateTimeInterface $date): ?array $dtLimitStart->getTimestamp(), $dtLimitEnd->getTimestamp() ); - if ($this->transitions === false) { + if (empty($this->transitions)) { return null; } $this->transitionsStart = $dtLimitStart->getTimestamp(); From 782ca5968ab8b954773518e9e49a6f892a34b2a8 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Sat, 10 Sep 2022 14:51:20 -0400 Subject: [PATCH 126/146] Changelog for 3.3.2 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99b587be..7b6df4b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Change Log +## [3.3.2] - 2022-09-19 + +### Added +- N/A + +### Changed +- Skip some daylight savings time tests for PHP 8.1 daylight savings time weirdness (#146) + +### Fixed +- Changed string interpolations to work better with PHP 8.2 (#142) + ## [3.3.1] - 2022-01-18 ### Added From 8e78e4452fe9ea5725c5c350239f71e2596b1730 Mon Sep 17 00:00:00 2001 From: Leo Viezens Date: Thu, 10 Aug 2023 21:22:46 +0200 Subject: [PATCH 127/146] #137 Add check for multiple question marks (#148) * #137 Add check for multiple question marks * #137 Validate position of question marks in expression --- src/Cron/CronExpression.php | 13 +++++++++++-- tests/Cron/CronExpressionTest.php | 7 +++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index d5337cc5..80aa035d 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -201,13 +201,22 @@ public function setExpression(string $value): CronExpression $split = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY); Assert::isArray($split); - $this->cronParts = $split; - if (\count($this->cronParts) < 5) { + $notEnoughParts = \count($split) < 5; + + $questionMarkInInvalidPart = array_key_exists(0, $split) && $split[0] === '?' + || array_key_exists(1, $split) && $split[1] === '?' + || array_key_exists(3, $split) && $split[3] === '?'; + + $tooManyQuestionMarks = array_key_exists(2, $split) && $split[2] === '?' + && array_key_exists(4, $split) && $split[4] === '?'; + + if ($notEnoughParts || $questionMarkInInvalidPart || $tooManyQuestionMarks) { throw new InvalidArgumentException( $value . ' is not a valid CRON expression' ); } + $this->cronParts = $split; foreach ($this->cronParts as $position => $part) { $this->setPart($position, $part); } diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 54e040a2..c50f87fc 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -563,6 +563,13 @@ public function testValidationWorks(): void // Issue #125, this is just all sorts of wrong $this->assertFalse(CronExpression::isValidExpression('990 14 * * mon-fri0345345')); + // Issue #137, multiple question marks are not allowed + $this->assertFalse(CronExpression::isValidExpression('0 8 ? * ?')); + // Question marks are only allowed in dom and dow part + $this->assertFalse(CronExpression::isValidExpression('? * * * *')); + $this->assertFalse(CronExpression::isValidExpression('* ? * * *')); + $this->assertFalse(CronExpression::isValidExpression('* * * ? *')); + // see https://github.com/dragonmantank/cron-expression/issues/5 $this->assertTrue(CronExpression::isValidExpression('2,17,35,47 5-7,11-13 * * *')); } From ab597c89669c0a2650efa446f130607351290c30 Mon Sep 17 00:00:00 2001 From: imyip <312301109qq@gmail.com> Date: Fri, 11 Aug 2023 03:24:26 +0800 Subject: [PATCH 128/146] Fix skipped next execution time (#160) --- src/Cron/MinutesField.php | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Cron/MinutesField.php b/src/Cron/MinutesField.php index eda91098..54859840 100644 --- a/src/Cron/MinutesField.php +++ b/src/Cron/MinutesField.php @@ -14,7 +14,7 @@ class MinutesField extends AbstractField /** * {@inheritdoc} */ - protected $rangeStart = 0; + protected $rangeStart = 0;  /** * {@inheritdoc} @@ -24,7 +24,7 @@ class MinutesField extends AbstractField /** * {@inheritdoc} */ - public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert):bool + public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert):bool  { if ($value === '?') { return true; @@ -43,15 +43,16 @@ public function increment(DateTimeInterface &$date, $invert = false, $parts = nu { if (is_null($parts)) { $date = $this->timezoneSafeModify($date, ($invert ? "-" : "+") ."1 minute"); - return $this; + return $this;  } - $current_minute = (int) $date->format('i'); + $current_minute = (int) $date->format('i');  - $parts = false !== strpos($parts, ',') ? explode(',', $parts) : [$parts]; - $minutes = []; + $parts = false !== strpos($parts, ',') ? explode(',', $parts) : [$parts];  + sort($parts); + $minutes = [];  foreach ($parts as $part) { - $minutes = array_merge($minutes, $this->getRangeForExpression($part, 59)); + $minutes = array_merge($minutes, $this->getRangeForExpression($part, 59));  } $position = $invert ? \count($minutes) - 1 : 0; @@ -67,7 +68,7 @@ public function increment(DateTimeInterface &$date, $invert = false, $parts = nu } $target = (int) $minutes[$position]; - $originalMinute = (int) $date->format("i"); + $originalMinute = (int) $date->format("i");  if (! $invert) { if ($originalMinute >= $target) { @@ -78,7 +79,7 @@ public function increment(DateTimeInterface &$date, $invert = false, $parts = nu } $distance = $target - $originalMinute; - $date = $this->timezoneSafeModify($date, "+{$distance} minutes"); + $date = $this->timezoneSafeModify($date, "+{$distance} minutes");  } else { if ($originalMinute <= $target) { $distance = ($originalMinute + 1); From fafeaac510b681cb0992bb0250b76f2e36756b6c Mon Sep 17 00:00:00 2001 From: Roy Garrido <109597356+onemoreangle@users.noreply.github.com> Date: Thu, 10 Aug 2023 21:24:55 +0200 Subject: [PATCH 129/146] Update CronExpression.php (#158) Added "throws" docblock section, so it's immediately obvious in which way an invalid cron expression will fail Co-authored-by: Roy Garrido <109597356+r-garrido@users.noreply.github.com> --- src/Cron/CronExpression.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index 80aa035d..216ce432 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -177,6 +177,7 @@ public static function isValidExpression(string $expression): bool * * @param string $expression CRON expression (e.g. '8 * * * *') * @param null|FieldFactoryInterface $fieldFactory Factory to create cron fields + * @throws InvalidArgumentException */ public function __construct(string $expression, FieldFactoryInterface $fieldFactory = null) { From 357e4a5763f67a4d3464f902b58432093866d765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Thu, 10 Aug 2023 21:25:15 +0200 Subject: [PATCH 130/146] Mark `phpstan.neon` as `export-ignore` in `.gitattributes` (#144) --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 435f1a8f..a325062b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,4 +8,5 @@ /.php_cs export-ignore /.styleci.yml export-ignore /.travis.yml export-ignore +/phpstan.neon export-ignore /phpunit.xml.dist export-ignore From 6dda2176a2e2092a2169bb7bab87046b8758165a Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 10 Aug 2023 14:25:38 -0500 Subject: [PATCH 131/146] Update .gitattributes (#143) --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index a325062b..e9211440 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,3 +10,4 @@ /.travis.yml export-ignore /phpstan.neon export-ignore /phpunit.xml.dist export-ignore +/phpstan.neon export-ignore From 9f6ad16b620a57fbe380e90c19a462ae8c28e6d2 Mon Sep 17 00:00:00 2001 From: PabloKowalczyk <11366345+PabloKowalczyk@users.noreply.github.com> Date: Thu, 10 Aug 2023 19:26:55 +0000 Subject: [PATCH 132/146] Change Crunz repository (#139) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e853ad45..494652c8 100644 --- a/README.md +++ b/README.md @@ -84,4 +84,4 @@ Projects that Use cron-expression ================================= * Part of the [Laravel Framework](https://github.com/laravel/framework/) * Available as a [Symfony Bundle - setono/cron-expression-bundle](https://github.com/Setono/CronExpressionBundle) -* Framework agnostic, PHP-based job scheduler - [Crunz](https://github.com/lavary/crunz) +* Framework agnostic, PHP-based job scheduler - [Crunz](https://github.com/crunzphp/crunz) From 93a36228dbecbdd6ff071c6f48346e9d664debbc Mon Sep 17 00:00:00 2001 From: imyip <312301109qq@gmail.com> Date: Fri, 11 Aug 2023 03:24:26 +0800 Subject: [PATCH 133/146] Fix skipped next execution time (#160) --- src/Cron/MinutesField.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Cron/MinutesField.php b/src/Cron/MinutesField.php index 54859840..f077e6ec 100644 --- a/src/Cron/MinutesField.php +++ b/src/Cron/MinutesField.php @@ -14,7 +14,7 @@ class MinutesField extends AbstractField /** * {@inheritdoc} */ - protected $rangeStart = 0;  + protected $rangeStart = 0; /** * {@inheritdoc} @@ -24,7 +24,7 @@ class MinutesField extends AbstractField /** * {@inheritdoc} */ - public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert):bool  + public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert):bool { if ($value === '?') { return true; @@ -43,16 +43,16 @@ public function increment(DateTimeInterface &$date, $invert = false, $parts = nu { if (is_null($parts)) { $date = $this->timezoneSafeModify($date, ($invert ? "-" : "+") ."1 minute"); - return $this;  + return $this; } - $current_minute = (int) $date->format('i');  + $current_minute = (int) $date->format('i'); - $parts = false !== strpos($parts, ',') ? explode(',', $parts) : [$parts];  + $parts = false !== strpos($parts, ',') ? explode(',', $parts) : [$parts]; sort($parts); - $minutes = [];  + $minutes = []; foreach ($parts as $part) { - $minutes = array_merge($minutes, $this->getRangeForExpression($part, 59));  + $minutes = array_merge($minutes, $this->getRangeForExpression($part, 59)); } $position = $invert ? \count($minutes) - 1 : 0; @@ -68,7 +68,7 @@ public function increment(DateTimeInterface &$date, $invert = false, $parts = nu } $target = (int) $minutes[$position]; - $originalMinute = (int) $date->format("i");  + $originalMinute = (int) $date->format("i"); if (! $invert) { if ($originalMinute >= $target) { @@ -79,7 +79,7 @@ public function increment(DateTimeInterface &$date, $invert = false, $parts = nu } $distance = $target - $originalMinute; - $date = $this->timezoneSafeModify($date, "+{$distance} minutes");  + $date = $this->timezoneSafeModify($date, "+{$distance} minutes"); } else { if ($originalMinute <= $target) { $distance = ($originalMinute + 1); From 13e72fc02afc6b26622c6e5ab454f4108e30a724 Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Thu, 10 Aug 2023 13:02:34 -0400 Subject: [PATCH 134/146] Added regression tests for issue #151 --- tests/Cron/CronExpressionTest.php | 7 +++++++ tests/Cron/DayOfMonthFieldTest.php | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index c50f87fc..1569a8e3 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -820,4 +820,11 @@ public function testIssue134ForeachInvalidArgumentOnHours() $prev = $cron->getPreviousRunDate(new \DateTimeImmutable('2021-09-07T09:36:00Z')); $this->assertEquals(new \DateTime('2021-01-01 00:00:00'), $prev); } + + public function testIssue151ExpressionSupportLW() + { + $cron = new CronExpression('0 10 LW * *'); + $this->assertTrue($cron->isDue(new \DateTimeImmutable('2023-08-31 10:00:00'))); + $this->assertFalse($cron->isDue(new \DateTimeImmutable('2023-08-30 10:00:00'))); + } } diff --git a/tests/Cron/DayOfMonthFieldTest.php b/tests/Cron/DayOfMonthFieldTest.php index 2fea8b1b..c4f3bb9e 100644 --- a/tests/Cron/DayOfMonthFieldTest.php +++ b/tests/Cron/DayOfMonthFieldTest.php @@ -115,4 +115,10 @@ public function testIncrementAcrossDstChangeLondon(): void $f->increment($d, true); $this->assertSame("2021-03-27 23:59:00", $d->format("Y-m-d H:i:s")); } + + public function testIssue151DOMFieldSupportLW() + { + $f = new DayOfMonthField(); + $this->assertTrue($f->validate('LW')); + } } From adfb1f505deb6384dc8b39804c5065dd3c8c8c0a Mon Sep 17 00:00:00 2001 From: Chris Tankersley Date: Thu, 10 Aug 2023 15:35:10 -0400 Subject: [PATCH 135/146] Added Changelog for 3.3.3 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b6df4b1..17ab2ce4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Change Log +## [3.3.3] - 2024-08-10 + +### Added +- N/A + +### Changed +- N/A + +### Fixed +- Added fixes for making sure `?` is not passed for both DOM and DOW (#148, thank you https://github.com/LeoVie) +- Fixed bug in Next Execution Time by sorting minutes properly (#160, thank you https://github.com/imyip) + ## [3.3.2] - 2022-09-19 ### Added From 102b869b989d2867b5f05248cedbfa6604aa3a0a Mon Sep 17 00:00:00 2001 From: Serhii Petrov Date: Thu, 5 Oct 2023 23:16:32 +0300 Subject: [PATCH 136/146] Tests against php 8.2 and 8.3 (#167) --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ac4b8752..a40c4d15 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - php: [7.2, 7.3, 7.4, 8.0, 8.1] + php: [7.2, 7.3, 7.4, 8.0, 8.1, 8.2, 8.3] steps: - name: "Checkout" @@ -41,7 +41,7 @@ jobs: strategy: fail-fast: false matrix: - php: [7.2, 7.3, 7.4, 8.0, 8.1] + php: [7.2, 7.3, 7.4, 8.0, 8.1, 8.2, 8.3] steps: - name: Checkout From 0b6a90e903f57f2fb0802beae24fb6ed99831aa0 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 12 Apr 2024 17:46:59 +0200 Subject: [PATCH 137/146] explicitly mark nullable parameters as nullable (#175) --- src/Cron/CronExpression.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index 216ce432..7f0dab04 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -148,7 +148,7 @@ public static function getAliases(): array /** * @deprecated since version 3.0.2, use __construct instead. */ - public static function factory(string $expression, FieldFactoryInterface $fieldFactory = null): CronExpression + public static function factory(string $expression, ?FieldFactoryInterface $fieldFactory = null): CronExpression { /** @phpstan-ignore-next-line */ return new static($expression, $fieldFactory); @@ -179,7 +179,7 @@ public static function isValidExpression(string $expression): bool * @param null|FieldFactoryInterface $fieldFactory Factory to create cron fields * @throws InvalidArgumentException */ - public function __construct(string $expression, FieldFactoryInterface $fieldFactory = null) + public function __construct(string $expression, ?FieldFactoryInterface $fieldFactory = null) { $shortcut = strtolower($expression); $expression = self::$registeredAliases[$shortcut] ?? $expression; From b6a3e45f015ede6d8d1eaf4cea4a1f385ac3ab30 Mon Sep 17 00:00:00 2001 From: chris Date: Fri, 12 Apr 2024 17:49:29 +0200 Subject: [PATCH 138/146] chore: remove travis, and change status badge to github actions (#164) Co-authored-by: Christopher Georg --- .travis.yml | 32 -------------------------------- README.md | 2 +- 2 files changed, 1 insertion(+), 33 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b8e30cec..00000000 --- a/.travis.yml +++ /dev/null @@ -1,32 +0,0 @@ -language: php - -dist: bionic - -matrix: - include: - - php: 7.1 - - php: 7.1 - env: dependencies=lowest - - php: 7.2 - - php: 7.2 - env: dependencies=lowest - - php: 7.3 - - php: 7.3 - env: dependencies=lowest - - php: 7.4 - - php: 7.4 - env: dependencies=lowest - - php: nightly - install: travis_retry composer install --no-interaction --prefer-dist --ignore-platform-reqs - -install: - - | - if [[ "$dependencies" = "lowest" ]]; then - travis_retry composer update --no-interaction --prefer-lowest --prefer-stable -n - else - travis_retry composer install --no-interaction --prefer-dist - fi - -script: - - vendor/bin/phpunit --coverage-text - - composer phpstan diff --git a/README.md b/README.md index 494652c8..eec534b9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ PHP Cron Expression Parser ========================== -[![Latest Stable Version](https://poser.pugx.org/dragonmantank/cron-expression/v/stable.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Total Downloads](https://poser.pugx.org/dragonmantank/cron-expression/downloads.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Build Status](https://secure.travis-ci.org/dragonmantank/cron-expression.png)](http://travis-ci.org/dragonmantank/cron-expression) [![StyleCI](https://github.styleci.io/repos/103715337/shield?branch=master)](https://github.styleci.io/repos/103715337) +[![Latest Stable Version](https://poser.pugx.org/dragonmantank/cron-expression/v/stable.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Total Downloads](https://poser.pugx.org/dragonmantank/cron-expression/downloads.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Tests](https://github.com/dragonmantank/cron-expression/actions/workflows/tests.yml/badge.svg)](https://github.com/dragonmantank/cron-expression/actions/workflows/tests.yml) [![StyleCI](https://github.styleci.io/repos/103715337/shield?branch=master)](https://github.styleci.io/repos/103715337) The PHP cron expression parser can parse a CRON expression, determine if it is due to run, calculate the next run date of the expression, and calculate the previous From dd6dec4524f5e78130dabb0f3bd0abbaa9e71391 Mon Sep 17 00:00:00 2001 From: chris Date: Fri, 12 Apr 2024 17:52:48 +0200 Subject: [PATCH 139/146] chore: improve ci, fix deprecations, add php 8.2 tests (#163) Co-authored-by: Christopher Georg Co-authored-by: Chris Tankersley --- .github/workflows/tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a40c4d15..2fa335f2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ jobs: steps: - name: "Checkout" - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -45,7 +45,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -61,10 +61,10 @@ jobs: - name: Get Composer cache directory id: composercache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache Composer dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.composercache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} From 1a39913ff9069354409da5f766ebb3e730b4743e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Fri, 12 Apr 2024 17:53:52 +0200 Subject: [PATCH 140/146] Remove duplicate `/phpstan.neon` entry in .gitattributes (#162) This duplicate was not added in alphabetical order and thus probably was missed during PR merge. see 6dda2176a2e2092a2169bb7bab87046b8758165a --- .gitattributes | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index e9211440..a325062b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,4 +10,3 @@ /.travis.yml export-ignore /phpstan.neon export-ignore /phpunit.xml.dist export-ignore -/phpstan.neon export-ignore From e4182563ac0ada1881e091b0974a143a1a8b3691 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 19 Apr 2024 19:16:33 +0200 Subject: [PATCH 141/146] Add branch-alias to composer.json (#182) --- composer.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/composer.json b/composer.json index 657a5b47..d77096c2 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,11 @@ "phpstan": "./vendor/bin/phpstan analyze", "test": "phpunit" }, + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, "config": { "allow-plugins": { "ocramius/package-versions": true, From ab30d1dd50a6da885eb356d6c9e7e9d9c3854cea Mon Sep 17 00:00:00 2001 From: Julius Kiekbusch Date: Wed, 9 Oct 2024 15:43:31 +0200 Subject: [PATCH 142/146] Add PHP 8.4 Support (#190) --- .github/workflows/tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2fa335f2..fe7ea1bd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,11 +16,11 @@ jobs: strategy: fail-fast: false matrix: - php: [7.2, 7.3, 7.4, 8.0, 8.1, 8.2, 8.3] + php: [7.2, 7.3, 7.4, '8.0', 8.1, 8.2, 8.3, 8.4] steps: - name: "Checkout" - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -45,7 +45,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -64,7 +64,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache Composer dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.composercache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} From ac0925a43d53b25089d4532243d3a2eec71a173b Mon Sep 17 00:00:00 2001 From: 8ctopus Date: Wed, 9 Oct 2024 17:45:23 +0400 Subject: [PATCH 143/146] Remove phpstan/phpstan-webmozart-assert dependency (#187) --- composer.json | 1 - src/Cron/CronExpression.php | 23 ++++++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index d77096c2..fdb46ee4 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,6 @@ "require-dev": { "phpstan/phpstan": "^1.0", "phpunit/phpunit": "^7.0|^8.0|^9.0", - "phpstan/phpstan-webmozart-assert": "^1.0", "phpstan/extension-installer": "^1.0" }, "autoload": { diff --git a/src/Cron/CronExpression.php b/src/Cron/CronExpression.php index 7f0dab04..f3d8eb00 100644 --- a/src/Cron/CronExpression.php +++ b/src/Cron/CronExpression.php @@ -12,7 +12,6 @@ use InvalidArgumentException; use LogicException; use RuntimeException; -use Webmozart\Assert\Assert; /** * CRON expression parser that can determine whether or not a CRON expression is @@ -200,7 +199,12 @@ public function __construct(string $expression, ?FieldFactoryInterface $fieldFac public function setExpression(string $value): CronExpression { $split = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY); - Assert::isArray($split); + + if (!\is_array($split)) { + throw new InvalidArgumentException( + $value . ' is not a valid CRON expression' + ); + } $notEnoughParts = \count($split) < 5; @@ -334,7 +338,10 @@ public function getMultipleRunDates(int $total, $currentTime = 'now', bool $inve $currentTime = new DateTime($currentTime); } - Assert::isInstanceOf($currentTime, DateTime::class); + if (!$currentTime instanceof DateTime) { + throw new InvalidArgumentException('invalid current time'); + } + $currentTime->setTimezone(new DateTimeZone($timeZone)); $matches = []; @@ -420,7 +427,10 @@ public function isDue($currentTime = 'now', $timeZone = null): bool $currentTime = new DateTime($currentTime); } - Assert::isInstanceOf($currentTime, DateTime::class); + if (!$currentTime instanceof DateTime) { + throw new InvalidArgumentException('invalid current time'); + } + $currentTime->setTimezone(new DateTimeZone($timeZone)); // drop the seconds to 0 @@ -462,7 +472,10 @@ protected function getRunDate($currentTime = null, int $nth = 0, bool $invert = $currentDate = new DateTime('now'); } - Assert::isInstanceOf($currentDate, DateTime::class); + if (!$currentDate instanceof DateTime) { + throw new InvalidArgumentException('invalid current date'); + } + $currentDate->setTimezone(new DateTimeZone($timeZone)); // Workaround for setTime causing an offset change: https://bugs.php.net/bug.php?id=81074 $currentDate = DateTime::createFromFormat("!Y-m-d H:iO", $currentDate->format("Y-m-d H:iP"), $currentDate->getTimezone()); From 963257179c7e67ee0de64bfa69920311a8672788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Barto=C5=A1?= Date: Wed, 9 Oct 2024 15:46:16 +0200 Subject: [PATCH 144/146] Docs: full syntax overview (#184) --- README.md | 76 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index eec534b9..3e7e7825 100644 --- a/README.md +++ b/README.md @@ -55,23 +55,65 @@ CRON Expressions A CRON expression is a string representing the schedule for a particular command to execute. The parts of a CRON schedule are as follows: - * * * * * - - - - - - - | | | | | - | | | | | - | | | | +----- day of week (0 - 7) (Sunday=0 or 7) - | | | +---------- month (1 - 12) - | | +--------------- day of month (1 - 31) - | +-------------------- hour (0 - 23) - +------------------------- min (0 - 59) - -This library also supports a few macros: - -* `@yearly`, `@annually` - Run once a year, midnight, Jan. 1 - `0 0 1 1 *` -* `@monthly` - Run once a month, midnight, first of month - `0 0 1 * *` -* `@weekly` - Run once a week, midnight on Sun - `0 0 * * 0` -* `@daily`, `@midnight` - Run once a day, midnight - `0 0 * * *` -* `@hourly` - Run once an hour, first minute - `0 * * * *` +``` +* * * * * +- - - - - +| | | | | +| | | | | +| | | | +----- day of week (0-7) (Sunday = 0 or 7) (or SUN-SAT) +| | | +--------- month (1-12) (or JAN-DEC) +| | +------------- day of month (1-31) +| +----------------- hour (0-23) ++--------------------- minute (0-59) +``` + +Each part of expression can also use wildcard, lists, ranges and steps: + +- wildcard - match always + - `* * * * *` - At every minute. + - day of week and day of month also support `?`, an alias to `*` +- lists - match list of values, ranges and steps + - e.g. `15,30 * * * *` - At minute 15 and 30. +- ranges - match values in range + - e.g. `1-9 * * * *` - At every minute from 1 through 9. +- steps - match every nth value in range + - e.g. `*/5 * * * *` - At every 5th minute. + - e.g. `0-30/5 * * * *` - At every 5th minute from 0 through 30. +- combinations + - e.g. `0-14,30-44 * * * *` - At every minute from 0 through 14 and every minute from 30 through 44. + +You can also use macro instead of an expression: + +- `@yearly`, `@annually` - At 00:00 on 1st of January. (same as `0 0 1 1 *`) +- `@monthly` - At 00:00 on day-of-month 1. (same as `0 0 1 * *`) +- `@weekly` - At 00:00 on Sunday. (same as `0 0 * * 0`) +- `@daily`, `@midnight` - At 00:00. (same as `0 0 * * *`) +- `@hourly` - At minute 0. (same as `0 * * * *`) + +Day of month extra features: + +- nearest weekday - weekday (Monday-Friday) nearest to the given day + - e.g. `* * 15W * *` - At every minute on a weekday nearest to the 15th. + - If you were to specify `15W` as the value, the meaning is: "the nearest weekday to the 15th of the month" + So if the 15th is a Saturday, the trigger will fire on Friday the 14th. + If the 15th is a Sunday, the trigger will fire on Monday the 16th. + If the 15th is a Tuesday, then it will fire on Tuesday the 15th. + - However, if you specify `1W` as the value for day-of-month, + and the 1st is a Saturday, the trigger will fire on Monday the 3rd, + as it will not 'jump' over the boundary of a month's days. +- last day of the month + - e.g. `* * L * *` - At every minute on a last day-of-month. +- last weekday of the month + - e.g. `* * LW * *` - At every minute on a last weekday. + +Day of week extra features: + +- nth day + - e.g. `* * * * 7#4` - At every minute on 4th Sunday. + - 1-5 + - Every day of week repeats 4-5 times a month. To target the last one, use "last day" feature instead. +- last day + - e.g. `* * * * 7L` - At every minute on the last Sunday. Requirements ============ From 8c784d071debd117328803d86b2097615b457500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Barto=C5=A1?= Date: Wed, 9 Oct 2024 15:47:03 +0200 Subject: [PATCH 145/146] Docs: new integrations (#183) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 3e7e7825..b9df3db5 100644 --- a/README.md +++ b/README.md @@ -127,3 +127,5 @@ Projects that Use cron-expression * Part of the [Laravel Framework](https://github.com/laravel/framework/) * Available as a [Symfony Bundle - setono/cron-expression-bundle](https://github.com/Setono/CronExpressionBundle) * Framework agnostic, PHP-based job scheduler - [Crunz](https://github.com/crunzphp/crunz) +* Framework agnostic job scheduler - with locks, parallelism, per-second scheduling and more - [orisai/scheduler](https://github.com/orisai/scheduler) +* Explain expression in English (and other languages) with [orisai/cron-expression-explainer](https://github.com/orisai/cron-expression-explainer) From f37e405332cd1cca9d287f5044ae2a72c2f53239 Mon Sep 17 00:00:00 2001 From: 8ctopus Date: Mon, 21 Oct 2024 16:57:55 +0400 Subject: [PATCH 146/146] Remove webmozart assert dependency (#192) --- composer.json | 3 +-- tests/Cron/CronExpressionTest.php | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index fdb46ee4..3d98ced4 100644 --- a/composer.json +++ b/composer.json @@ -12,8 +12,7 @@ } ], "require": { - "php": "^7.2|^8.0", - "webmozart/assert": "^1.0" + "php": "^7.2|^8.0" }, "require-dev": { "phpstan/phpstan": "^1.0", diff --git a/tests/Cron/CronExpressionTest.php b/tests/Cron/CronExpressionTest.php index 1569a8e3..e28f929c 100644 --- a/tests/Cron/CronExpressionTest.php +++ b/tests/Cron/CronExpressionTest.php @@ -339,22 +339,31 @@ public function testRecognisesTimezonesAsPartOfDateTime(): void $tzServer = new \DateTimeZone('Europe/London'); $dtCurrent = \DateTime::createFromFormat('!Y-m-d H:i:s', '2017-10-17 10:00:00', $tzServer); - Assert::isInstanceOf($dtCurrent, DateTime::class); + if (!$dtCurrent instanceof \DateTime) { + throw new InvalidArgumentException('invalid current date time'); + } + $dtPrev = $cron->getPreviousRunDate($dtCurrent, 0, true, $tzCron); $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format('U \\: c \\: e')); $dtCurrent = \DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', '2017-10-17 10:00:00', $tzServer); - Assert::isInstanceOf($dtCurrent, \DateTimeImmutable::class); + if (!$dtCurrent instanceof \DateTimeImmutable) { + throw new InvalidArgumentException('invalid current date time immutable'); + } $dtPrev = $cron->getPreviousRunDate($dtCurrent, 0, true, $tzCron); $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format('U \\: c \\: e')); $dtCurrent = \DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', '2017-10-17 10:00:00', $tzServer); - Assert::isInstanceOf($dtCurrent, \DateTimeImmutable::class); + if (!$dtCurrent instanceof \DateTimeImmutable) { + throw new InvalidArgumentException('invalid current date time immutable'); + } $dtPrev = $cron->getPreviousRunDate($dtCurrent->format('c'), 0, true, $tzCron); $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format('U \\: c \\: e')); $dtCurrent = \DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', '2017-10-17 10:00:00', $tzServer); - Assert::isInstanceOf($dtCurrent, \DateTimeImmutable::class); + if (!$dtCurrent instanceof \DateTimeImmutable) { + throw new InvalidArgumentException('invalid current date time immutable'); + } $dtPrev = $cron->getPreviousRunDate($dtCurrent->format('\\@U'), 0, true, $tzCron); $this->assertEquals('1508151600 : 2017-10-16T07:00:00-04:00 : America/New_York', $dtPrev->format('U \\: c \\: e')); }