2008-11-26 99 views
44

我有一个cron“时间定义”来计算一个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) 

被执行,我有一个UNIX时间戳。

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

我正在使用PHP,但问题应该是相当语言不可知的。

[更新]

类“PHP Cron Parser”(由Ray建议的)来计算最后一次CRON作业应该被执行,而不是下一个时间。

为了更容易:在我的情况下,cron时间参数只是绝对的,单个数字或“*”。没有时间范围,也没有“*/5”间隔。

回答

23

这基本上与检查当前时间是否符合条件相反。所以像这样:

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

我可能写了一点倒退。而且,如果在每个主要中,如果不是做大于检查,而只是将当前时间等级递增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; 
} 
+0

dlamblin:你的第二个版本有循环不变吗?显然它正在做的是越来越接近结果,但我试图证明它的正确性我自己,我无法弄清楚什么是循环不变式 – eeeeaaii 2011-12-16 19:00:49

+1

猜猜看什么?没有循环不变 - **因为它不是一个循环!**它基本上是一系列goto语句伪装成循环。要证明这一点,请注意,你可以用do {...}替换while(true)while(false)。 – eeeeaaii 2011-12-16 21:06:03

+0

实际上没有,因为“continue”实际上跳转到循环的结尾,而不是开始。在java。所以你还是要说do {...;打破; } while(true) – eeeeaaii 2011-12-16 23:58:29

4

检查this out

它可以计算出下一次计划的作业应该根据给定的cron定义来运行。
+0

其实该类计算最后一次工作所致。我需要找到下一个工作要去的时间:( – BlaM 2008-11-26 17:48:34

8

任何有兴趣,这是我最后的PHP实现,这几乎等于dlamblin伪代码:

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>'; 
30

下面是基于dlamblin的伪代码PHP项目。

它可以计算CRON表达式的下一个运行日期,即CRON表达式的上一个运行日期,并确定CRON表达式是否与给定时间匹配。可以跳过此CRON表达式分析器完全实现CRON:范围

  1. 增量(例如*/12,3-59/15)
  2. 间隔(例如1-4,周一至周五,JAN-MAR)
  3. 列表(例如1,2,3 | JAN,MAR,DEC)
  4. 一个月的最后一天(如L)一个月
  5. 最后给出工作日(例如5L)的
  6. 第N个工作日给出一个月(例如3#2,1#1,MON#4)
  7. 最近的一次kday到本月的某一天(例如, 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(); 
6

使用此功能:

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

创建了JavaScript API F或者根据@dlamblin的想法计算下一次运行时间。支持秒和年。还没有设法完全测试它,所以期待错误,但让我知道,如果找到任何。

Repository链接:https://bitbucket.org/nevity/cronner

2

感谢张贴此代码。它确实帮助我,甚至6年后。

试图实现我发现了一个小错误。

date('i G j n w', $time)为分钟返回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填充。

,我最终只是改变由日期函数返回和删除原始值通过运行$time[0] = $time[0] + 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"; 
     } 
    } 
} 

?> 
1

我的答案不是唯一的。用Java编写的@BlaM答案只是一个复制品,因为PHP的日期和时间是从Java有点不同。

该程序假定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; 
    } 
}