Question

I have a cron "time definition"

1 * * * * (every hour at xx:01)
2 5 * * * (every day at 05:02)
0 4 3 * * (every third day of the month at 04:00)
* 2 * * 5 (every minute between 02:00 and 02:59 on fridays)

And I have an unix timestamp.

Is there an obvious way to find (calculate) the next time (after that given timestamp) the job is due to be executed?

I'm using PHP, but the problem should be fairly language-agnostic.

[Update]

The class "PHP Cron Parser" (suggested by Ray) calculates the LAST time the CRON job was supposed to be executed, not the next time.

To make it easier: In my case the cron time parameters are only absolute, single numbers or "*". There are no time-ranges and no "*/5" intervals.

Was it helpful?

Solution

This is basically doing the reverse of checking if the current time fits the conditions. so something like:

//Totaly made up language
next = getTimeNow();
next.addMinutes(1) //so that next is never now
done = false;
while (!done) {
  if (cron.minute != '*' && next.minute != cron.minute) {
    if (next.minute > cron.minute) {
      next.addHours(1);
    }
    next.minute = cron.minute;
  }
  if (cron.hour != '*' && next.hour != cron.hour) {
    if (next.hour > cron.hour) {
      next.hour = cron.hour;
      next.addDays(1);
      next.minute = 0;
      continue;
    }
    next.hour = cron.hour;
    next.minute = 0;
    continue;
  }
  if (cron.weekday != '*' && next.weekday != cron.weekday) {
    deltaDays = cron.weekday - next.weekday //assume weekday is 0=sun, 1 ... 6=sat
    if (deltaDays < 0) { deltaDays+=7; }
    next.addDays(deltaDays);
    next.hour = 0;
    next.minute = 0;
    continue;
  }
  if (cron.day != '*' && next.day != cron.day) {
    if (next.day > cron.day || !next.month.hasDay(cron.day)) {
      next.addMonths(1);
      next.day = 1; //assume days 1..31
      next.hour = 0;
      next.minute = 0;
      continue;
    }
    next.day = cron.day
    next.hour = 0;
    next.minute = 0;
    continue;
  }
  if (cron.month != '*' && next.month != cron.month) {
    if (next.month > cron.month) {
      next.addMonths(12-next.month+cron.month)
      next.day = 1; //assume days 1..31
      next.hour = 0;
      next.minute = 0;
      continue;
    }
    next.month = cron.month;
    next.day = 1;
    next.hour = 0;
    next.minute = 0;
    continue;
  }
  done = true;
}

I might have written that a bit backwards. Also it can be a lot shorter if in every main if instead of doing the greater than check you merely increment the current time grade by one and set the lesser time grades to 0 then continue; however then you'll be looping a lot more. Like so:

//Shorter more loopy version
next = getTimeNow().addMinutes(1);
while (true) {
  if (cron.month != '*' && next.month != cron.month) {
    next.addMonths(1);
    next.day = 1;
    next.hour = 0;
    next.minute = 0;
    continue;
  }
  if (cron.day != '*' && next.day != cron.day) {
    next.addDays(1);
    next.hour = 0;
    next.minute = 0;
    continue;
  }
  if (cron.weekday != '*' && next.weekday != cron.weekday) {
    next.addDays(1);
    next.hour = 0;
    next.minute = 0;
    continue;
  }
  if (cron.hour != '*' && next.hour != cron.hour) {
    next.addHours(1);
    next.minute = 0;
    continue;
  }
  if (cron.minute != '*' && next.minute != cron.minute) {
    next.addMinutes(1);
    continue;
  }
  break;
}

OTHER TIPS

Here's a PHP project that is based on dlamblin's psuedo code.

It can calculate the next run date of a CRON expression, the previous run date of a CRON expression, and determine if a CRON expression matches a given time. You can skip This CRON expression parser fully implements CRON:

  1. Increments of ranges (e.g. */12, 3-59/15)
  2. Intervals (e.g. 1-4, MON-FRI, JAN-MAR )
  3. Lists (e.g. 1,2,3 | JAN,MAR,DEC)
  4. Last day of a month (e.g. L)
  5. Last given weekday of a month (e.g. 5L)
  6. Nth given weekday of a month (e.g. 3#2, 1#1, MON#4)
  7. Closest weekday to a given day of the month (e.g. 15W, 1W, 30W)

https://github.com/mtdowling/cron-expression

Usage (PHP 5.3+):

<?php

// Works with predefined scheduling definitions
$cron = Cron\CronExpression::factory('@daily');
$cron->isDue();
$cron->getNextRunDate();
$cron->getPreviousRunDate();

// Works with complex expressions
$cron = Cron\CronExpression::factory('15 2,6-12 */15 1 2-5');
$cron->getNextRunDate();

For anyone interested, here's my final PHP implementation, which pretty much equals dlamblin pseudo code:

class myMiniDate {
    var $myTimestamp;
    static private $dateComponent = array(
                                    'second' => 's',
                                    'minute' => 'i',
                                    'hour' => 'G',
                                    'day' => 'j',
                                    'month' => 'n',
                                    'year' => 'Y',
                                    'dow' => 'w',
                                    'timestamp' => 'U'
                                  );
    static private $weekday = array(
                                1 => 'monday',
                                2 => 'tuesday',
                                3 => 'wednesday',
                                4 => 'thursday',
                                5 => 'friday',
                                6 => 'saturday',
                                0 => 'sunday'
                              );

    function __construct($ts = NULL) { $this->myTimestamp = is_null($ts)?time():$ts; }

    function __set($var, $value) {
        list($c['second'], $c['minute'], $c['hour'], $c['day'], $c['month'], $c['year'], $c['dow']) = explode(' ', date('s i G j n Y w', $this->myTimestamp));
        switch ($var) {
            case 'dow':
                $this->myTimestamp = strtotime(self::$weekday[$value], $this->myTimestamp);
                break;

            case 'timestamp':
                $this->myTimestamp = $value;
                break;

            default:
                $c[$var] = $value;
                $this->myTimestamp = mktime($c['hour'], $c['minute'], $c['second'], $c['month'], $c['day'], $c['year']);
        }
    }


    function __get($var) {
        return date(self::$dateComponent[$var], $this->myTimestamp);
    }

    function modify($how) { return $this->myTimestamp = strtotime($how, $this->myTimestamp); }
}


$cron = new myMiniDate(time() + 60);
$cron->second = 0;
$done = 0;

echo date('Y-m-d H:i:s') . '<hr>' . date('Y-m-d H:i:s', $cron->timestamp) . '<hr>';

$Job = array(
            'Minute' => 5,
            'Hour' => 3,
            'Day' => 13,
            'Month' => null,
            'DOW' => 5,
       );

while ($done < 100) {
    if (!is_null($Job['Minute']) && ($cron->minute != $Job['Minute'])) {
        if ($cron->minute > $Job['Minute']) {
            $cron->modify('+1 hour');
        }
        $cron->minute = $Job['Minute'];
    }
    if (!is_null($Job['Hour']) && ($cron->hour != $Job['Hour'])) {
        if ($cron->hour > $Job['Hour']) {
            $cron->modify('+1 day');
        }
        $cron->hour = $Job['Hour'];
        $cron->minute = 0;
    }
    if (!is_null($Job['DOW']) && ($cron->dow != $Job['DOW'])) {
        $cron->dow = $Job['DOW'];
        $cron->hour = 0;
        $cron->minute = 0;
    }
    if (!is_null($Job['Day']) && ($cron->day != $Job['Day'])) {
        if ($cron->day > $Job['Day']) {
            $cron->modify('+1 month');
        }
        $cron->day = $Job['Day'];
        $cron->hour = 0;
        $cron->minute = 0;
    }
    if (!is_null($Job['Month']) && ($cron->month != $Job['Month'])) {
        if ($cron->month > $Job['Month']) {
            $cron->modify('+1 year');
        }
        $cron->month = $Job['Month'];
        $cron->day = 1;
        $cron->hour = 0;
        $cron->minute = 0;
    }

    $done = (is_null($Job['Minute']) || $Job['Minute'] == $cron->minute) &&
            (is_null($Job['Hour']) || $Job['Hour'] == $cron->hour) &&
            (is_null($Job['Day']) || $Job['Day'] == $cron->day) &&
            (is_null($Job['Month']) || $Job['Month'] == $cron->month) &&
            (is_null($Job['DOW']) || $Job['DOW'] == $cron->dow)?100:($done+1);
}

echo date('Y-m-d H:i:s', $cron->timestamp) . '<hr>';

Use this function:

function parse_crontab($time, $crontab)
         {$time=explode(' ', date('i G j n w', strtotime($time)));
          $crontab=explode(' ', $crontab);
          foreach ($crontab as $k=>&$v)
                  {$v=explode(',', $v);
                   foreach ($v as &$v1)
                           {$v1=preg_replace(array('/^\*$/', '/^\d+$/', '/^(\d+)\-(\d+)$/', '/^\*\/(\d+)$/'),
                                             array('true', '"'.$time[$k].'"==="\0"', '(\1<='.$time[$k].' and '.$time[$k].'<=\2)', $time[$k].'%\1===0'),
                                             $v1
                                            );
                           }
                   $v='('.implode(' or ', $v).')';
                  }
          $crontab=implode(' and ', $crontab);
          return eval('return '.$crontab.';');
         }
var_export(parse_crontab('2011-05-04 02:08:03', '*/2,3-5,9 2 3-5 */2 *'));
var_export(parse_crontab('2011-05-04 02:08:03', '*/8 */2 */4 */5 *'));

Edit Maybe this is more readable:

<?php

    function parse_crontab($frequency='* * * * *', $time=false) {
        $time = is_string($time) ? strtotime($time) : time();
        $time = explode(' ', date('i G j n w', $time));
        $crontab = explode(' ', $frequency);
        foreach ($crontab as $k => &$v) {
            $v = explode(',', $v);
            $regexps = array(
                '/^\*$/', # every 
                '/^\d+$/', # digit 
                '/^(\d+)\-(\d+)$/', # range
                '/^\*\/(\d+)$/' # every digit
            );
            $content = array(
                "true", # every
                "{$time[$k]} === 0", # digit
                "($1 <= {$time[$k]} && {$time[$k]} <= $2)", # range
                "{$time[$k]} % $1 === 0" # every digit
            );
            foreach ($v as &$v1)
                $v1 = preg_replace($regexps, $content, $v1);
            $v = '('.implode(' || ', $v).')';
        }
        $crontab = implode(' && ', $crontab);
        return eval("return {$crontab};");
    }

Usage:

<?php
if (parse_crontab('*/5 2 * * *')) {
    // should run cron
} else {
    // should not run cron
}

Check this out:

It can calculate the next time a scheduled job is supposed to be run based on the given cron definitions.

Created javascript API for calculating next run time based on @dlamblin idea. Supports seconds and years. Have not managed to test it fully yet so expect bugs but let me know if find any.

Repository link: https://bitbucket.org/nevity/cronner

Thanks for posting this code. It definitely helped me out, even 6 years later.

Trying to implement I found a small bug.

date('i G j n w', $time) returns a 0 padded integer for the minutes.

Later in the code, it does a modulus on that 0 padded integer. PHP doesn't seem to handle this as expected.

$ php
<?php
print 8 % 5 . "\n";
print 08 % 5 . "\n";
?>
3
0

As you can see, 08 % 5 returns 0, whereas 8 % 5 returns the expected 3. I couldn't find a non padded option for the date command. I tried fiddling with the {$time[$k]} % $1 === 0 line (like changing {$time[$k]} to ({$time[$k]}+0), but couldn't get it to drop the 0 padding during the modulus.

So, I ended up just changing the original value returned by the date function and removed the 0 by running $time[0] = $time[0] + 0;.

Here is my test.

<?php

function parse_crontab($frequency='* * * * *', $time=false) {
    $time = is_string($time) ? strtotime($time) : time();
    $time = explode(' ', date('i G j n w', $time));
    $time[0] = $time[0] + 0;
    $crontab = explode(' ', $frequency);
    foreach ($crontab as $k => &$v) {
        $v = explode(',', $v);
        $regexps = array(
            '/^\*$/', # every 
            '/^\d+$/', # digit 
            '/^(\d+)\-(\d+)$/', # range
            '/^\*\/(\d+)$/' # every digit
        );
        $content = array(
            "true", # every
            "{$time[$k]} === $0", # digit
            "($1 <= {$time[$k]} && {$time[$k]} <= $2)", # range
            "{$time[$k]} % $1 === 0" # every digit
        );
        foreach ($v as &$v1)
            $v1 = preg_replace($regexps, $content, $v1);
            $v = '('.implode(' || ', $v).')';
    }
    $crontab = implode(' && ', $crontab);
    return eval("return {$crontab};");
}

for($i=0; $i<24; $i++) {
    for($j=0; $j<60; $j++) {
        $date=sprintf("%d:%02d",$i,$j);
        if (parse_crontab('*/5 * * * *',$date)) {
             print "$date yes\n";
        } else {
             print "$date no\n";
        }
    }
}

?>

My answer is not unique. Just a replica of @BlaM answer written in java because PHP's date and time is a bit different from Java.

This program assumes that the CRON expression is simple. It can only contain digits or *.

Minute = 0-60
Hour = 0-23
Day = 1-31
MONTH = 1-12 where 1 = January.
WEEKDAY = 1-7 where 1 = Sunday.

Code:

package main;

import java.util.Calendar;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class CronPredict
{
    public static void main(String[] args)
    {
        String cronExpression = "5 3 27 3 3 ls -la > a.txt";
        CronPredict cronPredict = new CronPredict();
        String[] parsed = cronPredict.parseCronExpression(cronExpression);
        System.out.println(cronPredict.getNextExecution(parsed).getTime().toString());
    }

    //This method takes a cron string and separates entities like minutes, hours, etc.
    public String[] parseCronExpression(String cronExpression)
    {
        String[] parsedExpression = null;
        String cronPattern = "^([0-9]|[1-5][0-9]|\\*)\\s([0-9]|1[0-9]|2[0-3]|\\*)\\s"
                        + "([1-9]|[1-2][0-9]|3[0-1]|\\*)\\s([1-9]|1[0-2]|\\*)\\s"
                        + "([1-7]|\\*)\\s(.*)$";
        Pattern cronRegex = Pattern.compile(cronPattern);

        Matcher matcher = cronRegex.matcher(cronExpression);
        if(matcher.matches())
        {
            String minute = matcher.group(1);
            String hour = matcher.group(2);
            String day = matcher.group(3);
            String month = matcher.group(4);
            String weekday = matcher.group(5);
            String command = matcher.group(6);

            parsedExpression = new String[6];
            parsedExpression[0] = minute;
            parsedExpression[1] = hour;
            parsedExpression[2] = day;
            //since java's month start's from 0 as opposed to PHP which starts from 1.
            parsedExpression[3] = month.equals("*") ? month : (Integer.parseInt(month) - 1) + "";
            parsedExpression[4] = weekday;
            parsedExpression[5] = command;
        }

        return parsedExpression;
    }

    public Calendar getNextExecution(String[] job)
    {
        Calendar cron = Calendar.getInstance();
        cron.add(Calendar.MINUTE, 1);
        cron.set(Calendar.MILLISECOND, 0);
        cron.set(Calendar.SECOND, 0);

        int done = 0;
        //Loop because some dates are not valid.
        //e.g. March 29 which is a Friday may never come for atleast next 1000 years.
        //We do not want to keep looping. Also it protects against invalid dates such as feb 30.
        while(done < 100)
        {
            if(!job[0].equals("*") && cron.get(Calendar.MINUTE) != Integer.parseInt(job[0]))
            {
                if(cron.get(Calendar.MINUTE) > Integer.parseInt(job[0]))
                {
                    cron.add(Calendar.HOUR_OF_DAY, 1);
                }
                cron.set(Calendar.MINUTE, Integer.parseInt(job[0]));
            }

            if(!job[1].equals("*") && cron.get(Calendar.HOUR_OF_DAY) != Integer.parseInt(job[1]))
            {
                if(cron.get(Calendar.HOUR_OF_DAY) > Integer.parseInt(job[1]))
                {
                    cron.add(Calendar.DAY_OF_MONTH, 1);
                }
                cron.set(Calendar.HOUR_OF_DAY, Integer.parseInt(job[1]));
                cron.set(Calendar.MINUTE, 0);
            }

            if(!job[4].equals("*") && cron.get(Calendar.DAY_OF_WEEK) != Integer.parseInt(job[4]))
            {
                Date previousDate = cron.getTime();
                cron.set(Calendar.DAY_OF_WEEK, Integer.parseInt(job[4]));
                Date newDate = cron.getTime();

                if(newDate.before(previousDate))
                {
                    cron.add(Calendar.WEEK_OF_MONTH, 1);
                }

                cron.set(Calendar.HOUR_OF_DAY, 0);
                cron.set(Calendar.MINUTE, 0);
            }

            if(!job[2].equals("*") && cron.get(Calendar.DAY_OF_MONTH) != Integer.parseInt(job[2]))
            {
                if(cron.get(Calendar.DAY_OF_MONTH) > Integer.parseInt(job[2]))
                {
                    cron.add(Calendar.MONTH, 1);
                }
                cron.set(Calendar.DAY_OF_MONTH, Integer.parseInt(job[2]));
                cron.set(Calendar.HOUR_OF_DAY, 0);
                cron.set(Calendar.MINUTE, 0);
            }

            if(!job[3].equals("*") && cron.get(Calendar.MONTH) != Integer.parseInt(job[3]))
            {
                if(cron.get(Calendar.MONTH) > Integer.parseInt(job[3]))
                {
                    cron.add(Calendar.YEAR, 1);
                }
                cron.set(Calendar.MONTH, Integer.parseInt(job[3]));
                cron.set(Calendar.DAY_OF_MONTH, 1);
                cron.set(Calendar.HOUR_OF_DAY, 0);
                cron.set(Calendar.MINUTE, 0);
            }

            done =  (job[0].equals("*") || cron.get(Calendar.MINUTE) == Integer.parseInt(job[0])) &&
                    (job[1].equals("*") || cron.get(Calendar.HOUR_OF_DAY) == Integer.parseInt(job[1])) &&
                    (job[2].equals("*") || cron.get(Calendar.DAY_OF_MONTH) == Integer.parseInt(job[2])) &&
                    (job[3].equals("*") || cron.get(Calendar.MONTH) == Integer.parseInt(job[3])) &&
                    (job[4].equals("*") || cron.get(Calendar.DAY_OF_WEEK) == Integer.parseInt(job[4])) ? 100 : (done + 1);
        }

        return cron;
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top