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, }