Cron::parseNumber()   B
last analyzed

Complexity

Conditions 7
Paths 33

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 7
eloc 14
c 3
b 0
f 0
nc 33
nop 3
dl 0
loc 21
rs 8.8333
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   https://www.platine-php.com
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
        if ($crons === false) {
92
            return 0;
93
        }
94
95
        $start = time();
96
        if ($timestamp !== null) {
97
            $start = $timestamp;
98
        }
99
100
        $dates = [
101
            'minutes' => self::parseNumber($crons[0], 0, 59),
102
            'hours' => self::parseNumber($crons[1], 0, 23),
103
            'dom' => self::parseNumber($crons[2], 1, 31),
104
            'month' => self::parseNumber($crons[3], 1, 12),
105
            'dow' => self::parseNumber($crons[4], 0, 6),
106
        ];
107
108
        // limited to time()+366 - no need
109
        // to check more than 1 year ahead
110
        $total = 60 * 60 * 24 * 366;
111
112
        for ($i = 0; $i <= $total; $i += 60) {
113
            $current = $start + $i;
114
            if (
115
                in_array((int) date('j', $current), $dates['dom']) &&
116
                in_array((int) date('n', $current), $dates['month']) &&
117
                in_array((int) date('w', $current), $dates['dow']) &&
118
                in_array((int) date('G', $current), $dates['hours']) &&
119
                in_array((int) date('i', $current), $dates['minutes'])
120
            ) {
121
                return $current;
122
            }
123
        }
124
125
        return 0;
126
    }
127
128
    /**
129
     * Parse and return a single cron style notation
130
     * into numeric value
131
     * @param string $expression
132
     * @param int $min minimum possible value
133
     * @param int $max maximum possible value
134
     * @return array<int>
135
     */
136
    protected static function parseNumber(string $expression, int $min, int $max): array
137
    {
138
        $result = [];
139
        $values = explode(',', $expression);
140
        foreach ($values as $value) {
141
            $slashValues = explode('/', $value);
142
            $step = $slashValues[1] ?? 1;
143
            $minusValues = explode('-', $slashValues[0]);
144
            $minimum = count($minusValues) === 2 ? $minusValues[0]
145
                        : ($slashValues[0] === '*' ? $min : $slashValues[0]);
146
147
            $maximum = count($minusValues) === 2 ? $minusValues[1]
148
                        : ($slashValues[0] === '*' ? $max : $slashValues[0]);
149
150
            for ($i = $minimum; $i <= $maximum; $i += $step) {
151
                $result[$i] = intval($i);
152
            }
153
        }
154
        ksort($result);
155
156
        return $result;
157
    }
158
}
159