-
Notifications
You must be signed in to change notification settings - Fork 0
/
parser.go
196 lines (168 loc) · 5.42 KB
/
parser.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
// package cron provides ability to parse and run a cron (https://en.wikipedia.org/wiki/Cron) like schedule.
// It can use either SQL or Memory as a backend store. SQL store has the benefit or persistent store and
// the ability to run multiple instance of the app
//
// expression parsing is inspired by https://github.com/robfig/cron
package cron
import (
"errors"
"fmt"
"strconv"
"strings"
"time"
)
// represent '*' in cron where all bit is set to 1
const star = field(^uint64(0))
type field uint64
// match check if the current value matches the field bitmap.
func (f field) match(value int) bool {
return f&(1<<uint64(value)) != 0
}
func (f field) format() string {
if f == star {
return "*"
}
buffer := make([]string, 0)
for i := 0; i < 64; i++ {
if f.match(i) {
buffer = append(buffer, strconv.Itoa(i))
}
}
return strings.Join(buffer, ",")
}
// Entry represents a single cron entry
type Entry struct {
Name string
Meta string // optional metadata
Location *time.Location
// parsed representation of expression
minute, hour, dom, month, dow field
expression string
}
// Expression in string representation
func (e Entry) Expression() string {
return e.expression
}
// Match the entry with a time
func (e Entry) Match(t time.Time) bool {
t = t.In(e.Location)
return e.minute.match(t.Minute()) &&
e.hour.match(t.Hour()) &&
e.dom.match(t.Day()) &&
e.dow.match(int(t.Weekday())) &&
e.month.match(int(t.Month()))
}
func (e Entry) String() string {
str := []string{e.minute.format(), e.hour.format(), e.dom.format(), e.month.format(), e.dow.format()}
return fmt.Sprintf("{ name:%q schedule:%q, location:%q }", e.Name, strings.Join(str, " "), e.Location)
}
// Parse a cron expression on a location. If location is nil it uses UTC
// it does not support macro (ex: @monthly)
//
// ex format:
//
// +------------------ Minute (0-59) : [5]
// | +---------------- Hour (0-23) : [0, 1, 2, ..., 23]
// | | +------------ Day of month (1-31) : [5, 10, 15, 20, 30]
// | | | +------- Month (1-12) : [1, 3, 5, ..., 11]
// | | | | +- Day of Week (0-6) : [Sun, Mon, Tue, Wed]
// 5 * */5 1-12/2 0-3
func Parse(expression string, loc *time.Location, name string) (Entry, error) {
if loc == nil {
loc = time.UTC
}
e := Entry{
Name: name,
Location: loc,
expression: expression,
}
fields := strings.Fields(expression)
if len(fields) != 5 {
return e, fmt.Errorf("got %d want %d expressions", len(fields), 5)
}
var err error
e.minute, err = parseField(fields[0], 0, 59)
if err != nil {
return e, fmt.Errorf("failed parsing 'minute' field %q: %v", fields[0], err)
}
e.hour, err = parseField(fields[1], 0, 23)
if err != nil {
return e, fmt.Errorf("failed parsing 'hour' field %q: %v", fields[1], err)
}
e.dom, err = parseField(fields[2], 1, 31)
if err != nil {
return e, fmt.Errorf("failed parsing 'day of month' field %q: %v", fields[2], err)
}
e.month, err = parseField(fields[3], 1, 12)
if err != nil {
return e, fmt.Errorf("failed parsing 'month' field %q: %v", fields[3], err)
}
e.dow, err = parseField(fields[4], 0, 6)
if err != nil {
return e, fmt.Errorf("failed parsing 'day of week' field %q: %v", fields[4], err)
}
return e, nil
}
// parseField construct bitmap where position represents a value for that field
// ex: value of minutes `1,3,5`:
// bit 7654 3210
// possible value 6543 210
// bit value 0010 1010 -> [0,2,4] will be represented as uint64 value 42 (0x2A)
func parseField(s string, min, max int) (field, error) {
s = strings.TrimSpace(s)
if s == "" {
return 0, errors.New("empty field")
}
if s == "*" || s == "?" {
return star, nil
}
var f field
// parse single element or parse range (ex: '2' '1-5' '*/5' '1-30/2' )
// determine start, end and interval. Construct bitmap by traversing from start-end with interval.
for _, part := range strings.Split(s, ",") {
var (
err error
interval = 1
startInterval, endInterval = min, max
)
// parse interval (ex: '*/5' '1-30/2') if exists
if i := strings.IndexByte(part, '/'); i >= 0 {
if r := part[:i]; r != "*" && r != "?" && strings.IndexByte(r, '-') < 0 {
return 0, fmt.Errorf("step given without range, expression %q", s)
}
step := part[i+1:]
interval, err = strconv.Atoi(step)
if err != nil {
return 0, fmt.Errorf("failed parsing interval expression %q: %s", step, err)
}
part = part[:i]
}
start, end := part, part
// parse range if exist
if i := strings.IndexByte(part, '-'); i >= 0 {
start, end = part[:i], part[i+1:]
}
// determine start & end, some cron format use '?' instead of '*'
if start != "*" && start != "?" {
startInterval, err = strconv.Atoi(start)
if err != nil {
return 0, fmt.Errorf("failed parsing expression %q: %s", s, err)
}
// parse end interval if exists, else it will be same as start (single value)
if end != "" {
endInterval, err = strconv.Atoi(end)
if err != nil {
return 0, fmt.Errorf("failed parsing expression %q: %s", s, err)
}
}
}
if startInterval < min || endInterval > max || startInterval > endInterval {
return 0, fmt.Errorf("value out of range (%d - %d): %s", min, max, part)
}
// at this point we get the start, end, interval. Construct bitmap that represents possible values
for i := startInterval; i <= endInterval; i += interval {
f |= 1 << uint64(i)
}
}
return f, nil
}