2008-11-26 19 views
44

tengo una "definición de tiempo" croncuando una tarea programada se ejecutará entonces la próxima vez

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) 

y tengo una marca de tiempo Unix.

¿Existe una forma obvia de encontrar (calcular) la próxima vez (después de esa fecha y hora dada) que se va a ejecutar el trabajo?

Estoy usando PHP, pero el problema debería ser bastante independiente del lenguaje.

[Actualización]

La clase "PHP Cron Parser" (sugerido por Ray) calcula el tiempo pasado, el trabajo de cron se suponía que ser ejecutado, no la próxima vez.

Para hacerlo más fácil: en mi caso, los parámetros de tiempo cron son solo números absolutos, únicos o "*". No hay rangos de tiempo ni intervalos "*/5".

Respuesta

23

Esto es básicamente hacer el reverso de comprobar si la hora actual se ajusta a las condiciones. así que algo como:

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

que podría haber escrito que un poco hacia atrás. También puede ser mucho más corto si en cada main si en vez de hacer el check mayor, simplemente incrementa el grado de tiempo actual en uno y establece las calificaciones de tiempo menor en 0, luego continúa; sin embargo, entonces estarás repitiendo mucho más. De este modo:

//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: ¿su segunda versión tiene un bucle invariante? Obviamente, lo que está haciendo es acercarse cada vez más al resultado. Pero estoy tratando de demostrar su para mí mismo y no puedo entender qué sería el invariante de lazo. – eeeeaaii

+1

¿Adivina qué? No hay bucle invariante - ** ¡porque no es realmente un bucle! ** Básicamente es una serie de declaraciones goto enmascaradas como un bucle Para probar esto, tenga en cuenta que podría reemplazar el while (true) con un do {...} while (falso). – eeeeaaii

+0

en realidad no, porque "continuar" en realidad salta al final de un ciclo, no al principio. en java. Entonces todavía tienes que decir "do" ...; descanso; } while (true) – eeeeaaii

4

Comprobar this out:

Se puede calcular la próxima vez que se suponía un trabajo programado para ejecutarse en base a las definiciones dadas cron.
+0

En realidad esa clase calcula la última vez que el trabajo era debido. Necesito encontrar la próxima vez que el trabajo va a ser debido :( – BlaM

8

Para todos los interesados, aquí está mi aplicación PHP final, que más o menos igual a dlamblin pseudo código:

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>'; 
+1

Debe tenerse en cuenta que esto solo funciona con crons que no son complejos, es decir. simple - 30 8 5 7 1, complejo - * 2-4,8,10 * 7-8 * – buggedcom

+0

Me acabo de dar cuenta de que olvidé todo sobre listas y rangos en mi código de ejemplo. – dlamblin

30

Aquí es un proyecto PHP que se basa en pseudo código de dlamblin.

Puede calcular la siguiente fecha de ejecución de una expresión CRON, la fecha de ejecución anterior de una expresión CRON, y determinar si una expresión CRON coincide con un tiempo determinado. Puede omitir este analizador de expresiones CRON implementa totalmente CRON:

  1. Incrementos de rangos (por ejemplo, */12, 3-59/15)
  2. intervalos (por ejemplo, 1-4, MON-FRI, ENE-MAR)
  3. listas (por ejemplo, 1,2,3 | JAN, MAR, DEC)
  4. último día de un mes (por ejemplo, L)
  5. Ultimo día de la semana dada de un mes (por ejemplo, 5L)
  6. enésimo determinado día de la semana de un mes (por ejemplo, 3 # 2, 1 # 1, MON # 4)
  7. Lo más cerca posible kday a un día determinado del mes (p. 15W, 1W, 30W)

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

Uso (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

hey amigo. ¿Qué hay aquí 5.3 dependiente? no hay posibilidad de soporte 5.2.10? – onassar

+2

Esto podría transportarse a 5.2.x, pero necesitaría eliminar los espacios de nombres, las llamadas DateTime :: add() y las referencias de DateInterval. –

6

Utilice esta función:

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

Editar Tal vez esto es más fácil de leer:

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

Uso:

<?php 
if (parse_crontab('*/5 2 * * *')) { 
    // should run cron 
} else { 
    // should not run cron 
} 
+1

Una pequeña explicación adicional podría ayudar – dlamblin

+2

esto es súper brillante ... básicamente toma 'timestamp' y una frecuencia cron' * 2 5 * 3' como argumentos, divide 'timestamp' en' minute, hour, day of mes, número de mes, día de la semana', divide la frecuencia cron, verifica cada parte de la frecuencia cron y la reemplaza con el tiempo y la comparación apropiados. Todo esto genera una cadena que básicamente se ve como '(verdadero) y (verdadero) y (verdadero) y (" 4 "===" 2 ") y (verdadero)', luego 'eval's para devolver un' booleano'. Si 'boolean' es' true', se supone que el cron debe ejecutarse, de lo contrario puede ignorarse. – tester

+0

La segunda parte del contenido en $ debe ser "{$ time [$ k]} === $ 0", # dígito, usando $ 0 en lugar de 0. Gracias a Tester por señalar que esta respuesta es una forma inteligente de hacerlo. Podría haberlo pasado por alto de lo contrario. Además, gracias por la edición para la legibilidad. – Mnebuerquo

4

Creado Javascript API F o calculando el próximo tiempo de ejecución basado en la idea de @dlamblin. Admite segundos y años. Aún no he podido probarlo del todo, así que espere errores, pero avíseme si encuentra alguno.

enlace Repositorio: https://bitbucket.org/nevity/cronner

+0

¡Gracias por publicar esto! Muy útil para nosotros que preferimos Javascript. –

2

Gracias por publicar este código. Definitivamente me ayudó, incluso 6 años después.

Intentando implementar encontré un pequeño error.

date('i G j n w', $time) devuelve 0 enteros rellenos para los minutos.

Más adelante en el código, hace un módulo en ese 0 entero acolchado. PHP no parece manejar esto como se esperaba.

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

Como se puede ver, 08 % 5 devuelve 0, mientras que 8 % 5 devuelve la esperada 3. No pude encontrar una opción no acolchado para el comando date. He intentado tocar el violín con la línea {$time[$k]} % $1 === 0 (como cambiar {$time[$k]}-({$time[$k]}+0), pero no pude conseguir que deje caer el relleno de 0 durante el módulo.

Así, acabé cambiando el valor original devuelto por la función de fecha y eliminado del 0 al ejecutar $time[0] = $time[0] + 0;.

Aquí es mi prueba.

<?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

Escribí esto en un guión utilizable que buscará crones en el sistema y creará una agenda para ese día. El código está aquí si estás interesado. https://github.com/bepstein/cron_agenda – epepepep

1

mi respuesta no es única. Sólo una réplica de @BlaM respuesta escrita en Java, porque la fecha y hora de PHP es un poco diferente de Java.

Este programa asume que la expresión CRON es simple. Solo puede contener dígitos o *.

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

Código:

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