Skip to content

Commit

Permalink
Merge pull request robfig#74 from soltysh/issue33
Browse files Browse the repository at this point in the history
Refactor parsing to return error instead of using panic/recover
  • Loading branch information
robfig authored Sep 14, 2016
2 parents 23baae2 + fcc395c commit 783cfcb
Show file tree
Hide file tree
Showing 2 changed files with 237 additions and 110 deletions.
188 changes: 111 additions & 77 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cron

import (
"fmt"
"log"
"math"
"strconv"
"strings"
Expand All @@ -17,36 +16,46 @@ import (
// It accepts
// - Standard crontab specs, e.g. "* * * * ?"
// - Descriptors, e.g. "@midnight", "@every 1h30m"
func ParseStandard(standardSpec string) (_ Schedule, err error) {
// Convert panics into errors
defer func() {
if recovered := recover(); recovered != nil {
err = fmt.Errorf("%v", recovered)
}
}()

func ParseStandard(standardSpec string) (Schedule, error) {
if standardSpec[0] == '@' {
return parseDescriptor(standardSpec), nil
return parseDescriptor(standardSpec)
}

// Split on whitespace. We require exactly 5 fields.
// (minute) (hour) (day of month) (month) (day of week)
fields := strings.Fields(standardSpec)
if len(fields) != 5 {
log.Panicf("Expected exactly 5, found %d: %s", len(fields), standardSpec)
return nil, fmt.Errorf("Expected exactly 5 fields, found %d: %s", len(fields), standardSpec)
}

schedule := &SpecSchedule{
Second: 1 << seconds.min,
Minute: getField(fields[0], minutes),
Hour: getField(fields[1], hours),
Dom: getField(fields[2], dom),
Month: getField(fields[3], months),
Dow: getField(fields[4], dow),
var err error
field := func(field string, r bounds) uint64 {
if err != nil {
return uint64(0)
}
var bits uint64
bits, err = getField(field, r)
return bits
}
var (
minute = field(fields[0], minutes)
hour = field(fields[1], hours)
dayofmonth = field(fields[2], dom)
month = field(fields[3], months)
dayofweek = field(fields[4], dow)
)
if err != nil {
return nil, err
}

return schedule, nil

return &SpecSchedule{
Second: 1 << seconds.min,
Minute: minute,
Hour: hour,
Dom: dayofmonth,
Month: month,
Dow: dayofweek,
}, nil
}

// Parse returns a new crontab schedule representing the given spec.
Expand All @@ -55,63 +64,81 @@ func ParseStandard(standardSpec string) (_ Schedule, err error) {
// It accepts
// - Full crontab specs, e.g. "* * * * * ?"
// - Descriptors, e.g. "@midnight", "@every 1h30m"
func Parse(spec string) (_ Schedule, err error) {
// Convert panics into errors
defer func() {
if recovered := recover(); recovered != nil {
err = fmt.Errorf("%v", recovered)
}
}()

func Parse(spec string) (Schedule, error) {
if spec[0] == '@' {
return parseDescriptor(spec), nil
return parseDescriptor(spec)
}

// Split on whitespace. We require 5 or 6 fields.
// (second) (minute) (hour) (day of month) (month) (day of week, optional)
fields := strings.Fields(spec)
if len(fields) != 5 && len(fields) != 6 {
log.Panicf("Expected 5 or 6 fields, found %d: %s", len(fields), spec)
return nil, fmt.Errorf("Expected 5 or 6 fields, found %d: %s", len(fields), spec)
}

// If a sixth field is not provided (DayOfWeek), then it is equivalent to star.
if len(fields) == 5 {
fields = append(fields, "*")
}

schedule := &SpecSchedule{
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),
var err error
field := func(field string, r bounds) uint64 {
if err != nil {
return uint64(0)
}
var bits uint64
bits, err = getField(field, r)
return bits
}
var (
second = field(fields[0], seconds)
minute = field(fields[1], minutes)
hour = field(fields[2], hours)
dayofmonth = field(fields[3], dom)
month = field(fields[4], months)
dayofweek = field(fields[5], dow)
)
if err != nil {
return nil, err
}

return schedule, nil
return &SpecSchedule{
Second: second,
Minute: minute,
Hour: hour,
Dom: dayofmonth,
Month: month,
Dow: dayofweek,
}, nil
}

// getField returns 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 bounds) uint64 {
// list = range {"," range}
// the field represents or error parsing field value. A "field" is a comma-separated
// list of "ranges".
func getField(field string, r bounds) (uint64, error) {
var bits uint64
ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
for _, expr := range ranges {
bits |= getRange(expr, r)
bit, err := getRange(expr, r)
if err != nil {
return bits, err
}
bits |= bit
}
return bits
return bits, nil
}

// getRange returns the bits indicated by the given expression:
// number | number "-" number [ "/" number ]
func getRange(expr string, r bounds) uint64 {

// or error parsing range.
func getRange(expr string, r bounds) (uint64, error) {
var (
start, end, step uint
rangeAndStep = strings.Split(expr, "/")
lowAndHigh = strings.Split(rangeAndStep[0], "-")
singleDigit = len(lowAndHigh) == 1
err error
zero = uint64(0)
)

var extra_star uint64
Expand All @@ -120,68 +147,77 @@ func getRange(expr string, r bounds) uint64 {
end = r.max
extra_star = starBit
} else {
start = parseIntOrName(lowAndHigh[0], r.names)
start, err = parseIntOrName(lowAndHigh[0], r.names)
if err != nil {
return zero, err
}
switch len(lowAndHigh) {
case 1:
end = start
case 2:
end = parseIntOrName(lowAndHigh[1], r.names)
end, err = parseIntOrName(lowAndHigh[1], r.names)
if err != nil {
return zero, err
}
default:
log.Panicf("Too many hyphens: %s", expr)
return zero, fmt.Errorf("Too many hyphens: %s", expr)
}
}

switch len(rangeAndStep) {
case 1:
step = 1
case 2:
step = mustParseInt(rangeAndStep[1])
step, err = mustParseInt(rangeAndStep[1])
if err != nil {
return zero, err
}

// Special handling: "N/step" means "N-max/step".
if singleDigit {
end = r.max
}
default:
log.Panicf("Too many slashes: %s", expr)
return zero, fmt.Errorf("Too many slashes: %s", expr)
}

if start < r.min {
log.Panicf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
return zero, fmt.Errorf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
}
if end > r.max {
log.Panicf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
return zero, fmt.Errorf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
}
if start > end {
log.Panicf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
return zero, fmt.Errorf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
}
if step == 0 {
log.Panicf("Step of range should be a positive number: %s", expr)
return zero, fmt.Errorf("Step of range should be a positive number: %s", expr)
}

return getBits(start, end, step) | extra_star
return getBits(start, end, step) | extra_star, nil
}

// parseIntOrName returns the (possibly-named) integer contained in expr.
func parseIntOrName(expr string, names map[string]uint) uint {
func parseIntOrName(expr string, names map[string]uint) (uint, error) {
if names != nil {
if namedInt, ok := names[strings.ToLower(expr)]; ok {
return namedInt
return namedInt, nil
}
}
return mustParseInt(expr)
}

// mustParseInt parses the given expression as an int or panics.
func mustParseInt(expr string) uint {
// mustParseInt parses the given expression as an int or returns an error.
func mustParseInt(expr string) (uint, error) {
num, err := strconv.Atoi(expr)
if err != nil {
log.Panicf("Failed to parse int from %s: %s", expr, err)
return 0, fmt.Errorf("Failed to parse int from %s: %s", expr, err)
}
if num < 0 {
log.Panicf("Negative number (%d) not allowed: %s", num, expr)
return 0, fmt.Errorf("Negative number (%d) not allowed: %s", num, expr)
}

return uint(num)
return uint(num), nil
}

// getBits sets all bits in the range [min, max], modulo the given step size.
Expand All @@ -205,10 +241,9 @@ func all(r bounds) uint64 {
return getBits(r.min, r.max, 1) | starBit
}

// parseDescriptor returns a pre-defined schedule for the expression, or panics
// if none matches.
func parseDescriptor(spec string) Schedule {
switch spec {
// parseDescriptor returns a predefined schedule for the expression, or error if none matches.
func parseDescriptor(descriptor string) (Schedule, error) {
switch descriptor {
case "@yearly", "@annually":
return &SpecSchedule{
Second: 1 << seconds.min,
Expand All @@ -217,7 +252,7 @@ func parseDescriptor(spec string) Schedule {
Dom: 1 << dom.min,
Month: 1 << months.min,
Dow: all(dow),
}
}, nil

case "@monthly":
return &SpecSchedule{
Expand All @@ -227,7 +262,7 @@ func parseDescriptor(spec string) Schedule {
Dom: 1 << dom.min,
Month: all(months),
Dow: all(dow),
}
}, nil

case "@weekly":
return &SpecSchedule{
Expand All @@ -237,7 +272,7 @@ func parseDescriptor(spec string) Schedule {
Dom: all(dom),
Month: all(months),
Dow: 1 << dow.min,
}
}, nil

case "@daily", "@midnight":
return &SpecSchedule{
Expand All @@ -247,7 +282,7 @@ func parseDescriptor(spec string) Schedule {
Dom: all(dom),
Month: all(months),
Dow: all(dow),
}
}, nil

case "@hourly":
return &SpecSchedule{
Expand All @@ -257,18 +292,17 @@ func parseDescriptor(spec string) Schedule {
Dom: all(dom),
Month: all(months),
Dow: all(dow),
}
}, nil
}

const every = "@every "
if strings.HasPrefix(spec, every) {
duration, err := time.ParseDuration(spec[len(every):])
if strings.HasPrefix(descriptor, every) {
duration, err := time.ParseDuration(descriptor[len(every):])
if err != nil {
log.Panicf("Failed to parse duration %s: %s", spec, err)
return nil, fmt.Errorf("Failed to parse duration %s: %s", descriptor, err)
}
return Every(duration)
return Every(duration), nil
}

log.Panicf("Unrecognized descriptor: %s", spec)
return nil
return nil, fmt.Errorf("Unrecognized descriptor: %s", descriptor)
}
Loading

0 comments on commit 783cfcb

Please sign in to comment.