Skip to content

Commit 783cfcb

Browse files
authored
Merge pull request robfig#74 from soltysh/issue33
Refactor parsing to return error instead of using panic/recover
2 parents 23baae2 + fcc395c commit 783cfcb

File tree

2 files changed

+237
-110
lines changed

2 files changed

+237
-110
lines changed

parser.go

+111-77
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package cron
22

33
import (
44
"fmt"
5-
"log"
65
"math"
76
"strconv"
87
"strings"
@@ -17,36 +16,46 @@ import (
1716
// It accepts
1817
// - Standard crontab specs, e.g. "* * * * ?"
1918
// - Descriptors, e.g. "@midnight", "@every 1h30m"
20-
func ParseStandard(standardSpec string) (_ Schedule, err error) {
21-
// Convert panics into errors
22-
defer func() {
23-
if recovered := recover(); recovered != nil {
24-
err = fmt.Errorf("%v", recovered)
25-
}
26-
}()
27-
19+
func ParseStandard(standardSpec string) (Schedule, error) {
2820
if standardSpec[0] == '@' {
29-
return parseDescriptor(standardSpec), nil
21+
return parseDescriptor(standardSpec)
3022
}
3123

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

39-
schedule := &SpecSchedule{
40-
Second: 1 << seconds.min,
41-
Minute: getField(fields[0], minutes),
42-
Hour: getField(fields[1], hours),
43-
Dom: getField(fields[2], dom),
44-
Month: getField(fields[3], months),
45-
Dow: getField(fields[4], dow),
31+
var err error
32+
field := func(field string, r bounds) uint64 {
33+
if err != nil {
34+
return uint64(0)
35+
}
36+
var bits uint64
37+
bits, err = getField(field, r)
38+
return bits
39+
}
40+
var (
41+
minute = field(fields[0], minutes)
42+
hour = field(fields[1], hours)
43+
dayofmonth = field(fields[2], dom)
44+
month = field(fields[3], months)
45+
dayofweek = field(fields[4], dow)
46+
)
47+
if err != nil {
48+
return nil, err
4649
}
4750

48-
return schedule, nil
49-
51+
return &SpecSchedule{
52+
Second: 1 << seconds.min,
53+
Minute: minute,
54+
Hour: hour,
55+
Dom: dayofmonth,
56+
Month: month,
57+
Dow: dayofweek,
58+
}, nil
5059
}
5160

5261
// Parse returns a new crontab schedule representing the given spec.
@@ -55,63 +64,81 @@ func ParseStandard(standardSpec string) (_ Schedule, err error) {
5564
// It accepts
5665
// - Full crontab specs, e.g. "* * * * * ?"
5766
// - Descriptors, e.g. "@midnight", "@every 1h30m"
58-
func Parse(spec string) (_ Schedule, err error) {
59-
// Convert panics into errors
60-
defer func() {
61-
if recovered := recover(); recovered != nil {
62-
err = fmt.Errorf("%v", recovered)
63-
}
64-
}()
65-
67+
func Parse(spec string) (Schedule, error) {
6668
if spec[0] == '@' {
67-
return parseDescriptor(spec), nil
69+
return parseDescriptor(spec)
6870
}
6971

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

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

82-
schedule := &SpecSchedule{
83-
Second: getField(fields[0], seconds),
84-
Minute: getField(fields[1], minutes),
85-
Hour: getField(fields[2], hours),
86-
Dom: getField(fields[3], dom),
87-
Month: getField(fields[4], months),
88-
Dow: getField(fields[5], dow),
84+
var err error
85+
field := func(field string, r bounds) uint64 {
86+
if err != nil {
87+
return uint64(0)
88+
}
89+
var bits uint64
90+
bits, err = getField(field, r)
91+
return bits
92+
}
93+
var (
94+
second = field(fields[0], seconds)
95+
minute = field(fields[1], minutes)
96+
hour = field(fields[2], hours)
97+
dayofmonth = field(fields[3], dom)
98+
month = field(fields[4], months)
99+
dayofweek = field(fields[5], dow)
100+
)
101+
if err != nil {
102+
return nil, err
89103
}
90104

91-
return schedule, nil
105+
return &SpecSchedule{
106+
Second: second,
107+
Minute: minute,
108+
Hour: hour,
109+
Dom: dayofmonth,
110+
Month: month,
111+
Dow: dayofweek,
112+
}, nil
92113
}
93114

94115
// getField returns an Int with the bits set representing all of the times that
95-
// the field represents. A "field" is a comma-separated list of "ranges".
96-
func getField(field string, r bounds) uint64 {
97-
// list = range {"," range}
116+
// the field represents or error parsing field value. A "field" is a comma-separated
117+
// list of "ranges".
118+
func getField(field string, r bounds) (uint64, error) {
98119
var bits uint64
99120
ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
100121
for _, expr := range ranges {
101-
bits |= getRange(expr, r)
122+
bit, err := getRange(expr, r)
123+
if err != nil {
124+
return bits, err
125+
}
126+
bits |= bit
102127
}
103-
return bits
128+
return bits, nil
104129
}
105130

106131
// getRange returns the bits indicated by the given expression:
107132
// number | number "-" number [ "/" number ]
108-
func getRange(expr string, r bounds) uint64 {
109-
133+
// or error parsing range.
134+
func getRange(expr string, r bounds) (uint64, error) {
110135
var (
111136
start, end, step uint
112137
rangeAndStep = strings.Split(expr, "/")
113138
lowAndHigh = strings.Split(rangeAndStep[0], "-")
114139
singleDigit = len(lowAndHigh) == 1
140+
err error
141+
zero = uint64(0)
115142
)
116143

117144
var extra_star uint64
@@ -120,68 +147,77 @@ func getRange(expr string, r bounds) uint64 {
120147
end = r.max
121148
extra_star = starBit
122149
} else {
123-
start = parseIntOrName(lowAndHigh[0], r.names)
150+
start, err = parseIntOrName(lowAndHigh[0], r.names)
151+
if err != nil {
152+
return zero, err
153+
}
124154
switch len(lowAndHigh) {
125155
case 1:
126156
end = start
127157
case 2:
128-
end = parseIntOrName(lowAndHigh[1], r.names)
158+
end, err = parseIntOrName(lowAndHigh[1], r.names)
159+
if err != nil {
160+
return zero, err
161+
}
129162
default:
130-
log.Panicf("Too many hyphens: %s", expr)
163+
return zero, fmt.Errorf("Too many hyphens: %s", expr)
131164
}
132165
}
133166

134167
switch len(rangeAndStep) {
135168
case 1:
136169
step = 1
137170
case 2:
138-
step = mustParseInt(rangeAndStep[1])
171+
step, err = mustParseInt(rangeAndStep[1])
172+
if err != nil {
173+
return zero, err
174+
}
139175

140176
// Special handling: "N/step" means "N-max/step".
141177
if singleDigit {
142178
end = r.max
143179
}
144180
default:
145-
log.Panicf("Too many slashes: %s", expr)
181+
return zero, fmt.Errorf("Too many slashes: %s", expr)
146182
}
147183

148184
if start < r.min {
149-
log.Panicf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
185+
return zero, fmt.Errorf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
150186
}
151187
if end > r.max {
152-
log.Panicf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
188+
return zero, fmt.Errorf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
153189
}
154190
if start > end {
155-
log.Panicf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
191+
return zero, fmt.Errorf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
156192
}
157193
if step == 0 {
158-
log.Panicf("Step of range should be a positive number: %s", expr)
194+
return zero, fmt.Errorf("Step of range should be a positive number: %s", expr)
159195
}
160196

161-
return getBits(start, end, step) | extra_star
197+
return getBits(start, end, step) | extra_star, nil
162198
}
163199

164200
// parseIntOrName returns the (possibly-named) integer contained in expr.
165-
func parseIntOrName(expr string, names map[string]uint) uint {
201+
func parseIntOrName(expr string, names map[string]uint) (uint, error) {
166202
if names != nil {
167203
if namedInt, ok := names[strings.ToLower(expr)]; ok {
168-
return namedInt
204+
return namedInt, nil
169205
}
170206
}
171207
return mustParseInt(expr)
172208
}
173209

174-
// mustParseInt parses the given expression as an int or panics.
175-
func mustParseInt(expr string) uint {
210+
// mustParseInt parses the given expression as an int or returns an error.
211+
func mustParseInt(expr string) (uint, error) {
176212
num, err := strconv.Atoi(expr)
177213
if err != nil {
178-
log.Panicf("Failed to parse int from %s: %s", expr, err)
214+
return 0, fmt.Errorf("Failed to parse int from %s: %s", expr, err)
179215
}
180216
if num < 0 {
181-
log.Panicf("Negative number (%d) not allowed: %s", num, expr)
217+
return 0, fmt.Errorf("Negative number (%d) not allowed: %s", num, expr)
182218
}
183219

184-
return uint(num)
220+
return uint(num), nil
185221
}
186222

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

208-
// parseDescriptor returns a pre-defined schedule for the expression, or panics
209-
// if none matches.
210-
func parseDescriptor(spec string) Schedule {
211-
switch spec {
244+
// parseDescriptor returns a predefined schedule for the expression, or error if none matches.
245+
func parseDescriptor(descriptor string) (Schedule, error) {
246+
switch descriptor {
212247
case "@yearly", "@annually":
213248
return &SpecSchedule{
214249
Second: 1 << seconds.min,
@@ -217,7 +252,7 @@ func parseDescriptor(spec string) Schedule {
217252
Dom: 1 << dom.min,
218253
Month: 1 << months.min,
219254
Dow: all(dow),
220-
}
255+
}, nil
221256

222257
case "@monthly":
223258
return &SpecSchedule{
@@ -227,7 +262,7 @@ func parseDescriptor(spec string) Schedule {
227262
Dom: 1 << dom.min,
228263
Month: all(months),
229264
Dow: all(dow),
230-
}
265+
}, nil
231266

232267
case "@weekly":
233268
return &SpecSchedule{
@@ -237,7 +272,7 @@ func parseDescriptor(spec string) Schedule {
237272
Dom: all(dom),
238273
Month: all(months),
239274
Dow: 1 << dow.min,
240-
}
275+
}, nil
241276

242277
case "@daily", "@midnight":
243278
return &SpecSchedule{
@@ -247,7 +282,7 @@ func parseDescriptor(spec string) Schedule {
247282
Dom: all(dom),
248283
Month: all(months),
249284
Dow: all(dow),
250-
}
285+
}, nil
251286

252287
case "@hourly":
253288
return &SpecSchedule{
@@ -257,18 +292,17 @@ func parseDescriptor(spec string) Schedule {
257292
Dom: all(dom),
258293
Month: all(months),
259294
Dow: all(dow),
260-
}
295+
}, nil
261296
}
262297

263298
const every = "@every "
264-
if strings.HasPrefix(spec, every) {
265-
duration, err := time.ParseDuration(spec[len(every):])
299+
if strings.HasPrefix(descriptor, every) {
300+
duration, err := time.ParseDuration(descriptor[len(every):])
266301
if err != nil {
267-
log.Panicf("Failed to parse duration %s: %s", spec, err)
302+
return nil, fmt.Errorf("Failed to parse duration %s: %s", descriptor, err)
268303
}
269-
return Every(duration)
304+
return Every(duration), nil
270305
}
271306

272-
log.Panicf("Unrecognized descriptor: %s", spec)
273-
return nil
307+
return nil, fmt.Errorf("Unrecognized descriptor: %s", descriptor)
274308
}

0 commit comments

Comments
 (0)