Skip to content

Commit e7c07c3

Browse files
authored
Merge pull request #13 from adhocore/develop
Develop: Bulk due checker
2 parents f0425da + d933099 commit e7c07c3

File tree

5 files changed

+163
-32
lines changed

5 files changed

+163
-32
lines changed

readme.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ composer require adhocore/cron-expr
2121

2222
## Usage
2323

24+
**Basic**
25+
2426
```php
2527
use Ahc\Cron\Expression;
2628

@@ -34,6 +36,32 @@ $expr = new Expression;
3436
$expr->isCronDue('*/1 * * * *', time());
3537
```
3638

39+
**Bulk checks**
40+
41+
When checking for several jobs at once, if more than one of the jobs share equivalent expression
42+
then the evaluation is done only once per go thus greatly improving performnce.
43+
44+
```php
45+
use Ahc\Cron\Expression;
46+
47+
$jobs = [
48+
'job1' => '*/2 */2 * * *',
49+
'job1' => '* 20,21,22 * * *',
50+
'job3' => '7-9 * */9 * *',
51+
'job4' => '*/5 * * * *',
52+
'job5' => '@5minutes', // equivalent to job4 (so it is due if job4 is due)
53+
'job6' => '7-9 * */9 * *', // exact same as job3 (so it is due if job3 is due)
54+
];
55+
56+
// The second param $time can be used same as above: null/time()/date string/DateTime
57+
$dueJobs = Expression::getDues($jobs, '2015-08-10 21:50:00');
58+
// ['job1', 'job4', 'job5']
59+
60+
// Dont like static calls? Below is possible too!
61+
$expr = new Expression;
62+
$dueJobs = $expr->filter($jobs, time());
63+
```
64+
3765
### Real Abbreviations
3866

3967
You can use real abbreviations for month and week days. eg: `JAN`, `dec`, `fri`, `SUN`

src/Expression.php

Lines changed: 81 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@
2121
*/
2222
class Expression
2323
{
24+
/** @var Expression */
25+
protected static $instance;
26+
27+
/** @var SegmentChecker */
28+
protected $checker;
29+
2430
protected static $expressions = [
2531
'@yearly' => '0 0 1 1 *',
2632
'@annually' => '0 0 1 1 *',
@@ -57,6 +63,24 @@ class Expression
5763
'dec' => 12,
5864
];
5965

66+
public function __construct(SegmentChecker $checker = null)
67+
{
68+
$this->checker = $checker ?: new SegmentChecker;
69+
70+
if (null === static::$instance) {
71+
static::$instance = $this;
72+
}
73+
}
74+
75+
public static function instance()
76+
{
77+
if (null === static::$instance) {
78+
static::$instance = new static;
79+
}
80+
81+
return static::$instance;
82+
}
83+
6084
/**
6185
* Parse cron expression to decide if it can be run on given time (or default now).
6286
*
@@ -67,13 +91,20 @@ class Expression
6791
*/
6892
public static function isDue($expr, $time = null)
6993
{
70-
static $instance;
71-
72-
if (!$instance) {
73-
$instance = new static;
74-
}
94+
return static::instance()->isCronDue($expr, $time);
95+
}
7596

76-
return $instance->isCronDue($expr, $time);
97+
/**
98+
* Filter only the jobs that are due.
99+
*
100+
* @param array $jobs Jobs with cron exprs. [job1 => cron-expr1, job2 => cron-expr2, ...]
101+
* @param mixed $time The timestamp to validate the cron expr against. Defaults to now.
102+
*
103+
* @return array Due job names: [job1name, ...];
104+
*/
105+
public static function getDues(array $jobs, $time = null)
106+
{
107+
return static::instance()->filter($jobs, $time);
77108
}
78109

79110
/**
@@ -82,28 +113,55 @@ public static function isDue($expr, $time = null)
82113
* Parse cron expression to decide if it can be run on given time (or default now).
83114
*
84115
* @param string $expr The cron expression.
85-
* @param int $time The timestamp to validate the cron expr against. Defaults to now.
116+
* @param mixed $time The timestamp to validate the cron expr against. Defaults to now.
86117
*
87118
* @return bool
88119
*/
89120
public function isCronDue($expr, $time = null)
90121
{
91-
list($expr, $time) = $this->process($expr, $time);
122+
list($expr, $times) = $this->process($expr, $time);
92123

93-
$checker = new SegmentChecker;
94124
foreach ($expr as $pos => $segment) {
95125
if ($segment === '*' || $segment === '?') {
96126
continue;
97127
}
98128

99-
if (!$checker->checkDue($segment, $pos, $time)) {
129+
if (!$this->checker->checkDue($segment, $pos, $times)) {
100130
return false;
101131
}
102132
}
103133

104134
return true;
105135
}
106136

137+
/**
138+
* Filter only the jobs that are due.
139+
*
140+
* @param array $jobs Jobs with cron exprs. [job1 => cron-expr1, job2 => cron-expr2, ...]
141+
* @param mixed $time The timestamp to validate the cron expr against. Defaults to now.
142+
*
143+
* @return array Due job names: [job1name, ...];
144+
*/
145+
public function filter(array $jobs, $time = null)
146+
{
147+
$dues = $cache = [];
148+
$time = $this->normalizeTime($time);
149+
150+
foreach ($jobs as $name => $expr) {
151+
$expr = $this->normalizeExpr($expr);
152+
153+
if (!isset($cache[$expr])) {
154+
$cache[$expr] = $this->isCronDue($expr, $time);
155+
}
156+
157+
if ($cache[$expr]) {
158+
$dues[] = $name;
159+
}
160+
}
161+
162+
return $dues;
163+
}
164+
107165
/**
108166
* Process and prepare input.
109167
*
@@ -114,10 +172,7 @@ public function isCronDue($expr, $time = null)
114172
*/
115173
protected function process($expr, $time)
116174
{
117-
if (isset(static::$expressions[$expr])) {
118-
$expr = static::$expressions[$expr];
119-
}
120-
175+
$expr = $this->normalizeExpr($expr);
121176
$expr = \str_ireplace(\array_keys(static::$literals), \array_values(static::$literals), $expr);
122177
$expr = \explode(' ', $expr);
123178

@@ -127,11 +182,10 @@ protected function process($expr, $time)
127182
);
128183
}
129184

130-
$time = static::normalizeTime($time);
185+
$time = static::normalizeTime($time);
186+
$times = \array_map('intval', \explode(' ', \date('i G j n w Y t d m N', $time)));
131187

132-
$time = \array_map('intval', \explode(' ', \date('i G j n w Y t d m N', $time)));
133-
134-
return [$expr, $time];
188+
return [$expr, $times];
135189
}
136190

137191
protected function normalizeTime($time)
@@ -146,4 +200,13 @@ protected function normalizeTime($time)
146200

147201
return $time;
148202
}
203+
204+
protected function normalizeExpr($expr)
205+
{
206+
if (isset(static::$expressions[$expr])) {
207+
$expr = static::$expressions[$expr];
208+
}
209+
210+
return $expr;
211+
}
149212
}

src/SegmentChecker.php

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,27 @@ class SegmentChecker
2323
/** @var Validator */
2424
protected $validator;
2525

26-
public function __construct()
26+
public function __construct(Validator $validator = null)
2727
{
28-
$this->validator = new Validator;
28+
$this->validator = $validator ?: new Validator;
2929
}
3030

3131
/**
3232
* Checks if a cron segment satisfies given time.
3333
*
3434
* @param string $segment
3535
* @param int $pos
36-
* @param int $time
36+
* @param array $times
3737
*
3838
* @return bool
3939
*/
40-
public function checkDue($segment, $pos, $time)
40+
public function checkDue($segment, $pos, $times)
4141
{
4242
$isDue = true;
4343
$offsets = \explode(',', \trim($segment));
4444

4545
foreach ($offsets as $offset) {
46-
if (null === $isDue = $this->isOffsetDue($offset, $pos, $time)) {
46+
if (null === $isDue = $this->isOffsetDue($offset, $pos, $times)) {
4747
throw new \UnexpectedValueException(
4848
sprintf('Invalid offset value at segment #%d: %s', $pos, $offset)
4949
);
@@ -62,37 +62,37 @@ public function checkDue($segment, $pos, $time)
6262
*
6363
* @param string $offset
6464
* @param int $pos
65-
* @param array $time
65+
* @param array $times
6666
*
6767
* @return bool|null
6868
*/
69-
protected function isOffsetDue($offset, $pos, $time)
69+
protected function isOffsetDue($offset, $pos, $times)
7070
{
7171
if (\strpos($offset, '/') !== false) {
72-
return $this->validator->inStep($time[$pos], $offset);
72+
return $this->validator->inStep($times[$pos], $offset);
7373
}
7474

7575
if (\strpos($offset, '-') !== false) {
76-
return $this->validator->inRange($time[$pos], $offset);
76+
return $this->validator->inRange($times[$pos], $offset);
7777
}
7878

7979
if (\is_numeric($offset)) {
80-
return $time[$pos] == $offset;
80+
return $times[$pos] == $offset;
8181
}
8282

83-
return $this->checkModifier($offset, $pos, $time);
83+
return $this->checkModifier($offset, $pos, $times);
8484
}
8585

86-
protected function checkModifier($offset, $pos, $time)
86+
protected function checkModifier($offset, $pos, $times)
8787
{
8888
$isModifier = \strpbrk($offset, 'LCW#');
8989

9090
if ($pos === 2 && $isModifier) {
91-
return $this->validator->isValidMonthDay($offset, $time);
91+
return $this->validator->isValidMonthDay($offset, $times);
9292
}
9393

9494
if ($pos === 4 && $isModifier) {
95-
return $this->validator->isValidWeekDay($offset, $time);
95+
return $this->validator->isValidWeekDay($offset, $times);
9696
}
9797

9898
return null;

src/Validator.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,29 @@
2020
*/
2121
class Validator
2222
{
23+
/**
24+
* Check if the value is in range of given offset.
25+
*
26+
* @param int $value
27+
* @param string $offset
28+
*
29+
* @return bool
30+
*/
2331
public function inRange($value, $offset)
2432
{
2533
$parts = \explode('-', $offset);
2634

2735
return $parts[0] <= $value && $value <= $parts[1];
2836
}
2937

38+
/**
39+
* Check if the value is in step of given offset.
40+
*
41+
* @param int $value
42+
* @param string $offset
43+
*
44+
* @return bool
45+
*/
3046
public function inStep($value, $offset)
3147
{
3248
$parts = \explode('/', $offset, 2);
@@ -45,6 +61,16 @@ public function inStep($value, $offset)
4561
return $this->inStepRange($value, $subparts[0], $subparts[1], $parts[1]);
4662
}
4763

64+
/**
65+
* Check if the value falls between start and end when advanved by step.
66+
*
67+
* @param int $value
68+
* @param int $start
69+
* @param int $end
70+
* @param int $step
71+
*
72+
* @return bool
73+
*/
4874
public function inStepRange($value, $start, $end, $step)
4975
{
5076
if (($start + $step) > $end) {

tests/ExpressionTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,20 @@ public function test_isDue_throws_if_modifier_invalid()
5858
Expression::isDue('* * 2L * *');
5959
}
6060

61+
public function test_filter_getDues()
62+
{
63+
$jobs = [
64+
'job1' => '*/2 */2 * * *',
65+
'job1' => '* 20,21,22 * * *',
66+
'job3' => '7-9 * */9 * *',
67+
'job4' => '*/5 * * * *',
68+
'job5' => '@5minutes',
69+
'job6' => '7-9 * */9 * *',
70+
];
71+
72+
$this->assertSame(['job1', 'job4', 'job5'], Expression::getDues($jobs, '2015-08-10 21:50:00'));
73+
}
74+
6175
/**
6276
* Data provider for cron schedule.
6377
*

0 commit comments

Comments
 (0)