-
Notifications
You must be signed in to change notification settings - Fork 339
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 8be6958
Showing
6 changed files
with
376 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
nbproject | ||
coverage | ||
phpunit.xml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
Cron Parser | ||
=========== | ||
|
||
The Cron\Parser class can parse a CRON expression, determine if it is due to run, and calculate the next run date of the expression. The parser can handle simple increment of ranges (e.g. */12), intervals (e.g. 0-9), and lists (e.g. 1,2,3). | ||
|
||
Requirements | ||
------------ | ||
|
||
#. PHP 5.3+ | ||
#. PHPUnit is required to run the unit tests | ||
|
||
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: | ||
|
||
* * * * * command to be executed | ||
- - - - - | ||
| | | | | | ||
| | | | | | ||
| | | | +----- day of week (0 - 7) (Sunday=0 or 7) | ||
| | | +---------- month (1 - 12) | ||
| | +--------------- day of month (1 - 31) | ||
| +-------------------- hour (0 - 23) | ||
+------------------------- min (0 - 59) | ||
|
||
TODO | ||
---- | ||
|
||
Here are the features lacking in the current implementation: | ||
|
||
#. Implement complex increment ranges (e.g. 3-59/15) | ||
#. Implement L for last day of the week and last day of the month | ||
#. Implement W to find the closest day of the week for a day of the month (e.g. 1W) | ||
#. Implement the pound sign for the day of the week field to handle things like "the second friday of a given month" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<phpunit bootstrap="./tests/bootstrap.php" | ||
colors="true" | ||
processIsolation="false" | ||
stopOnFailure="false" | ||
syntaxCheck="false" | ||
convertErrorsToExceptions="true" | ||
convertNoticesToExceptions="true" | ||
convertWarningsToExceptions="true" | ||
testSuiteLoaderClass="PHPUnit_Runner_StandardTestSuiteLoader"> | ||
|
||
<testsuites> | ||
<testsuite name="Cron"> | ||
<directory>./tests</directory> | ||
</testsuite> | ||
</testsuites> | ||
|
||
<filter> | ||
<whitelist> | ||
<directory suffix=".php">./src/Cron</directory> | ||
</whitelist> | ||
</filter> | ||
|
||
</phpunit> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
<?php | ||
|
||
namespace Cron; | ||
|
||
use DateInterval; | ||
use DateTime; | ||
use InvalidArgumentException; | ||
|
||
/** | ||
* CRON expression parser that can determine whether or not a CRON expression is | ||
* due to run and the next run date of a cron schedule. The determinations made | ||
* by this class are accurate if checked run once per minute. | ||
* | ||
* The parser can handle ranges (10-12) and intervals (*\/10). | ||
* | ||
* Schedule parts must map to: | ||
* minute [0-59], hour [0-23], day of month, month [1-12], day of week [1-7] | ||
* | ||
* @author Michael Dowling <mtdowling@gmail.com> | ||
* @link http://en.wikipedia.org/wiki/Cron | ||
*/ | ||
class Parser | ||
{ | ||
const MINUTE = 0; | ||
const HOUR = 1; | ||
const DAY = 2; | ||
const MONTH = 3; | ||
const WEEKDAY = 4; | ||
|
||
/** | ||
* @var array CRON expression parts | ||
*/ | ||
private $cronParts; | ||
|
||
/** | ||
* Parse a CRON expression | ||
* | ||
* @param string $schedule CRON expression schedule string (e.g. '8 * * * *') | ||
* | ||
* @throws InvalidArgumentException if not a valid CRON expression | ||
*/ | ||
public function __construct($schedule) | ||
{ | ||
$this->cronParts = explode(' ', $schedule); | ||
|
||
if (count($this->cronParts) != 5) { | ||
throw new InvalidArgumentException( | ||
$schedule . ' is not a valid CRON expression' | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* Get the date in which the CRON will run next | ||
* | ||
* @param string $currentTime (optional) Optionally set the current date | ||
* time for testing purposes or disregarding the current second | ||
* | ||
* @return DateTime | ||
*/ | ||
public function getNextRunDate($currentTime = 'now') | ||
{ | ||
$currentDate = $currentTime instanceof DateTime | ||
? $currentTime | ||
: new DateTime($currentTime ?: 'now'); | ||
|
||
$nextRun = clone $currentDate; | ||
$nextRun->setTime($nextRun->format('H'), $nextRun->format('i'), 0); | ||
|
||
// Set a hard limit to bail on an impossible date | ||
for ($i = 0; $i < 10000; $i++) { | ||
|
||
// Adjust the month until it matches. Reset day to 1 and reset time. | ||
if (!$this->unitSatisfiesCron($nextRun, 'm', $this->getExpression(self::MONTH))) { | ||
$nextRun->add(new DateInterval('P1M')); | ||
$nextRun->setDate($nextRun->format('Y'), $nextRun->format('m'), 1); | ||
$nextRun->setTime(0, 0, 0); | ||
continue; | ||
} | ||
|
||
// Adjust the day of the month by incrementing the day until it matches. Reset time. | ||
if (!$this->unitSatisfiesCron($nextRun, 'd', $this->getExpression(self::DAY))) { | ||
$nextRun->add(new DateInterval('P1D')); | ||
$nextRun->setTime(0, 0, 0); | ||
continue; | ||
} | ||
|
||
// Adjust the day of week by incrementing the day until it matches. Resest time. | ||
if (!$this->unitSatisfiesCron($nextRun, 'N', $this->getExpression(self::WEEKDAY))) { | ||
$nextRun->add(new DateInterval('P1D')); | ||
$nextRun->setTime(0, 0, 0); | ||
continue; | ||
} | ||
|
||
// Adjust the hour until it matches the set hour. Set seconds and minutes to 0 | ||
if (!$this->unitSatisfiesCron($nextRun, 'H', $this->getExpression(self::HOUR))) { | ||
$nextRun->add(new DateInterval('PT1H')); | ||
$nextRun->setTime($nextRun->format('H'), 0, 0); | ||
continue; | ||
} | ||
|
||
// Adjust the minutes until it matches a set minute | ||
if (!$this->unitSatisfiesCron($nextRun, 'i', $this->getExpression(self::MINUTE))) { | ||
$nextRun->add(new DateInterval('PT1M')); | ||
continue; | ||
} | ||
|
||
// If the suggested next run time is not after the current time, then keep iterating | ||
if ($currentTime != 'now' && $currentDate > $nextRun) { | ||
$nextRun->add(new DateInterval('PT1M')); | ||
continue; | ||
} | ||
|
||
break; | ||
} | ||
|
||
return $nextRun; | ||
} | ||
|
||
/** | ||
* Get all or part of the CRON expression | ||
* | ||
* @param string $part (optional) 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 | ||
* CRON expression, or NULL if the part was specified but not found | ||
*/ | ||
public function getExpression($part = null) | ||
{ | ||
if (null === $part) { | ||
return implode(' ', $this->cronParts); | ||
} else if (array_key_exists($part, $this->cronParts)) { | ||
return $this->cronParts[$part]; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
/** | ||
* Deterime if the cron is due to run based on the current time. Unless | ||
* a string is passed, this method assumes that the current number of | ||
* seconds are irrelevant, and that this method will be called once per | ||
* minute. | ||
* | ||
* @param string|DateTime $currentTime (optional) Set the current time | ||
* If left NULL, the current time is used, less seconds | ||
* If a DateTime object is passed, the DateTime is used less seconds | ||
* If a string is used, the exact strotime of the string is used | ||
* | ||
* @return bool Returns TRUE if the cron is due to run or FALSE if not | ||
*/ | ||
public function isDue($currentTime = null) | ||
{ | ||
if (null === $currentTime || 'now' === $currentTime) { | ||
$currentDate = date('Y-m-d H:i'); | ||
$currentTime = strtotime($currentDate); | ||
} else if ($currentTime instanceof DateTime) { | ||
$currentDate = $currentTime->format('Y-m-d H:i'); | ||
$currentTime = strtotime($currentDate); | ||
} else { | ||
$currentDate = $currentTime; | ||
$currentTime = strtotime($currentTime); | ||
} | ||
|
||
return $this->getNextRunDate($currentDate)->getTimestamp() == $currentTime; | ||
} | ||
|
||
/** | ||
* Check if a date/time unit value satisfies a crontab unit | ||
* | ||
* @param DateTime $nextRun Current next run date | ||
* @param string $unit Date/time unit type (e.g. Y, m, d, H, i) | ||
* @param string $schedule Cron schedule variable | ||
* | ||
* @return bool Returns TRUE if the unit satisfies the constraint | ||
*/ | ||
protected function unitSatisfiesCron(DateTime $nextRun, $unit, $schedule) | ||
{ | ||
if ($schedule === '*') { | ||
return true; | ||
} | ||
|
||
$unitValue = (int) $nextRun->format($unit); | ||
|
||
// Check increments of ranges | ||
if (strpos($schedule, '*/') !== false) { | ||
list($delimiter, $interval) = explode('*/', $schedule); | ||
return $unitValue % (int) $interval == 0; | ||
} | ||
|
||
// Check intervals | ||
if (strpos($schedule, '-')) { | ||
list($first, $last) = explode('-', $schedule); | ||
return $unitValue >= $first && $unitValue <= $last; | ||
} | ||
|
||
// Check lists of values | ||
if (strpos($schedule, ',')) { | ||
foreach (array_map('trim', explode(',', $schedule)) as $test) { | ||
if ($this->unitSatisfiesCron($nextRun, $unit, $test)) { | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
return $unitValue == (int) $schedule; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
<?php | ||
|
||
namespace Cron\Tests; | ||
|
||
use Cron\Parser; | ||
|
||
/** | ||
* @author Michael Dowling <mtdowling@gmail.com> | ||
*/ | ||
class ParserTest extends \PHPUnit_Framework_TestCase | ||
{ | ||
/** | ||
* @covers Cron\Parser::__construct | ||
* @covers Cron\Parser::getExpression | ||
*/ | ||
public function testParsesCronSchedule() | ||
{ | ||
$cron = new Parser('1 2-4 * 4,5,6 */3', '2010-09-10 12:00:00'); | ||
$this->assertEquals('1', $cron->getExpression(Parser::MINUTE)); | ||
$this->assertEquals('2-4', $cron->getExpression(Parser::HOUR)); | ||
$this->assertEquals('*', $cron->getExpression(Parser::DAY)); | ||
$this->assertEquals('4,5,6', $cron->getExpression(Parser::MONTH)); | ||
$this->assertEquals('*/3', $cron->getExpression(Parser::WEEKDAY)); | ||
$this->assertEquals('1 2-4 * 4,5,6 */3', $cron->getExpression()); | ||
$this->assertNull($cron->getExpression('foo')); | ||
} | ||
|
||
/** | ||
* @covers Cron\Parser::__construct | ||
* @expectedException InvalidArgumentException | ||
*/ | ||
public function testInvalidCronsWillFail() | ||
{ | ||
// Only four values | ||
$cron = new Parser('* * * 1'); | ||
} | ||
|
||
/** | ||
* Data provider for cron schedule | ||
* | ||
* @return array | ||
*/ | ||
public function scheduleProvider() | ||
{ | ||
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:51:00', true), | ||
array('* 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), | ||
// CSV values can be complex | ||
array('* 5,21-22 * * *', '2015-08-10 21:50:00', '2015-08-10 21:50:00', true), | ||
array('7-9 * */9 * *', '2015-08-10 22:02:33', '2015-08-18 00:07:00', false), | ||
// Minutes 12-19, every 3 hours, every 5 days, in June, on Sunday | ||
array('12-19 */3 */5 6 7', '2015-08-10 22:05:51', '2016-06-05 00:12:00', false), | ||
// 15th minute, of the second hour, every 15 days, in January, every Friday | ||
array('15 2 */15 1 */5', '2015-08-10 22:10:19', '2016-01-15 02:15:00', false), | ||
// 15th minute, of the second hour, every 15 days, in January, Tuesday-Friday | ||
array('15 2 */15 1 2-5', '2015-08-10 22:10:19', '2016-01-15 02:15:00', false), | ||
array('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-11 21:47:00', false), | ||
); | ||
} | ||
|
||
/** | ||
* @covers Cron\Parser::isDue | ||
* @covers Cron\Parser::getNextRunDate | ||
* @covers Cron\Parser::unitSatisfiesCron | ||
* @dataProvider scheduleProvider | ||
*/ | ||
public function testDeterminesIfCronIsDue($schedule, $relativeTime, $nextRun, $isDue) | ||
{ | ||
$cron = new Parser($schedule); | ||
if (is_string($relativeTime)) { | ||
$relativeTime = new \DateTime($relativeTime); | ||
} else if (is_int($relativeTime)) { | ||
$relativeTime = date('Y-m-d H:i:s', $relativeTime); | ||
} | ||
$this->assertEquals($isDue, $cron->isDue($relativeTime)); | ||
$this->assertEquals(new \DateTime($nextRun), $cron->getNextRunDate($relativeTime)); | ||
} | ||
|
||
/** | ||
* @covers Cron\Parser::isDue | ||
*/ | ||
public function testIsDueHandlesDifferentDates() | ||
{ | ||
$cron = new Parser('* * * * *'); | ||
$this->assertTrue($cron->isDue()); | ||
$this->assertTrue($cron->isDue('now')); | ||
$this->assertTrue($cron->isDue(new \DateTime('now'))); | ||
$this->assertTrue($cron->isDue(date('Y-m-d H:i'))); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<?php | ||
|
||
namespace Cron\Tests; | ||
|
||
error_reporting(E_ALL | E_STRICT); | ||
|
||
require_once 'PHPUnit/TextUI/TestRunner.php'; | ||
require_once __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'Parser.php'; |