Calculate when a cron job will be executed then next time

后端 未结 8 908
庸人自扰
庸人自扰 2020-12-02 08:19

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          


        
相关标签:
8条回答
  • 2020-12-02 08:45

    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";
            }
        }
    }
    
    ?>
    
    0 讨论(0)
  • 2020-12-02 08:48

    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
    }
    
    0 讨论(0)
  • 2020-12-02 08:49

    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

    0 讨论(0)
  • 2020-12-02 08:52

    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();
    
    0 讨论(0)
  • 2020-12-02 08:56

    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;
        }
    }
    
    0 讨论(0)
  • 2020-12-02 08:58

    Check this out:

    It can calculate the next time a scheduled job is supposed to be run based on the given cron definitions.
    0 讨论(0)
提交回复
热议问题