Passed
Push — develop ( 86d31c...191f96 )
by nguereza
03:18
created

Cron::parse()   B

Complexity

Conditions 9
Paths 7

Size

Total Lines 47
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 31
c 1
b 0
f 0
nc 7
nop 2
dl 0
loc 47
rs 8.0555
1
<?php
2
3
/**
4
 * Platine Framework
5
 *
6
 * Platine Framework is a lightweight, high-performance, simple and elegant
7
 * PHP Web framework
8
 *
9
 * This content is released under the MIT License (MIT)
10
 *
11
 * Copyright (c) 2020 Platine Framework
12
 *
13
 * Permission is hereby granted, free of charge, to any person obtaining a copy
14
 * of this software and associated documentation files (the "Software"), to deal
15
 * in the Software without restriction, including without limitation the rights
16
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
 * copies of the Software, and to permit persons to whom the Software is
18
 * furnished to do so, subject to the following conditions:
19
 *
20
 * The above copyright notice and this permission notice shall be included in all
21
 * copies or substantial portions of the Software.
22
 *
23
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
 * SOFTWARE.
30
 */
31
32
/**
33
 *  @file Cron.php
34
 *
35
 *  The Cron expression parser class
36
 *
37
 *  @package    Platine\Framework\Task
38
 *  @author Platine Developers team
39
 *  @copyright  Copyright (c) 2020
40
 *  @license    http://opensource.org/licenses/MIT  MIT License
41
 *  @link   http://www.iacademy.cf
42
 *  @version 1.0.0
43
 *  @filesource
44
 */
45
46
declare(strict_types=1);
47
48
namespace Platine\Framework\Task;
49
50
use InvalidArgumentException;
51
52
/**
53
 * @class Cron
54
 * @package Platine\Framework\Task
55
 */
56
class Cron
57
{
58
    /**
59
     * Parse the given cron expression and finds next execution time(stamp)
60
     * @param string $expression
61
     *      0     1    2    3    4
62
     *      *     *    *    *    *
63
     *      -     -    -    -    -
64
     *      |     |    |    |    |
65
     *      |     |    |    |    +----- day of week (0 - 6) (Sunday=0)
66
     *      |     |    |    +------- month (1 - 12)
67
     *      |     |    +--------- day of month (1 - 31)
68
     *      |     +----------- hour (0 - 23)
69
     *      +------------- min (0 - 59)
70
     *
71
     * @param int $timestamp the after timestamp [default=current timestamp]
72
     * @return int the Unix timestamp
73
     */
74
    public static function parse(string $expression, ?int $timestamp = null): int
75
    {
76
        $cronExpression = trim($expression);
77
78
        $cronRegex = '/^((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+'
79
                . '((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+'
80
                . '((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+'
81
                . '((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+'
82
                . '((\*(\/[0-9]+)?)|[0-9\-\,\/]+)$/i';
83
        if (!preg_match($cronRegex, $cronExpression)) {
84
            throw new InvalidArgumentException(sprintf(
85
                'Invalid cron expression [%s]',
86
                $expression
87
            ));
88
        }
89
90
        $crons = preg_split('/[\s]+/i', $cronExpression);
91
        $start = time();
92
        if ($timestamp !== null) {
93
            $start = $timestamp;
94
        }
95
96
        $dates = [
97
            'minutes' => self::parseNumber($crons[0], 0, 59),
98
            'hours' => self::parseNumber($crons[1], 0, 23),
99
            'dom' => self::parseNumber($crons[2], 1, 31),
100
            'month' => self::parseNumber($crons[3], 1, 12),
101
            'dow' => self::parseNumber($crons[4], 0, 6),
102
        ];
103
104
        // limited to time()+366 - no need
105
        // to check more than 1 year ahead
106
        $total = 60 * 60 * 24 * 366;
107
        for ($i = 0; $i <= $total; $i += 60) {
108
            $current = $start + $i;
109
            if (
110
                in_array((int) date('j', $current), $dates['dom']) &&
111
                in_array((int) date('n', $current), $dates['month']) &&
112
                in_array((int) date('w', $current), $dates['dow']) &&
113
                in_array((int) date('G', $current), $dates['hours']) &&
114
                in_array((int) date('i', $current), $dates['minutes'])
115
            ) {
116
                return $current;
117
            }
118
        }
119
120
        return $start;
121
    }
122
123
    /**
124
     * Parse and return a single cron style notation
125
     * into numeric value
126
     * @param string $expression
127
     * @param int $min minimum possible value
128
     * @param int $max maximum possible value
129
     * @return array<int>
130
     */
131
    protected static function parseNumber(string $expression, int $min, int $max): array
132
    {
133
        $result = [];
134
        $values = explode(',', $expression);
135
        foreach ($values as $value) {
136
            $slashValues = explode('/', $value);
137
            $step = $slashValues[1] ?? 1;
138
            $minusValues = explode('-', $slashValues[0]);
139
            $minimum = count($minusValues) === 2 ? $minusValues[0]
140
                        : ($slashValues[0] === '*' ? $min : $slashValues[0]);
141
142
            $maximum = count($minusValues) === 2 ? $minusValues[1]
143
                        : ($slashValues[0] === '*' ? $max : $slashValues[0]);
144
145
            for ($i = $minimum; $i <= $maximum; $i += $step) {
146
                $result[$i] = intval($i);
147
            }
148
        }
149
        ksort($result);
150
151
        return $result;
152
    }
153
}
154