-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathcron.go
183 lines (157 loc) · 4.14 KB
/
cron.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
package cronmask
import (
"errors"
"fmt"
"strconv"
"strings"
"time"
)
var (
validRanges = []struct {
start, end int
}{
{start: 0, end: 59},
{start: 0, end: 23},
{start: 1, end: 31},
{start: 1, end: 12},
{start: 0, end: 6},
}
)
// CronMask interface exposes a method to check whether the
// given time.Time matches the expression CronMask was constructed with.
type CronMask struct {
minute field
hour field
dayOfMonth field
month field
dayOfWeek field
}
func (c *CronMask) Match(t time.Time) bool {
return c.minute.match(t.Minute()) &&
c.hour.match(t.Hour()) &&
c.dayOfMonth.match(t.Day()) &&
c.month.match(int(t.Month())) &&
c.dayOfWeek.match(int(t.Weekday()))
}
type field interface {
match(val int) bool
}
type wildcard struct {
}
func (wildcard) match(val int) bool {
return true
}
type constant struct {
val int
}
func (f constant) match(val int) bool {
return f.val == val
}
// range is a reserved keyword
type rangeF struct {
start int
end int
}
func (f rangeF) match(val int) bool {
return val >= f.start && val <= f.end
}
type list struct {
parts []field
}
func (f list) match(val int) bool {
for _, p := range f.parts {
if p.match(val) {
return true
}
}
return false
}
func validateRange(i, val int) error {
validRange := validRanges[i]
if validRange.start <= val && validRange.end >= val {
return nil
}
return fmt.Errorf("expected %d to be in [%d,%d] range", val, validRange.start, validRange.end)
}
func parseValue(fieldIdx int, raw string) (int, error) {
parsed, err := strconv.Atoi(raw)
if err != nil {
return 0, err
}
if err := validateRange(fieldIdx, parsed); err != nil {
return 0, err
}
return parsed, nil
}
func parseCronField(fieldIdx int, fieldStr string) (field, error) {
if fieldStr == "*" {
return wildcard{}, nil
}
parts := strings.Split(fieldStr, ",")
fields := make([]field, 0, len(parts))
for _, p := range parts {
if p == "" {
return nil, fmt.Errorf("could not parse the cron field: %s. invalid list item: %s", fieldStr, p)
}
possibleRangeFields := strings.Split(p, "-")
if len(possibleRangeFields) == 1 {
parsed, err := parseValue(fieldIdx, possibleRangeFields[0])
if err != nil {
return nil, fmt.Errorf("could not parse cron field: %s. invalid list item: %s: %w", fieldStr, p, err)
}
fields = append(fields, constant{val: parsed})
} else if len(possibleRangeFields) == 2 {
start, err := parseValue(fieldIdx, possibleRangeFields[0])
if err != nil {
return nil, fmt.Errorf("could not parse cron field: %s. invalid list item: %s: %w", fieldStr, p, err)
}
end, err := parseValue(fieldIdx, possibleRangeFields[1])
if err != nil {
return nil, fmt.Errorf("could not parse cron field: %s. invalid list item: %s: %w", fieldStr, p, err)
}
fields = append(fields, rangeF{start: start, end: end})
} else {
return nil, fmt.Errorf("could not parse cron field: %s. invalid list item: %s", fieldStr, p)
}
}
return list{parts: fields}, nil
}
// New constructs a new CronMask instance that can be used to check if a given time.Time
// matches the expression or not.
//
// For CRON expressions (https://en.wikipedia.org/wiki/Cron#CRON_expression):
//
// Expressions are expected to be in the same time zone as the system that generates the time.Time instances.
//
// You can check the tests for what is possible.
//
// Unsupported features:
//
// - Non-standard characters (https://en.wikipedia.org/wiki/Cron#Non-standard_characters)
//
// - Year field
//
// - Command section
//
// - Text representation of the fields "month" and "day of week"
func New(expr string) (*CronMask, error) {
parts := strings.Fields(expr)
if len(parts) != 5 {
return nil, errors.New("invalid cron mask expression. expected 5 fields separated by whitespaces")
}
var err error
var minute, hour, dayOfMonth, month, dayOfWeek field
fields := []*field{&minute, &hour, &dayOfMonth, &month, &dayOfWeek}
for i, p := range parts {
if *fields[i], err = parseCronField(i, p); err != nil {
return nil, err
}
}
return &CronMask{
minute: minute,
hour: hour,
dayOfMonth: dayOfMonth,
month: month,
dayOfWeek: dayOfWeek,
}, nil
}