-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrule.go
307 lines (255 loc) · 7.45 KB
/
rule.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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
package cronrange
import (
"fmt"
"sort"
"strconv"
"strings"
"time"
)
// Rule represents a single cronrange rule
type Rule struct {
timeRange TimeRange
dow Field // 0-6 (Sunday = 0)
dom Field // 1-31
month Field // 1-12
}
// TimeRange represents a time period within a day
type TimeRange struct {
start time.Duration // minutes since midnight
end time.Duration
all bool
overnight bool // true if range spans across midnight
hasSeconds bool // track if the original format included seconds
}
// Field represents a cronrange field that can contain multiple values
type Field struct {
values map[int]bool
all bool
}
// parseRule parses a cronrange rule string and returns a Rule struct or an error if the input is invalid
func parseRule(rule string) (Rule, error) {
parts := strings.Fields(rule)
if len(parts) != 4 {
return Rule{}, fmt.Errorf("rule must have 4 fields: time dow dom month")
}
timeRange, err := parseTimeRange(parts[0])
if err != nil {
return Rule{}, err
}
dow, err := parseField(parts[1], 0, 6)
if err != nil {
return Rule{}, fmt.Errorf("invalid dow: %w", err)
}
dom, err := parseField(parts[2], 1, 31)
if err != nil {
return Rule{}, fmt.Errorf("invalid dom: %w", err)
}
month, err := parseField(parts[3], 1, 12)
if err != nil {
return Rule{}, fmt.Errorf("invalid month: %w", err)
}
return Rule{
timeRange: timeRange,
dow: dow,
dom: dom,
month: month,
}, nil
}
// parseTimeRange parses a time range string in the following formats: HH:MM-HH:MM, HH:MM:SS-HH:MM:SS
// or a single asterisk for all day. Handles ranges that span across midnight.
func parseTimeRange(s string) (TimeRange, error) {
if s == "*" {
return TimeRange{all: true}, nil
}
parts := strings.Split(s, "-")
if len(parts) != 2 {
return TimeRange{}, fmt.Errorf("invalid time range format")
}
start, hasStartSeconds, err := parseTime(parts[0])
if err != nil {
return TimeRange{}, err
}
end, hasEndSeconds, err := parseTime(parts[1])
if err != nil {
return TimeRange{}, err
}
// Check if this is an overnight range
overnight := false
if end < start {
overnight = true
}
return TimeRange{
start: start,
end: end,
overnight: overnight,
hasSeconds: hasStartSeconds || hasEndSeconds,
}, nil
}
// parseTime parses a time string in the formats HH:MM or HH:MM:SS.
// It returns the duration since midnight, a boolean indicating if seconds were specified, and an error if the input is invalid.
// The function splits the input string by colons, converts the parts to integers, and validates the values.
func parseTime(s string) (time.Duration, bool, error) {
parts := strings.Split(s, ":")
if len(parts) < 2 || len(parts) > 3 { // ensure the time string has either 2 or 3 parts
return 0, false, fmt.Errorf("invalid time format")
}
hours, err := strconv.Atoi(parts[0]) // convert the first part to hours
if err != nil {
return 0, false, err
}
minutes, err := strconv.Atoi(parts[1]) // convert the second part to minutes
if err != nil {
return 0, false, err
}
seconds := 0
hasSeconds := len(parts) == 3 // check if the seconds' part is present
if hasSeconds {
seconds, err = strconv.Atoi(parts[2]) // convert the third part to seconds
if err != nil {
return 0, false, err
}
}
if hours < 0 || hours > 23 || minutes < 0 || minutes > 59 || seconds < 0 || seconds > 59 { // validate the time values
return 0, false, fmt.Errorf("invalid time values")
}
return time.Duration(hours)*time.Hour + time.Duration(minutes)*time.Minute + time.Duration(seconds)*time.Second, hasSeconds, nil
}
// parseField parses a field string in the following formats: 1,2,3, 1-3,5-6 or a single asterisk for all values.
// The min and max arguments define the range of valid values for the field. The function returns a Field with
// the parsed values or an error if the input is invalid. Values in the Field are stored in a map
// for fast lookup of allowed values.
func parseField(s string, min, max int) (Field, error) {
if s == "*" {
return Field{all: true}, nil
}
values := make(map[int]bool)
ranges := strings.Split(s, ",")
for _, r := range ranges {
if strings.Contains(r, "-") {
parts := strings.Split(r, "-")
if len(parts) != 2 {
return Field{}, fmt.Errorf("invalid range format")
}
start, err := strconv.Atoi(parts[0])
if err != nil {
return Field{}, err
}
end, err := strconv.Atoi(parts[1])
if err != nil {
return Field{}, err
}
if start < min || end > max || start > end {
return Field{}, fmt.Errorf("values out of range")
}
for i := start; i <= end; i++ {
values[i] = true
}
continue
}
val, err := strconv.Atoi(r)
if err != nil {
return Field{}, err
}
if val < min || val > max {
return Field{}, fmt.Errorf("value out of range")
}
values[val] = true
}
return Field{values: values}, nil
}
// matches checks if the current time falls within the time range,
// handling ranges that span across midnight
func (r Rule) matches(t time.Time) bool {
if !r.month.matches(int(t.Month())) {
return false
}
if !r.dom.matches(t.Day()) {
return false
}
if !r.dow.matches(int(t.Weekday())) {
return false
}
if r.timeRange.all {
return true
}
currentTime := time.Duration(t.Hour())*time.Hour +
time.Duration(t.Minute())*time.Minute +
time.Duration(t.Second())*time.Second
if r.timeRange.overnight {
// for overnight ranges (e.g. 23:00-02:00)
// the time matches if it's:
// - after or equal to start time (e.g. >= 23:00) OR
// - before or equal to end time (e.g. <= 02:00)
return currentTime >= r.timeRange.start || currentTime <= r.timeRange.end
}
// For same-day ranges, time must be between start and end
return currentTime >= r.timeRange.start && currentTime <= r.timeRange.end
}
func (f Field) matches(val int) bool {
return f.all || f.values[val]
}
// String returns the string representation of a Rule
func (r Rule) String() string {
return fmt.Sprintf("%s %s %s %s",
r.timeRange.String(),
r.dow.String(),
r.dom.String(),
r.month.String(),
)
}
// String returns the string representation of a TimeRange
func (tr TimeRange) String() string {
if tr.all {
return "*"
}
startH := tr.start / time.Hour
startM := (tr.start % time.Hour) / time.Minute
startS := (tr.start % time.Minute) / time.Second
endH := tr.end / time.Hour
endM := (tr.end % time.Hour) / time.Minute
endS := (tr.end % time.Minute) / time.Second
if tr.hasSeconds {
return fmt.Sprintf("%02d:%02d:%02d-%02d:%02d:%02d",
startH, startM, startS, endH, endM, endS)
}
return fmt.Sprintf("%02d:%02d-%02d:%02d", startH, startM, endH, endM)
}
// String returns the string representation of a Field
func (f Field) String() string {
if f.all {
return "*"
}
// get all values from the map
var vals []int
for v := range f.values {
vals = append(vals, v)
}
if len(vals) == 0 {
return "*"
}
// sort values
sort.Ints(vals)
// find ranges and individual values
var ranges []string
start := vals[0]
prev := start
for i := 1; i < len(vals); i++ {
if vals[i] != prev+1 {
// end of a range or single value
if start == prev {
ranges = append(ranges, fmt.Sprintf("%d", start))
} else {
ranges = append(ranges, fmt.Sprintf("%d-%d", start, prev))
}
start = vals[i]
}
prev = vals[i]
}
// handle the last range or value
if start == prev {
ranges = append(ranges, fmt.Sprintf("%d", start))
} else {
ranges = append(ranges, fmt.Sprintf("%d-%d", start, prev))
}
return strings.Join(ranges, ",")
}