├── VERSION
├── .github
└── FUNDING.yml
├── .gitignore
├── .travis.yml
├── .editorconfig
├── phpunit.xml.dist
├── composer.json
├── LICENSE
├── src
├── Normalizer.php
├── ReferenceTime.php
├── SegmentChecker.php
├── Expression.php
└── Validator.php
├── README.md
├── CHANGELOG.md
└── tests
└── ExpressionTest.php
/VERSION:
--------------------------------------------------------------------------------
1 | 1.1.3
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: adhocore
2 | custom: ['https://paypal.me/ji10']
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # standards
2 | /.cache/
3 | /.env
4 | /.idea/
5 | /vendor/
6 | composer.lock
7 | coverage.xml
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 7.0
5 | - 7.1
6 | - 7.2
7 | - 7.3
8 | - 7.4
9 |
10 | install:
11 | - composer install --prefer-dist
12 |
13 | before_script:
14 | - for P in src tests; do find $P -type f -name '*.php' -exec php -l {} \;; done
15 |
16 | script:
17 | - composer test:cov
18 |
19 | after_success:
20 | - bash <(curl -s https://codecov.io/bash)
21 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | ; http://editorconfig.org
2 | ;
3 | ; Sublime: https://github.com/sindresorhus/editorconfig-sublime
4 | ; Phpstorm: https://plugins.jetbrains.com/plugin/7294-editorconfig
5 |
6 | root = true
7 |
8 | [*]
9 | indent_style = space
10 | indent_size = 2
11 | end_of_line = lf
12 | charset = utf-8
13 | trim_trailing_whitespace = true
14 | insert_final_newline = true
15 |
16 | [{*.md,*.php,composer.json,composer.lock}]
17 | indent_size = 4
18 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
15 | ./tests/
16 |
17 |
18 |
19 |
20 | ./src
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "adhocore/cron-expr",
3 | "description": "Ultra lightweight Cron Expression parser for PHP",
4 | "type": "library",
5 | "keywords": [
6 | "cron", "cron-expression", "cron-parser", "cron-expr"
7 | ],
8 | "license": "MIT",
9 | "authors": [
10 | {
11 | "name": "Jitendra Adhikari",
12 | "email": "jiten.adhikary@gmail.com"
13 | }
14 | ],
15 | "autoload": {
16 | "psr-4": {
17 | "Ahc\\Cron\\": "src/"
18 | }
19 | },
20 | "autoload-dev": {
21 | "psr-4": {
22 | "Ahc\\Cron\\Test\\": "tests/"
23 | }
24 | },
25 | "require": {
26 | "php": ">=7.0"
27 | },
28 | "require-dev": {
29 | "phpunit/phpunit": "^6.5 || ^7.5"
30 | },
31 | "scripts": {
32 | "test": "phpunit",
33 | "test:cov": "phpunit --coverage-text --coverage-clover coverage.xml --coverage-html vendor/cov"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Jitendra Adhikari
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/Normalizer.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | *
11 | * Licensed under MIT license.
12 | */
13 |
14 | namespace Ahc\Cron;
15 |
16 | class Normalizer
17 | {
18 | const YEARLY = '@yearly';
19 | const ANNUALLY = '@annually';
20 | const MONTHLY = '@monthly';
21 | const WEEKLY = '@weekly';
22 | const DAILY = '@daily';
23 | const HOURLY = '@hourly';
24 | const ALWAYS = '@always';
25 | const FIVE_MIN = '@5minutes';
26 | const TEN_MIN = '@10minutes';
27 | const FIFTEEN_MIN = '@15minutes';
28 | const THIRTY_MIN = '@30minutes';
29 |
30 | protected static $expressions = [
31 | self::YEARLY => '0 0 1 1 *',
32 | self::ANNUALLY => '0 0 1 1 *',
33 | self::MONTHLY => '0 0 1 * *',
34 | self::WEEKLY => '0 0 * * 0',
35 | self::DAILY => '0 0 * * *',
36 | self::HOURLY => '0 * * * *',
37 | self::ALWAYS => '* * * * *',
38 | self::FIVE_MIN => '*/5 * * * *',
39 | self::TEN_MIN => '*/10 * * * *',
40 | self::FIFTEEN_MIN => '*/15 * * * *',
41 | self::THIRTY_MIN => '0,30 * * * *',
42 | ];
43 |
44 | protected static $literals = [
45 | 'sun' => 0,
46 | 'mon' => 1,
47 | 'tue' => 2,
48 | 'wed' => 3,
49 | 'thu' => 4,
50 | 'fri' => 5,
51 | 'sat' => 6,
52 |
53 | 'jan' => 1,
54 | 'feb' => 2,
55 | 'mar' => 3,
56 | 'apr' => 4,
57 | 'may' => 5,
58 | 'jun' => 6,
59 | 'jul' => 7,
60 | 'aug' => 8,
61 | 'sep' => 9,
62 | 'oct' => 10,
63 | 'nov' => 11,
64 | 'dec' => 12,
65 | ];
66 |
67 | public function normalizeExpr(string $expr): string
68 | {
69 | $expr = \trim($expr);
70 |
71 | if (isset(static::$expressions[$exp = \strtolower($expr)])) {
72 | return static::$expressions[$exp];
73 | }
74 |
75 | $expr = \preg_replace('~\s+~', ' ', $expr);
76 | $count = \substr_count($expr, ' ');
77 |
78 | if ($count < 4 || $count > 5) {
79 | throw new \UnexpectedValueException(
80 | 'Cron $expr should have 5 or 6 segments delimited by space'
81 | );
82 | }
83 |
84 | return \str_ireplace(\array_keys(static::$literals), \array_values(static::$literals), $expr);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/ReferenceTime.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | *
11 | * Licensed under MIT license.
12 | */
13 |
14 | namespace Ahc\Cron;
15 |
16 | /**
17 | * @method int minute()
18 | * @method int hour()
19 | * @method int monthDay()
20 | * @method int month()
21 | * @method int weekDay() 0 based day of week.
22 | * @method int year()
23 | * @method int day()
24 | * @method int weekDay1() 1 based day of week.
25 | * @method int numDays() Number of days in the month.
26 | */
27 | class ReferenceTime
28 | {
29 | // The cron parts. (Donot change it)
30 | const MINUTE = 0;
31 | const HOUR = 1;
32 | const MONTHDAY = 2;
33 | const MONTH = 3;
34 | const WEEKDAY = 4;
35 | const YEAR = 5;
36 |
37 | // Meta data parts.
38 | const DAY = 6;
39 | const WEEKDAY1 = 7;
40 | const NUMDAYS = 8;
41 |
42 | /** @var array The data */
43 | protected $values = [];
44 |
45 | /** @var array The Magic methods */
46 | protected $methods = [];
47 |
48 | public function __construct($time)
49 | {
50 | $timestamp = $this->normalizeTime($time);
51 |
52 | $this->values = $this->parse($timestamp);
53 | $this->methods = (new \ReflectionClass($this))->getConstants();
54 | }
55 |
56 | public function __call(string $method, array $args): int
57 | {
58 | $method = \preg_replace('/^GET/', '', \strtoupper($method));
59 | if (isset($this->methods[$method])) {
60 | return $this->values[$this->methods[$method]];
61 | }
62 |
63 | // @codeCoverageIgnoreStart
64 | throw new \BadMethodCallException("Method '$method' doesnot exist in ReferenceTime.");
65 | // @codeCoverageIgnoreEnd
66 | }
67 |
68 | public function get(int $segment): int
69 | {
70 | return $this->values[$segment];
71 | }
72 |
73 | public function isAt($value, int $segment): bool
74 | {
75 | return $this->values[$segment] == $value;
76 | }
77 |
78 | protected function normalizeTime($time): int
79 | {
80 | if (empty($time)) {
81 | $time = \time();
82 | } elseif (\is_string($time)) {
83 | $time = \strtotime($time);
84 | } elseif ($time instanceof \DateTime) {
85 | $time = $time->getTimestamp();
86 | }
87 |
88 | return $time;
89 | }
90 |
91 | protected function parse(int $timestamp): array
92 | {
93 | $parts = \date('i G j n w Y d N t', $timestamp);
94 | $parts = \explode(' ', $parts);
95 | $parts = \array_map('intval', $parts);
96 |
97 | return $parts;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/SegmentChecker.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | *
11 | * Licensed under MIT license.
12 | */
13 |
14 | namespace Ahc\Cron;
15 |
16 | /**
17 | * Cron Expression segment checker.
18 | *
19 | * This class checks if a cron segment satisfies given time.
20 | *
21 | * @author Jitendra Adhikari
22 | */
23 | class SegmentChecker
24 | {
25 | /** @var ReferenceTime */
26 | protected $reference;
27 |
28 | /** @var Validator */
29 | protected $validator;
30 |
31 | public function __construct(Validator $validator = null)
32 | {
33 | $this->validator = $validator ?: new Validator;
34 | }
35 |
36 | public function setReference(ReferenceTime $reference)
37 | {
38 | $this->reference = $reference;
39 | }
40 |
41 | /**
42 | * Checks if a cron segment satisfies given time.
43 | *
44 | * @param string $segment
45 | * @param int $pos
46 | *
47 | * @return bool
48 | */
49 | public function checkDue(string $segment, int $pos): bool
50 | {
51 | $offsets = \explode(',', \trim($segment));
52 |
53 | foreach ($offsets as $offset) {
54 | if ($this->isOffsetDue($offset, $pos)) {
55 | return true;
56 | }
57 | }
58 |
59 | return false;
60 | }
61 |
62 | /**
63 | * Check if a given offset at a position is due with respect to given time.
64 | *
65 | * @param string $offset
66 | * @param int $pos
67 | *
68 | * @return bool
69 | */
70 | protected function isOffsetDue(string $offset, int $pos): bool
71 | {
72 | if (\strpos($offset, '/') !== false) {
73 | return $this->validator->inStep($this->reference->get($pos), $offset);
74 | }
75 |
76 | if (\strpos($offset, '-') !== false) {
77 | return $this->validator->inRange($this->reference->get($pos), $offset);
78 | }
79 |
80 | if (\is_numeric($offset)) {
81 | return $this->reference->isAt($offset, $pos);
82 | }
83 |
84 | return $this->checkModifier($offset, $pos);
85 | }
86 |
87 | protected function checkModifier(string $offset, int $pos): bool
88 | {
89 | $isModifier = \strpbrk($offset, 'LCW#');
90 |
91 | if ($pos === ReferenceTime::MONTHDAY && $isModifier) {
92 | return $this->validator->isValidMonthDay($offset, $this->reference);
93 | }
94 |
95 | if ($pos === ReferenceTime::WEEKDAY && $isModifier) {
96 | return $this->validator->isValidWeekDay($offset, $this->reference);
97 | }
98 |
99 | $this->validator->unexpectedValue($pos, $offset);
100 | // @codeCoverageIgnoreStart
101 | }
102 | // @codeCoverageIgnoreEnd
103 | }
104 |
--------------------------------------------------------------------------------
/src/Expression.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | *
11 | * Licensed under MIT license.
12 | */
13 |
14 | namespace Ahc\Cron;
15 |
16 | /**
17 | * Cron Expression Parser.
18 | *
19 | * This class checks if a cron expression is due to run on given timestamp (or default now).
20 | * Acknowledgement: The initial idea came from {@link http://stackoverflow.com/a/5727346}.
21 | *
22 | * @author Jitendra Adhikari
23 | */
24 | class Expression
25 | {
26 | /** @var Expression */
27 | protected static $instance;
28 |
29 | /** @var SegmentChecker */
30 | protected $checker;
31 |
32 | /** @var Normalizer */
33 | protected $normalizer;
34 |
35 | public function __construct(SegmentChecker $checker = null, Normalizer $normalizer = null)
36 | {
37 | $this->checker = $checker ?: new SegmentChecker;
38 | $this->normalizer = $normalizer ?: new Normalizer;
39 |
40 | if (null === static::$instance) {
41 | static::$instance = $this;
42 | }
43 | }
44 |
45 | public static function instance(): self
46 | {
47 | if (null === static::$instance) {
48 | static::$instance = new static;
49 | }
50 |
51 | return static::$instance;
52 | }
53 |
54 | /**
55 | * Parse cron expression to decide if it can be run on given time (or default now).
56 | *
57 | * @param string $expr The cron expression.
58 | * @param mixed $time The timestamp to validate the cron expr against. Defaults to now.
59 | *
60 | * @return bool
61 | */
62 | public static function isDue(string $expr, $time = null): bool
63 | {
64 | return static::instance()->isCronDue($expr, $time);
65 | }
66 |
67 | /**
68 | * Filter only the jobs that are due.
69 | *
70 | * @param array $jobs Jobs with cron exprs. [job1 => cron-expr1, job2 => cron-expr2, ...]
71 | * @param mixed $time The timestamp to validate the cron expr against. Defaults to now.
72 | *
73 | * @return array Due job names: [job1name, ...];
74 | */
75 | public static function getDues(array $jobs, $time = null): array
76 | {
77 | return static::instance()->filter($jobs, $time);
78 | }
79 |
80 | /**
81 | * Instance call.
82 | *
83 | * Parse cron expression to decide if it can be run on given time (or default now).
84 | *
85 | * @param string $expr The cron expression.
86 | * @param mixed $time The timestamp to validate the cron expr against. Defaults to now.
87 | *
88 | * @return bool
89 | */
90 | public function isCronDue(string $expr, $time = null): bool
91 | {
92 | $this->checker->setReference(new ReferenceTime($time));
93 |
94 | foreach (\explode(' ', $this->normalizer->normalizeExpr($expr)) as $pos => $segment) {
95 | if ($segment === '*' || $segment === '?') {
96 | continue;
97 | }
98 |
99 | if (!$this->checker->checkDue($segment, $pos)) {
100 | return false;
101 | }
102 | }
103 |
104 | return true;
105 | }
106 |
107 | /**
108 | * Filter only the jobs that are due.
109 | *
110 | * @param array $jobs Jobs with cron exprs. [job1 => cron-expr1, job2 => cron-expr2, ...]
111 | * @param mixed $time The timestamp to validate the cron expr against. Defaults to now.
112 | *
113 | * @return array Due job names: [job1name, ...];
114 | */
115 | public function filter(array $jobs, $time = null): array
116 | {
117 | $dues = $cache = [];
118 |
119 | foreach ($jobs as $name => $expr) {
120 | $expr = $this->normalizer->normalizeExpr($expr);
121 |
122 | if (!isset($cache[$expr])) {
123 | $cache[$expr] = $this->isCronDue($expr, $time);
124 | }
125 |
126 | if ($cache[$expr]) {
127 | $dues[] = $name;
128 | }
129 | }
130 |
131 | return $dues;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## adhocore/cron-expr
2 |
3 | [](https://github.com/adhocore/php-cron-expr/releases)
4 | [](https://travis-ci.org/adhocore/php-cron-expr?branch=master)
5 | [](https://scrutinizer-ci.com/g/adhocore/php-cron-expr/?branch=master)
6 | [](https://codecov.io/gh/adhocore/php-cron-expr)
7 | [](https://styleci.io/repos/94228363)
8 | [](LICENSE)
9 | [](https://twitter.com/intent/tweet?text=Lightweight+and+fast+cron+expression+parser+for+PHP&url=https://github.com/adhocore/php-cron-expr&hashtags=php,cron,cronparser,parser,cronexpr)
10 | [](https://github.com/sponsors/adhocore)
11 |
14 |
15 |
16 | - Lightweight Cron expression parser library for PHP.
17 | - Zero dependency.
18 | - Very **fast** because it bails early in case a segment doesnt match.
19 | - Real [benchmark](https://github.com/adhocore/php-cron-bench) shows it is about 7.54x to 12.92x faster than `dragonmantank/cron-expression`
20 |
21 | ## Installation
22 |
23 | ```bash
24 | composer require adhocore/cron-expr
25 |
26 | # PHP5.6 or lower
27 | composer require adhocore/cron-expr:0.1.0
28 | ```
29 |
30 | ## Usage
31 |
32 | **Basic**
33 |
34 | ```php
35 | use Ahc\Cron\Expression;
36 | use Ahc\Cron\Normalizer;
37 |
38 | Expression::isDue('@always');
39 | Expression::isDue(Normalizer::HOURLY, '2015-01-01 00:00:00');
40 | Expression::isDue('*/20 * * * *', new DateTime);
41 | Expression::isDue('5-34/4 * * * *', time());
42 |
43 | // Dont like static calls? Below is possible too!
44 | $expr = new Expression;
45 | $expr->isCronDue('*/1 * * * *', time());
46 | ```
47 |
48 | **Bulk checks**
49 |
50 | When checking for several jobs at once, if more than one of the jobs share equivalent expression
51 | then the evaluation is done only once per go thus greatly improving performnce.
52 |
53 | ```php
54 | use Ahc\Cron\Expression;
55 |
56 | $jobs = [
57 | 'job1' => '*/2 */2 * * *',
58 | 'job1' => '* 20,21,22 * * *',
59 | 'job3' => '7-9 * */9 * *',
60 | 'job4' => '*/5 * * * *',
61 | 'job5' => '@5minutes', // equivalent to job4 (so it is due if job4 is due)
62 | 'job6' => '7-9 * */9 * *', // exact same as job3 (so it is due if job3 is due)
63 | ];
64 |
65 | // The second param $time can be used same as above: null/time()/date string/DateTime
66 | $dues = Expression::getDues($jobs, '2015-08-10 21:50:00');
67 | // ['job1', 'job4', 'job5']
68 |
69 | // Dont like static calls? Below is possible too!
70 | $expr = new Expression;
71 | $dues = $expr->filter($jobs, time());
72 | ```
73 |
74 | ### Cron Expression
75 |
76 | Cron expression normally consists of 5 segments viz:
77 | ```
78 |
79 | ```
80 | and sometimes there can be 6th segment for `` at the end.
81 |
82 | ### Real Abbreviations
83 |
84 | You can use real abbreviations for month and week days. eg: `JAN`, `dec`, `fri`, `SUN`
85 |
86 | ### Tags
87 |
88 | Following tags are available and they are converted to real cron expressions before parsing:
89 |
90 | - *@yearly* or *@annually* - every year
91 | - *@monthly* - every month
92 | - *@daily* - every day
93 | - *@weekly* - every week
94 | - *@hourly* - every hour
95 | - *@5minutes* - every 5 minutes
96 | - *@10minutes* - every 10 minutes
97 | - *@15minutes* - every 15 minutes
98 | - *@30minutes* - every 30 minutes
99 | - *@always* - every minute
100 |
101 | > You can refer them with constants from `Ahc\Cron\Normalizer` like `Ahc\Cron\Normalizer::WEEKLY`
102 |
103 | ### Modifiers
104 |
105 | Following modifiers supported
106 |
107 | - *Day of Month / 3rd segment:*
108 | - `L` stands for last day of month (eg: `L` could mean 29th for February in leap year)
109 | - `W` stands for closest week day (eg: `10W` is closest week days (MON-FRI) to 10th date)
110 | - *Day of Week / 5th segment:*
111 | - `L` stands for last weekday of month (eg: `2L` is last monday)
112 | - `#` stands for nth day of week in the month (eg: `1#2` is second sunday)
113 |
114 | ## LICENSE
115 |
116 | > © [MIT](./LICENSE) | 2017-2019, Jitendra Adhikari
117 |
118 | ## Credits
119 |
120 | This project is release managed by [please](https://github.com/adhocore/please).
121 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [1.1.3](https://github.com/adhocore/php-cron-expr/releases/tag/1.1.3) (2021-10-20)
2 |
3 | ### Bug Fixes
4 | - **Validator**: Strict checks for in-step, handle weekends correctly (Jitendra Adhikari) [_b894e4e_](https://github.com/adhocore/php-cron-expr/commit/b894e4e)
5 | - **Normalizer**: Expr maybe lowercased (Jitendra Adhikari) [_e6c11df_](https://github.com/adhocore/php-cron-expr/commit/e6c11df)
6 |
7 |
8 | ## [1.1.2](https://github.com/adhocore/php-cron-expr/releases/tag/1.1.2) (2021-02-20)
9 |
10 | ### Features
11 | - Add expression normalizer (Jitendra Adhikari) [_afb57c4_](https://github.com/adhocore/php-cron-expr/commit/afb57c4)
12 |
13 | ### Internal Refactors
14 | - Use Normalizer instead (Jitendra Adhikari) [_42700ab_](https://github.com/adhocore/php-cron-expr/commit/42700ab)
15 |
16 | ### Documentations
17 | - Tags as constants (Jitendra Adhikari) [_1382d7d_](https://github.com/adhocore/php-cron-expr/commit/1382d7d)
18 |
19 |
20 | ## [1.1.1](https://github.com/adhocore/php-cron-expr/releases/tag/1.1.1) (2020-01-09)
21 |
22 | ### Miscellaneous
23 | - **Validator**: Cleanup redundancy (Jitendra Adhikari) [_00983d4_](https://github.com/adhocore/php-cron-expr/commit/00983d4)
24 | - **Travis**: Script (Jitendra Adhikari) [_fec5332_](https://github.com/adhocore/php-cron-expr/commit/fec5332)
25 | - **Composer**: Tweak script.test (Jitendra Adhikari) [_0a5b4fb_](https://github.com/adhocore/php-cron-expr/commit/0a5b4fb)
26 |
27 | ### Documentations
28 | - Update benchmark (Jitendra Adhikari) [_fe9dffd_](https://github.com/adhocore/php-cron-expr/commit/fe9dffd)
29 |
30 |
31 | ## [1.1.0](https://github.com/adhocore/php-cron-expr/releases/tag/1.1.0) (2019-12-27)
32 |
33 | ### Features
34 | - Add ref time class (Jitendra Adhikari) [_2ad504b_](https://github.com/adhocore/php-cron-expr/commit/2ad504b)
35 |
36 | ### Bug Fixes
37 | - **Expr**: Replace literals case insensitive (Jitendra Adhikari) [_a5c179f_](https://github.com/adhocore/php-cron-expr/commit/a5c179f)
38 |
39 | ### Internal Refactors
40 | - **Validator**: Use reference time class (Jitendra Adhikari) [_05f139d_](https://github.com/adhocore/php-cron-expr/commit/05f139d)
41 | - **Checker**: Use reference time class (Jitendra Adhikari) [_7b4138f_](https://github.com/adhocore/php-cron-expr/commit/7b4138f)
42 | - **Expr**: Use reference time class, cleanup process() (Jitendra Adhikari) [_1ce873d_](https://github.com/adhocore/php-cron-expr/commit/1ce873d)
43 |
44 | ### Miscellaneous
45 | - **Reftime**: Add method annot for magic calls (Jitendra Adhikari) [_96b78d1_](https://github.com/adhocore/php-cron-expr/commit/96b78d1)
46 | - Ignore coverage xml (Jitendra Adhikari) [_2a9505a_](https://github.com/adhocore/php-cron-expr/commit/2a9505a)
47 | - **Composer**: Bump deps version, fix test:cov (Jitendra Adhikari) [_14e9117_](https://github.com/adhocore/php-cron-expr/commit/14e9117)
48 |
49 | ### Documentations
50 | - About cron (Jitendra Adhikari) [_a3760f8_](https://github.com/adhocore/php-cron-expr/commit/a3760f8)
51 |
52 |
53 | ## [1.0.0](https://github.com/adhocore/php-cron-expr/releases/tag/1.0.0) (2019-12-22)
54 |
55 | ### Internal Refactors
56 | - Strict php7 typehints (Jitendra Adhikari) [_e0967be_](https://github.com/adhocore/php-cron-expr/commit/e0967be)
57 |
58 | ### Miscellaneous
59 | - Add composer script (Jitendra Adhikari) [_594e7e1_](https://github.com/adhocore/php-cron-expr/commit/594e7e1)
60 |
61 | ### Documentations
62 | - Update for v1.0 (Jitendra Adhikari) [_7224037_](https://github.com/adhocore/php-cron-expr/commit/7224037)
63 |
64 | ### Builds
65 | - **Travis**: Php7 only (Jitendra Adhikari) [_b0170db_](https://github.com/adhocore/php-cron-expr/commit/b0170db)
66 |
67 |
68 | ## [v0.1.0](https://github.com/adhocore/php-cron-expr/releases/tag/v0.1.0) (2019-09-22)
69 |
70 | ### Documentations
71 | - About stability (Jitendra Adhikari) [_1672edc_](https://github.com/adhocore/php-cron-expr/commit/1672edc)
72 | - Add php support info (Jitendra Adhikari) [_9d21717_](https://github.com/adhocore/php-cron-expr/commit/9d21717)
73 |
74 |
75 | ## [v0.0.7](https://github.com/adhocore/php-cron-expr/releases/tag/v0.0.7) (2019-08-12)
76 |
77 | ### Internal Refactors
78 | - **Expr**: Normalize expr, use regex split instead (Jitendra Adhikari) [_74f8dfc_](https://github.com/adhocore/php-cron-expr/commit/74f8dfc)
79 |
80 |
81 | ## [v0.0.6] 2018-08-16 00:08:45 UTC
82 |
83 | - [d933099](https://github.com/adhocore/php-cron-expr/commit/d933099) fix(expr): static ::instance()
84 | - [48eef4a](https://github.com/adhocore/php-cron-expr/commit/48eef4a) docs: bulk checks/filters
85 | - [8c23489](https://github.com/adhocore/php-cron-expr/commit/8c23489) test(expr): ...
86 | - [aab941e](https://github.com/adhocore/php-cron-expr/commit/aab941e) refactor(expr): simplify filter(), also cache undue case
87 | - [cd3cd33](https://github.com/adhocore/php-cron-expr/commit/cd3cd33) test(expr): filter(), getDues()
88 | - [7ec07c8](https://github.com/adhocore/php-cron-expr/commit/7ec07c8) feat(checker): make validator injectable
89 | - [0d6d85a](https://github.com/adhocore/php-cron-expr/commit/0d6d85a) feat(expr): add construct with injection/initiliazation
90 | - [b8aa4ce](https://github.com/adhocore/php-cron-expr/commit/b8aa4ce) feat(expr): add ::instance() api
91 | - [cee9bf7](https://github.com/adhocore/php-cron-expr/commit/cee9bf7) feat(expr): add filter(), ::getDues() and normalizeExpr()
92 | - [d73a078](https://github.com/adhocore/php-cron-expr/commit/d73a078) refactor(docblocks): document public apis/methods
93 | - [ee2cc96](https://github.com/adhocore/php-cron-expr/commit/ee2cc96) refactor: rename array of time parts to times
94 |
95 | ## [v0.0.5] 2018-08-15 10:08:16 UTC
96 |
97 | - [6fccf54](https://github.com/adhocore/php-cron-expr/commit/6fccf54) test(validator): instep changes
98 | - [93d2f7f](https://github.com/adhocore/php-cron-expr/commit/93d2f7f) fix(validator): inStep when start+step>end | closes #12
99 | - [895eade](https://github.com/adhocore/php-cron-expr/commit/895eade) docs: update readme
100 | - [bc65912](https://github.com/adhocore/php-cron-expr/commit/bc65912) docs: fix badge
101 |
--------------------------------------------------------------------------------
/src/Validator.php:
--------------------------------------------------------------------------------
1 |
9 | *
10 | *
11 | * Licensed under MIT license.
12 | */
13 |
14 | namespace Ahc\Cron;
15 |
16 | /**
17 | * Cron segment validator.
18 | *
19 | * This class checks if a cron segment is valid.
20 | *
21 | * @author Jitendra Adhikari
22 | */
23 | class Validator
24 | {
25 | /**
26 | * Check if the value is in range of given offset.
27 | *
28 | * @param int $value
29 | * @param string $offset
30 | *
31 | * @return bool
32 | */
33 | public function inRange(int $value, string $offset): bool
34 | {
35 | $parts = \explode('-', $offset);
36 |
37 | return $parts[0] <= $value && $value <= $parts[1];
38 | }
39 |
40 | /**
41 | * Check if the value is in step of given offset.
42 | *
43 | * @param int $value
44 | * @param string $offset
45 | *
46 | * @return bool
47 | */
48 | public function inStep(int $value, string $offset): bool
49 | {
50 | $parts = \explode('/', $offset, 2);
51 |
52 | if (empty($parts[1])) {
53 | return false;
54 | }
55 |
56 | if (\strpos($offset, '*/') === 0 || \strpos($offset, '0/') === 0) {
57 | return $value % $parts[1] === 0;
58 | }
59 |
60 | $subparts = \explode('-', $parts[0], 2) + [1 => $value];
61 |
62 | return $this->inStepRange((int) $value, (int) $subparts[0], (int) $subparts[1], (int) $parts[1]);
63 | }
64 |
65 | /**
66 | * Check if the value falls between start and end when advanved by step.
67 | *
68 | * @param int $value
69 | * @param int $start
70 | * @param int $end
71 | * @param int $step
72 | *
73 | * @return bool
74 | */
75 | public function inStepRange(int $value, int $start, int $end, int $step): bool
76 | {
77 | if (($start + $step) > $end) {
78 | return false;
79 | }
80 |
81 | if ($start <= $value && $value <= $end) {
82 | return \in_array($value, \range($start, $end, $step));
83 | }
84 |
85 | return false;
86 | }
87 |
88 | /**
89 | * Check if month modifiers [L C W #] are satisfied.
90 | *
91 | * @internal
92 | *
93 | * @param string $value
94 | * @param ReferenceTime $reference
95 | *
96 | * @return bool
97 | */
98 | public function isValidMonthDay(string $value, ReferenceTime $reference): bool
99 | {
100 | if ($value == 'L') {
101 | return $reference->monthDay() == $reference->numDays();
102 | }
103 |
104 | if ($pos = \strpos($value, 'W')) {
105 | $value = \substr($value, 0, $pos);
106 | $month = $this->zeroPad($reference->month());
107 |
108 | return $this->isClosestWeekDay((int) $value, $month, $reference);
109 | }
110 |
111 | $this->unexpectedValue(2, $value);
112 | // @codeCoverageIgnoreStart
113 | }
114 |
115 | // @codeCoverageIgnoreEnd
116 |
117 | protected function isClosestWeekDay(int $value, string $month, ReferenceTime $reference): bool
118 | {
119 | foreach ([0, -1, 1, -2, 2] as $i) {
120 | $incr = $value + $i;
121 | if ($incr < 1 || $incr > $reference->numDays()) {
122 | continue;
123 | }
124 |
125 | $incr = $this->zeroPad($incr);
126 | $parts = \explode(' ', \date('N m j', \strtotime("{$reference->year()}-$month-$incr")));
127 | if ($parts[0] < 6 && $parts[1] == $month) {
128 | return $reference->monthDay() == $parts[2];
129 | }
130 | }
131 |
132 | // @codeCoverageIgnoreStart
133 | return false;
134 | // @codeCoverageIgnoreEnd
135 | }
136 |
137 | /**
138 | * Check if week modifiers [L C W #] are satisfied.
139 | *
140 | * @internal
141 | *
142 | * @param string $value
143 | * @param ReferenceTime $reference
144 | *
145 | * @return bool
146 | */
147 | public function isValidWeekDay(string $value, ReferenceTime $reference): bool
148 | {
149 | $month = $this->zeroPad($reference->month());
150 |
151 | if (\strpos($value, 'L')) {
152 | return $this->isLastWeekDay($value, $month, $reference);
153 | }
154 |
155 | if (!\strpos($value, '#')) {
156 | $this->unexpectedValue(4, $value);
157 | }
158 |
159 | list($day, $nth) = \explode('#', \str_replace('7#', '0#', $value));
160 |
161 | if (!$this->isNthWeekDay((int) $day, (int) $nth) || $reference->weekDay() != $day) {
162 | return false;
163 | }
164 |
165 | return \intval($reference->day() / 7) == $nth - 1;
166 | }
167 |
168 | /**
169 | * Throws UnexpectedValueException.
170 | *
171 | * @param int $pos
172 | * @param string $value
173 | *
174 | * @throws \UnexpectedValueException
175 | */
176 | public function unexpectedValue(int $pos, string $value)
177 | {
178 | throw new \UnexpectedValueException(
179 | \sprintf('Invalid offset value at segment #%d: %s', $pos, $value)
180 | );
181 | }
182 |
183 | protected function isLastWeekDay(string $value, string $month, ReferenceTime $reference): bool
184 | {
185 | $decr = $reference->numDays();
186 | $value = \explode('L', \str_replace('7L', '0L', $value));
187 |
188 | for ($i = 0; $i < 7; $i++) {
189 | $decr -= $i;
190 | if (\date('w', \strtotime("{$reference->year()}-$month-$decr")) == $value[0]) {
191 | return $reference->monthDay() == $decr;
192 | }
193 | }
194 |
195 | return false;
196 | }
197 |
198 | protected function isNthWeekDay(int $day, int $nth): bool
199 | {
200 | return !($day < 0 || $day > 7 || $nth < 1 || $nth > 5);
201 | }
202 |
203 | protected function zeroPad($value): string
204 | {
205 | return \str_pad((string) $value, 2, '0', \STR_PAD_LEFT);
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/tests/ExpressionTest.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | *
9 | * Licensed under MIT license.
10 | */
11 |
12 | namespace Ahc\Cron\Test;
13 |
14 | use Ahc\Cron\Expression;
15 | use PHPUnit\Framework\TestCase;
16 |
17 | class ExpressionTest extends TestCase
18 | {
19 | /**
20 | * @dataProvider scheduleProvider
21 | */
22 | public function test_isDue($expr, $time, $foo, $expected, $throwsAt = false)
23 | {
24 | $actual = Expression::isDue($expr, $time);
25 |
26 | $this->assertSame($expected, $actual, 'The expression ' . $expr . ' has failed');
27 | }
28 |
29 | /**
30 | * @dataProvider invalidScheduleProvider
31 | *
32 | * @expectedException \UnexpectedValueException
33 | */
34 | public function test_isDue_on_invalid_expression($expr, $time, $foo, $expected, $throwsAt = false)
35 | {
36 | Expression::isDue($expr, $time);
37 | }
38 |
39 | public function test_isCronDue()
40 | {
41 | $expr = new Expression;
42 |
43 | $this->assertInternalType('boolean', $expr->isCronDue('*/1 * * * *', time()));
44 | }
45 |
46 | /**
47 | * @expectedException \UnexpectedValueException
48 | */
49 | public function test_isDue_throws_if_expr_invalid()
50 | {
51 | Expression::isDue('@invalid');
52 | }
53 |
54 | /**
55 | * @expectedException \UnexpectedValueException
56 | */
57 | public function test_isDue_throws_if_modifier_invalid()
58 | {
59 | Expression::isDue('* * 2L * *');
60 | }
61 |
62 | public function test_filter_getDues()
63 | {
64 | $jobs = [
65 | 'job1' => '*/2 */2 * * *',
66 | 'job1' => '* 20,21,22 * * *',
67 | 'job3' => '7-9 * */9 * *',
68 | 'job4' => '*/5 * * * *',
69 | 'job5' => '@5minutes',
70 | 'job6' => '7-9 * */9 * *',
71 | ];
72 |
73 | $this->assertSame(['job1', 'job4', 'job5'], Expression::getDues($jobs, '2015-08-10 21:50:00'));
74 | }
75 |
76 | /**
77 | * Data provider for cron schedule.
78 | *
79 | * @return array
80 | */
81 | public function invalidScheduleProvider()
82 | {
83 | return [
84 | ['* * * * 4W', strtotime('2011-07-01 00:00:00'), '2011-07-27 00:00:00', false, 4], // seg 4
85 | ['* * * 1L *', strtotime('2011-07-01 00:00:00'), '2011-07-27 00:00:00', false, 3], // seg 3
86 | ];
87 | }
88 |
89 | /**
90 | * The test cases are taken from awesome mtdowling/cron-expression package. Thank you.
91 | *
92 | * @link https://github.com/mtdowling/cron-expression/
93 | *
94 | * Data provider for cron schedule
95 | *
96 | * @return array
97 | */
98 | public function scheduleProvider()
99 | {
100 | return [
101 | ['@always', time(), '', true],
102 | ['* * * * * 2018', null, '', false],
103 | ['* * * * * 2018', new \DateTime, '', false],
104 | ['@5minutes', new \DateTime('2017-05-10 02:30:00'), '', true],
105 | ['* * 7W * *', '2017-10-15 20:00:00', '', false],
106 |
107 | ['*/2 */2 * * *', '2015-08-10 21:47:27', '2015-08-10 22:00:00', false],
108 | ['* * * * *', '2015-08-10 21:50:37', '2015-08-10 21:50:00', true],
109 | ['* * * * * ', '2015-08-10 21:50:37', '2015-08-10 21:50:00', true],
110 | ['* * * * *', '2015-08-10 21:50:37', '2015-08-10 21:50:00', true],
111 | ["*\t*\t*\t*\t*", '2015-08-10 21:50:37', '2015-08-10 21:50:00', true],
112 | ["*\t\t*\t\t*\t\t*\t\t*", '2015-08-10 21:50:37', '2015-08-10 21:50:00', true],
113 | ['* 20,21,22 * * *', '2015-08-10 21:50:00', '2015-08-10 21:50:00', true],
114 | // Handles CSV values
115 | ['* 20,22 * * *', '2015-08-10 21:50:00', '2015-08-10 22:00:00', false],
116 | // CSV values can be complex
117 | ['* 5,21-22 * * *', '2015-08-10 21:50:00', '2015-08-10 21:50:00', true],
118 | ['7-9 * */9 * *', '2015-08-10 22:02:33', '2015-08-18 00:07:00', false],
119 | // 15th minute, of the second hour, every 15 days, in January, every Friday
120 | ['1 * * * 7', '2015-08-10 21:47:27', '2015-08-16 00:01:00', false],
121 | // Test with exact times
122 | ['47 21 * * *', strtotime('2015-08-10 21:47:30'), '2015-08-10 21:47:00', true],
123 | // Test Day of the week (issue #1)
124 | // According cron implementation, 0|7 = sunday, 1 => monday, etc
125 | ['* * * * 0', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false],
126 | ['* * * * 7', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false],
127 | ['* * * * 1', strtotime('2011-06-15 23:09:00'), '2011-06-20 00:00:00', false],
128 | // Should return the sunday date as 7 equals 0
129 | ['0 0 * * MON,SUN', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false],
130 | ['0 0 * * 1,7', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false],
131 | ['0 0 * * 0-4', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false],
132 | ['0 0 * * 7-4', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false],
133 | ['0 0 * * 4-7', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false],
134 | ['0 0 * * 7-3', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false],
135 | ['0 0 * * 3-7', strtotime('2011-06-15 23:09:00'), '2011-06-16 00:00:00', false],
136 | ['0 0 * * 3-7', strtotime('2011-06-18 23:09:00'), '2011-06-19 00:00:00', false],
137 | // Test lists of values and ranges (Abhoryo)
138 | ['0 0 * * 2-7', strtotime('2011-06-20 23:09:00'), '2011-06-21 00:00:00', false],
139 | ['0 0 * * 0,2-6', strtotime('2011-06-20 23:09:00'), '2011-06-21 00:00:00', false],
140 | ['0 0 * * 2-7', strtotime('2011-06-18 23:09:00'), '2011-06-19 00:00:00', false],
141 | ['0 0 * * 4-7', strtotime('2011-07-19 00:00:00'), '2011-07-21 00:00:00', false],
142 | // Test increments of ranges
143 | ['0-12/4 * * * *', strtotime('2011-06-20 12:04:00'), '2011-06-20 12:04:00', true],
144 | ['4-59/2 * * * *', strtotime('2011-06-20 12:04:00'), '2011-06-20 12:04:00', true],
145 | ['4-59/2 * * * *', strtotime('2011-06-20 12:06:00'), '2011-06-20 12:06:00', true],
146 | ['4-59/3 * * * *', strtotime('2011-06-20 12:06:00'), '2011-06-20 12:07:00', false],
147 | ['0 0 * * 0,2-6', strtotime('2011-06-20 23:09:00'), '2011-06-21 00:00:00', false],
148 | // Test Day of the Week and the Day of the Month (issue #1)
149 | ['0 0 1 1 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false],
150 | ['0 0 1 JAN 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false],
151 | ['0 0 1 * 0', strtotime('2011-06-15 23:09:00'), '2012-01-01 00:00:00', false],
152 | ['0 0 L * *', strtotime('2011-07-15 00:00:00'), '2011-07-31 00:00:00', false],
153 | // Test the W day of the week modifier for day of the month field
154 | ['0 0 2W * *', strtotime('2011-07-01 00:00:00'), '2011-07-01 00:00:00', true],
155 | ['0 0 1W * *', strtotime('2011-05-01 00:00:00'), '2011-05-02 00:00:00', false],
156 | ['0 0 1W * *', strtotime('2011-07-01 00:00:00'), '2011-07-01 00:00:00', true],
157 | ['0 0 3W * *', strtotime('2011-07-01 00:00:00'), '2011-07-04 00:00:00', false],
158 | ['0 0 16W * *', strtotime('2011-07-01 00:00:00'), '2011-07-15 00:00:00', false],
159 | ['0 0 28W * *', strtotime('2011-07-01 00:00:00'), '2011-07-28 00:00:00', false],
160 | ['0 0 30W * *', strtotime('2011-07-01 00:00:00'), '2011-07-29 00:00:00', false],
161 | ['0 0 31W * *', strtotime('2011-07-01 00:00:00'), '2011-07-29 00:00:00', false],
162 | // Test the year field
163 | ['* * * * * 2012', strtotime('2011-05-01 00:00:00'), '2012-01-01 00:00:00', false],
164 | // Test the last weekday of a month
165 | ['* * * * 5L', strtotime('2011-07-01 00:00:00'), '2011-07-29 00:00:00', false],
166 | ['* * * * 6L', strtotime('2011-07-01 00:00:00'), '2011-07-30 00:00:00', false],
167 | ['* * * * 7L', strtotime('2011-07-01 00:00:00'), '2011-07-31 00:00:00', false],
168 | ['* * * * 1L', strtotime('2011-07-24 00:00:00'), '2011-07-25 00:00:00', false],
169 | ['* * * * TUEL', strtotime('2011-07-24 00:00:00'), '2011-07-26 00:00:00', false],
170 | ['* * * 1 5L', strtotime('2011-12-25 00:00:00'), '2012-01-27 00:00:00', false],
171 | // Test the hash symbol for the nth weekday of a given month
172 | ['* * * * 5#2', strtotime('2011-07-01 00:00:00'), '2011-07-08 00:00:00', false],
173 | ['* * * * 5#1', strtotime('2011-07-01 00:00:00'), '2011-07-01 00:00:00', true],
174 | ['* * * * 3#4', strtotime('2011-07-01 00:00:00'), '2011-07-27 00:00:00', false],
175 | ['5/0 * * * *', time(), '2011-07-27 00:00:00', false],
176 | ['5/20 * * * *', '2018-08-13 00:24:00', '2011-07-27 00:00:00', false], // issue #12
177 | ['5/20 * * * *', strtotime('2018-08-13 00:45:00'), '2011-07-27 00:00:00', true], // issue #12
178 | ['5-11/4 * * * *', strtotime('2018-08-13 00:03:00'), '2011-07-27 00:00:00', false],
179 |
180 | // New from https://github.com/dragonmantank/cron-expression
181 | ['0 0 L * 0', strtotime('2011-06-15 23:09:00'), '2011-06-19 00:00:00', false],
182 | ['3-59/15 6-12 */15 1 2-5', strtotime('2017-01-08 00:00:00'), '2017-01-10 06:03:00', false],
183 | ['* * * * MON-FRI', strtotime('2017-01-08 00:00:00'), strtotime('2017-01-09 00:00:00'), false],
184 | ['* * * * TUE', strtotime('2017-01-08 00:00:00'), strtotime('2017-01-10 00:00:00'), false],
185 | ['0 1 15 JUL mon,Wed,FRi', strtotime('2019-11-14 00:00:00'), strtotime('2020-07-01 01:00:00'), false],
186 | ['0 1 15 jul mon,Wed,FRi', strtotime('2019-11-14 00:00:00'), strtotime('2020-07-01 01:00:00'), false],
187 | ['@weekly', strtotime('2019-11-14 00:00:00'), strtotime('2019-11-17 00:00:00'), false],
188 | ['@weekly', strtotime('2019-11-14 00:00:00'), strtotime('2019-11-17 00:00:00'), false],
189 | ['@weekly', strtotime('2019-11-14 00:00:00'), strtotime('2019-11-17 00:00:00'), false],
190 | ['0 12 * * ?', strtotime('2020-08-20 00:00:00'), strtotime('2020-08-20 12:00:00'), false],
191 | ['0 12 ? * *', strtotime('2020-08-20 00:00:00'), strtotime('2020-08-20 12:00:00'), false],
192 | ];
193 | }
194 | }
195 |
--------------------------------------------------------------------------------