@@ -8,88 +8,111 @@ import (
8
8
"time"
9
9
)
10
10
11
- // ParseStandard returns a new crontab schedule representing the given standardSpec
12
- // (https://en.wikipedia.org/wiki/Cron). It differs from Parse requiring to always
13
- // pass 5 entries representing: minute, hour, day of month, month and day of week,
14
- // in that order. It returns a descriptive error if the spec is not valid.
15
- //
16
- // It accepts
17
- // - Standard crontab specs, e.g. "* * * * ?"
18
- // - Descriptors, e.g. "@midnight", "@every 1h30m"
19
- func ParseStandard (standardSpec string ) (Schedule , error ) {
20
- if standardSpec [0 ] == '@' {
21
- return parseDescriptor (standardSpec )
22
- }
11
+ // Configuration options for creating a parser. Most options specify which
12
+ // fields should be included, while others enable features. If a field is not
13
+ // included the parser will assume a default value. These options do not change
14
+ // the order fields are parse in.
15
+ type ParseOption int
23
16
24
- // Split on whitespace. We require exactly 5 fields.
25
- // (minute) (hour) (day of month) (month) (day of week)
26
- fields := strings .Fields (standardSpec )
27
- if len (fields ) != 5 {
28
- return nil , fmt .Errorf ("Expected exactly 5 fields, found %d: %s" , len (fields ), standardSpec )
29
- }
17
+ const (
18
+ Second ParseOption = 1 << iota // Seconds field, default 0
19
+ Minute // Minutes field, default 0
20
+ Hour // Hours field, default 0
21
+ Dom // Day of month field, default *
22
+ Month // Month field, default *
23
+ Dow // Day of week field, default *
24
+ DowOptional // Optional day of week field, default *
25
+ Descriptor // Allow descriptors such as @monthly, @weekly, etc.
26
+ )
30
27
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
49
- }
28
+ var places = []ParseOption {
29
+ Second ,
30
+ Minute ,
31
+ Hour ,
32
+ Dom ,
33
+ Month ,
34
+ Dow ,
35
+ }
50
36
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
37
+ var defaults = []string {
38
+ "0" ,
39
+ "0" ,
40
+ "0" ,
41
+ "*" ,
42
+ "*" ,
43
+ "*" ,
44
+ }
45
+
46
+ // A custom Parser that can be configured.
47
+ type Parser struct {
48
+ options ParseOption
49
+ optionals int
50
+ }
51
+
52
+ // Creates a custom Parser with custom options.
53
+ //
54
+ // // Standard parser without descriptors
55
+ // specParser := NewParser(Minute | Hour | Dom | Month | Dow)
56
+ // sched, err := specParser.Parse("0 0 15 */3 *")
57
+ //
58
+ // // Same as above, just excludes time fields
59
+ // subsParser := NewParser(Dom | Month | Dow)
60
+ // sched, err := specParser.Parse("15 */3 *")
61
+ //
62
+ // // Same as above, just makes Dow optional
63
+ // subsParser := NewParser(Dom | Month | DowOptional)
64
+ // sched, err := specParser.Parse("15 */3")
65
+ //
66
+ func NewParser (options ParseOption ) Parser {
67
+ optionals := 0
68
+ if options & DowOptional > 0 {
69
+ options |= Dow
70
+ optionals ++
71
+ }
72
+ return Parser {options , optionals }
59
73
}
60
74
61
75
// Parse returns a new crontab schedule representing the given spec.
62
76
// It returns a descriptive error if the spec is not valid.
63
- //
64
- // It accepts
65
- // - Full crontab specs, e.g. "* * * * * ?"
66
- // - Descriptors, e.g. "@midnight", "@every 1h30m"
67
- func Parse (spec string ) (Schedule , error ) {
68
- if spec [0 ] == '@' {
77
+ // It accepts crontab specs and features configured by NewParser.
78
+ func (p Parser ) Parse (spec string ) (Schedule , error ) {
79
+ if spec [0 ] == '@' && p .options & Descriptor > 0 {
69
80
return parseDescriptor (spec )
70
81
}
71
82
72
- // Split on whitespace. We require 5 or 6 fields.
73
- // (second) (minute) (hour) (day of month) (month) (day of week, optional)
74
- fields := strings .Fields (spec )
75
- if len (fields ) != 5 && len (fields ) != 6 {
76
- return nil , fmt .Errorf ("Expected 5 or 6 fields, found %d: %s" , len (fields ), spec )
83
+ // Figure out how many fields we need
84
+ max := 0
85
+ for _ , place := range places {
86
+ if p .options & place > 0 {
87
+ max ++
88
+ }
77
89
}
90
+ min := max - p .optionals
91
+
92
+ // Split fields on whitespace
93
+ fields := strings .Fields (spec )
78
94
79
- // If a sixth field is not provided (DayOfWeek), then it is equivalent to star.
80
- if len (fields ) == 5 {
81
- fields = append (fields , "*" )
95
+ // Validate number of fields
96
+ if count := len (fields ); count < min || count > max {
97
+ if min == max {
98
+ return nil , fmt .Errorf ("Expected exactly %d fields, found %d: %s" , min , count , spec )
99
+ }
100
+ return nil , fmt .Errorf ("Expected %d to %d fields, found %d: %s" , min , max , count , spec )
82
101
}
83
102
103
+ // Fill in missing fields
104
+ fields = expandFields (fields , p .options )
105
+
84
106
var err error
85
107
field := func (field string , r bounds ) uint64 {
86
108
if err != nil {
87
- return uint64 ( 0 )
109
+ return 0
88
110
}
89
111
var bits uint64
90
112
bits , err = getField (field , r )
91
113
return bits
92
114
}
115
+
93
116
var (
94
117
second = field (fields [0 ], seconds )
95
118
minute = field (fields [1 ], minutes )
@@ -112,6 +135,53 @@ func Parse(spec string) (Schedule, error) {
112
135
}, nil
113
136
}
114
137
138
+ func expandFields (fields []string , options ParseOption ) []string {
139
+ n := 0
140
+ count := len (fields )
141
+ expFields := make ([]string , len (places ))
142
+ copy (expFields , defaults )
143
+ for i , place := range places {
144
+ if options & place > 0 {
145
+ expFields [i ] = fields [n ]
146
+ n ++
147
+ }
148
+ if n == count {
149
+ break
150
+ }
151
+ }
152
+ return expFields
153
+ }
154
+
155
+ var standardParser = NewParser (
156
+ Minute | Hour | Dom | Month | Dow | Descriptor ,
157
+ )
158
+
159
+ // ParseStandard returns a new crontab schedule representing the given standardSpec
160
+ // (https://en.wikipedia.org/wiki/Cron). It differs from Parse requiring to always
161
+ // pass 5 entries representing: minute, hour, day of month, month and day of week,
162
+ // in that order. It returns a descriptive error if the spec is not valid.
163
+ //
164
+ // It accepts
165
+ // - Standard crontab specs, e.g. "* * * * ?"
166
+ // - Descriptors, e.g. "@midnight", "@every 1h30m"
167
+ func ParseStandard (standardSpec string ) (Schedule , error ) {
168
+ return standardParser .Parse (standardSpec )
169
+ }
170
+
171
+ var defaultParser = NewParser (
172
+ Second | Minute | Hour | Dom | Month | DowOptional | Descriptor ,
173
+ )
174
+
175
+ // Parse returns a new crontab schedule representing the given spec.
176
+ // It returns a descriptive error if the spec is not valid.
177
+ //
178
+ // It accepts
179
+ // - Full crontab specs, e.g. "* * * * * ?"
180
+ // - Descriptors, e.g. "@midnight", "@every 1h30m"
181
+ func Parse (spec string ) (Schedule , error ) {
182
+ return defaultParser .Parse (spec )
183
+ }
184
+
115
185
// getField returns an Int with the bits set representing all of the times that
116
186
// the field represents or error parsing field value. A "field" is a comma-separated
117
187
// list of "ranges".
@@ -138,29 +208,28 @@ func getRange(expr string, r bounds) (uint64, error) {
138
208
lowAndHigh = strings .Split (rangeAndStep [0 ], "-" )
139
209
singleDigit = len (lowAndHigh ) == 1
140
210
err error
141
- zero = uint64 (0 )
142
211
)
143
212
144
- var extra_star uint64
213
+ var extra uint64
145
214
if lowAndHigh [0 ] == "*" || lowAndHigh [0 ] == "?" {
146
215
start = r .min
147
216
end = r .max
148
- extra_star = starBit
217
+ extra = starBit
149
218
} else {
150
219
start , err = parseIntOrName (lowAndHigh [0 ], r .names )
151
220
if err != nil {
152
- return zero , err
221
+ return 0 , err
153
222
}
154
223
switch len (lowAndHigh ) {
155
224
case 1 :
156
225
end = start
157
226
case 2 :
158
227
end , err = parseIntOrName (lowAndHigh [1 ], r .names )
159
228
if err != nil {
160
- return zero , err
229
+ return 0 , err
161
230
}
162
231
default :
163
- return zero , fmt .Errorf ("Too many hyphens: %s" , expr )
232
+ return 0 , fmt .Errorf ("Too many hyphens: %s" , expr )
164
233
}
165
234
}
166
235
@@ -170,31 +239,31 @@ func getRange(expr string, r bounds) (uint64, error) {
170
239
case 2 :
171
240
step , err = mustParseInt (rangeAndStep [1 ])
172
241
if err != nil {
173
- return zero , err
242
+ return 0 , err
174
243
}
175
244
176
245
// Special handling: "N/step" means "N-max/step".
177
246
if singleDigit {
178
247
end = r .max
179
248
}
180
249
default :
181
- return zero , fmt .Errorf ("Too many slashes: %s" , expr )
250
+ return 0 , fmt .Errorf ("Too many slashes: %s" , expr )
182
251
}
183
252
184
253
if start < r .min {
185
- return zero , fmt .Errorf ("Beginning of range (%d) below minimum (%d): %s" , start , r .min , expr )
254
+ return 0 , fmt .Errorf ("Beginning of range (%d) below minimum (%d): %s" , start , r .min , expr )
186
255
}
187
256
if end > r .max {
188
- return zero , fmt .Errorf ("End of range (%d) above maximum (%d): %s" , end , r .max , expr )
257
+ return 0 , fmt .Errorf ("End of range (%d) above maximum (%d): %s" , end , r .max , expr )
189
258
}
190
259
if start > end {
191
- return zero , fmt .Errorf ("Beginning of range (%d) beyond end of range (%d): %s" , start , end , expr )
260
+ return 0 , fmt .Errorf ("Beginning of range (%d) beyond end of range (%d): %s" , start , end , expr )
192
261
}
193
262
if step == 0 {
194
- return zero , fmt .Errorf ("Step of range should be a positive number: %s" , expr )
263
+ return 0 , fmt .Errorf ("Step of range should be a positive number: %s" , expr )
195
264
}
196
265
197
- return getBits (start , end , step ) | extra_star , nil
266
+ return getBits (start , end , step ) | extra , nil
198
267
}
199
268
200
269
// parseIntOrName returns the (possibly-named) integer contained in expr.
0 commit comments