Permalink
Browse files

dev release

  • Loading branch information...
0 parents commit bcaba09229545cbb972e45035357460ace6564d8 heartsentwined committed Sep 19, 2012
3 .gitignore
@@ -0,0 +1,3 @@
+.git
+composer.lock
+vendor/
10 .travis.yml
@@ -0,0 +1,10 @@
+language: php
+php:
+ - "5.3"
+ - "5.4"
+before_script: composer install
+script: phpunit --configuration test/phpunit.xml
+notifications:
+ email:
+ on_success: always
+ on_failure: always
21 README.md
@@ -0,0 +1,21 @@
+# Heartsentwined\CronExprParser
+
+[![Build Status](https://secure.travis-ci.org/heartsentwined/cron-expr-parser.png)](http://travis-ci.org/heartsentwined/cron-expr-parser)
+
+Parse cron expressions and match them against time.
+
+# Installation
+
+[Composer](http://getcomposer.org/):
+
+```json
+{
+ "require": {
+ "heartsentwined/cron-expr-parser": "1.*"
+ }
+}
+```
+
+# Usage
+
+todo
23 composer.json
@@ -0,0 +1,23 @@
+{
+ "name": "heartsentwined/cron-expr-parser",
+ "description": "Parse cron expressions and match them against time.",
+ "license": "GPL-3.0",
+ "keywords": ["cron"],
+ "homepage": "https://github.com/heartsentwined/cron-expr-parser",
+ "authors": [
+ {
+ "name": "heartsentwined",
+ "email": "heartsentwined@cogito-lab.com",
+ "role": "Developer"
+ }
+ ],
+ "autoload": {
+ "psr-0": {
+ "Heartsentwined\\CronExprParser": "src/"
+ }
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "heartsentwined/arg-validator": "1.*"
+ }
+}
10 src/Heartsentwined/CronExprParser/Exception/InvalidArgumentException.php
@@ -0,0 +1,10 @@
+<?php
+namespace Heartsentwined\CronExprParser\Exception;
+
+use Heartsentwined\CronExprParser\ExceptionInterface;
+
+class InvalidArgumentException
+ extends \InvalidArgumentException
+ implements ExceptionInterface
+{
+}
6 src/Heartsentwined/CronExprParser/ExceptionInterface.php
@@ -0,0 +1,6 @@
+<?php
+namespace Heartsentwined\CronExprParser;
+
+interface ExceptionInterface
+{
+}
212 src/Heartsentwined/CronExprParser/Parser.php
@@ -0,0 +1,212 @@
+<?php
+namespace Heartsentwined\CronExprParser;
+
+use Heartsentwined\ArgValidator\ArgValidator;
+use Heartsentwined\CronExprParser\Exception;
+
+class Parser
+{
+ /**
+ * determine whether a given time falls within the given cron expr
+ *
+ * @param string $frequency
+ * any valid cron expression, in addition supporting:
+ * range: '0-5'
+ * range + interval: '10-59/5'
+ * comma-separated combinations of these: '1,4,7,10-20'
+ * English months: 'january'
+ * English months (abbreviated to three letters): 'jan'
+ * English weekdays: 'monday'
+ * English weekdays (abbreviated to three letters): 'mon'
+ * These text counterparts can be used in all places where their
+ * numerical counterparts are allowed, e.g. 'jan-jun/2'
+ * A full example:
+ * '0-5,10-59/5 * 2-10,15-25 january-june/2 mon-fri' -
+ * every minute between minute 0-5 + every 5th min between 10-59
+ * every hour
+ * every day between day 2-10 and day 15-25
+ * every 2nd month between January-June
+ * Monday-Friday
+ * @param string|int $time
+ * timestamp or strtotime()-compatible string
+ * @throws Exception\InvalidArgumentException on invalid cron expression
+ * @return bool
+ */
+ public static function matchTime($time, $expr)
+ {
+ ArgValidator::assert($time, array('string', 'int'));
+ ArgValidator::assert($expr, 'string');
+
+ $cronExpr = preg_split('/\s+/', $expr, null, PREG_SPLIT_NO_EMPTY);
+ if (count($cronExpr) !== 5) {
+ throw new Exception\InvalidArgumentException(sprintf(
+ 'cron expression should have exactly 5 arguments, "%s" given',
+ $expr
+ ));
+ }
+
+ if (is_string($time)) $time = strtotime($time);
+
+ $date = getdate($time);
+
+ return self::matchTimeComponent($cronExpr[0], $date['minutes'])
+ && self::matchTimeComponent($cronExpr[1], $date['hours'])
+ && self::matchTimeComponent($cronExpr[2], $date['mday'])
+ && self::matchTimeComponent($cronExpr[3], $date['mon'])
+ && self::matchTimeComponent($cronExpr[4], $date['wday']);
+ }
+
+ /**
+ * match a cron expression component to a given corresponding date/time
+ *
+ * In the expression, * * * * *, each component
+ * *[1] *[2] *[3] *[4] *[5]
+ * will correspond to a getdate() component
+ * 1. $date['minutes']
+ * 2. $date['hours']
+ * 3. $date['mday']
+ * 4. $date['mon']
+ * 5. $date['wday']
+ *
+ * @see self::exprToNumeric() for additional valid string values
+ *
+ * @param string $expr
+ * @param numeric $num
+ * @throws Exception\InvalidArgumentException on invalid expression
+ * @return bool
+ */
+ public static function matchTimeComponent($expr, $num)
+ {
+ ArgValidator::assert($expr, 'string');
+ ArgValidator::assert($num, 'numeric');
+
+ //handle all match
+ if ($expr === '*') {
+ return true;
+ }
+
+ //handle multiple options
+ if (strpos($expr, ',') !== false) {
+ $args = explode(',', $expr);
+ foreach ($args as $arg) {
+ if (self::matchTimeComponent($arg, $num)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ //handle modulus
+ if (strpos($expr, '/') !== false) {
+ $arg = explode('/', $expr);
+ if (count($arg) !== 2) {
+ throw new Exception\InvalidArgumentException(sprintf(
+ 'invalid cron expression component: '
+ . 'expecting match/modulus, "%s" given',
+ $expr
+ ));
+ }
+ if (!is_numeric($arg[1])) {
+ throw new Exception\InvalidArgumentException(sprintf(
+ 'invalid cron expression component: '
+ . 'expecting numeric modulus, "%s" given',
+ $expr
+ ));
+ }
+
+ $expr = $arg[0];
+ $mod = $arg[1];
+ } else {
+ $mod = 1;
+ }
+
+ //handle all match by modulus
+ if ($expr === '*') {
+ $from = 0;
+ $to = 60;
+ }
+ //handle range
+ elseif (strpos($expr, '-') !== false) {
+ $arg = explode('-', $expr);
+ if (count($arg) !== 2) {
+ throw new Exception\InvalidArgumentException(sprintf(
+ 'invalid cron expression component: '
+ . 'expecting from-to structure, "%s" given',
+ $expr
+ ));
+ }
+ $from = self::exprToNumeric($arg[0]);
+ $to = self::exprToNumeric($arg[1]);
+ }
+ //handle regular token
+ else {
+ $from = self::exprToNumeric($expr);
+ $to = $from;
+ }
+
+ if ($from === false || $to === false) {
+ throw new Exception\InvalidArgumentException(sprintf(
+ 'invalid cron expression component: '
+ . 'expecting numeric or valid string, "%s" given',
+ $expr
+ ));
+ }
+
+ return ($num >= $from) && ($num <= $to) && ($num % $mod === 0);
+ }
+
+ /**
+ * parse a string month / weekday expression to its numeric equivalent
+ *
+ * @param string|numeric $value
+ * accepts, case insensitive,
+ * - Jan - Dec
+ * - Sun - Sat
+ * - (or their long forms - only the first three letters important)
+ * @return int|false
+ */
+ public static function exprToNumeric($value)
+ {
+ ArgValidator::assert($value, array('string', 'numeric'));
+
+ static $data = array(
+ 'jan' => 1,
+ 'feb' => 2,
+ 'mar' => 3,
+ 'apr' => 4,
+ 'may' => 5,
+ 'jun' => 6,
+ 'jul' => 7,
+ 'aug' => 8,
+ 'sep' => 9,
+ 'oct' => 10,
+ 'nov' => 11,
+ 'dec' => 12,
+
+ 'sun' => 0,
+ 'mon' => 1,
+ 'tue' => 2,
+ 'wed' => 3,
+ 'thu' => 4,
+ 'fri' => 5,
+ 'sat' => 6,
+ );
+
+ if (is_numeric($value)) {
+ if (in_array((int)$value, $data, true)) {
+ return $value;
+ } else {
+ return false;
+ }
+ }
+
+ if (is_string($value)) {
+ $value = strtolower(substr($value, 0, 3));
+ if (isset($data[$value])) {
+ return $data[$value];
+ }
+ }
+
+ return false;
+ }
+}
295 test/Heartsentwined/Test/CronExprParser/ParserTest.php
@@ -0,0 +1,295 @@
+<?php
+namespace Heartsentwined\Test\CronExprParser;
+
+use Heartsentwined\CronExprParser\Parser;
+use Heartsentwined\CronExprParser\Exception;
+
+class ParserTest extends \PHPUnit_Framework_TestCase
+{
+ public function testExprToNumeric()
+ {
+ $data = array(
+ 'jan' => 1,
+ 'feb' => 2,
+ 'mar' => 3,
+ 'apr' => 4,
+ 'may' => 5,
+ 'jun' => 6,
+ 'jul' => 7,
+ 'aug' => 8,
+ 'sep' => 9,
+ 'oct' => 10,
+ 'nov' => 11,
+ 'dec' => 12,
+
+ 'sun' => 0,
+ 'mon' => 1,
+ 'tue' => 2,
+ 'wed' => 3,
+ 'thu' => 4,
+ 'fri' => 5,
+ 'sat' => 6,
+ );
+ foreach ($data as $str => $num) {
+ $this->assertSame($num,
+ Parser::exprToNumeric($str));
+ }
+
+ // variants of case
+ $this->assertSame(1,
+ Parser::exprToNumeric('jan'));
+ $this->assertSame(1,
+ Parser::exprToNumeric('JAN'));
+ $this->assertSame(1,
+ Parser::exprToNumeric('Jan'));
+ $this->assertSame(1,
+ Parser::exprToNumeric('jAn'));
+ $this->assertSame(1,
+ Parser::exprToNumeric('jaN'));
+
+ // long forms
+ $this->assertSame(1,
+ Parser::exprToNumeric('january'));
+ $this->assertSame(1,
+ Parser::exprToNumeric('janrubbish'));
+
+ // invalid string input
+ $this->assertSame(false,
+ Parser::exprToNumeric(''));
+ $this->assertSame(false,
+ Parser::exprToNumeric('rubbish'));
+
+ // straight numeric input
+ $this->assertSame(1,
+ Parser::exprToNumeric(1));
+ // invalid numeric input
+ $this->assertSame(false,
+ Parser::exprToNumeric(13));
+ }
+
+ /**
+ * @depends testExprToNumeric
+ */
+ public function testMatchTimeComponent()
+ {
+ // anything
+ $this->assertTrue(
+ Parser::matchTimeComponent('*', 0));
+ $this->assertTrue(
+ Parser::matchTimeComponent('*', 1));
+
+ // comma-separated
+ $this->assertFalse(
+ Parser::matchTimeComponent('1,2,3', 0));
+ $this->assertTrue(
+ Parser::matchTimeComponent('1,2,3', 1));
+ $this->assertTrue(
+ Parser::matchTimeComponent('1,2,3', 2));
+ $this->assertTrue(
+ Parser::matchTimeComponent('1,2,3', 3));
+ $this->assertFalse(
+ Parser::matchTimeComponent('1,2,3', 4));
+
+ // modulus
+ $this->assertFalse(
+ Parser::matchTimeComponent('*/5', 1));
+ $this->assertTrue(
+ Parser::matchTimeComponent('*/5', 0));
+ $this->assertTrue(
+ Parser::matchTimeComponent('*/5', 5));
+
+ // range
+ $this->assertFalse(
+ Parser::matchTimeComponent('1-3', 0));
+ $this->assertTrue(
+ Parser::matchTimeComponent('1-3', 1));
+ $this->assertTrue(
+ Parser::matchTimeComponent('1-3', 2));
+ $this->assertTrue(
+ Parser::matchTimeComponent('1-3', 3));
+ $this->assertFalse(
+ Parser::matchTimeComponent('1-3', 4));
+
+ // string
+ $this->assertTrue(
+ Parser::matchTimeComponent('mar', 3));
+ $this->assertFalse(
+ Parser::matchTimeComponent('mar', 4));
+
+ // combinations
+
+ $this->assertTrue(
+ Parser::matchTimeComponent('0,2,4/2', 0));
+ $this->assertTrue(
+ Parser::matchTimeComponent('0,2,4/2', 2));
+ $this->assertTrue(
+ Parser::matchTimeComponent('0,2,4/2', 4));
+ $this->assertFalse(
+ Parser::matchTimeComponent('0,2,4/2', 1));
+ $this->assertFalse(
+ Parser::matchTimeComponent('0,2,4/2', 3));
+ $this->assertFalse(
+ Parser::matchTimeComponent('0,2,4/2', 5));
+
+ $this->assertTrue(
+ Parser::matchTimeComponent('0-4/2', 0));
+ $this->assertTrue(
+ Parser::matchTimeComponent('0-4/2', 2));
+ $this->assertTrue(
+ Parser::matchTimeComponent('0-4/2', 4));
+ $this->assertFalse(
+ Parser::matchTimeComponent('0-4/2', 1));
+ $this->assertFalse(
+ Parser::matchTimeComponent('0-4/2', 3));
+ $this->assertFalse(
+ Parser::matchTimeComponent('0-4/2', 5));
+
+ $this->assertTrue(
+ Parser::matchTimeComponent('sun,tue,thu/2', 0));
+ $this->assertTrue(
+ Parser::matchTimeComponent('sun,tue,thu/2', 2));
+ $this->assertTrue(
+ Parser::matchTimeComponent('sun,tue,thu/2', 4));
+ $this->assertFalse(
+ Parser::matchTimeComponent('sun,tue,thu/2', 1));
+ $this->assertFalse(
+ Parser::matchTimeComponent('sun,tue,thu/2', 3));
+ $this->assertFalse(
+ Parser::matchTimeComponent('sun,tue,thu/2', 5));
+
+ $this->assertTrue(
+ Parser::matchTimeComponent('sun-thu/2', 0));
+ $this->assertTrue(
+ Parser::matchTimeComponent('sun-thu/2', 2));
+ $this->assertTrue(
+ Parser::matchTimeComponent('sun-thu/2', 4));
+ $this->assertFalse(
+ Parser::matchTimeComponent('sun-thu/2', 1));
+ $this->assertFalse(
+ Parser::matchTimeComponent('sun-thu/2', 3));
+ $this->assertFalse(
+ Parser::matchTimeComponent('sun-thu/2', 5));
+
+ $this->assertTrue(
+ Parser::matchTimeComponent('0-thu/2', 0));
+ $this->assertTrue(
+ Parser::matchTimeComponent('0-thu/2', 2));
+ $this->assertTrue(
+ Parser::matchTimeComponent('0-thu/2', 4));
+ $this->assertFalse(
+ Parser::matchTimeComponent('0-thu/2', 1));
+ $this->assertFalse(
+ Parser::matchTimeComponent('0-thu/2', 3));
+ $this->assertFalse(
+ Parser::matchTimeComponent('0-thu/2', 5));
+
+ $this->assertTrue(
+ Parser::matchTimeComponent('sun-4/2', 0));
+ $this->assertTrue(
+ Parser::matchTimeComponent('sun-4/2', 2));
+ $this->assertTrue(
+ Parser::matchTimeComponent('sun-4/2', 4));
+ $this->assertFalse(
+ Parser::matchTimeComponent('sun-4/2', 1));
+ $this->assertFalse(
+ Parser::matchTimeComponent('sun-4/2', 3));
+ $this->assertFalse(
+ Parser::matchTimeComponent('sun-4/2', 5));
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testExprModulusEmpty()
+ {
+ Parser::matchTimeComponent('/', 2);
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testExprModulusNoDividend()
+ {
+ Parser::matchTimeComponent('/2', 2);
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testExprModulusNoDivisor()
+ {
+ Parser::matchTimeComponent('2/', 2);
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testExprModulusTooManyArgs()
+ {
+ Parser::matchTimeComponent('2/3/4', 2);
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testExprModulusStringDivisor()
+ {
+ Parser::matchTimeComponent('2/foo', 2);
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testExprRangeEmpty()
+ {
+ Parser::matchTimeComponent('-', 2);
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testExprRangeNoFrom()
+ {
+ Parser::matchTimeComponent('-2', 2);
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testExprRangeNoTo()
+ {
+ Parser::matchTimeComponent('2-', 2);
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testExprRangeTooManyArgs()
+ {
+ Parser::matchTimeComponent('2-3-4', 2);
+ }
+
+ public function testMatchTime()
+ {
+ $this->assertTrue(
+ Parser::matchTime(time(), '* * * * *'));
+ $this->assertTrue(
+ Parser::matchTime('now', '* * * * *'));
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testMatchTimeTooFewArgs()
+ {
+ Parser::matchTime('now', '* * * *');
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testMatchTimeTooManyArgs()
+ {
+ Parser::matchTime('now', '* * * * * *');
+ }
+}
3 test/bootstrap.php
@@ -0,0 +1,3 @@
+<?php
+$loader = require_once __DIR__ . '/../vendor/autoload.php';
+$loader->add('Heartsentwined\Test', __DIR__);
7 test/phpunit.xml
@@ -0,0 +1,7 @@
+<phpunit bootstrap="./bootstrap.php" colors="true">
+ <testsuites>
+ <testsuite>
+ <directory>./</directory>
+ </testsuite>
+ </testsuites>
+</phpunit>

0 comments on commit bcaba09

Please sign in to comment.