|
|
@@ -7,59 +7,93 @@ import (
|
|
|
"strings"
|
|
|
)
|
|
|
|
|
|
-type Entry struct {
|
|
|
- Minute, Hour, Dom, Month, Dow uint64
|
|
|
- Func func()
|
|
|
+// A cron schedule that specifies a duty cycle (to the second granularity).
|
|
|
+// Schedules are computed initially and stored as bit sets.
|
|
|
+type Schedule struct {
|
|
|
+ Second, Minute, Hour, Dom, Month, Dow uint64
|
|
|
}
|
|
|
|
|
|
-type Range struct{ min, max uint }
|
|
|
+// A range of acceptable values.
|
|
|
+type bounds struct {
|
|
|
+ min, max uint
|
|
|
+ names map[string]uint
|
|
|
+}
|
|
|
|
|
|
+// The bounds for each field.
|
|
|
var (
|
|
|
- minutes = Range{0, 59}
|
|
|
- hours = Range{0, 23}
|
|
|
- dom = Range{1, 31}
|
|
|
- months = Range{1, 12}
|
|
|
- dow = Range{0, 7}
|
|
|
+ seconds = bounds{0, 59, nil}
|
|
|
+ minutes = bounds{0, 59, nil}
|
|
|
+ hours = bounds{0, 23, nil}
|
|
|
+ dom = bounds{1, 31, nil}
|
|
|
+ months = bounds{1, 12, map[string]uint{
|
|
|
+ "jan": 1,
|
|
|
+ "feb": 2,
|
|
|
+ "mar": 3,
|
|
|
+ "apr": 4,
|
|
|
+ "may": 5,
|
|
|
+ "jun": 6,
|
|
|
+ "jul": 7,
|
|
|
+ "aug": 8,
|
|
|
+ "sep": 9,
|
|
|
+ "oct": 10,
|
|
|
+ "nov": 11,
|
|
|
+ "dec": 12,
|
|
|
+ }}
|
|
|
+ dow = bounds{0, 7, map[string]uint{
|
|
|
+ "sun": 0,
|
|
|
+ "mon": 1,
|
|
|
+ "tue": 2,
|
|
|
+ "wed": 3,
|
|
|
+ "thu": 4,
|
|
|
+ "fri": 5,
|
|
|
+ "sat": 6,
|
|
|
+ }}
|
|
|
+)
|
|
|
+
|
|
|
+const (
|
|
|
+ // Set the top bit if a star was included in the expression.
|
|
|
+ STAR_BIT = 1 << 63
|
|
|
)
|
|
|
|
|
|
-// Returns a new crontab entry representing the given spec.
|
|
|
+// Returns a new crontab schedule representing the given spec.
|
|
|
// Panics with a descriptive error if the spec is not valid.
|
|
|
-func NewEntry(spec string, cmd func()) *Entry {
|
|
|
+func Parse(spec string) *Schedule {
|
|
|
if spec[0] == '@' {
|
|
|
- entry := parseDescriptor(spec)
|
|
|
- entry.Func = cmd
|
|
|
- return entry
|
|
|
+ return parseDescriptor(spec)
|
|
|
}
|
|
|
|
|
|
// Split on whitespace. We require 4 or 5 fields.
|
|
|
// (minute) (hour) (day of month) (month) (day of week, optional)
|
|
|
fields := strings.Fields(spec)
|
|
|
- if len(fields) != 4 && len(fields) != 5 {
|
|
|
- log.Panicf("Expected 4 or 5 fields, found %d: %s", len(fields), spec)
|
|
|
+ if len(fields) != 5 && len(fields) != 6 {
|
|
|
+ log.Panicf("Expected 5 or 6 fields, found %d: %s", len(fields), spec)
|
|
|
}
|
|
|
|
|
|
- entry := &Entry{
|
|
|
- Minute: getField(fields[0], minutes),
|
|
|
- Hour: getField(fields[1], hours),
|
|
|
- Dom: getField(fields[2], dom),
|
|
|
- Month: getField(fields[3], months),
|
|
|
- Func: cmd,
|
|
|
- }
|
|
|
+ // If a fifth field is not provided (DayOfWeek), then it is equivalent to star.
|
|
|
if len(fields) == 5 {
|
|
|
- entry.Dow = getField(fields[4], dow)
|
|
|
+ fields = append(fields, "*")
|
|
|
+ }
|
|
|
|
|
|
- // If either bit 0 or 7 are set, set both. (both accepted as Sunday)
|
|
|
- if entry.Dow&1|entry.Dow&1<<7 > 0 {
|
|
|
- entry.Dow = entry.Dow | 1 | 1<<7
|
|
|
- }
|
|
|
+ schedule := &Schedule{
|
|
|
+ Second: getField(fields[0], seconds),
|
|
|
+ Minute: getField(fields[1], minutes),
|
|
|
+ Hour: getField(fields[2], hours),
|
|
|
+ Dom: getField(fields[3], dom),
|
|
|
+ Month: getField(fields[4], months),
|
|
|
+ Dow: getField(fields[5], dow),
|
|
|
+ }
|
|
|
+
|
|
|
+ // If either bit 0 or 7 are set, set both. (both accepted as Sunday)
|
|
|
+ if 1&schedule.Dow|1<<7&schedule.Dow > 0 {
|
|
|
+ schedule.Dow = schedule.Dow | 1 | 1<<7
|
|
|
}
|
|
|
|
|
|
- return entry
|
|
|
+ return schedule
|
|
|
}
|
|
|
|
|
|
// Return an Int with the bits set representing all of the times that the field represents.
|
|
|
// A "field" is a comma-separated list of "ranges".
|
|
|
-func getField(field string, r Range) uint64 {
|
|
|
+func getField(field string, r bounds) uint64 {
|
|
|
// list = range {"," range}
|
|
|
var bits uint64
|
|
|
ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
|
|
|
@@ -69,24 +103,29 @@ func getField(field string, r Range) uint64 {
|
|
|
return bits
|
|
|
}
|
|
|
|
|
|
-func getRange(expr string, r Range) uint64 {
|
|
|
+func getRange(expr string, r bounds) uint64 {
|
|
|
// number | number "-" number [ "/" number ]
|
|
|
- var start, end, step uint
|
|
|
- rangeAndStep := strings.Split(expr, "/")
|
|
|
- lowAndHigh := strings.Split(rangeAndStep[0], "-")
|
|
|
+ var (
|
|
|
+ start, end, step uint
|
|
|
+ rangeAndStep = strings.Split(expr, "/")
|
|
|
+ lowAndHigh = strings.Split(rangeAndStep[0], "-")
|
|
|
+ singleDigit = len(lowAndHigh) == 1
|
|
|
+ )
|
|
|
|
|
|
- if lowAndHigh[0] == "*" {
|
|
|
+ var extra_star uint64
|
|
|
+ if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
|
|
|
start = r.min
|
|
|
end = r.max
|
|
|
+ extra_star = STAR_BIT
|
|
|
} else {
|
|
|
- start = mustParseInt(lowAndHigh[0])
|
|
|
+ start = parseIntOrName(lowAndHigh[0], r.names)
|
|
|
switch len(lowAndHigh) {
|
|
|
case 1:
|
|
|
end = start
|
|
|
case 2:
|
|
|
- end = mustParseInt(lowAndHigh[1])
|
|
|
+ end = parseIntOrName(lowAndHigh[1], r.names)
|
|
|
default:
|
|
|
- log.Panicf("Too many commas: %s", expr)
|
|
|
+ log.Panicf("Too many hyphens: %s", expr)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -95,6 +134,11 @@ func getRange(expr string, r Range) uint64 {
|
|
|
step = 1
|
|
|
case 2:
|
|
|
step = mustParseInt(rangeAndStep[1])
|
|
|
+
|
|
|
+ // Special handling: "N/step" means "N-max/step".
|
|
|
+ if singleDigit {
|
|
|
+ end = r.max
|
|
|
+ }
|
|
|
default:
|
|
|
log.Panicf("Too many slashes: %s", expr)
|
|
|
}
|
|
|
@@ -109,7 +153,16 @@ func getRange(expr string, r Range) uint64 {
|
|
|
log.Panicf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
|
|
|
}
|
|
|
|
|
|
- return getBits(start, end, step)
|
|
|
+ return getBits(start, end, step) | extra_star
|
|
|
+}
|
|
|
+
|
|
|
+func parseIntOrName(expr string, names map[string]uint) uint {
|
|
|
+ if names != nil {
|
|
|
+ if namedInt, ok := names[strings.ToLower(expr)]; ok {
|
|
|
+ return namedInt
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return mustParseInt(expr)
|
|
|
}
|
|
|
|
|
|
func mustParseInt(expr string) uint {
|
|
|
@@ -139,18 +192,19 @@ func getBits(min, max, step uint) uint64 {
|
|
|
return bits
|
|
|
}
|
|
|
|
|
|
-func all(r Range) uint64 {
|
|
|
- return getBits(r.min, r.max, 1)
|
|
|
+func all(r bounds) uint64 {
|
|
|
+ return getBits(r.min, r.max, 1) | STAR_BIT
|
|
|
}
|
|
|
|
|
|
-func first(r Range) uint64 {
|
|
|
+func first(r bounds) uint64 {
|
|
|
return getBits(r.min, r.min, 1)
|
|
|
}
|
|
|
|
|
|
-func parseDescriptor(spec string) *Entry {
|
|
|
+func parseDescriptor(spec string) *Schedule {
|
|
|
switch spec {
|
|
|
case "@yearly", "@annually":
|
|
|
- return &Entry{
|
|
|
+ return &Schedule{
|
|
|
+ Second: 1 << seconds.min,
|
|
|
Minute: 1 << minutes.min,
|
|
|
Hour: 1 << hours.min,
|
|
|
Dom: 1 << dom.min,
|
|
|
@@ -159,7 +213,8 @@ func parseDescriptor(spec string) *Entry {
|
|
|
}
|
|
|
|
|
|
case "@monthly":
|
|
|
- return &Entry{
|
|
|
+ return &Schedule{
|
|
|
+ Second: 1 << seconds.min,
|
|
|
Minute: 1 << minutes.min,
|
|
|
Hour: 1 << hours.min,
|
|
|
Dom: 1 << dom.min,
|
|
|
@@ -168,7 +223,8 @@ func parseDescriptor(spec string) *Entry {
|
|
|
}
|
|
|
|
|
|
case "@weekly":
|
|
|
- return &Entry{
|
|
|
+ return &Schedule{
|
|
|
+ Second: 1 << seconds.min,
|
|
|
Minute: 1 << minutes.min,
|
|
|
Hour: 1 << hours.min,
|
|
|
Dom: all(dom),
|
|
|
@@ -177,7 +233,8 @@ func parseDescriptor(spec string) *Entry {
|
|
|
}
|
|
|
|
|
|
case "@daily", "@midnight":
|
|
|
- return &Entry{
|
|
|
+ return &Schedule{
|
|
|
+ Second: 1 << seconds.min,
|
|
|
Minute: 1 << minutes.min,
|
|
|
Hour: 1 << hours.min,
|
|
|
Dom: all(dom),
|
|
|
@@ -186,7 +243,8 @@ func parseDescriptor(spec string) *Entry {
|
|
|
}
|
|
|
|
|
|
case "@hourly":
|
|
|
- return &Entry{
|
|
|
+ return &Schedule{
|
|
|
+ Second: 1 << seconds.min,
|
|
|
Minute: 1 << minutes.min,
|
|
|
Hour: all(hours),
|
|
|
Dom: all(dom),
|