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);