Files
crawlab/core/utils/cron.go
2024-06-14 15:42:50 +08:00

177 lines
3.9 KiB
Go

package utils
import (
"fmt"
"math"
"strconv"
"strings"
)
// cronBounds provides a range of acceptable values (plus a map of name to value).
type cronBounds struct {
min, max uint
names map[string]uint
}
type cronUtils struct {
// The cronBounds for each field.
seconds cronBounds
minutes cronBounds
hours cronBounds
dom cronBounds
months cronBounds
dow cronBounds
// Set the top bit if a star was included in the expression.
starBit uint64
}
// getRange returns the bits indicated by the given expression:
// number | number "-" number [ "/" number ]
// or error parsing range.
func (u *cronUtils) getRange(expr string, r cronBounds) (uint64, error) {
var (
start, end, step uint
rangeAndStep = strings.Split(expr, "/")
lowAndHigh = strings.Split(rangeAndStep[0], "-")
singleDigit = len(lowAndHigh) == 1
err error
)
var extra uint64
if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
start = r.min
end = r.max
extra = CronUtils.starBit
} else {
start, err = u.parseIntOrName(lowAndHigh[0], r.names)
if err != nil {
return 0, err
}
switch len(lowAndHigh) {
case 1:
end = start
case 2:
end, err = u.parseIntOrName(lowAndHigh[1], r.names)
if err != nil {
return 0, err
}
default:
return 0, fmt.Errorf("too many hyphens: %s", expr)
}
}
switch len(rangeAndStep) {
case 1:
step = 1
case 2:
step, err = u.mustParseInt(rangeAndStep[1])
if err != nil {
return 0, err
}
// Special handling: "N/step" means "N-max/step".
if singleDigit {
end = r.max
}
if step > 1 {
extra = 0
}
default:
return 0, fmt.Errorf("too many slashes: %s", expr)
}
if start < r.min {
return 0, fmt.Errorf("beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
}
if end > r.max {
return 0, fmt.Errorf("end of range (%d) above maximum (%d): %s", end, r.max, expr)
}
if start > end {
return 0, fmt.Errorf("beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
}
if step == 0 {
return 0, fmt.Errorf("step of range should be a positive number: %s", expr)
}
return u.getBits(start, end, step) | extra, nil
}
// parseIntOrName returns the (possibly-named) integer contained in expr.
func (u *cronUtils) parseIntOrName(expr string, names map[string]uint) (uint, error) {
if names != nil {
if namedInt, ok := names[strings.ToLower(expr)]; ok {
return namedInt, nil
}
}
return u.mustParseInt(expr)
}
// mustParseInt parses the given expression as an int or returns an error.
func (u *cronUtils) mustParseInt(expr string) (uint, error) {
num, err := strconv.Atoi(expr)
if err != nil {
return 0, fmt.Errorf("failed to parse int from %s: %s", expr, err)
}
if num < 0 {
return 0, fmt.Errorf("negative number (%d) not allowed: %s", num, expr)
}
return uint(num), nil
}
// getBits sets all bits in the range [min, max], modulo the given step size.
func (u *cronUtils) getBits(min, max, step uint) uint64 {
var bits uint64
// If step is 1, use shifts.
if step == 1 {
return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
}
// Else, use a simple loop.
for i := min; i <= max; i += step {
bits |= 1 << i
}
return bits
}
// all returns all bits within the given cronBounds. (plus the star bit)
func (u *cronUtils) all(r cronBounds) uint64 {
return u.getBits(r.min, r.max, 1) | CronUtils.starBit
}
var CronUtils = cronUtils{
// The cronBounds for each field.
seconds: cronBounds{0, 59, nil},
minutes: cronBounds{0, 59, nil},
hours: cronBounds{0, 23, nil},
dom: cronBounds{1, 31, nil},
months: cronBounds{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: cronBounds{0, 6, map[string]uint{
"sun": 0,
"mon": 1,
"tue": 2,
"wed": 3,
"thu": 4,
"fri": 5,
"sat": 6,
}},
// Set the top bit if a star was included in the expression.
starBit: 1 << 63,
}