3 define('IDX_MINUTE', 0);
6 define('IDX_MONTH', 3);
7 define('IDX_WEEKDAY', 4);
11 * tdCron v0.0.1 beta - CRON-Parser for PHP
13 * Copyright (c) 2010 Christian Land / tagdocs.de
15 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
16 * associated documentation files (the "Software"), to deal in the Software without restriction,
17 * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
18 * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
19 * subject to the following conditions:
21 * The above copyright notice and this permission notice shall be included in all copies or substantial
22 * portions of the Software.
24 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
25 * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
26 * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
27 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
28 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30 * @author Christian Land <devel@tagdocs.de>
32 * @copyright Copyright (c) 2010, Christian Land / tagdocs.de
33 * @version v0.0.1 beta
38 * Parsed cron-expressions cache.
41 static private $pcron = array();
44 * getNextOccurrence() uses a cron-expression to calculate the time and date at which a cronjob
45 * should be executed the next time. If a reference-time is passed, the next time and date
46 * after that time is calculated.
49 * @param string $expression cron-expression to use
50 * @param int $timestamp optional reference-time
53 static public function getNextOccurrence($expression, $timestamp = null) {
57 // Convert timestamp to array
59 $next = self::getTimestamp($timestamp);
61 // Calculate date/time
63 $next_time = self::calculateDateTime($expression, $next);
65 } catch (Exception $e) {
71 // return calculated time
78 * getLastOccurrence() does pretty much the same as getNextOccurrence(). The only difference
79 * is, that it doesn't calculate the next but the last time a cronjob should have been executed.
82 * @param string $expression cron-expression to use
83 * @param int $timestamp optional reference-time
87 static public function getLastOccurrence($expression, $timestamp = null) {
91 // Convert timestamp to array
93 $last = self::getTimestamp($timestamp);
95 // Calculate date/time
97 $last_time = self::calculateDateTime($expression, $last, false);
99 } catch (Exception $e) {
105 // return calculated time
112 * calculateDateTime() is the function where all the magic happens :-)
114 * It calculates the time and date at which the next/last call of a cronjob is/was due.
117 * @param mixed $value cron-expression
118 * @param mixed $rtime reference-time
119 * @param bool $next true = nextOccurence, false = lastOccurence
123 static private function calculateDateTime($expression, $rtime, $next = true) {
129 // Parse cron-expression (if neccessary)
131 $cron = self::getExpression($expression, !$next);
133 // OK, lets see if the day/month/weekday of the reference-date exist in our
136 if (!in_array($rtime[IDX_DAY], $cron[IDX_DAY]) ||
137 !in_array($rtime[IDX_MONTH], $cron[IDX_MONTH]) ||
138 !in_array($rtime[IDX_WEEKDAY], $cron[IDX_WEEKDAY])) {
140 // OK, things are easy. The day/month/weekday of the reference time
141 // can't be found in the $cron-array. This means that no matter what
142 // happens, we WILL end up at at a different date than that of our
143 // reference-time. And in this case, the lastOccurrence will ALWAYS
144 // happen at the latest possible time of the day and the nextOccurrence
145 // at the earliest possible time.
147 // In both cases, the time can be found in the first elements of the
148 // hour/minute cron-arrays.
150 $rtime[IDX_HOUR] = reset($cron[IDX_HOUR]);
151 $rtime[IDX_MINUTE] = reset($cron[IDX_MINUTE]);
155 // OK, things are getting a little bit more complicated...
157 $nhour = self::findValue($rtime[IDX_HOUR], $cron[IDX_HOUR], $next);
159 // Meh. Such a cruel world. Something has gone awry. Lets see HOW awry it went.
161 if ($nhour === false) { // Fix as per http://www.phpclasses.org/discuss/package/6699/thread/3/
163 // Ah, the hour-part went wrong. Thats easy. Wrong hour means that no
164 // matter what we do we'll end up at a different date. Thus we can use
165 // some simple operations to make things look pretty ;-)
167 // As alreasy mentioned before -> different date means earliest/latest
170 $rtime[IDX_HOUR] = reset($cron[IDX_HOUR]);
171 $rtime[IDX_MINUTE] = reset($cron[IDX_MINUTE]);
173 // Now all we have to do is add/subtract a day to get a new reference time
174 // to use later to find the right date. The following line probably looks
175 // a little odd but thats the easiest way of adding/substracting a day without
176 // screwing up the date. Just trust me on that one ;-)
178 $rtime = explode(',', strftime('%M,%H,%d,%m,%w,%Y', mktime($rtime[IDX_HOUR], $rtime[IDX_MINUTE], 0, $rtime[IDX_MONTH], $rtime[IDX_DAY], $rtime[IDX_YEAR]) + ((($next) ? 1 : -1) * 86400)));
182 // OK, there is a higher/lower hour available. Check the minutes-part.
184 $nminute = self::findValue($rtime[IDX_MINUTE], $cron[IDX_MINUTE], $next);
186 if ($nminute === false) {
188 // No matching minute-value found... lets see what happens if we substract/add an hour
190 $nhour = self::findValue($rtime[IDX_HOUR] + (($next) ? 1 : -1), $cron[IDX_HOUR], $next);
192 if ($nhour === false) {
194 // No more hours available... add/substract a day... you know what happens ;-)
196 $nminute = reset($cron[IDX_MINUTE]);
197 $nhour = reset($cron[IDX_HOUR]);
199 $rtime = explode(',', strftime('%M,%H,%d,%m,%w,%Y', mktime($nhour, $nminute, 0, $rtime[IDX_MONTH], $rtime[IDX_DAY], $rtime[IDX_YEAR]) + ((($next) ? 1 : -1) * 86400)));
203 // OK, there was another hour. Set the right minutes-value
205 $rtime[IDX_HOUR] = $nhour;
206 $rtime[IDX_MINUTE] = (($next) ? reset($cron[IDX_MINUTE]) : end($cron[IDX_MINUTE]));
214 // OK, there is a matching minute... reset minutes if hour has changed
216 if ($nhour <> $rtime[IDX_HOUR]) {
217 $nminute = reset($cron[IDX_MINUTE]);
222 $rtime[IDX_HOUR] = $nhour;
223 $rtime[IDX_MINUTE] = $nminute;
233 // If we have to calculate the date... we'll do so
237 if (in_array($rtime[IDX_DAY], $cron[IDX_DAY]) &&
238 in_array($rtime[IDX_MONTH], $cron[IDX_MONTH]) &&
239 in_array($rtime[IDX_WEEKDAY], $cron[IDX_WEEKDAY])) {
241 return mktime($rtime[1], $rtime[0], 0, $rtime[3], $rtime[2], $rtime[5]);
245 // OK, some searching necessary...
247 $cdate = mktime(0, 0, 0, $rtime[IDX_MONTH], $rtime[IDX_DAY], $rtime[IDX_YEAR]);
249 // OK, these three nested loops are responsible for finding the date...
251 // The class has 2 limitations/bugs right now:
253 // -> it doesn't work for dates in 2036 or later!
254 // -> it will most likely fail if you search for a Feburary, 29th with a given weekday
255 // (this does happen because the class only searches in the next/last 10 years! And
256 // while it usually takes less than 10 years for a "normal" date to iterate through
257 // all weekdays, it can take 20+ years for Feb, 29th to iterate through all weekdays!
259 for ($nyear = $rtime[IDX_YEAR];(($next) ? ($nyear <= $rtime[IDX_YEAR] + 10) : ($nyear >= $rtime[IDX_YEAR] -10));$nyear = $nyear + (($next) ? 1 : -1)) {
261 foreach ($cron[IDX_MONTH] as $nmonth) {
263 foreach ($cron[IDX_DAY] as $nday) {
265 if (checkdate($nmonth,$nday,$nyear)) {
267 $ndate = mktime(0,0,1,$nmonth,$nday,$nyear);
269 if (($next) ? ($ndate >= $cdate) : ($ndate <= $cdate)) {
271 $dow = date('w',$ndate);
273 // The date is "OK" - lets see if the weekday matches, too...
275 if (in_array($dow,$cron[IDX_WEEKDAY])) {
277 // WIN! :-) We found a valid date...
279 $rtime = explode(',', strftime('%M,%H,%d,%m,%w,%Y', mktime($rtime[IDX_HOUR], $rtime[IDX_MINUTE], 0, $nmonth, $nday, $nyear)));
281 return mktime($rtime[1], $rtime[0], 0, $rtime[3], $rtime[2], $rtime[5]);
297 throw new Exception('Failed to find date, No matching date found in a 10 years range!', 10004);
301 return mktime($rtime[1], $rtime[0], 0, $rtime[3], $rtime[2], $rtime[5]);
306 * getTimestamp() converts an unix-timestamp to an array. The returned array contains the following values:
315 * The array is used by various functions.
318 * @param int $timestamp If none is given, the current time is used
322 static private function getTimestamp($timestamp = null) {
324 if (is_null($timestamp)) {
325 $arr = explode(',', strftime('%M,%H,%d,%m,%w,%Y', time()));
327 $arr = explode(',', strftime('%M,%H,%d,%m,%w,%Y', $timestamp));
330 // Remove leading zeros (or we'll get in trouble ;-)
332 foreach ($arr as $key=>$value) {
333 $arr[$key] = (int)ltrim($value,'0');
341 * findValue() checks if the given value exists in an array. If it does not exist, the next
342 * higher/lower value is returned (depending on $next). If no higher/lower value exists,
352 static private function findValue($value, $data, $next = true) {
354 if (in_array($value, $data)) {
360 if (($next) ? ($value <= end($data)) : ($value >= end($data))) {
362 foreach ($data as $curval) {
364 if (($next) ? ($value <= (int)$curval) : ($curval <= $value)) {
381 * getExpression() returns a parsed cron-expression. Parsed cron-expressions are cached to reduce
382 * unneccessary calls of the parser.
385 * @param string $value
386 * @param bool $reverse
390 static private function getExpression($expression, $reverse=false) {
392 // First of all we cleanup the expression and remove all duplicate tabs/spaces/etc.
393 // For example "* * * * *" would be converted to "* * * * *", etc.
395 $expression = preg_replace('/(\s+)/', ' ', strtolower(trim($expression)));
397 // Lets see if we've already parsed that expression
399 if (!isset(self::$pcron[$expression])) {
405 self::$pcron[$expression] = tdCronEntry::parse($expression);
406 self::$pcron['reverse'][$expression] = self::arrayReverse(self::$pcron[$expression]);
408 } catch (Exception $e) {
416 return ($reverse ? self::$pcron['reverse'][$expression] : self::$pcron[$expression]);
421 * arrayReverse() reverses all sub-arrays of our cron array. The reversed values are used for calculations
422 * that are run when getLastOccurence() is called.
429 static private function arrayReverse($cron) {
431 foreach ($cron as $key=>$value) {
433 $cron[$key] = array_reverse($value);