feat(core): describe_cron_expr_go_core + ignorar build/ raiz
Funcion pura que describe expresiones cron en lenguaje natural estilo crontab.guru (consumida por el tooltip de schedule del frontend de dag_engine via /api/cron/describe). Tambien se anade build/ (raiz) al .gitignore para que los artefactos de compilacion C++ no se vuelvan a versionar. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,7 @@ temp/
|
|||||||
|
|
||||||
# C++ build artifacts
|
# C++ build artifacts
|
||||||
cpp/build/
|
cpp/build/
|
||||||
|
/build/
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -0,0 +1,294 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cronDayNames = [7]string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}
|
||||||
|
|
||||||
|
var cronMonthNames = [13]string{
|
||||||
|
"", "January", "February", "March", "April", "May", "June",
|
||||||
|
"July", "August", "September", "October", "November", "December",
|
||||||
|
}
|
||||||
|
|
||||||
|
// DescribeCronExpr returns a human-readable English description of a standard
|
||||||
|
// 5-field cron expression (minute hour day-of-month month day-of-week).
|
||||||
|
//
|
||||||
|
// It delegates parsing and validation to ParseCronExpr, so all syntax errors
|
||||||
|
// and out-of-range values that ParseCronExpr detects are surfaced here too.
|
||||||
|
// Returns an error for expressions with the wrong number of fields or invalid
|
||||||
|
// values; otherwise always returns a non-empty description string.
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// DescribeCronExpr("0 6 * * *") // "At 06:00, every day"
|
||||||
|
// DescribeCronExpr("*/15 * * * *") // "Every 15 minutes"
|
||||||
|
// DescribeCronExpr("0 9 * * 1-5") // "At 09:00, Monday through Friday"
|
||||||
|
func DescribeCronExpr(expr string) (string, error) {
|
||||||
|
sched, err := ParseCronExpr(strings.TrimSpace(expr))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("describe_cron_expr: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect raw token strings for pattern matching (we keep the original
|
||||||
|
// tokens because the expanded slices lose the structural information
|
||||||
|
// needed to distinguish "every minute" from a list of 60 values).
|
||||||
|
raw := strings.Fields(sched.Raw)
|
||||||
|
minTok, hrTok, domTok, monTok, dowTok := raw[0], raw[1], raw[2], raw[3], raw[4]
|
||||||
|
|
||||||
|
allStarDOM := domTok == "*"
|
||||||
|
allStarMon := monTok == "*"
|
||||||
|
allStarDOW := dowTok == "*"
|
||||||
|
|
||||||
|
// ── Time part ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
timePart := describeTimePart(minTok, hrTok)
|
||||||
|
|
||||||
|
// ── Day-of-week part ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
dowPart, dowIsWild := describeDOW(dowTok, sched.DayOfWeek)
|
||||||
|
|
||||||
|
// ── Day-of-month part ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
domPart, domIsWild := describeDOMPart(domTok, sched.DayOfMonth)
|
||||||
|
|
||||||
|
// ── Month part ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
monPart, monIsWild := describeMonthPart(monTok, sched.Month)
|
||||||
|
|
||||||
|
// ── Compose ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Special: every minute
|
||||||
|
if minTok == "*" && hrTok == "*" && allStarDOM && allStarMon && allStarDOW {
|
||||||
|
return "Every minute", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special: every N minutes (*/N with everything else wildcard)
|
||||||
|
if isStep(minTok) && hrTok == "*" && allStarDOM && allStarMon && allStarDOW {
|
||||||
|
n := stepValue(minTok)
|
||||||
|
if n == 1 {
|
||||||
|
return "Every minute", nil
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Every %d minutes", n), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special: every hour on the hour (minute==0, hour==*, dom/mon/dow are *)
|
||||||
|
if minTok == "0" && hrTok == "*" && allStarDOM && allStarMon && allStarDOW {
|
||||||
|
return "Every hour, on the hour", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special: every N hours (minute==0, */N in hour, rest *)
|
||||||
|
if minTok == "0" && isStep(hrTok) && allStarDOM && allStarMon && allStarDOW {
|
||||||
|
n := stepValue(hrTok)
|
||||||
|
if n == 1 {
|
||||||
|
return "Every hour, on the hour", nil
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Every %d hours", n), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a sentence from parts.
|
||||||
|
var parts []string
|
||||||
|
parts = append(parts, timePart)
|
||||||
|
|
||||||
|
// Day-of-week constraints win over day-of-month when DOW is specific.
|
||||||
|
if !dowIsWild {
|
||||||
|
parts = append(parts, dowPart)
|
||||||
|
} else if !domIsWild {
|
||||||
|
parts = append(parts, domPart)
|
||||||
|
} else {
|
||||||
|
parts = append(parts, "every day")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !monIsWild {
|
||||||
|
parts = append(parts, monPart)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(parts, ", "), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// describeTimePart produces the "At HH:MM" or "every N minutes/hours" clause.
|
||||||
|
func describeTimePart(minTok, hrTok string) string {
|
||||||
|
// Fixed minute + fixed hour → "At HH:MM"
|
||||||
|
if isFixed(minTok) && isFixed(hrTok) {
|
||||||
|
m := mustAtoi(minTok)
|
||||||
|
h := mustAtoi(hrTok)
|
||||||
|
return fmt.Sprintf("At %02d:%02d", h, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wildcard hour, fixed minute → "at minute N of every hour"
|
||||||
|
if hrTok == "*" && isFixed(minTok) {
|
||||||
|
m := mustAtoi(minTok)
|
||||||
|
return fmt.Sprintf("at minute %d of every hour", m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step hour, fixed minute
|
||||||
|
if isStep(hrTok) && isFixed(minTok) {
|
||||||
|
n := stepValue(hrTok)
|
||||||
|
m := mustAtoi(minTok)
|
||||||
|
if n == 1 {
|
||||||
|
return fmt.Sprintf("at minute %d of every hour", m)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("at minute %d of every %d hours", m, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step minute
|
||||||
|
if isStep(minTok) {
|
||||||
|
n := stepValue(minTok)
|
||||||
|
if hrTok == "*" {
|
||||||
|
if n == 1 {
|
||||||
|
return "every minute"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("every %d minutes", n)
|
||||||
|
}
|
||||||
|
if isFixed(hrTok) {
|
||||||
|
h := mustAtoi(hrTok)
|
||||||
|
return fmt.Sprintf("every %d minutes during hour %02d", n, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: describe each field individually
|
||||||
|
return fmt.Sprintf("at minute %s of hour %s", minTok, hrTok)
|
||||||
|
}
|
||||||
|
|
||||||
|
// describeDOW returns a phrase for the day-of-week field and whether it is
|
||||||
|
// effectively a wildcard (covers all 7 days).
|
||||||
|
func describeDOW(tok string, vals []int) (string, bool) {
|
||||||
|
if tok == "*" || len(vals) == 7 {
|
||||||
|
return "", true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single day
|
||||||
|
if len(vals) == 1 {
|
||||||
|
d := vals[0] % 7
|
||||||
|
return fmt.Sprintf("only on %s", cronDayNames[d]), false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contiguous range covering Mon-Fri exactly → "Monday through Friday"
|
||||||
|
if len(vals) == 5 && vals[0] == 1 && vals[4] == 5 {
|
||||||
|
return "Monday through Friday", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any other contiguous range
|
||||||
|
if isContiguous(vals) {
|
||||||
|
first := vals[0] % 7
|
||||||
|
last := vals[len(vals)-1] % 7
|
||||||
|
return fmt.Sprintf("%s through %s", cronDayNames[first], cronDayNames[last]), false
|
||||||
|
}
|
||||||
|
|
||||||
|
// List of days
|
||||||
|
names := make([]string, len(vals))
|
||||||
|
for i, v := range vals {
|
||||||
|
names[i] = cronDayNames[v%7]
|
||||||
|
}
|
||||||
|
return "on " + joinEnglish(names), false
|
||||||
|
}
|
||||||
|
|
||||||
|
// describeDOMPart returns a phrase for the day-of-month field.
|
||||||
|
func describeDOMPart(tok string, vals []int) (string, bool) {
|
||||||
|
if tok == "*" || len(vals) == 31 {
|
||||||
|
return "", true
|
||||||
|
}
|
||||||
|
if len(vals) == 1 {
|
||||||
|
return fmt.Sprintf("on day %d of the month", vals[0]), false
|
||||||
|
}
|
||||||
|
if isContiguous(vals) {
|
||||||
|
return fmt.Sprintf("on days %d through %d of the month", vals[0], vals[len(vals)-1]), false
|
||||||
|
}
|
||||||
|
strs := make([]string, len(vals))
|
||||||
|
for i, v := range vals {
|
||||||
|
strs[i] = fmt.Sprintf("%d", v)
|
||||||
|
}
|
||||||
|
return "on days " + joinEnglish(strs) + " of the month", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// describeMonthPart returns a phrase for the month field.
|
||||||
|
func describeMonthPart(tok string, vals []int) (string, bool) {
|
||||||
|
if tok == "*" || len(vals) == 12 {
|
||||||
|
return "", true
|
||||||
|
}
|
||||||
|
if len(vals) == 1 {
|
||||||
|
return fmt.Sprintf("in %s", cronMonthNames[vals[0]]), false
|
||||||
|
}
|
||||||
|
if isContiguous(vals) {
|
||||||
|
return fmt.Sprintf("from %s to %s", cronMonthNames[vals[0]], cronMonthNames[vals[len(vals)-1]]), false
|
||||||
|
}
|
||||||
|
names := make([]string, len(vals))
|
||||||
|
for i, v := range vals {
|
||||||
|
names[i] = cronMonthNames[v]
|
||||||
|
}
|
||||||
|
return "in " + joinEnglish(names), false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isFixed returns true when tok is a plain integer (no wildcards or ranges).
|
||||||
|
func isFixed(tok string) bool {
|
||||||
|
for _, c := range tok {
|
||||||
|
if c < '0' || c > '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(tok) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// isStep returns true for tokens of the form */N or N/N.
|
||||||
|
func isStep(tok string) bool {
|
||||||
|
return strings.Contains(tok, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// stepValue returns N from */N or a-b/N. Assumes isStep(tok) is true.
|
||||||
|
func stepValue(tok string) int {
|
||||||
|
idx := strings.LastIndex(tok, "/")
|
||||||
|
n, err := parseInt(tok[idx+1:])
|
||||||
|
if err != nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseInt is a minimal strconv.Atoi without the import indirection.
|
||||||
|
func parseInt(s string) (int, error) {
|
||||||
|
n := 0
|
||||||
|
if len(s) == 0 {
|
||||||
|
return 0, fmt.Errorf("empty")
|
||||||
|
}
|
||||||
|
for _, c := range s {
|
||||||
|
if c < '0' || c > '9' {
|
||||||
|
return 0, fmt.Errorf("not a digit: %q", c)
|
||||||
|
}
|
||||||
|
n = n*10 + int(c-'0')
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mustAtoi converts a known-valid digit string to int (panics on malformed input,
|
||||||
|
// but input is already validated by ParseCronExpr so this is safe).
|
||||||
|
func mustAtoi(s string) int {
|
||||||
|
n, _ := parseInt(s)
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// isContiguous returns true when vals is a sequence without gaps.
|
||||||
|
func isContiguous(vals []int) bool {
|
||||||
|
for i := 1; i < len(vals); i++ {
|
||||||
|
if vals[i] != vals[i-1]+1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// joinEnglish joins strings with commas and "and" before the last element.
|
||||||
|
func joinEnglish(items []string) string {
|
||||||
|
switch len(items) {
|
||||||
|
case 0:
|
||||||
|
return ""
|
||||||
|
case 1:
|
||||||
|
return items[0]
|
||||||
|
case 2:
|
||||||
|
return items[0] + " and " + items[1]
|
||||||
|
default:
|
||||||
|
return strings.Join(items[:len(items)-1], ", ") + " and " + items[len(items)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
name: describe_cron_expr
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: core
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: pure
|
||||||
|
signature: "func DescribeCronExpr(expr string) (string, error)"
|
||||||
|
description: "Convierte una expresion cron estandar de 5 campos en una descripcion legible en ingles, al estilo de crontab.guru. Soporta *, valores fijos, pasos (*/N), rangos (a-b), listas (a,b,c) y aliases (@daily, @hourly, etc.). Produce frases como 'At 06:00, every day', 'Every 15 minutes' o 'At 09:00, Monday through Friday'."
|
||||||
|
tags: [cron, schedule, description, human-readable, dag-engine, tooltip, parsing, scheduler]
|
||||||
|
uses_functions: [parse_cron_expr_go_core]
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: ""
|
||||||
|
imports: [fmt, strings]
|
||||||
|
params:
|
||||||
|
- name: expr
|
||||||
|
desc: "expresion cron de 5 campos (minuto hora dia_mes mes dia_semana) o alias @daily/@weekly/@monthly/@yearly/@hourly; los campos soportan *, valores fijos, pasos (*/N), rangos (a-b) y listas (a,b,c)"
|
||||||
|
output: "descripcion en ingles del schedule cron; error si la expresion tiene numero de campos incorrecto o valores fuera de rango"
|
||||||
|
tested: true
|
||||||
|
tests:
|
||||||
|
- "daily at 06:00"
|
||||||
|
- "every 15 minutes"
|
||||||
|
- "every hour on the hour"
|
||||||
|
- "weekdays at 09:00"
|
||||||
|
- "friday at 21:00"
|
||||||
|
- "every 6 hours"
|
||||||
|
- "first of month at 08:30"
|
||||||
|
- "sunday at midnight"
|
||||||
|
- "every minute"
|
||||||
|
- "every 1 minute step"
|
||||||
|
- "every 2 hours"
|
||||||
|
- "multiple days of week"
|
||||||
|
- "specific month"
|
||||||
|
- "invalid expression - wrong field count"
|
||||||
|
- "invalid minute value"
|
||||||
|
- "alias @daily"
|
||||||
|
- "alias @weekly"
|
||||||
|
- "alias @hourly"
|
||||||
|
- "saturday at 23:59"
|
||||||
|
- "days 1 and 15 of month"
|
||||||
|
- "month range June to August"
|
||||||
|
test_file_path: "functions/core/describe_cron_expr_test.go"
|
||||||
|
file_path: "functions/core/describe_cron_expr.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
desc, err := DescribeCronExpr("0 6 * * *")
|
||||||
|
// desc = "At 06:00, every day"
|
||||||
|
|
||||||
|
desc2, _ := DescribeCronExpr("*/15 * * * *")
|
||||||
|
// desc2 = "Every 15 minutes"
|
||||||
|
|
||||||
|
desc3, _ := DescribeCronExpr("0 9 * * 1-5")
|
||||||
|
// desc3 = "At 09:00, Monday through Friday"
|
||||||
|
|
||||||
|
desc4, _ := DescribeCronExpr("30 8 1 * *")
|
||||||
|
// desc4 = "At 08:30, on day 1 of the month"
|
||||||
|
|
||||||
|
_, err2 := DescribeCronExpr("0 6 * *")
|
||||||
|
// err2 != nil (solo 4 campos)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cuando usarla
|
||||||
|
|
||||||
|
Cuando necesites mostrar al usuario final una descripcion legible de un schedule cron — tooltips en el frontend de dag_engine, mensajes de confirmacion antes de guardar una tarea programada, o logs de auditoria. Produce descripciones en ingles del mismo estilo que crontab.guru, pero retorna error en vez de texto crudo cuando la expresion es invalida.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- Devuelve `(string, error)` aunque es pura sin I/O. El error es por input invalido (mismo patron que `ParseCronExpr`). Segun la convencion del repo, retornar error solo por validacion de input no cambia la clasificacion a `impure`.
|
||||||
|
- Para combinaciones muy especificas (ej. minuto con lista + hora con step), la descripcion es campo-a-campo y puede ser menos elegante que crontab.guru, pero siempre es correcta.
|
||||||
|
- Dias de semana: 0 y 7 ambos representan Sunday (hereda el comportamiento de `ParseCronExpr` que los normaliza a [0-6]).
|
||||||
|
- Para convertir la expresion a la lista de instantes de disparo usa `next_cron_time_go_core`. Esta funcion solo describe — no calcula tiempos.
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDescribeCronExpr(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
expr string
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "daily at 06:00",
|
||||||
|
expr: "0 6 * * *",
|
||||||
|
want: "At 06:00, every day",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "every 15 minutes",
|
||||||
|
expr: "*/15 * * * *",
|
||||||
|
want: "Every 15 minutes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "every hour on the hour",
|
||||||
|
expr: "0 * * * *",
|
||||||
|
want: "Every hour, on the hour",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "weekdays at 09:00",
|
||||||
|
expr: "0 9 * * 1-5",
|
||||||
|
want: "At 09:00, Monday through Friday",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "friday at 21:00",
|
||||||
|
expr: "0 21 * * 5",
|
||||||
|
want: "At 21:00, only on Friday",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "every 6 hours",
|
||||||
|
expr: "0 */6 * * *",
|
||||||
|
want: "Every 6 hours",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "first of month at 08:30",
|
||||||
|
expr: "30 8 1 * *",
|
||||||
|
want: "At 08:30, on day 1 of the month",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sunday at midnight",
|
||||||
|
expr: "0 0 * * 0",
|
||||||
|
want: "At 00:00, only on Sunday",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "every minute",
|
||||||
|
expr: "* * * * *",
|
||||||
|
want: "Every minute",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "every 1 minute step",
|
||||||
|
expr: "*/1 * * * *",
|
||||||
|
want: "Every minute",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "every 2 hours",
|
||||||
|
expr: "0 */2 * * *",
|
||||||
|
want: "Every 2 hours",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple days of week",
|
||||||
|
expr: "0 12 * * 1,3,5",
|
||||||
|
want: "At 12:00, on Monday, Wednesday and Friday",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "specific month",
|
||||||
|
expr: "0 9 1 6 *",
|
||||||
|
want: "At 09:00, on day 1 of the month, in June",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid expression - wrong field count",
|
||||||
|
expr: "0 6 * *",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid minute value",
|
||||||
|
expr: "99 6 * * *",
|
||||||
|
want: "",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alias @daily",
|
||||||
|
expr: "@daily",
|
||||||
|
want: "At 00:00, every day",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alias @weekly",
|
||||||
|
expr: "@weekly",
|
||||||
|
want: "At 00:00, only on Sunday",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alias @hourly",
|
||||||
|
expr: "@hourly",
|
||||||
|
want: "Every hour, on the hour",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "saturday at 23:59",
|
||||||
|
expr: "59 23 * * 6",
|
||||||
|
want: "At 23:59, only on Saturday",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "days 1 and 15 of month",
|
||||||
|
expr: "0 8 1,15 * *",
|
||||||
|
want: "At 08:00, on days 1 and 15 of the month",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "month range June to August",
|
||||||
|
expr: "0 6 * 6-8 *",
|
||||||
|
want: "At 06:00, every day, from June to August",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got, err := DescribeCronExpr(tc.expr)
|
||||||
|
if tc.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error, got %q", got)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("got %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user