Рассчитать, когда задание 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" (предложен Рэем) вычисляет ПОСЛЕДНИЙ раз, когда Задание CRON должно было быть выполнено, а не в следующий раз.

Чтобы было проще: в моем случае параметры времени cron являются только абсолютными, одиночными числами или «*». Нет временных диапазонов и интервалов "*/5".


person BlaM    schedule 26.11.2008    source источник


Ответы (8)


Это в основном делает обратную проверку, соответствует ли текущее время условиям. так что-то вроде:

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

Я мог бы написать это немного назад. Также это может быть намного короче, если в каждом основном случае, если вместо проверки большего, чем вы, вы просто увеличиваете текущую временную оценку на единицу и устанавливаете меньшую временную оценку на 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;
}
person dlamblin    schedule 26.11.2008
comment
dlamblin: в вашей второй версии есть инвариант цикла? Очевидно, то, что он делает, становится все ближе и ближе к результату. Но я пытаюсь доказать себе его правильность и не могу понять, каким будет инвариант цикла. - person eeeeaaii; 16.12.2011
comment
Угадай, что? Не существует инварианта цикла -- потому что на самом деле это не цикл! По сути, это последовательность операторов goto, маскирующихся под цикл. Чтобы доказать это, обратите внимание, что вы можете заменить while(true) на do {...} while (false). - person eeeeaaii; 17.12.2011
comment
на самом деле нет, потому что continue на самом деле переходит в конец цикла, а не в начало. По крайней мере в джаве. Итак, вам все еще нужно сказать do {...; перерыв; } пока (правда) - person eeeeaaii; 17.12.2011
comment
ваш что-то вроде кода работал у меня безупречно :) Но будьте осторожны, вы должны разорвать цикл, если точное совпадение недоступно, чтобы предотвратить бесконечный цикл. - person Mehmet Fide; 26.04.2014
comment
@eeeeaaii Я забыл об этом блоке псевдо-кода. Знаете, с чем он не справляется? Списки из нескольких значений или разделенных значений. Это как бы предполагается в притворном объекте cron. - person dlamblin; 22.07.2014
comment
Спасибо за это, @dlamblin. Просто преобразовал его в T-SQL, и он работает как шарм. - person groundh0g; 16.12.2016

Вот проект PHP, основанный на псевдокоде dlamblin.

Он может рассчитать следующую дату выполнения выражения CRON, предыдущую дату выполнения выражения CRON и определить, соответствует ли выражение CRON заданному времени. Вы можете пропустить. Этот парсер выражений CRON полностью реализует CRON:

  1. Шаги диапазонов (например, */12, 3-59/15)
  2. Интервалы (например, 1-4, ПН-ПТ, ЯНВ-МАР)
  3. Списки (например, 1,2,3 | ЯНВ,МАР,ДЕКАБРЬ)
  4. Последний день месяца (например, L)
  5. Последний заданный день недели месяца (например, 5L)
  6. N-й день недели месяца (например, 3#2, 1#1, ПН#4)
  7. Ближайший день недели к данному дню месяца (например, 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();
person Michael Dowling    schedule 10.08.2010
comment
Эй чувак. что здесь зависит от 5.3? нет шансов на поддержку 5.2.10? - person onassar; 20.08.2010
comment
Это может быть перенесено в 5.2.x, но вам нужно будет удалить пространства имен, вызовы DateTime::add() и ссылки DateInterval. - person Michael Dowling; 25.08.2010

Кому интересно, вот моя окончательная реализация 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>';
person BlaM    schedule 27.11.2008
comment
Следует отметить, что это работает только с несложными cron, т.е. простые - 30 8 5 7 1, сложные - *2-4,8,10*7-8* - person buggedcom; 05.07.2010
comment
Я только что понял, что совсем забыл о списках и диапазонах в своем примере кода. - person dlamblin; 18.12.2011

Используйте эту функцию:

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
}
person diyism    schedule 20.04.2011
comment
Небольшое дополнительное объяснение может помочь - person dlamblin; 18.12.2011
comment
это на самом деле супер блестяще ... он в основном принимает timestamp и частоту cron * 2 5 * 3 в качестве аргументов, разбивает timestamp на minute, hour, day of month, month number, day of week, разбивает частоту cron, проверяет каждую часть частоты cron и заменяет ее соответствующим временем и сравнением. Все это создает строку, которая в основном выглядит как (true) and (true) and (true) and ("4"==="2") and (true), затем evals возвращает boolean. Если boolean равно true, предполагается, что cron запустится, иначе его можно проигнорировать. - person tester; 19.04.2013
comment
Вторая часть $content должна быть {$time[$k]} === $0, # цифра, используя $0 вместо 0. Спасибо тестировщику за указание, что этот ответ — умный способ сделать это. Иначе я мог бы не заметить этого. Кроме того, спасибо за редактирование для удобства чтения. - person Mnebuerquo; 02.04.2014
comment
Вы используете date('i', time()), который добавляет начальный ноль, но вы проверяете его с помощью ===, что приводит к чему-то вроде (04 === 4) и, следовательно, всегда ложно. Изменение чека на == у меня отлично работает. - person KittMedia; 15.06.2016

Проверьте это:

It can calculate the next time a scheduled job is supposed to be run based on the given cron definitions.
person Ray    schedule 26.11.2008
comment
На самом деле этот класс вычисляет последний раз, когда задание БЫЛО выполнено. Мне нужно найти в следующий раз, когда работа БУДЕТ сдана :( - person BlaM; 26.11.2008

Создан API javascript для расчета следующего времени выполнения на основе идеи @dlamblin. Поддерживает секунды и годы. Пока не удалось полностью протестировать его, поэтому ожидайте ошибок, но дайте мне знать, если найдете их.

Ссылка на репозиторий: https://bitbucket.org/nevity/cronner

person Tauri28    schedule 26.06.2014
comment
Спасибо за публикацию! Очень полезно для нас, кто предпочитает Javascript. - person Patrick Chu; 15.08.2015

Спасибо за публикацию этого кода. Это определенно помогло мне, даже спустя 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. Я не смог найти опцию без дополнений для команды даты. Я попытался возиться со строкой {$time[$k]} % $1 === 0 (например, изменить {$time[$k]} на ({$time[$k]}+0), но не смог заставить ее отбросить отступ 0 во время модуля.

Итак, в итоге я просто изменил исходное значение, возвращаемое функцией даты, и удалил 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";
        }
    }
}

?>
person epepepep    schedule 30.01.2015
comment
Я записал это в удобный скрипт, который будет искать в системе cron и создавать повестку дня. Код здесь, если вам интересно. github.com/bepstein/cron_agenda - person epepepep; 02.02.2015

Мой ответ не уникален. Просто копия ответа @BlaM, написанного на java, потому что дата и время 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;
    }
}
person Rash    schedule 14.05.2015