CronSchedule   F
last analyzed

Complexity

Total Complexity 182

Size/Duplication

Total Lines 1225
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 659
dl 0
loc 1225
rs 1.941
c 0
b 0
f 0
wmc 182

30 Methods

Rating   Name   Duplication   Size   Complexity  
A previousAsTime() 0 3 1
A previousAsString() 0 3 1
A classIsSingleFixed() 0 3 2
A __construct() 0 3 1
A natlangPad2() 0 3 2
A nextAsString() 0 3 1
A nextAsTime() 0 3 1
F cronInterpret() 0 88 24
C next() 0 56 14
A natlangElementDayOfMonth() 0 11 5
A natlangElementMonth() 0 11 5
B natlangApply() 0 23 7
B natlangElementHour() 0 19 8
A dtFromParameters() 0 16 5
A getEarliestItem() 0 21 5
A fromCronString() 0 29 3
C previous() 0 56 14
A classIsSpecified() 0 16 5
A natlangElementYear() 0 11 5
B initLang() 0 239 3
A cronCreateItems() 0 15 4
B natlangElementMinute() 0 15 7
F asNaturalLanguage() 0 155 25
A recedeItem() 0 18 4
B match() 0 31 7
A advanceItem() 0 18 4
A getClass() 0 10 3
A getLatestItem() 0 22 5
A natlangRange() 0 12 5
A dtAsString() 0 7 6

How to fix   Complexity   

Complex Class

Complex classes like CronSchedule often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CronSchedule, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Created by PhpStorm.
4
 * User: sheldon
5
 * Date: 18-4-18
6
 * Time: 下午5:39.
7
 */
8
9
namespace Yeelight\Models\Tools\Scheduling;
10
11
/*
12
 * Plugin:        StreamlineFoundation
13
 *
14
 * Class:        Schedule
15
 *
16
 * Description:    Provides scheduling mechanics including creating a schedule, testing if a specific moment is part of the schedule, moving back
17
 *                and forth between scheduled moments in time and translating the created schedule back to a human readable form.
18
 *
19
 * Usage:        ::fromCronString() creates a new Schedule class and requires a string in the cron ('* * * * *', $language) format.
20
 *
21
 *                ->next(<datetime>) returns the first scheduled datetime after <datetime> in array format.
22
 *                ->nextAsString(<datetime>) does the same with an ISO string as the result.
23
 *                ->nextAsTime(<datetime>) does the same with a UNIX timestamp as the result.
24
 *
25
 *                ->previous(<datetime>) returns the first scheduled datetime before <datetime> in array format.
26
 *                ->previousAsString(<datetime>) does the same with an ISO string as the result.
27
 *                ->previousAsTime(<datetime>) does the same with a UNIX timestamp as the result.
28
 *
29
 *                ->asNaturalLanguage() returns the entire schedule in natural language form.
30
 *
31
 *                In the next and previous functions, <datetime> can be a UNIX timestamp, an ISO string or an array format such as returned by
32
 *                next() and previous().
33
 *
34
 * Copyright:    2012 Joost Brugman ([email protected], [email protected])
35
 *
36
 *                This file is part of the Streamline plugin "StreamlineFoundation" and referenced in the next paragraphs inside this comment block as "this
37
 *                plugin". It is based on the Streamline application framework.
38
 *
39
 *                This plugin is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as
40
 *                published by the Free Software Foundation, either version 3 of the License, or any later version. This plugin is distributed in the
41
 *                hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
42
 *                PARTICULAR PURPOSE.  See the GNU General Public License for more details. You should have received a copy of the GNU General Public
43
 *                License along with Streamline.  If not, see <http://www.gnu.org/licenses/>.
44
 */
45
class CronSchedule
46
{
47
    // The actual minutes, hours, daysOfMonth, months, daysOfWeek and years selected by the provided cron specification.
48
    private $_minutes = [];
49
    private $_hours = [];
50
    private $_daysOfMonth = [];
51
    private $_months = [];
52
    private $_daysOfWeek = [];
53
    private $_years = [];
54
    // The original cron specification in compiled form.
55
    private $_cronMinutes = [];
56
    private $_cronHours = [];
57
    private $_cronDaysOfMonth = [];
58
    private $_cronMonths = [];
59
    private $_cronDaysOfWeek = [];
60
    private $_cronYears = [];
61
    // The language table
62
    private $_lang = false;
63
    /**
64
     * Minimum and maximum years to cope with the Year 2038 problem in UNIX. We run PHP which most likely runs on a UNIX environment so we
65
     * must assume vulnerability.
66
     */
67
    protected $RANGE_YEARS_MIN = 1970;    // Must match date range supported by date(). See also: http://en.wikipedia.org/wiki/Year_2038_problem
68
    protected $RANGE_YEARS_MAX = 2037;    // Must match date range supported by date(). See also: http://en.wikipedia.org/wiki/Year_2038_problem
69
70
    /**
71
     * Function:    __construct.
72
     *
73
     * Description:    Performs only base initialization, including language initialization.
74
     *
75
     * Parameters:    $language            The languagecode of the chosen language.
76
     */
77
    public function __construct($language = 'en')
78
    {
79
        $this->initLang($language);
80
    }
81
82
    //
83
    // Function:    fromCronString
84
    //
85
    // Description:    Creates a new Schedule object based on a Cron specification.
86
    //
87
    // Parameters:    $cronSpec            A string containing a cron specification.
88
    //                $language            The language to use to create a natural language representation of the string
89
    //
90
    // Result:        A new Schedule object. An \Exception is thrown if the specification is invalid.
91
    //
92
    final public static function fromCronString($cronSpec = '* * * * * *', $language = 'en')
93
    {
94
        // Split input liberal. Single or multiple Spaces, Tabs and Newlines are all allowed as separators.
95
        if (count($elements = preg_split('/\s+/', $cronSpec)) < 5) {
0 ignored issues
show
Bug introduced by
It seems like $elements = preg_split('/\s+/', $cronSpec) can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

95
        if (count(/** @scrutinizer ignore-type */ $elements = preg_split('/\s+/', $cronSpec)) < 5) {
Loading history...
96
            throw new \Exception('Invalid specification.');
97
        }
98
        /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
99
        // Named ranges in cron entries
100
        /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
101
        $arrMonths = ['JAN' => 1, 'FEB' => 2, 'MAR' => 3, 'APR' => 4, 'MAY' => 5, 'JUN' => 6, 'JUL' => 7, 'AUG' => 8, 'SEP' => 9, 'OCT' => 10, 'NOV' => 11, 'DEC' => 12];
102
        $arrDaysOfWeek = ['SUN' => 0, 'MON' => 1, 'TUE' => 2, 'WED' => 3, 'THU' => 4, 'FRI' => 5, 'SAT' => 6];
103
        // Translate the cron specification into arrays that hold specifications of the actual dates
104
        $newCron = new self($language);
105
        $newCron->_cronMinutes = $newCron->cronInterpret($elements[0], 0, 59, [], 'minutes');
106
        $newCron->_cronHours = $newCron->cronInterpret($elements[1], 0, 23, [], 'hours');
107
        $newCron->_cronDaysOfMonth = $newCron->cronInterpret($elements[2], 1, 31, [], 'daysOfMonth');
108
        $newCron->_cronMonths = $newCron->cronInterpret($elements[3], 1, 12, $arrMonths, 'months');
109
        $newCron->_cronDaysOfWeek = $newCron->cronInterpret($elements[4], 0, 6, $arrDaysOfWeek, 'daysOfWeek');
110
        $newCron->_minutes = $newCron->cronCreateItems($newCron->_cronMinutes);
111
        $newCron->_hours = $newCron->cronCreateItems($newCron->_cronHours);
112
        $newCron->_daysOfMonth = $newCron->cronCreateItems($newCron->_cronDaysOfMonth);
113
        $newCron->_months = $newCron->cronCreateItems($newCron->_cronMonths);
114
        $newCron->_daysOfWeek = $newCron->cronCreateItems($newCron->_cronDaysOfWeek);
115
        if (isset($elements[5])) {
116
            $newCron->_cronYears = $newCron->cronInterpret($elements[5], $newCron->RANGE_YEARS_MIN, $newCron->RANGE_YEARS_MAX, [], 'years');
117
            $newCron->_years = $newCron->cronCreateItems($newCron->_cronYears);
118
        }
119
120
        return $newCron;
121
    }
122
123
    /*
124
     * Function:    cronInterpret
125
     *
126
     * Description:    Interprets a single field from a cron specification. Throws an \Exception if the specification is in some way invalid.
127
     *
128
     * Parameters:    $specification        The actual text from the spefication, such as 12-38/3
129
     *                $rangeMin            The lowest value for specification.
130
     *                $rangeMax            The highest value for specification
131
     *                $namesItems            A key/value pair where value is a value between $rangeMin and $rangeMax and key is the name for that value.
132
     *                $errorName            The name of the category to use in case of an error.
133
     *
134
     * Result:        An array with entries, each of which is an array with the following fields:
135
     *                'number1'            The first number of the range or the number specified
136
     *                'number2'            The second number of the range if a range is specified
137
     *                'hasInterval'        TRUE if a range is specified. FALSE otherwise
138
     *                'interval'            The interval if a range is specified.
139
     */
140
    final private function cronInterpret($specification, $rangeMin, $rangeMax, $namedItems, $errorName)
141
    {
142
        if ((!is_string($specification)) && (!(is_int($specification)))) {
143
            throw new \Exception('Invalid specification.');
144
        }
145
        // Multiple values, separated by comma
146
        $specs = [];
147
        $specs['rangeMin'] = $rangeMin;
148
        $specs['rangeMax'] = $rangeMax;
149
        $specs['elements'] = [];
150
        $arrSegments = explode(',', $specification);
151
        foreach ($arrSegments as $segment) {
152
            $hasRange = (($posRange = strpos($segment, '-')) !== false);
153
            $hasInterval = (($posIncrement = strpos($segment, '/')) !== false);
154
            /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
155
            // Check: Increment without range is invalid
156
            //if(!$hasRange && $hasInterval)                                                throw new \Exception("Invalid Range ($errorName).");
157
            // Check: Increment must be final specification
158
            if ($hasRange && $hasInterval) {
159
                if ($posIncrement < $posRange) {
160
                    throw new \Exception("Invalid order ($errorName).");
161
                }
162
            }
163
            // GetSegments
164
            $segmentNumber1 = $segment;
165
            $segmentNumber2 = '';
166
            $segmentIncrement = '';
167
            $intIncrement = 1;
168
            if ($hasInterval) {
169
                $segmentNumber1 = substr($segment, 0, $posIncrement);
170
                $segmentIncrement = substr($segment, $posIncrement + 1);
171
            }
172
            if ($hasRange) {
173
                $segmentNumber2 = substr($segmentNumber1, $posRange + 1);
174
                $segmentNumber1 = substr($segmentNumber1, 0, $posRange);
175
            }
176
            // Get and validate first value in range
177
            if ($segmentNumber1 == '*') {
178
                $intNumber1 = $rangeMin;
179
                $intNumber2 = $rangeMax;
180
                $hasRange = true;
181
            } else {
182
                if (array_key_exists(strtoupper($segmentNumber1), $namedItems)) {
183
                    $segmentNumber1 = $namedItems[strtoupper($segmentNumber1)];
184
                }
185
                if (((string) ($intNumber1 = (int) $segmentNumber1)) != $segmentNumber1) {
186
                    throw new \Exception("Invalid symbol ($errorName).");
187
                }
188
                if (($intNumber1 < $rangeMin) || ($intNumber1 > $rangeMax)) {
189
                    throw new \Exception("Out of bounds ($errorName).");
190
                }
191
                // Get and validate second value in range
192
                if ($hasRange) {
193
                    if (array_key_exists(strtoupper($segmentNumber2), $namedItems)) {
194
                        $segmentNumber2 = $namedItems[strtoupper($segmentNumber2)];
195
                    }
196
                    if (((string) ($intNumber2 = (int) $segmentNumber2)) != $segmentNumber2) {
197
                        throw new \Exception("Invalid symbol ($errorName).");
198
                    }
199
                    if (($intNumber2 < $rangeMin) || ($intNumber2 > $rangeMax)) {
200
                        throw new \Exception("Out of bounds ($errorName).");
201
                    }
202
                    if ($intNumber1 > $intNumber2) {
203
                        throw new \Exception("Invalid range ($errorName).");
204
                    }
205
                }
206
            }
207
            // Get and validate increment
208
            if ($hasInterval) {
209
                if (($intIncrement = (int) $segmentIncrement) != $segmentIncrement) {
210
                    throw new \Exception("Invalid symbol ($errorName).");
211
                }
212
                if ($intIncrement < 1) {
213
                    throw new \Exception("Out of bounds ($errorName).");
214
                }
215
            }
216
            // Apply range and increment
217
            $elem = [];
218
            $elem['number1'] = $intNumber1;
219
            $elem['hasInterval'] = $hasRange;
220
            if ($hasRange) {
221
                $elem['number2'] = $intNumber2;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $intNumber2 does not seem to be defined for all execution paths leading up to this point.
Loading history...
222
                $elem['interval'] = $intIncrement;
223
            }
224
            $specs['elements'][] = $elem;
225
        }
226
227
        return $specs;
228
    }
229
230
    //
231
    // Function:    cronCreateItems
232
    //
233
    // Description:    Uses the interpreted cron specification of a single item from a cron specification to create an array with keys that match the
234
    //                selected items.
235
    //
236
    // Parameters:    $cronInterpreted    The interpreted specification
237
    //
238
    // Result:        An array where each key identifies a matching entry. E.g. the cron specification */10 for minutes will yield an array
239
    //                [0] => 1
240
    //                [10] => 1
241
    //                [20] => 1
242
    //                [30] => 1
243
    //                [40] => 1
244
    //                [50] => 1
245
    //
246
    final private function cronCreateItems($cronInterpreted)
247
    {
248
        $items = [];
249
        foreach ($cronInterpreted['elements'] as $elem) {
250
            if (!$elem['hasInterval']) {
251
                $items[$elem['number1']] = true;
252
            } else {
253
                for ($number = $elem['number1']; $number <= $elem['number2']; $number += $elem['interval']) {
254
                    $items[$number] = true;
255
                }
256
            }
257
        }
258
        ksort($items);
259
260
        return $items;
261
    }
262
263
    //
264
    // Function:    dtFromParameters
265
    //
266
    // Description:    Transforms a flexible parameter passing of a datetime specification into an internally used array.
267
    //
268
    // Parameters:    $time                If a string interpreted as a datetime string in the YYYY-MM-DD HH:II format and other parameters ignored.
269
    //                                    If an array $minute, $hour, $day, $month and $year are passed as keys 0-4 and other parameters ignored.
270
    //                                    If a string, interpreted as unix time.
271
    //                                    If omitted or specified FALSE, defaults to the current time.
272
    //
273
    // Result:        An array with indices 0-4 holding the actual interpreted values for $minute, $hour, $day, $month and $year.
274
    //
275
    final private function dtFromParameters($time = false)
276
    {
277
        if ($time === false) {
278
            $arrTime = getdate();
279
280
            return [$arrTime['minutes'], $arrTime['hours'], $arrTime['mday'], $arrTime['mon'], $arrTime['year']];
281
        } elseif (is_array($time)) {
282
            return $time;
283
        } elseif (is_string($time)) {
284
            $arrTime = getdate(strtotime($time));
285
286
            return [$arrTime['minutes'], $arrTime['hours'], $arrTime['mday'], $arrTime['mon'], $arrTime['year']];
287
        } elseif (is_int($time)) {
288
            $arrTime = getdate($time);
289
290
            return [$arrTime['minutes'], $arrTime['hours'], $arrTime['mday'], $arrTime['mon'], $arrTime['year']];
291
        }
292
    }
293
294
    final private function dtAsString($arrDt)
295
    {
296
        if ($arrDt === false) {
297
            return false;
298
        }
299
300
        return $arrDt[4].'-'.(strlen($arrDt[3]) == 1 ? '0' : '').$arrDt[3].'-'.(strlen($arrDt[2]) == 1 ? '0' : '').$arrDt[2].' '.(strlen($arrDt[1]) == 1 ? '0' : '').$arrDt[1].':'.(strlen($arrDt[0]) == 1 ? '0' : '').$arrDt[0].':00';
301
    }
302
303
    //
304
    // Function:    match
305
    //
306
    // Description:    Returns TRUE if the specified date and time corresponds to a scheduled point in time. FALSE otherwise.
307
    //
308
    // Parameters:    $time                If a string interpreted as a datetime string in the YYYY-MM-DD HH:II format and other parameters ignored.
309
    //                                    If an array $minute, $hour, $day, $month and $year are passed as keys 0-4 and other parameters ignored.
310
    //                                    If a string, interpreted as unix time.
311
    //                                    If omitted or specified FALSE, defaults to the current time.
312
    //
313
    // Result:        TRUE if the schedule matches the specified datetime. FALSE otherwise.
314
    //
315
    final public function match($time = false)
316
    {
317
        // Convert parameters to array datetime
318
        $arrDT = $this->dtFromParameters($time);
319
        // Verify match
320
        // Years
321
        if (!array_key_exists($arrDT[4], $this->_years)) {
322
            return false;
323
        }
324
        // Day of week
325
        if (!array_key_exists(date('w', strtotime($arrDT[4].'-'.$arrDT[3].'-'.$arrDT[2])), $this->_daysOfWeek)) {
326
            return false;
327
        }
328
        // Month
329
        if (!array_key_exists($arrDT[3], $this->_months)) {
330
            return false;
331
        }
332
        // Day of month
333
        if (!array_key_exists($arrDT[2], $this->_daysOfMonth)) {
334
            return false;
335
        }
336
        // Hours
337
        if (!array_key_exists($arrDT[1], $this->_hours)) {
338
            return false;
339
        }
340
        // Minutes
341
        if (!array_key_exists($arrDT[0], $this->_minutes)) {
342
            return false;
343
        }
344
345
        return true;
346
    }
347
348
    //
349
    // Function:    next
350
    //
351
    // Description:    Acquires the first scheduled datetime beyond the provided one.
352
    //
353
    // Parameters:    $time                If a string interpreted as a datetime string in the YYYY-MM-DD HH:II format and other parameters ignored.
354
    //                                    If an array $minute, $hour, $day, $month and $year are passed as keys 0-4 and other parameters ignored.
355
    //                                    If a string, interpreted as unix time.
356
    //                                    If omitted or specified FALSE, defaults to the current time.
357
    //
358
    // Result:        An array with the following keys:
359
    //                0                    Next scheduled minute
360
    //                1                    Next scheduled hour
361
    //                2                    Next scheduled date
362
    //                3                    Next scheduled month
363
    //                4                    Next scheduled year
364
    //
365
    final public function next($time = false)
366
    {
367
        // Convert parameters to array datetime
368
        $arrDT = $this->dtFromParameters($time);
369
        while (1) {
370
            // Verify the current date is in range. If not, move into range and consider this the next position
371
            if (!array_key_exists($arrDT[4], $this->_years)) {
372
                if (($arrDT[4] = $this->getEarliestItem($this->_years, $arrDT[4], false)) === false) {
373
                    return false;
374
                }
375
                $arrDT[3] = $this->getEarliestItem($this->_months);
376
                $arrDT[2] = $this->getEarliestItem($this->_daysOfMonth);
377
                $arrDT[1] = $this->getEarliestItem($this->_hours);
378
                $arrDT[0] = $this->getEarliestItem($this->_minutes);
379
                break;
380
            } elseif (!array_key_exists($arrDT[3], $this->_months)) {
381
                $arrDT[3] = $this->getEarliestItem($this->_months, $arrDT[3]);
382
                $arrDT[2] = $this->getEarliestItem($this->_daysOfMonth);
383
                $arrDT[1] = $this->getEarliestItem($this->_hours);
384
                $arrDT[0] = $this->getEarliestItem($this->_minutes);
385
                break;
386
            } elseif (!array_key_exists($arrDT[2], $this->_daysOfMonth)) {
387
                $arrDT[2] = $this->getEarliestItem($this->_daysOfMonth, $arrDT[2]);
388
                $arrDT[1] = $this->getEarliestItem($this->_hours);
389
                $arrDT[0] = $this->getEarliestItem($this->_minutes);
390
                break;
391
            } elseif (!array_key_exists($arrDT[1], $this->_hours)) {
392
                $arrDT[1] = $this->getEarliestItem($this->_hours, $arrDT[1]);
393
                $arrDT[0] = $this->getEarliestItem($this->_minutes);
394
                break;
395
            } elseif (!array_key_exists($arrDT[1], $this->_hours)) {
396
                $arrDT[0] = $this->getEarliestItem($this->_minutes, $arrDT[0]);
397
                break;
398
            }
399
            // Advance minute, hour, date, month and year while overflowing.
400
            $daysInThisMonth = date('t', strtotime($arrDT[4].'-'.$arrDT[3]));
401
            if ($this->advanceItem($this->_minutes, 0, 59, $arrDT[0])) {
402
                if ($this->advanceItem($this->_hours, 0, 23, $arrDT[1])) {
403
                    if ($this->advanceItem($this->_daysOfMonth, 0, $daysInThisMonth, $arrDT[2])) {
404
                        if ($this->advanceItem($this->_months, 1, 12, $arrDT[3])) {
405
                            if ($this->advanceItem($this->_years, $this->RANGE_YEARS_MIN, $this->RANGE_YEARS_MAX, $arrDT[4])) {
406
                                return false;
407
                            }
408
                        }
409
                    }
410
                }
411
            }
412
            break;
413
        }
414
        // If Datetime now points to a day that is schedule then return.
415
        $dayOfWeek = date('w', strtotime($this->dtAsString($arrDT)));
416
        if (array_key_exists($dayOfWeek, $this->_daysOfWeek)) {
417
            return $arrDT;
418
        }
419
        // Otherwise move to next scheduled date
420
        return $this->next($arrDT);
421
    }
422
423
    final public function nextAsString($time = false)
424
    {
425
        return $this->dtAsString($this->next($time));
426
    }
427
428
    final public function nextAsTime($time = false)
429
    {
430
        return strtotime($this->dtAsString($this->next($time)));
0 ignored issues
show
Bug introduced by
It seems like $this->dtAsString($this->next($time)) can also be of type false; however, parameter $time of strtotime() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

430
        return strtotime(/** @scrutinizer ignore-type */ $this->dtAsString($this->next($time)));
Loading history...
431
    }
432
433
    //
434
    // Function:    advanceItem
435
    //
436
    // Description:    Advances the current item to the next one (the next minute, the next hour, etc.).
437
    //
438
    // Parameters:    $arrItems            A reference to the collection in which to advance.
439
    //                $rangeMin            The lowest possible value for $current.
440
    //                $rangeMax            The highest possible value for $current
441
    //                $current            The index that is being incremented.
442
    //
443
    // Result:        FALSE if current did not overflow (reset back to the earliest possible value). TRUE if it did.
444
    //
445
    final private function advanceItem($arrItems, $rangeMin, $rangeMax, &$current)
446
    {
447
        // Advance pointer
448
        $current++;
449
        // If still before start, move to earliest
450
        if ($current < $rangeMin) {
451
            $current = $this->getEarliestItem($arrItems);
452
        }
453
        // Parse items until found or overflow
454
        for (; $current <= $rangeMax; $current++) {
455
            if (array_key_exists($current, $arrItems)) {
456
                return false;
457
            }
458
        } // We did not overflow
459
        // Or overflow
460
        $current = $this->getEarliestItem($arrItems);
461
462
        return true;
463
    }
464
465
    //
466
    // Function:    getEarliestItem
467
    //
468
    // Description:    Retrieves the earliest item in a collection, e.g. the earliest minute or the earliest month.
469
    //
470
    // Parameters:    $arrItems            A reference to the collection in which to search.
471
    //                $afterItem            The highest index that is to be skipped.
472
    //
473
    final private function getEarliestItem($arrItems, $afterItem = false, $allowOverflow = true)
474
    {
475
        // If no filter is specified, return the earliest listed item.
476
        if ($afterItem === false) {
477
            reset($arrItems);
478
479
            return key($arrItems);
480
        }
481
        // Or parse until we passed $afterItem
482
        foreach ($arrItems as $key => $value) {
483
            if ($key > $afterItem) {
484
                return $key;
485
            }
486
        }
487
        // If still nothing found, we may have exhausted our options.
488
        if (!$allowOverflow) {
489
            return false;
490
        }
491
        reset($arrItems);
492
493
        return key($arrItems);
494
    }
495
496
    //
497
    // Function:    previous
498
    //
499
    // Description:    Acquires the first scheduled datetime before the provided one.
500
    //
501
    // Parameters:    $time                If a string interpreted as a datetime string in the YYYY-MM-DD HH:II format and other parameters ignored.
502
    //                                    If an array $minute, $hour, $day, $month and $year are passed as keys 0-4 and other parameters ignored.
503
    //                                    If a string, interpreted as unix time.
504
    //                                    If omitted or specified FALSE, defaults to the current time.
505
    //
506
    // Result:        An array with the following keys:
507
    //                0                    Previous scheduled minute
508
    //                1                    Previous scheduled hour
509
    //                2                    Previous scheduled date
510
    //                3                    Previous scheduled month
511
    //                4                    Previous scheduled year
512
    //
513
    final public function previous($time = false)
514
    {
515
        // Convert parameters to array datetime
516
        $arrDT = $this->dtFromParameters($time);
517
        while (1) {
518
            // Verify the current date is in range. If not, move into range and consider this the previous position
519
            if (!array_key_exists($arrDT[4], $this->_years)) {
520
                if (($arrDT[4] = $this->getLatestItem($this->_years, $arrDT[4], false)) === false) {
521
                    return false;
522
                }
523
                $arrDT[3] = $this->getLatestItem($this->_months);
524
                $arrDT[2] = $this->getLatestItem($this->_daysOfMonth);
525
                $arrDT[1] = $this->getLatestItem($this->_hours);
526
                $arrDT[0] = $this->getLatestItem($this->_minutes);
527
                break;
528
            } elseif (!array_key_exists($arrDT[3], $this->_months)) {
529
                $arrDT[3] = $this->getLatestItem($this->_months, $arrDT[3]);
530
                $arrDT[2] = $this->getLatestItem($this->_daysOfMonth);
531
                $arrDT[1] = $this->getLatestItem($this->_hours);
532
                $arrDT[0] = $this->getLatestItem($this->_minutes);
533
                break;
534
            } elseif (!array_key_exists($arrDT[2], $this->_daysOfMonth)) {
535
                $arrDT[2] = $this->getLatestItem($this->_daysOfMonth, $arrDT[2]);
536
                $arrDT[1] = $this->getLatestItem($this->_hours);
537
                $arrDT[0] = $this->getLatestItem($this->_minutes);
538
                break;
539
            } elseif (!array_key_exists($arrDT[1], $this->_hours)) {
540
                $arrDT[1] = $this->getLatestItem($this->_hours, $arrDT[1]);
541
                $arrDT[0] = $this->getLatestItem($this->_minutes);
542
                break;
543
            } elseif (!array_key_exists($arrDT[1], $this->_hours)) {
544
                $arrDT[0] = $this->getLatestItem($this->_minutes, $arrDT[0]);
545
                break;
546
            }
547
            // Recede minute, hour, date, month and year while overflowing.
548
            $daysInPreviousMonth = date('t', strtotime('-1 month', strtotime($arrDT[4].'-'.$arrDT[3])));
549
            if ($this->recedeItem($this->_minutes, 0, 59, $arrDT[0])) {
550
                if ($this->recedeItem($this->_hours, 0, 23, $arrDT[1])) {
551
                    if ($this->recedeItem($this->_daysOfMonth, 0, $daysInPreviousMonth, $arrDT[2])) {
552
                        if ($this->recedeItem($this->_months, 1, 12, $arrDT[3])) {
553
                            if ($this->recedeItem($this->_years, $this->RANGE_YEARS_MIN, $this->RANGE_YEARS_MAX, $arrDT[4])) {
554
                                return false;
555
                            }
556
                        }
557
                    }
558
                }
559
            }
560
            break;
561
        }
562
        // If Datetime now points to a day that is schedule then return.
563
        $dayOfWeek = date('w', strtotime($this->dtAsString($arrDT)));
564
        if (array_key_exists($dayOfWeek, $this->_daysOfWeek)) {
565
            return $arrDT;
566
        }
567
        // Otherwise move to next scheduled date
568
        return $this->previous($arrDT);
569
    }
570
571
    final public function previousAsString($time = false)
572
    {
573
        return $this->dtAsString($this->previous($time));
574
    }
575
576
    final public function previousAsTime($time = false)
577
    {
578
        return strtotime($this->dtAsString($this->previous($time)));
0 ignored issues
show
Bug introduced by
It seems like $this->dtAsString($this->previous($time)) can also be of type false; however, parameter $time of strtotime() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

578
        return strtotime(/** @scrutinizer ignore-type */ $this->dtAsString($this->previous($time)));
Loading history...
579
    }
580
581
    //
582
    // Function:    recedeItem
583
    //
584
    // Description:    Recedes the current item to the previous one (the previous minute, the previous hour, etc.).
585
    //
586
    // Parameters:    $arrItems            A reference to the collection in which to recede.
587
    //                $rangeMin            The lowest possible value for $current.
588
    //                $rangeMax            The highest possible value for $current
589
    //                $current            The index that is being decremented.
590
    //
591
    // Result:        FALSE if current did not overflow (reset back to the highest possible value). TRUE if it did.
592
    //
593
    final private function recedeItem($arrItems, $rangeMin, $rangeMax, &$current)
594
    {
595
        // Recede pointer
596
        $current--;
597
        // If still above highest, move to highest
598
        if ($current > $rangeMax) {
599
            $current = $this->getLatestItem($arrItems, $rangeMax + 1);
600
        }
601
        // Parse items until found or overflow
602
        for (; $current >= $rangeMin; $current--) {
603
            if (array_key_exists($current, $arrItems)) {
604
                return false;
605
            }
606
        } // We did not overflow
607
        // Or overflow
608
        $current = $this->getLatestItem($arrItems, $rangeMax + 1);
609
610
        return true;
611
    }
612
613
    //
614
    // Function:    getLatestItem
615
    //
616
    // Description:    Retrieves the latest item in a collection, e.g. the latest minute or the latest month.
617
    //
618
    // Parameters:    $arrItems            A reference to the collection in which to search.
619
    //                $beforeItem            The lowest index that is to be skipped.
620
    //
621
    final private function getLatestItem($arrItems, $beforeItem = false, $allowOverflow = true)
622
    {
623
        // If no filter is specified, return the latestlisted item.
624
        if ($beforeItem === false) {
625
            end($arrItems);
626
627
            return key($arrItems);
628
        }
629
        // Or parse until we passed $beforeItem
630
        end($arrItems);
631
        do {
632
            if (($key = key($arrItems)) < $beforeItem) {
633
                return $key;
634
            }
635
        } while (prev($arrItems));
636
        // If still nothing found, we may have exhausted our options.
637
        if (!$allowOverflow) {
638
            return false;
639
        }
640
        end($arrItems);
641
642
        return key($arrItems);
643
    }
644
645
    //
646
    // Function:
647
    //
648
    // Description:
649
    //
650
    // Parameters:
651
    //
652
    // Result:
653
    //
654
    final private function getClass($spec)
655
    {
656
        if (!$this->classIsSpecified($spec)) {
657
            return '0';
658
        }
659
        if ($this->classIsSingleFixed($spec)) {
660
            return '1';
661
        }
662
663
        return '2';
664
    }
665
666
    //
667
    // Function:
668
    //
669
    // Description:    Returns TRUE if the Cron Specification is specified. FALSE otherwise. This is true if the specification has more than one entry
670
    //                or is anything than the entire approved range ("*").
671
    //
672
    // Parameters:
673
    //
674
    // Result:
675
    //
676
    final private function classIsSpecified($spec)
677
    {
678
        if ($spec['elements'][0]['hasInterval'] == false) {
679
            return true;
680
        }
681
        if ($spec['elements'][0]['number1'] != $spec['rangeMin']) {
682
            return true;
683
        }
684
        if ($spec['elements'][0]['number2'] != $spec['rangeMax']) {
685
            return true;
686
        }
687
        if ($spec['elements'][0]['interval'] != 1) {
688
            return true;
689
        }
690
691
        return false;
692
    }
693
694
    //
695
    // Function:
696
    //
697
    // Description:    Returns TRUE if the Cron Specification is specified as a single value. FALSE otherwise. This is true only if there is only
698
    //                one entry and the entry is only a single number (e.g. "10")
699
    //
700
    // Parameters:
701
    //
702
    // Result:
703
    //
704
    final private function classIsSingleFixed($spec)
705
    {
706
        return (count($spec['elements']) == 1) && (!$spec['elements'][0]['hasInterval']);
707
    }
708
709
    final private function initLang($language = 'en')
710
    {
711
        switch ($language) {
712
            case 'en':
713
                $this->_lang['elemMin: at_the_hour'] = 'at the hour';
714
                $this->_lang['elemMin: after_the_hour_every_X_minute'] = 'every minute';
715
                $this->_lang['elemMin: after_the_hour_every_X_minute_plural'] = 'every @1 minutes';
716
                $this->_lang['elemMin: every_consecutive_minute'] = 'every consecutive minute';
717
                $this->_lang['elemMin: every_consecutive_minute_plural'] = 'every consecutive @1 minutes';
718
                $this->_lang['elemMin: every_minute'] = 'every minute';
719
                $this->_lang['elemMin: between_X_and_Y'] = 'from the @1 to the @2';
720
                $this->_lang['elemMin: at_X:Y'] = 'At @1:@2';
721
                $this->_lang['elemHour: past_X:00'] = 'past @1:00';
722
                $this->_lang['elemHour: between_X:00_and_Y:59'] = 'between @1:00 and @2:59';
723
                $this->_lang['elemHour: in_the_60_minutes_past_'] = 'in the 60 minutes past every consecutive hour';
724
                $this->_lang['elemHour: in_the_60_minutes_past__plural'] = 'in the 60 minutes past every consecutive @1 hours';
725
                $this->_lang['elemHour: past_every_consecutive_'] = 'past every consecutive hour';
726
                $this->_lang['elemHour: past_every_consecutive__plural'] = 'past every consecutive @1 hours';
727
                $this->_lang['elemHour: past_every_hour'] = 'past every hour';
728
                $this->_lang['elemDOM: the_X'] = 'the @1';
729
                $this->_lang['elemDOM: every_consecutive_day'] = 'every consecutive day';
730
                $this->_lang['elemDOM: every_consecutive_day_plural'] = 'every consecutive @1 days';
731
                $this->_lang['elemDOM: on_every_day'] = 'on every day';
732
                $this->_lang['elemDOM: between_the_Xth_and_Yth'] = 'between the @1 and the @2';
733
                $this->_lang['elemDOM: on_the_X'] = 'on the @1';
734
                $this->_lang['elemDOM: on_X'] = 'on @1';
735
                $this->_lang['elemMonth: every_X'] = 'every @1';
736
                $this->_lang['elemMonth: every_consecutive_month'] = 'every consecutive month';
737
                $this->_lang['elemMonth: every_consecutive_month_plural'] = 'every consecutive @1 months';
738
                $this->_lang['elemMonth: between_X_and_Y'] = 'from @1 to @2';
739
                $this->_lang['elemMonth: of_every_month'] = 'of every month';
740
                $this->_lang['elemMonth: during_every_X'] = 'during every @1';
741
                $this->_lang['elemMonth: during_X'] = 'during @1';
742
                $this->_lang['elemYear: in_X'] = 'in @1';
743
                $this->_lang['elemYear: every_consecutive_year'] = 'every consecutive year';
744
                $this->_lang['elemYear: every_consecutive_year_plural'] = 'every consecutive @1 years';
745
                $this->_lang['elemYear: from_X_through_Y'] = 'from @1 through @2';
746
                $this->_lang['elemDOW: on_every_day'] = 'on every day';
747
                $this->_lang['elemDOW: on_X'] = 'on @1';
748
                $this->_lang['elemDOW: but_only_on_X'] = 'but only if the event takes place on @1';
749
                $this->_lang['separator_and'] = 'and';
750
                $this->_lang['separator_or'] = 'or';
751
                $this->_lang['day: 0_plural'] = 'Sundays';
752
                $this->_lang['day: 1_plural'] = 'Mondays';
753
                $this->_lang['day: 2_plural'] = 'Tuesdays';
754
                $this->_lang['day: 3_plural'] = 'Wednesdays';
755
                $this->_lang['day: 4_plural'] = 'Thursdays';
756
                $this->_lang['day: 5_plural'] = 'Fridays';
757
                $this->_lang['day: 6_plural'] = 'Saturdays';
758
                $this->_lang['month: 1'] = 'January';
759
                $this->_lang['month: 2'] = 'February';
760
                $this->_lang['month: 3'] = 'March';
761
                $this->_lang['month: 4'] = 'April';
762
                $this->_lang['month: 5'] = 'May';
763
                $this->_lang['month: 6'] = 'June';
764
                $this->_lang['month: 7'] = 'July';
765
                $this->_lang['month: 8'] = 'Augustus';
766
                $this->_lang['month: 9'] = 'September';
767
                $this->_lang['month: 10'] = 'October';
768
                $this->_lang['month: 11'] = 'November';
769
                $this->_lang['month: 12'] = 'December';
770
                $this->_lang['ordinal: 1'] = '1st';
771
                $this->_lang['ordinal: 2'] = '2nd';
772
                $this->_lang['ordinal: 3'] = '3rd';
773
                $this->_lang['ordinal: 4'] = '4th';
774
                $this->_lang['ordinal: 5'] = '5th';
775
                $this->_lang['ordinal: 6'] = '6th';
776
                $this->_lang['ordinal: 7'] = '7th';
777
                $this->_lang['ordinal: 8'] = '8th';
778
                $this->_lang['ordinal: 9'] = '9th';
779
                $this->_lang['ordinal: 10'] = '10th';
780
                $this->_lang['ordinal: 11'] = '11th';
781
                $this->_lang['ordinal: 12'] = '12th';
782
                $this->_lang['ordinal: 13'] = '13th';
783
                $this->_lang['ordinal: 14'] = '14th';
784
                $this->_lang['ordinal: 15'] = '15th';
785
                $this->_lang['ordinal: 16'] = '16th';
786
                $this->_lang['ordinal: 17'] = '17th';
787
                $this->_lang['ordinal: 18'] = '18th';
788
                $this->_lang['ordinal: 19'] = '19th';
789
                $this->_lang['ordinal: 20'] = '20th';
790
                $this->_lang['ordinal: 21'] = '21st';
791
                $this->_lang['ordinal: 22'] = '22nd';
792
                $this->_lang['ordinal: 23'] = '23rd';
793
                $this->_lang['ordinal: 24'] = '24th';
794
                $this->_lang['ordinal: 25'] = '25th';
795
                $this->_lang['ordinal: 26'] = '26th';
796
                $this->_lang['ordinal: 27'] = '27th';
797
                $this->_lang['ordinal: 28'] = '28th';
798
                $this->_lang['ordinal: 29'] = '29th';
799
                $this->_lang['ordinal: 30'] = '30th';
800
                $this->_lang['ordinal: 31'] = '31st';
801
                $this->_lang['ordinal: 32'] = '32nd';
802
                $this->_lang['ordinal: 33'] = '33rd';
803
                $this->_lang['ordinal: 34'] = '34th';
804
                $this->_lang['ordinal: 35'] = '35th';
805
                $this->_lang['ordinal: 36'] = '36th';
806
                $this->_lang['ordinal: 37'] = '37th';
807
                $this->_lang['ordinal: 38'] = '38th';
808
                $this->_lang['ordinal: 39'] = '39th';
809
                $this->_lang['ordinal: 40'] = '40th';
810
                $this->_lang['ordinal: 41'] = '41st';
811
                $this->_lang['ordinal: 42'] = '42nd';
812
                $this->_lang['ordinal: 43'] = '43rd';
813
                $this->_lang['ordinal: 44'] = '44th';
814
                $this->_lang['ordinal: 45'] = '45th';
815
                $this->_lang['ordinal: 46'] = '46th';
816
                $this->_lang['ordinal: 47'] = '47th';
817
                $this->_lang['ordinal: 48'] = '48th';
818
                $this->_lang['ordinal: 49'] = '49th';
819
                $this->_lang['ordinal: 50'] = '50th';
820
                $this->_lang['ordinal: 51'] = '51st';
821
                $this->_lang['ordinal: 52'] = '52nd';
822
                $this->_lang['ordinal: 53'] = '53rd';
823
                $this->_lang['ordinal: 54'] = '54th';
824
                $this->_lang['ordinal: 55'] = '55th';
825
                $this->_lang['ordinal: 56'] = '56th';
826
                $this->_lang['ordinal: 57'] = '57th';
827
                $this->_lang['ordinal: 58'] = '58th';
828
                $this->_lang['ordinal: 59'] = '59th';
829
                break;
830
            case 'nl':
831
                $this->_lang['elemMin: at_the_hour'] = 'op het hele uur';
832
                $this->_lang['elemMin: after_the_hour_every_X_minute'] = 'elke minuut';
833
                $this->_lang['elemMin: after_the_hour_every_X_minute_plural'] = 'elke @1 minuten';
834
                $this->_lang['elemMin: every_consecutive_minute'] = 'elke opeenvolgende minuut';
835
                $this->_lang['elemMin: every_consecutive_minute_plural'] = 'elke opeenvolgende @1 minuten';
836
                $this->_lang['elemMin: every_minute'] = 'elke minuut';
837
                $this->_lang['elemMin: between_X_and_Y'] = 'van de @1 tot en met de @2';
838
                $this->_lang['elemMin: at_X:Y'] = 'Om @1:@2';
839
                $this->_lang['elemHour: past_X:00'] = 'na @1:00';
840
                $this->_lang['elemHour: between_X:00_and_Y:59'] = 'tussen @1:00 en @2:59';
841
                $this->_lang['elemHour: in_the_60_minutes_past_'] = 'in de 60 minuten na elk opeenvolgend uur';
842
                $this->_lang['elemHour: in_the_60_minutes_past__plural'] = 'in de 60 minuten na elke opeenvolgende @1 uren';
843
                $this->_lang['elemHour: past_every_consecutive_'] = 'na elk opeenvolgend uur';
844
                $this->_lang['elemHour: past_every_consecutive__plural'] = 'na elke opeenvolgende @1 uren';
845
                $this->_lang['elemHour: past_every_hour'] = 'na elk uur';
846
                $this->_lang['elemDOM: the_X'] = 'de @1';
847
                $this->_lang['elemDOM: every_consecutive_day'] = 'elke opeenvolgende dag';
848
                $this->_lang['elemDOM: every_consecutive_day_plural'] = 'elke opeenvolgende @1 dagen';
849
                $this->_lang['elemDOM: on_every_day'] = 'op elke dag';
850
                $this->_lang['elemDOM: between_the_Xth_and_Yth'] = 'tussen de @1 en de @2';
851
                $this->_lang['elemDOM: on_the_X'] = 'op de @1';
852
                $this->_lang['elemDOM: on_X'] = 'op @1';
853
                $this->_lang['elemMonth: every_X'] = 'elke @1';
854
                $this->_lang['elemMonth: every_consecutive_month'] = 'elke opeenvolgende maand';
855
                $this->_lang['elemMonth: every_consecutive_month_plural'] = 'elke opeenvolgende @1 maanden';
856
                $this->_lang['elemMonth: between_X_and_Y'] = 'van @1 tot @2';
857
                $this->_lang['elemMonth: of_every_month'] = 'van elke maand';
858
                $this->_lang['elemMonth: during_every_X'] = 'tijdens elke @1';
859
                $this->_lang['elemMonth: during_X'] = 'tijdens @1';
860
                $this->_lang['elemYear: in_X'] = 'in @1';
861
                $this->_lang['elemYear: every_consecutive_year'] = 'elk opeenvolgend jaar';
862
                $this->_lang['elemYear: every_consecutive_year_plural'] = 'elke opeenvolgende @1 jaren';
863
                $this->_lang['elemYear: from_X_through_Y'] = 'van @1 tot en met @2';
864
                $this->_lang['elemDOW: on_every_day'] = 'op elke dag';
865
                $this->_lang['elemDOW: on_X'] = 'op @1';
866
                $this->_lang['elemDOW: but_only_on_X'] = 'maar alleen als het plaatsvindt op @1';
867
                $this->_lang['separator_and'] = 'en';
868
                $this->_lang['separator_of'] = 'of';
869
                $this->_lang['day: 0_plural'] = 'zondagen';
870
                $this->_lang['day: 1_plural'] = 'maandagen';
871
                $this->_lang['day: 2_plural'] = 'dinsdagen';
872
                $this->_lang['day: 3_plural'] = 'woensdagen';
873
                $this->_lang['day: 4_plural'] = 'donderdagen';
874
                $this->_lang['day: 5_plural'] = 'vrijdagen';
875
                $this->_lang['day: 6_plural'] = 'zaterdagen';
876
                $this->_lang['month: 1'] = 'januari';
877
                $this->_lang['month: 2'] = 'februari';
878
                $this->_lang['month: 3'] = 'maart';
879
                $this->_lang['month: 4'] = 'april';
880
                $this->_lang['month: 5'] = 'mei';
881
                $this->_lang['month: 6'] = 'juni';
882
                $this->_lang['month: 7'] = 'juli';
883
                $this->_lang['month: 8'] = 'augustus';
884
                $this->_lang['month: 9'] = 'september';
885
                $this->_lang['month: 10'] = 'october';
886
                $this->_lang['month: 11'] = 'november';
887
                $this->_lang['month: 12'] = 'december';
888
                $this->_lang['ordinal: 1'] = '1e';
889
                $this->_lang['ordinal: 2'] = '2e';
890
                $this->_lang['ordinal: 3'] = '3e';
891
                $this->_lang['ordinal: 4'] = '4e';
892
                $this->_lang['ordinal: 5'] = '5e';
893
                $this->_lang['ordinal: 6'] = '6e';
894
                $this->_lang['ordinal: 7'] = '7e';
895
                $this->_lang['ordinal: 8'] = '8e';
896
                $this->_lang['ordinal: 9'] = '9e';
897
                $this->_lang['ordinal: 10'] = '10e';
898
                $this->_lang['ordinal: 11'] = '11e';
899
                $this->_lang['ordinal: 12'] = '12e';
900
                $this->_lang['ordinal: 13'] = '13e';
901
                $this->_lang['ordinal: 14'] = '14e';
902
                $this->_lang['ordinal: 15'] = '15e';
903
                $this->_lang['ordinal: 16'] = '16e';
904
                $this->_lang['ordinal: 17'] = '17e';
905
                $this->_lang['ordinal: 18'] = '18e';
906
                $this->_lang['ordinal: 19'] = '19e';
907
                $this->_lang['ordinal: 20'] = '20e';
908
                $this->_lang['ordinal: 21'] = '21e';
909
                $this->_lang['ordinal: 22'] = '22e';
910
                $this->_lang['ordinal: 23'] = '23e';
911
                $this->_lang['ordinal: 24'] = '24e';
912
                $this->_lang['ordinal: 25'] = '25e';
913
                $this->_lang['ordinal: 26'] = '26e';
914
                $this->_lang['ordinal: 27'] = '27e';
915
                $this->_lang['ordinal: 28'] = '28e';
916
                $this->_lang['ordinal: 29'] = '29e';
917
                $this->_lang['ordinal: 30'] = '30e';
918
                $this->_lang['ordinal: 31'] = '31e';
919
                $this->_lang['ordinal: 32'] = '32e';
920
                $this->_lang['ordinal: 33'] = '33e';
921
                $this->_lang['ordinal: 34'] = '34e';
922
                $this->_lang['ordinal: 35'] = '35e';
923
                $this->_lang['ordinal: 36'] = '36e';
924
                $this->_lang['ordinal: 37'] = '37e';
925
                $this->_lang['ordinal: 38'] = '38e';
926
                $this->_lang['ordinal: 39'] = '39e';
927
                $this->_lang['ordinal: 40'] = '40e';
928
                $this->_lang['ordinal: 41'] = '41e';
929
                $this->_lang['ordinal: 42'] = '42e';
930
                $this->_lang['ordinal: 43'] = '43e';
931
                $this->_lang['ordinal: 44'] = '44e';
932
                $this->_lang['ordinal: 45'] = '45e';
933
                $this->_lang['ordinal: 46'] = '46e';
934
                $this->_lang['ordinal: 47'] = '47e';
935
                $this->_lang['ordinal: 48'] = '48e';
936
                $this->_lang['ordinal: 49'] = '49e';
937
                $this->_lang['ordinal: 50'] = '50e';
938
                $this->_lang['ordinal: 51'] = '51e';
939
                $this->_lang['ordinal: 52'] = '52e';
940
                $this->_lang['ordinal: 53'] = '53e';
941
                $this->_lang['ordinal: 54'] = '54e';
942
                $this->_lang['ordinal: 55'] = '55e';
943
                $this->_lang['ordinal: 56'] = '56e';
944
                $this->_lang['ordinal: 57'] = '57e';
945
                $this->_lang['ordinal: 58'] = '58e';
946
                $this->_lang['ordinal: 59'] = '59e';
947
                break;
948
        }
949
    }
950
951
    final private function natlangPad2($number)
952
    {
953
        return (strlen($number) == 1 ? '0' : '').$number;
954
    }
955
956
    final private function natlangApply($id, $p1 = false, $p2 = false, $p3 = false, $p4 = false, $p5 = false, $p6 = false)
957
    {
958
        $txt = $this->_lang[$id];
959
        if ($p1 !== false) {
960
            $txt = str_replace('@1', $p1, $txt);
961
        }
962
        if ($p2 !== false) {
963
            $txt = str_replace('@2', $p2, $txt);
964
        }
965
        if ($p3 !== false) {
966
            $txt = str_replace('@3', $p3, $txt);
967
        }
968
        if ($p4 !== false) {
969
            $txt = str_replace('@4', $p4, $txt);
970
        }
971
        if ($p5 !== false) {
972
            $txt = str_replace('@5', $p5, $txt);
973
        }
974
        if ($p6 !== false) {
975
            $txt = str_replace('@6', $p6, $txt);
976
        }
977
978
        return $txt;
979
    }
980
981
    //
982
    // Function:    natlangRange
983
    //
984
    // Description:    Converts a range into natural language
985
    //
986
    // Parameters:
987
    //
988
    // Result:
989
    //
990
    final private function natlangRange($spec, $entryFunction, $p1 = false)
991
    {
992
        $arrIntervals = [];
993
        foreach ($spec['elements'] as $elem) {
994
            $arrIntervals[] = call_user_func($entryFunction, $elem, $p1);
995
        }
996
        $txt = '';
997
        for ($index = 0; $index < count($arrIntervals); $index++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
998
            $txt .= ($index == 0 ? '' : ($index == (count($arrIntervals) - 1) ? ' '.$this->natlangApply('separator_and').' ' : ', ')).$arrIntervals[$index];
999
        }
1000
1001
        return $txt;
1002
    }
1003
1004
    //
1005
    // Function:    natlangElementMinute
1006
    //
1007
    // Description:    Converts an entry from the minute specification to natural language.
1008
    //
1009
    final private function natlangElementMinute($elem)
1010
    {
1011
        if (!$elem['hasInterval']) {
1012
            if ($elem['number1'] == 0) {
1013
                return $this->natlangApply('elemMin: at_the_hour');
1014
            } else {
1015
                return $this->natlangApply('elemMin: after_the_hour_every_X_minute'.($elem['number1'] == 1 ? '' : '_plural'), $elem['number1']);
1016
            }
1017
        }
1018
        $txt = $this->natlangApply('elemMin: every_consecutive_minute'.($elem['interval'] == 1 ? '' : '_plural'), $elem['interval']);
1019
        if (($elem['number1'] != $this->_cronMinutes['rangeMin']) || ($elem['number2'] != $this->_cronMinutes['rangeMax'])) {
1020
            $txt .= ' ('.$this->natlangApply('elemMin: between_X_and_Y', $this->natlangApply('ordinal: '.$elem['number1']), $this->natlangApply('ordinal: '.$elem['number2'])).')';
1021
        }
1022
1023
        return $txt;
1024
    }
1025
1026
    //
1027
    // Function:    natlangElementHour
1028
    //
1029
    // Description:    Converts an entry from the hour specification to natural language.
1030
    //
1031
    final private function natlangElementHour($elem, $asBetween)
1032
    {
1033
        if (!$elem['hasInterval']) {
1034
            if ($asBetween) {
1035
                return $this->natlangApply('elemHour: between_X:00_and_Y:59', $this->natlangPad2($elem['number1']), $this->natlangPad2($elem['number1']));
1036
            } else {
1037
                return $this->natlangApply('elemHour: past_X:00', $this->natlangPad2($elem['number1']));
1038
            }
1039
        }
1040
        if ($asBetween) {
1041
            $txt = $this->natlangApply('elemHour: in_the_60_minutes_past_'.($elem['interval'] == 1 ? '' : '_plural'), $elem['interval']);
1042
        } else {
1043
            $txt = $this->natlangApply('elemHour: past_every_consecutive_'.($elem['interval'] == 1 ? '' : '_plural'), $elem['interval']);
1044
        }
1045
        if (($elem['number1'] != $this->_cronHours['rangeMin']) || ($elem['number2'] != $this->_cronHours['rangeMax'])) {
1046
            $txt .= ' ('.$this->natlangApply('elemHour: between_X:00_and_Y:59', $elem['number1'], $elem['number2']).')';
1047
        }
1048
1049
        return $txt;
1050
    }
1051
1052
    //
1053
    // Function:    natlangElementDayOfMonth
1054
    //
1055
    // Description:    Converts an entry from the day of month specification to natural language.
1056
    //
1057
    final private function natlangElementDayOfMonth($elem)
1058
    {
1059
        if (!$elem['hasInterval']) {
1060
            return $this->natlangApply('elemDOM: the_X', $this->natlangApply('ordinal: '.$elem['number1']));
1061
        }
1062
        $txt = $this->natlangApply('elemDOM: every_consecutive_day'.($elem['interval'] == 1 ? '' : '_plural'), $elem['interval']);
1063
        if (($elem['number1'] != $this->_cronHours['rangeMin']) || ($elem['number2'] != $this->_cronHours['rangeMax'])) {
1064
            $txt .= ' ('.$this->natlangApply('elemDOM: between_the_Xth_and_Yth', $this->natlangApply('ordinal: '.$elem['number1']), $this->natlangApply('ordinal: '.$elem['number2'])).')';
1065
        }
1066
1067
        return $txt;
1068
    }
1069
1070
    //
1071
    // Function:    natlangElementDayOfMonth
1072
    //
1073
    // Description:    Converts an entry from the month specification to natural language.
1074
    //
1075
    final private function natlangElementMonth($elem)
1076
    {
1077
        if (!$elem['hasInterval']) {
1078
            return $this->natlangApply('elemMonth: every_X', $this->natlangApply('month: '.$elem['number1']));
1079
        }
1080
        $txt = $this->natlangApply('elemMonth: every_consecutive_month'.($elem['interval'] == 1 ? '' : '_plural'), $elem['interval']);
1081
        if (($elem['number1'] != $this->_cronMonths['rangeMin']) || ($elem['number2'] != $this->_cronMonths['rangeMax'])) {
1082
            $txt .= ' ('.$this->natlangApply('elemMonth: between_X_and_Y', $this->natlangApply('month: '.$elem['number1']), $this->natlangApply('month: '.$elem['number2'])).')';
1083
        }
1084
1085
        return $txt;
1086
    }
1087
1088
    //
1089
    // Function:    natlangElementYear
1090
    //
1091
    // Description:    Converts an entry from the year specification to natural language.
1092
    //
1093
    final private function natlangElementYear($elem)
1094
    {
1095
        if (!$elem['hasInterval']) {
1096
            return $elem['number1'];
1097
        }
1098
        $txt = $this->natlangApply('elemYear: every_consecutive_year'.($elem['interval'] == 1 ? '' : '_plural'), $elem['interval']);
1099
        if (($elem['number1'] != $this->_cronMonths['rangeMin']) || ($elem['number2'] != $this->_cronMonths['rangeMax'])) {
1100
            $txt .= ' ('.$this->natlangApply('elemYear: from_X_through_Y', $elem['number1'], $elem['number2']).')';
1101
        }
1102
1103
        return $txt;
1104
    }
1105
1106
    //
1107
    // Function:    asNaturalLanguage
1108
    //
1109
    // Description:    Returns the current cron specification in natural language.
1110
    //
1111
    // Parameters:    None
1112
    //
1113
    // Result:        A string containing a natural language text.
1114
    //
1115
    final public function asNaturalLanguage()
1116
    {
1117
        $switchForceDateExplaination = false;
1118
        $switchDaysOfWeekAreExcluding = true;
1119
        // Generate Time String
1120
        $txtMinutes = [];
1121
        $txtMinutes[0] = $this->natlangApply('elemMin: every_minute');
1122
        $txtMinutes[1] = $this->natlangElementMinute($this->_cronMinutes['elements'][0]);
1123
        $txtMinutes[2] = $this->natlangRange($this->_cronMinutes, [$this, 'natlangElementMinute']);
1124
        $txtHours = [];
1125
        $txtHours[0] = $this->natlangApply('elemHour: past_every_hour');
1126
        $txtHours[1] = [];
1127
        $txtHours[1]['between'] = $this->natlangRange($this->_cronHours, [$this, 'natlangElementHour'], true);
1128
        $txtHours[1]['past'] = $this->natlangRange($this->_cronHours, [$this, 'natlangElementHour'], false);
1129
        $txtHours[2] = [];
1130
        $txtHours[2]['between'] = $this->natlangRange($this->_cronHours, [$this, 'natlangElementHour'], true);
1131
        $txtHours[2]['past'] = $this->natlangRange($this->_cronHours, [$this, 'natlangElementHour'], false);
1132
        $classMinutes = $this->getClass($this->_cronMinutes);
1133
        $classHours = $this->getClass($this->_cronHours);
1134
        switch ($classMinutes.$classHours) {
1135
            // Special case: Unspecified date + Unspecified month
1136
            //
1137
            // Rule: The language for unspecified fields is omitted if a more detailed field has already been explained.
1138
            //
1139
            // The minutes field always yields an explaination, at the very least in the form of 'every minute'. This rule states that if the
1140
            // hour is not specified, it can be omitted because 'every minute' is already sufficiently clear.
1141
            //
1142
            case '00':
1143
                $txtTime = $txtMinutes[0];
1144
                break;
1145
            // Special case: Fixed minutes and fixed hours
1146
            //
1147
            // The default writing would be something like 'every 20 minutes past 04:00', but the more common phrasing would be: At 04:20.
1148
            //
1149
            // We will switch ForceDateExplaination on, so that even a non-specified date yields an explaination (e.g. 'every day')
1150
            //
1151
            case '11':
1152
                $txtTime = $this->natlangApply('elemMin: at_X:Y', $this->natlangPad2($this->_cronHours['elements'][0]['number1']), $this->natlangPad2($this->_cronMinutes['elements'][0]['number1']));
1153
                $switchForceDateExplaination = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $switchForceDateExplaination is dead and can be removed.
Loading history...
1154
                break;
1155
            // Special case: Between :00 and :59
1156
            //
1157
            // If hours are specified, but minutes are not, then the minutes string will yield something like 'every minute'. We must the
1158
            // differentiate the hour specification because the minutes specification does not relate to all minutes past the hour, but only to
1159
            // those minutes between :00 and :59
1160
            //
1161
            // We will switch ForceDateExplaination on, so that even a non-specified date yields an explaination (e.g. 'every day')
1162
            //
1163
            case '01':
1164
            case '02':
1165
                $txtTime = $txtMinutes[$classMinutes].' '.$txtHours[$classHours]['between'];
1166
                $switchForceDateExplaination = true;
1167
                break;
1168
            // Special case: Past the hour
1169
            //
1170
            // If minutes are specified and hours are specified, then the specification of minutes is always limited to a maximum of 60 minutes
1171
            // and always applies to the minutes 'past the hour'.
1172
            //
1173
            // We will switch ForceDateExplaination on, so that even a non-specified date yields an explaination (e.g. 'every day')
1174
            //
1175
            case '12':
1176
            case '22':
1177
            case '21':
1178
                $txtTime = $txtMinutes[$classMinutes].' '.$txtHours[$classHours]['past'];
1179
                $switchForceDateExplaination = true;
1180
                break;
1181
            default:
1182
                $txtTime = $txtMinutes[$classMinutes].' '.$txtHours[$classHours];
1183
                break;
1184
        }
1185
        // Generate Date String
1186
        $txtDaysOfMonth = [];
1187
        $txtDaysOfMonth[0] = '';
1188
        $txtDaysOfMonth[1] = $this->natlangApply('elemDOM: on_the_X', $this->natlangApply('ordinal: '.$this->_cronDaysOfMonth['elements'][0]['number1']));
1189
        $txtDaysOfMonth[2] = $this->natlangApply('elemDOM: on_X', $this->natlangRange($this->_cronDaysOfMonth, [$this, 'natlangElementDayOfMonth']));
1190
        $txtMonths = [];
1191
        $txtMonths[0] = $this->natlangApply('elemMonth: of_every_month');
1192
        $txtMonths[1] = $this->natlangApply('elemMonth: during_every_X', $this->natlangApply('month: '.$this->_cronMonths['elements'][0]['number1']));
1193
        $txtMonths[2] = $this->natlangApply('elemMonth: during_X', $this->natlangRange($this->_cronMonths, [$this, 'natlangElementMonth']));
1194
        $classDaysOfMonth = $this->getClass($this->_cronDaysOfMonth);
1195
        $classMonths = $this->getClass($this->_cronMonths);
1196
        if ($classDaysOfMonth == '0') {
1197
            $switchDaysOfWeekAreExcluding = false;
1198
        }
1199
        switch ($classDaysOfMonth.$classMonths) {
1200
            // Special case: Unspecified date + Unspecified month
1201
            //
1202
            // Rule: The language for unspecified fields is omitted if a more detailed field has already been explained.
1203
            //
1204
            // The time fields always yield an explaination, at the very least in the form of 'every minute'. This rule states that if the date
1205
            // is not specified, it can be omitted because 'every minute' is already sufficiently clear.
1206
            //
1207
            // There are some time specifications that do not contain an 'every' reference, but reference a specific time of day. In those cases
1208
            // the date explaination is enforced.
1209
            //
1210
            case '00':
1211
                $txtDate = '';
1212
                break;
1213
            default:
1214
                $txtDate = ' '.$txtDaysOfMonth[$classDaysOfMonth].' '.$txtMonths[$classMonths];
1215
                break;
1216
        }
1217
        // Generate Year String
1218
        if ($this->_cronYears) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->_cronYears of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1219
            $txtYears = [];
1220
            $txtYears[0] = '';
1221
            $txtYears[1] = ' '.$this->natlangApply('elemYear: in_X', $this->_cronYears['elements'][0]['number1']);
1222
            $txtYears[2] = ' '.$this->natlangApply('elemYear: in_X', $this->natlangRange($this->_cronYears, [$this, 'natlangElementYear']));
1223
            $classYears = $this->getClass($this->_cronYears);
1224
            $txtYear = $txtYears[$classYears];
1225
        }
1226
        // Generate DaysOfWeek String
1227
        $collectDays = 0;
1228
        foreach ($this->_cronDaysOfWeek['elements'] as $elem) {
1229
            if ($elem['hasInterval']) {
1230
                for ($x = $elem['number1']; $x <= $elem['number2']; $x += $elem['interval']) {
1231
                    $collectDays |= pow(2, $x);
1232
                }
1233
            } else {
1234
                $collectDays |= pow(2, $elem['number1']);
1235
            }
1236
        }
1237
        if ($collectDays == 127) {    // * all days
1238
            if (!$switchDaysOfWeekAreExcluding) {
1239
                $txtDays = ' '.$this->natlangApply('elemDOM: on_every_day');
1240
            } else {
1241
                $txtDays = '';
1242
            }
1243
        } else {
1244
            $arrDays = [];
1245
            for ($x = 0; $x <= 6; $x++) {
1246
                if ($collectDays & pow(2, $x)) {
1247
                    $arrDays[] = $x;
1248
                }
1249
            }
1250
            $txtDays = '';
1251
            for ($index = 0; $index < count($arrDays); $index++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
1252
                $txtDays .= ($index == 0 ? '' : ($index == (count($arrDays) - 1) ? ' '.$this->natlangApply($switchDaysOfWeekAreExcluding ? 'separator_or' : 'separator_and').' ' : ', ')).$this->natlangApply('day: '.$arrDays[$index].'_plural');
1253
            }
1254
            if ($switchDaysOfWeekAreExcluding) {
1255
                $txtDays = ' '.$this->natlangApply('elemDOW: but_only_on_X', $txtDays);
1256
            } else {
1257
                $txtDays = ' '.$this->natlangApply('elemDOW: on_X', $txtDays);
1258
            }
1259
        }
1260
        $txtResult = ucfirst($txtTime).$txtDate.$txtDays;
1261
        if (isset($txtYear)) {
1262
            if ($switchDaysOfWeekAreExcluding) {
1263
                $txtResult = ucfirst($txtTime).$txtDate.$txtYear.$txtDays;
1264
            } else {
1265
                $txtResult = ucfirst($txtTime).$txtDate.$txtDays.$txtYear;
1266
            }
1267
        }
1268
1269
        return $txtResult.'.';
1270
    }
1271
}
1272