Рассчитайте, когда будет выполняться задание 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 » (предложенный Рей) вычисляет время LAST Задача CRON должна была быть выполнена, а не в следующий раз.

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

0

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') . '

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

';

$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) . '

';
0
добавлено
Я просто понял, что забыл все о списках и диапазонах в моем примере кода.
добавлено автор dlamblin, источник
Следует отметить, что это работает только с кронами, которые не являются сложными, т.е. простой - 30 8 5 7 1, комплекс - * 2-4,8,10 * 7-8 *
добавлено автор buggedcom, источник

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

//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;
}
0
добавлено
@eeeeaaii Я забыл об этом блоке псевдо-иш-кода. Вы знаете, с чем это не справляется? Списки из нескольких значений или разделенных значений. Предполагается, что это подразумевается в объекте cron.
добавлено автор dlamblin, источник
dlamblin: ваша вторая версия имеет инвариант цикла? Очевидно, что он делает все ближе и ближе к результату. Но я пытаюсь доказать свою правильность самому себе, и я не могу понять, что такое инвариант цикла.
добавлено автор eeeeaaii, источник
Угадай, что? Нет никакого циклического инварианта - , потому что это не цикл! Это в основном серия операторов goto, маскирующихся как цикл. Чтобы это доказать, обратите внимание, что вы могли бы заменить while (true) на do {...} while (false).
добавлено автор eeeeaaii, источник
на самом деле нет, потому что «продолжить» фактически переходит к концу цикла, а не к началу. По крайней мере, в java. Так что вы все еще должны сказать, что {...; ломать; } while (true)
добавлено автор eeeeaaii, источник
ваш код «что-то вроде» работал безупречно для меня :) Но будьте осторожны, вы должны разбить цикл, если точное совпадение недоступно для предотвращения бесконечного цикла.
добавлено автор Mehmet Fide, источник
Спасибо за это, @dlamblin. Просто преобразовал его в T-SQL, и он работает как шарм.
добавлено автор groundh0g, источник

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

Он может рассчитать следующий раз, когда запланированное задание должно выполняться на основе данных определений cron.
0
добавлено
Фактически этот класс вычисляет последний раз, когда задание должно быть выполнено. Мне нужно найти следующий раз, когда задание должно быть выполнено :(
добавлено автор BlaM, источник

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

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

0
добавлено
Спасибо за публикацию! Очень полезно для нас, кто предпочитает Javascript.
добавлено автор Patrick Chu, источник

Мой ответ не уникален. Просто реплика ответа @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;
    }
}
0
добавлено

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

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

Применение:

<?php
if (parse_crontab('*/5 2 * * *')) {
   //should run cron
} else {
   //should not run cron
}
0
добавлено
Немного подробное объяснение может помочь
добавлено автор dlamblin, источник
Вы используете date ('i', time ()) , который добавляет начальный ноль, но вы проверяете его с помощью === , что приведет к чему-то вроде < code> (04 === 4) и, следовательно, всегда false. Изменение проверки на == отлично подходит для меня.
добавлено автор KittMedia, источник
это на самом деле супер блестящий .. он в основном принимает и частоту cron * 2 5 * 3 в качестве аргументов, разбивает метку timestamp на минута, час, день месяца, номер месяца, день недели , разбивает частоту cron, проверяет каждую часть частоты cron и заменяет ее соответствующим временем и сравнением. Вся эта вещь генерирует строку, которая в основном выглядит как (true) и (true) и (true) и ("4" === "2") и (true) , затем eval , чтобы вернуть boolean . Если boolean -
добавлено автор tester, источник
Вторая часть $ content должна быть «{$ time [$ k]} === $ 0", # digit, используя $ 0 вместо 0. Спасибо тестеру за указание, что этот ответ - разумный способ сделать это. Я мог бы забыть об этом иначе. Кроме того, спасибо за редактирование для удобства чтения.
добавлено автор Mnebuerquo, источник

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

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

  1. Увеличение диапазонов (например, */12, 3-59/15)
  2. Интервалы (например, 1-4, MON-FRI, JAN-MAR).
  3. Списки (например, 1,2,3 | JAN, MAR, DEC)
  4. Последний день месяца (например, L)
  5. Последний заданный рабочий день месяца (например, 5L)
  6. N-й день недели месяца (например, 3 # 2, 1 # 1, MON # 4)
  7. Ближайший будний день к данному дню месяца (например, 15 Вт, 1 Вт, 30 Вт).

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();
0
добавлено
Привет чувак. что здесь 5.3 зависит? нет шансов на поддержку 5.2.10?
добавлено автор onassar, источник
Это можно портировать на 5.2.x, но вам нужно будет удалить пространства имен, вызовы DateTime :: add() и ссылки DateInterval.
добавлено автор Michael Dowling, источник

Спасибо, что опубликовали этот код. Это определенно помогло мне, даже через 6 лет.

Пытаясь реализовать, я обнаружил небольшую ошибку.

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

Позже в коде он имеет модуль, равный 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";
        }
    }
}

?>
0
добавлено
Я написал это в полезный сценарий, который будет искать систему для хернов и создать повестку дня на весь день. Код здесь, если вы заинтересованы. github.com/bepstein/cron_agenda
добавлено автор epepepep, источник