web-dev-qa-db-fra.com

Calculer quand un travail cron sera exécuté puis la prochaine fois

J'ai une "définition de temps" 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)

Et j'ai un horodatage Unix.

Existe-t-il un moyen évident de trouver (calculer) la prochaine fois (après cet horodatage donné) que le travail doit être exécuté?

J'utilise PHP, mais le problème devrait être assez indépendant du langage.

[Mettre à jour]

La classe " PHP Cron Parser " (suggérée par Ray) calcule la DERNIÈRE fois que la tâche CRON devait être exécutée, pas la prochaine fois.

Pour le rendre plus facile: Dans mon cas, les paramètres de temps cron ne sont que des nombres absolus, simples ou "*". Il n'y a pas de plages de temps et pas d'intervalles "*/5".

47
BlaM

Cela revient essentiellement à vérifier si l'heure actuelle correspond aux conditions. donc quelque chose comme:

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

J'aurais peut-être écrit cela un peu en arrière. En outre, il peut être beaucoup plus court si, dans chaque domaine principal, si au lieu de faire la vérification supérieure à, vous augmentez simplement la note de temps actuelle de un et définissez les notes de temps inférieures à 0, puis continuez; cependant, vous bouclerez beaucoup plus. Ainsi:

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

Voici un projet PHP basé sur le code psuedo de dlamblin.

Il peut calculer la date d'exécution suivante d'une expression CRON, la date d'exécution précédente d'une expression CRON et déterminer si une expression CRON correspond à une heure donnée. Vous pouvez ignorer Cet analyseur d'expression CRON implémente entièrement CRON:

  1. Incréments de plages (par exemple */12, 3-59/15)
  2. Intervalles (par exemple 1-4, MON-FRI, JAN-MAR)
  3. Listes (par exemple 1,2,3 | JAN, MAR, DEC)
  4. Dernier jour d'un mois (par exemple L)
  5. Dernier jour de la semaine donné d'un mois (par exemple 5L)
  6. Nième jour de la semaine donné d'un mois (par exemple 3 # 2, 1 # 1, MON # 4)
  7. Jour de semaine le plus proche d'un jour donné du mois (par exemple, 15 W, 1 W, 30 W)

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

Utilisation (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();
31
Michael Dowling

Pour toute personne intéressée, voici mon implémentation finale PHP, qui équivaut à peu près au pseudo-code 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>';
8
BlaM

Utilisez cette fonction:

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 C'est peut-être plus lisible:

<?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
}
6
diyism

API javascript créée pour calculer la prochaine exécution en fonction de l'idée de @dlamblin. Prend en charge les secondes et les années. Je n'ai pas encore réussi à le tester complètement, alors attendez-vous à des bugs, mais faites-moi savoir si vous en trouvez.

Lien vers le référentiel: https://bitbucket.org/nevity/cronner

4
Tauri28

Vérifiez ceci :

Il peut calculer la prochaine fois qu'un travail planifié doit être exécuté en fonction des définitions de cron données.
4
Ray

Merci d'avoir publié ce code. Cela m'a définitivement aidé, même 6 ans plus tard.

En essayant d'implémenter, j'ai trouvé un petit bug.

date('i G j n w', $time) retourne un entier rempli de 0 pour les minutes.

Plus loin dans le code, il fait un module sur cet entier rempli de 0. PHP ne semble pas gérer cela comme prévu.

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

Comme vous pouvez le voir, 08 % 5 renvoie 0, tandis que 8 % 5 renvoie la valeur attendue 3. Je n'ai pas trouvé d'option non complétée pour la commande date. J'ai essayé de jouer avec le {$time[$k]} % $1 === 0 ligne (comme changer {$time[$k]} à ({$time[$k]}+0), mais n'a pas réussi à supprimer le remplissage 0 pendant le module.

J'ai donc fini par changer la valeur d'origine renvoyée par la fonction date et j'ai supprimé le 0 en exécutant $time[0] = $time[0] + 0;.

Voici mon 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";
        }
    }
}

?>
2
epepepep

Ma réponse n'est pas unique. Juste une réplique de la réponse @BlaM écrite en Java parce que la date et l'heure de PHP sont un peu différentes de Java.

Ce programme suppose que l'expression CRON est simple. Il ne peut contenir que des chiffres ou *.

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