1 package cron
2
3 import (
4 "fmt"
5 "math"
6 "strconv"
7 "strings"
8 "time"
9 )
10
11
12
13
14
15 type ParseOption int
16
17 const (
18 Second ParseOption = 1 << iota
19 SecondOptional
20 Minute
21 Hour
22 Dom
23 Month
24 Dow
25 DowOptional
26 Descriptor
27 )
28
29 var places = []ParseOption{
30 Second,
31 Minute,
32 Hour,
33 Dom,
34 Month,
35 Dow,
36 }
37
38 var defaults = []string{
39 "0",
40 "0",
41 "0",
42 "*",
43 "*",
44 "*",
45 }
46
47
48 type Parser struct {
49 options ParseOption
50 }
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71 func NewParser(options ParseOption) Parser {
72 optionals := 0
73 if options&DowOptional > 0 {
74 optionals++
75 }
76 if options&SecondOptional > 0 {
77 optionals++
78 }
79 if optionals > 1 {
80 panic("multiple optionals may not be configured")
81 }
82 return Parser{options}
83 }
84
85
86
87
88 func (p Parser) Parse(spec string) (Schedule, error) {
89 if len(spec) == 0 {
90 return nil, fmt.Errorf("empty spec string")
91 }
92
93
94 var loc = time.Local
95 if strings.HasPrefix(spec, "TZ=") || strings.HasPrefix(spec, "CRON_TZ=") {
96 var err error
97 i := strings.Index(spec, " ")
98 eq := strings.Index(spec, "=")
99 if loc, err = time.LoadLocation(spec[eq+1 : i]); err != nil {
100 return nil, fmt.Errorf("provided bad location %s: %v", spec[eq+1:i], err)
101 }
102 spec = strings.TrimSpace(spec[i:])
103 }
104
105
106 if strings.HasPrefix(spec, "@") {
107 if p.options&Descriptor == 0 {
108 return nil, fmt.Errorf("parser does not accept descriptors: %v", spec)
109 }
110 return parseDescriptor(spec, loc)
111 }
112
113
114 fields := strings.Fields(spec)
115
116
117 var err error
118 fields, err = normalizeFields(fields, p.options)
119 if err != nil {
120 return nil, err
121 }
122
123 field := func(field string, r bounds) uint64 {
124 if err != nil {
125 return 0
126 }
127 var bits uint64
128 bits, err = getField(field, r)
129 return bits
130 }
131
132 var (
133 second = field(fields[0], seconds)
134 minute = field(fields[1], minutes)
135 hour = field(fields[2], hours)
136 dayofmonth = field(fields[3], dom)
137 month = field(fields[4], months)
138 dayofweek = field(fields[5], dow)
139 )
140 if err != nil {
141 return nil, err
142 }
143
144 return &SpecSchedule{
145 Second: second,
146 Minute: minute,
147 Hour: hour,
148 Dom: dayofmonth,
149 Month: month,
150 Dow: dayofweek,
151 Location: loc,
152 }, nil
153 }
154
155
156
157
158
159
160 func normalizeFields(fields []string, options ParseOption) ([]string, error) {
161
162 optionals := 0
163 if options&SecondOptional > 0 {
164 options |= Second
165 optionals++
166 }
167 if options&DowOptional > 0 {
168 options |= Dow
169 optionals++
170 }
171 if optionals > 1 {
172 return nil, fmt.Errorf("multiple optionals may not be configured")
173 }
174
175
176 max := 0
177 for _, place := range places {
178 if options&place > 0 {
179 max++
180 }
181 }
182 min := max - optionals
183
184
185 if count := len(fields); count < min || count > max {
186 if min == max {
187 return nil, fmt.Errorf("expected exactly %d fields, found %d: %s", min, count, fields)
188 }
189 return nil, fmt.Errorf("expected %d to %d fields, found %d: %s", min, max, count, fields)
190 }
191
192
193 if min < max && len(fields) == min {
194 switch {
195 case options&DowOptional > 0:
196 fields = append(fields, defaults[5])
197 case options&SecondOptional > 0:
198 fields = append([]string{defaults[0]}, fields...)
199 default:
200 return nil, fmt.Errorf("unknown optional field")
201 }
202 }
203
204
205 n := 0
206 expandedFields := make([]string, len(places))
207 copy(expandedFields, defaults)
208 for i, place := range places {
209 if options&place > 0 {
210 expandedFields[i] = fields[n]
211 n++
212 }
213 }
214 return expandedFields, nil
215 }
216
217 var standardParser = NewParser(
218 Minute | Hour | Dom | Month | Dow | Descriptor,
219 )
220
221
222
223
224
225
226
227
228
229 func ParseStandard(standardSpec string) (Schedule, error) {
230 return standardParser.Parse(standardSpec)
231 }
232
233
234
235
236 func getField(field string, r bounds) (uint64, error) {
237 var bits uint64
238 ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
239 for _, expr := range ranges {
240 bit, err := getRange(expr, r)
241 if err != nil {
242 return bits, err
243 }
244 bits |= bit
245 }
246 return bits, nil
247 }
248
249
250
251
252 func getRange(expr string, r bounds) (uint64, error) {
253 var (
254 start, end, step uint
255 rangeAndStep = strings.Split(expr, "/")
256 lowAndHigh = strings.Split(rangeAndStep[0], "-")
257 singleDigit = len(lowAndHigh) == 1
258 err error
259 )
260
261 var extra uint64
262 if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
263 start = r.min
264 end = r.max
265 extra = starBit
266 } else {
267 start, err = parseIntOrName(lowAndHigh[0], r.names)
268 if err != nil {
269 return 0, err
270 }
271 switch len(lowAndHigh) {
272 case 1:
273 end = start
274 case 2:
275 end, err = parseIntOrName(lowAndHigh[1], r.names)
276 if err != nil {
277 return 0, err
278 }
279 default:
280 return 0, fmt.Errorf("too many hyphens: %s", expr)
281 }
282 }
283
284 switch len(rangeAndStep) {
285 case 1:
286 step = 1
287 case 2:
288 step, err = mustParseInt(rangeAndStep[1])
289 if err != nil {
290 return 0, err
291 }
292
293
294 if singleDigit {
295 end = r.max
296 }
297 if step > 1 {
298 extra = 0
299 }
300 default:
301 return 0, fmt.Errorf("too many slashes: %s", expr)
302 }
303
304 if start < r.min {
305 return 0, fmt.Errorf("beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
306 }
307 if end > r.max {
308 return 0, fmt.Errorf("end of range (%d) above maximum (%d): %s", end, r.max, expr)
309 }
310 if start > end {
311 return 0, fmt.Errorf("beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
312 }
313 if step == 0 {
314 return 0, fmt.Errorf("step of range should be a positive number: %s", expr)
315 }
316
317 return getBits(start, end, step) | extra, nil
318 }
319
320
321 func parseIntOrName(expr string, names map[string]uint) (uint, error) {
322 if names != nil {
323 if namedInt, ok := names[strings.ToLower(expr)]; ok {
324 return namedInt, nil
325 }
326 }
327 return mustParseInt(expr)
328 }
329
330
331 func mustParseInt(expr string) (uint, error) {
332 num, err := strconv.Atoi(expr)
333 if err != nil {
334 return 0, fmt.Errorf("failed to parse int from %s: %s", expr, err)
335 }
336 if num < 0 {
337 return 0, fmt.Errorf("negative number (%d) not allowed: %s", num, expr)
338 }
339
340 return uint(num), nil
341 }
342
343
344 func getBits(min, max, step uint) uint64 {
345 var bits uint64
346
347
348 if step == 1 {
349 return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
350 }
351
352
353 for i := min; i <= max; i += step {
354 bits |= 1 << i
355 }
356 return bits
357 }
358
359
360 func all(r bounds) uint64 {
361 return getBits(r.min, r.max, 1) | starBit
362 }
363
364
365 func parseDescriptor(descriptor string, loc *time.Location) (Schedule, error) {
366 switch descriptor {
367 case "@yearly", "@annually":
368 return &SpecSchedule{
369 Second: 1 << seconds.min,
370 Minute: 1 << minutes.min,
371 Hour: 1 << hours.min,
372 Dom: 1 << dom.min,
373 Month: 1 << months.min,
374 Dow: all(dow),
375 Location: loc,
376 }, nil
377
378 case "@monthly":
379 return &SpecSchedule{
380 Second: 1 << seconds.min,
381 Minute: 1 << minutes.min,
382 Hour: 1 << hours.min,
383 Dom: 1 << dom.min,
384 Month: all(months),
385 Dow: all(dow),
386 Location: loc,
387 }, nil
388
389 case "@weekly":
390 return &SpecSchedule{
391 Second: 1 << seconds.min,
392 Minute: 1 << minutes.min,
393 Hour: 1 << hours.min,
394 Dom: all(dom),
395 Month: all(months),
396 Dow: 1 << dow.min,
397 Location: loc,
398 }, nil
399
400 case "@daily", "@midnight":
401 return &SpecSchedule{
402 Second: 1 << seconds.min,
403 Minute: 1 << minutes.min,
404 Hour: 1 << hours.min,
405 Dom: all(dom),
406 Month: all(months),
407 Dow: all(dow),
408 Location: loc,
409 }, nil
410
411 case "@hourly":
412 return &SpecSchedule{
413 Second: 1 << seconds.min,
414 Minute: 1 << minutes.min,
415 Hour: all(hours),
416 Dom: all(dom),
417 Month: all(months),
418 Dow: all(dow),
419 Location: loc,
420 }, nil
421
422 }
423
424 const every = "@every "
425 if strings.HasPrefix(descriptor, every) {
426 duration, err := time.ParseDuration(descriptor[len(every):])
427 if err != nil {
428 return nil, fmt.Errorf("failed to parse duration %s: %s", descriptor, err)
429 }
430 return Every(duration), nil
431 }
432
433 return nil, fmt.Errorf("unrecognized descriptor: %s", descriptor)
434 }
435
View as plain text