Ferramentas / Validador de expressão cron

Validador de expressão cron

Digite uma expressão cron e veja se ela é válida, o que ela significa e quando será executada nas próximas 5 vezes.

Processamento local: nada do que você digita sai do seu navegador.

Como usar

Digite os 5 campos separados por espaço: minuto (0-59), hora (0-23), dia do mês (1-31), mês (1-12 ou JAN-DEC) e dia da semana (0-7 ou SUN-SAT, sendo 0 e 7 domingo). Em cada campo vale * (todos), listas (1,15), faixas (9-18) e passos (*/15 ou 10-30/5). Também aceitamos atalhos como @daily e @hourly.

Detalhes importantes

  • Se o dia do mês e o dia da semana estiverem ambos preenchidos (sem *), o cron padrão executa quando qualquer um dos dois bater, não os dois ao mesmo tempo.
  • As próximas execuções são calculadas no fuso horário do seu navegador. O servidor onde o cron roda pode usar outro fuso, geralmente UTC.
  • Esta ferramenta cobre o formato clássico de 5 campos. Extensões como segundos, ano, L, W ou # (Quartz, Spring, AWS) não são suportadas.

Como implementar em código

Exemplos prontos para usar no seu projeto.

const FAIXAS = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 7]]; // min, hora, dia, mês, dia da semana

function cronValido(expr) {
  const campos = expr.trim().split(/\s+/);
  if (campos.length !== 5) return false;
  return campos.every((campo, i) => campo.split(',').every(parte => {
    const m = /^(?:\*|(\d+)(?:-(\d+))?)(?:\/(\d+))?$/.exec(parte);
    if (!m) return false;
    if (m[3] !== undefined && +m[3] < 1) return false;               // passo >= 1
    if (m[1] === undefined) return true;                              // "*" ou "*/n"
    const a = +m[1], b = m[2] === undefined ? a : +m[2];
    return a >= FAIXAS[i][0] && b <= FAIXAS[i][1] && a <= b;
  }));
}

console.log(cronValido('*/15 9-18 * * 1-5')); // true
console.log(cronValido('60 * * * *'));        // false (minuto > 59)

Não cobre nomes (JAN, MON) nem atalhos (@daily). Para calcular execuções use a lib cron-parser.

import re

FAIXAS = [(0, 59), (0, 23), (1, 31), (1, 12), (0, 7)]  # min, hora, dia, mês, dia da semana
PARTE = re.compile(r"^(?:\*|(\d+)(?:-(\d+))?)(?:/(\d+))?$")

def cron_valido(expr: str) -> bool:
    campos = expr.split()
    if len(campos) != 5:
        return False
    for campo, (mn, mx) in zip(campos, FAIXAS):
        for parte in campo.split(","):
            m = PARTE.match(parte)
            if not m:
                return False
            ini, fim, passo = m.groups()
            if passo is not None and int(passo) < 1:
                return False
            if ini is not None:
                a = int(ini)
                b = int(fim) if fim is not None else a
                if a < mn or b > mx or a > b:
                    return False
    return True

print(cron_valido("*/15 9-18 * * 1-5"))  # True
print(cron_valido("60 * * * *"))         # False

Para calcular as próximas execuções use o pacote croniter.

package main

import (
	"fmt"
	"regexp"
	"strconv"
	"strings"
)

var faixas = [5][2]int{{0, 59}, {0, 23}, {1, 31}, {1, 12}, {0, 7}} // min, hora, dia, mês, dia da semana
var parteRe = regexp.MustCompile(`^(?:\*|(\d+)(?:-(\d+))?)(?:/(\d+))?$`)

func cronValido(expr string) bool {
	campos := strings.Fields(expr)
	if len(campos) != 5 {
		return false
	}
	for i, campo := range campos {
		for _, parte := range strings.Split(campo, ",") {
			m := parteRe.FindStringSubmatch(parte)
			if m == nil {
				return false
			}
			if m[3] != "" {
				if p, _ := strconv.Atoi(m[3]); p < 1 {
					return false
				}
			}
			if m[1] != "" {
				a, _ := strconv.Atoi(m[1])
				b := a
				if m[2] != "" {
					b, _ = strconv.Atoi(m[2])
				}
				if a < faixas[i][0] || b > faixas[i][1] || a > b {
					return false
				}
			}
		}
	}
	return true
}

func main() {
	fmt.Println(cronValido("*/15 9-18 * * 1-5")) // true
	fmt.Println(cronValido("60 * * * *"))        // false
}

Para analisar e calcular execuções use github.com/robfig/cron/v3 (cron.ParseStandard).

const faixas = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 7]]; // min, hora, dia, mês, dia da semana
final parteRe = RegExp(r'^(?:\*|(\d+)(?:-(\d+))?)(?:/(\d+))?$');

bool cronValido(String expr) {
  final campos = expr.trim().split(RegExp(r'\s+'));
  if (campos.length != 5) return false;
  for (var i = 0; i < 5; i++) {
    for (final parte in campos[i].split(',')) {
      final m = parteRe.firstMatch(parte);
      if (m == null) return false;
      if (m.group(3) != null && int.parse(m.group(3)!) < 1) return false;
      if (m.group(1) != null) {
        final a = int.parse(m.group(1)!);
        final b = m.group(2) != null ? int.parse(m.group(2)!) : a;
        if (a < faixas[i][0] || b > faixas[i][1] || a > b) return false;
      }
    }
  }
  return true;
}

void main() {
  print(cronValido('*/15 9-18 * * 1-5')); // true
  print(cronValido('60 * * * *'));        // false
}
import java.util.regex.*;

public class CronValidator {
    static final int[][] FAIXAS = {{0, 59}, {0, 23}, {1, 31}, {1, 12}, {0, 7}}; // min, hora, dia, mês, dia da semana
    static final Pattern PARTE = Pattern.compile("^(?:\\*|(\\d+)(?:-(\\d+))?)(?:/(\\d+))?$");

    static boolean cronValido(String expr) {
        String[] campos = expr.trim().split("\\s+");
        if (campos.length != 5) return false;
        for (int i = 0; i < 5; i++) {
            for (String parte : campos[i].split(",", -1)) {
                Matcher m = PARTE.matcher(parte);
                if (!m.matches()) return false;
                if (m.group(3) != null && Integer.parseInt(m.group(3)) < 1) return false;
                if (m.group(1) != null) {
                    int a = Integer.parseInt(m.group(1));
                    int b = m.group(2) != null ? Integer.parseInt(m.group(2)) : a;
                    if (a < FAIXAS[i][0] || b > FAIXAS[i][1] || a > b) return false;
                }
            }
        }
        return true;
    }

    public static void main(String[] args) {
        System.out.println(cronValido("*/15 9-18 * * 1-5")); // true
        System.out.println(cronValido("60 * * * *"));        // false
    }
}

Para o cron do Spring (@Scheduled), CronExpression.isValidExpression(expr) já faz isso, mas exige 6 campos (com segundos).

<?php
function cronValido(string $expr): bool {
    $faixas = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 7]]; // min, hora, dia, mês, dia da semana
    $campos = preg_split('/\s+/', trim($expr));
    if (count($campos) !== 5) return false;
    foreach ($campos as $i => $campo) {
        foreach (explode(',', $campo) as $parte) {
            if (!preg_match('~^(?:\*|(\d+)(?:-(\d+))?)(?:/(\d+))?$~', $parte, $m)) return false;
            if (isset($m[3]) && $m[3] !== '' && (int)$m[3] < 1) return false;
            if (isset($m[1]) && $m[1] !== '') {
                $a = (int)$m[1];
                $b = (isset($m[2]) && $m[2] !== '') ? (int)$m[2] : $a;
                if ($a < $faixas[$i][0] || $b > $faixas[$i][1] || $a > $b) return false;
            }
        }
    }
    return true;
}

var_dump(cronValido('*/15 9-18 * * 1-5')); // bool(true)
var_dump(cronValido('60 * * * *'));        // bool(false)

Para calcular as próximas execuções use o pacote dragonmantank/cron-expression.

using System;
using System.Text.RegularExpressions;

class Program
{
    static readonly int[][] Faixas = { new[]{0,59}, new[]{0,23}, new[]{1,31}, new[]{1,12}, new[]{0,7} }; // min, hora, dia, mês, dia da semana
    static readonly Regex Parte = new(@"^(?:\*|(\d+)(?:-(\d+))?)(?:/(\d+))?$");

    static bool CronValido(string expr)
    {
        var campos = expr.Split((char[])null, StringSplitOptions.RemoveEmptyEntries);
        if (campos.Length != 5) return false;
        for (int i = 0; i < 5; i++)
            foreach (var parte in campos[i].Split(','))
            {
                var m = Parte.Match(parte);
                if (!m.Success) return false;
                if (m.Groups[3].Success && int.Parse(m.Groups[3].Value) < 1) return false;
                if (m.Groups[1].Success)
                {
                    int a = int.Parse(m.Groups[1].Value);
                    int b = m.Groups[2].Success ? int.Parse(m.Groups[2].Value) : a;
                    if (a < Faixas[i][0] || b > Faixas[i][1] || a > b) return false;
                }
            }
        return true;
    }

    static void Main()
    {
        Console.WriteLine(CronValido("*/15 9-18 * * 1-5")); // True
        Console.WriteLine(CronValido("60 * * * *"));        // False
    }
}

Para calcular execuções use o pacote NuGet Cronos (CronExpression.Parse).

Perguntas frequentes

Qual a diferença entre cron de 5 e de 6 campos?

O cron clássico do Unix tem 5 campos (minuto a dia da semana). Alguns sistemas, como Quartz e Spring, adicionam segundos no início, e AWS EventBridge adiciona o ano. Aqui validamos só o formato de 5 campos.

O que significa */5 no campo de minutos?

Significa a cada 5 minutos, começando do 0: nos minutos 0, 5, 10 e assim por diante até 55.

Por que a expressão 0 0 31 2 * não tem próxima execução?

Porque fevereiro nunca tem dia 31. A expressão é sintaticamente válida, mas nunca dispara.

Veja também

Precisa de um sistema ou app sob medida?

A VcInova desenvolve software, aplicativos e integrações para empresas de todo o Brasil.

Fale com a VcInova