代码之家  ›  专栏  ›  技术社区  ›  Marcelo Cantos

计算下一次执行cron作业的时间

  •  46
  • Marcelo Cantos  · 技术社区  · 17 年前

    我有一个cron“时间定义”

    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)
    

    是否有一种明显的方法来查找(计算)下一次(在给定的时间戳之后)作业将要执行的时间?

    [更新]

    “班级” PHP Cron Parser

    更简单:在我的例子中,cron时间参数仅为绝对值、单个数字或“*”。没有时间范围,也没有“*/5”间隔。

    8 回复  |  直到 17 年前
        1
  •  32
  •   Michael Dowling    15 年前

    下面是一个基于dlamblin的psuedo代码的PHP项目。

    1. 范围增量(例如*/12、3-59/15)
    2. 列表(例如1,2,3 | 1月、3月、12月)
    3. 一个月的最后一天(如L)
    4. 一个月的最后一个给定工作日(例如5L)
    5. 一个月的第n个给定工作日(例如3#2、1#1、周一#4)
    6. 最接近当月某一天的工作日(例如15W、1W、30W)

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

    用法(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();
    
        2
  •  29
  •   dlamblin    17 年前

    这基本上与检查当前时间是否符合条件相反。比如:

    //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;
    }
    

    我可能写得有点反了。此外,如果在每个主if中,您只将当前时间等级增加1,并将较小的时间等级设置为0,而不是执行大于检查,则时间等级可能会短得多;然而,然后你将循环更多。像这样:

    //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;
    }
    
        3
  •  8
  •   Marcelo Cantos    17 年前

    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>';
    
        4
  •  6
  •   tester    13 年前

    使用此功能:

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

    编辑 也许这更具可读性:

    <?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};");
        }
    

    用法:

    <?php
    if (parse_crontab('*/5 2 * * *')) {
        // should run cron
    } else {
        // should not run cron
    }
    
        5
  •  4
  •   Ray    17 年前

    this out

    它可以根据给定的cron定义计算计划作业下一次应该运行的时间。
        6
  •  4
  •   Tauri28    12 年前

    基于@dlamblin思想创建了用于计算下一次运行时的javascript API。支持秒和年。还没有完全测试它,所以请期待错误,但如果发现任何,请告诉我。

    https://bitbucket.org/nevity/cronner

        7
  •  3
  •   epepepep    11 年前

    感谢您发布此代码。它确实帮助了我,即使是6年后。

    尝试实现时,我发现了一个小错误。

    date('i G j n w', $time) 返回分钟数的0填充整数。

    在代码的后面,它对0填充的整数进行模运算。PHP似乎没有像预期的那样处理这个问题。

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

    如你所见, 08 % 5 返回0,而 8 % 5 返回预期的3。我找不到date命令的非填充选项。我试着摆弄那张桌子 {$time[$k]} % $1 === 0 {$time[$k]} 到 ({$time[$k]}+0) ,但无法使其在模数转换期间丢弃0填充。

    因此,我最终只是更改了date函数返回的原始值,并通过运行删除了0 $time[0] = $time[0] + 0; .

    这是我的测验。

    <?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";
            }
        }
    }
    
    ?>
    
        8
  •  2
  •   Rash    11 年前

    该程序假定CRON表达式很简单。它只能包含数字或*。

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

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