Skip to content

Commit f524c57

Browse files
author
noah
committed
Init project
0 parents  commit f524c57

File tree

7 files changed

+615
-0
lines changed

7 files changed

+615
-0
lines changed

.gitignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# If you prefer the allow list template instead of the deny list, see community template:
2+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3+
#
4+
# Binaries for programs and plugins
5+
*.exe
6+
*.exe~
7+
*.dll
8+
*.so
9+
*.dylib
10+
11+
# Test binary, built with `go test -c`
12+
*.test
13+
14+
# Output of the go coverage tool, specifically when used with LiteIDE
15+
*.out
16+
17+
# Dependency directories (remove the comment below to include it)
18+
# vendor/
19+
20+
# Go workspace file
21+
go.work

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/gitploy-io/cronexpr
2+
3+
go 1.17

parser.go

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package cronexpr
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
)
8+
9+
type (
10+
bitset uint64
11+
12+
bound struct {
13+
min, max int
14+
}
15+
16+
translater map[string]int
17+
)
18+
19+
const (
20+
bitsetStar = 1<<64 - 1
21+
)
22+
23+
var (
24+
boundMinute = bound{0, 59}
25+
boundHour = bound{0, 24}
26+
boundDOM = bound{1, 31}
27+
boundMonth = bound{1, 12}
28+
boundDOW = bound{0, 6}
29+
)
30+
31+
var (
32+
translaterMonth = translater{
33+
"JAN": 1,
34+
"FEB": 2,
35+
"MAR": 3,
36+
"APR": 4,
37+
"MAY": 5,
38+
"JUN": 6,
39+
"JUL": 7,
40+
"AUG": 8,
41+
"SEP": 9,
42+
"OCT": 10,
43+
"NOV": 11,
44+
"DEC": 12,
45+
}
46+
47+
translaterDay = translater{
48+
"SUN": 0,
49+
"MON": 1,
50+
"TUE": 2,
51+
"WED": 3,
52+
"THU": 4,
53+
"FRI": 5,
54+
"SAT": 6,
55+
}
56+
)
57+
58+
func MustParse(expr string) *Schedule {
59+
s, err := Parse(expr)
60+
if err != nil {
61+
panic(err)
62+
}
63+
64+
return s
65+
}
66+
67+
func Parse(expr string) (*Schedule, error) {
68+
err := verifyExpr(expr)
69+
if err != nil {
70+
return nil, err
71+
}
72+
73+
var (
74+
minute, hour, dom, month, dow bitset
75+
)
76+
77+
fields := strings.Fields(strings.TrimSpace(expr))
78+
79+
if minute, err = parseField(fields[0], boundMinute, translater{}); err != nil {
80+
return nil, err
81+
}
82+
83+
if hour, err = parseField(fields[1], boundMinute, translater{}); err != nil {
84+
return nil, err
85+
}
86+
87+
if dom, err = parseField(fields[2], boundMinute, translater{}); err != nil {
88+
return nil, err
89+
}
90+
91+
if month, err = parseField(fields[3], boundMinute, translaterMonth); err != nil {
92+
return nil, err
93+
}
94+
95+
if dow, err = parseField(fields[4], boundMinute, translaterDay); err != nil {
96+
return nil, err
97+
}
98+
99+
return &Schedule{
100+
Minute: minute,
101+
Hour: hour,
102+
Dom: dom,
103+
Month: month,
104+
Dow: dow,
105+
}, nil
106+
}
107+
108+
// parseField returns an int with the bits set representing all of the times that
109+
// the field represents or error parsing field value.
110+
func parseField(field string, b bound, t translater) (bitset, error) {
111+
var bitsets bitset = 0
112+
113+
// Split with "," (OR).
114+
fieldexprs := strings.Split(field, ",")
115+
for _, fieldexpr := range fieldexprs {
116+
b, err := parseFieldExpr(fieldexpr, b, t)
117+
if err != nil {
118+
return 0, err
119+
}
120+
121+
bitsets = bitsets | b
122+
}
123+
124+
return bitsets, nil
125+
}
126+
127+
// parseFieldExpr returns the bits indicated by the given expression:
128+
// number | number "-" number [ "/" number ]
129+
func parseFieldExpr(fieldexpr string, b bound, t translater) (bitset, error) {
130+
if fieldexpr == "*" {
131+
return bitsetStar, nil
132+
}
133+
134+
rangeAndStep := strings.Split(fieldexpr, "/")
135+
if !(len(rangeAndStep) == 1 || len(rangeAndStep) == 2) {
136+
return 0, fmt.Errorf("Failed to parse the expr '%s', too many '/'", fieldexpr)
137+
}
138+
139+
hasStep := len(rangeAndStep) == 2
140+
141+
// Parse the range, first.
142+
var (
143+
begin, end int
144+
)
145+
{
146+
lowAndHigh := strings.Split(rangeAndStep[0], "-")
147+
if !(len(lowAndHigh) == 1 || len(lowAndHigh) == 2) {
148+
return 0, fmt.Errorf("Failed to parse the expr '%s', too many '-'", fieldexpr)
149+
}
150+
151+
low, err := parseInt(lowAndHigh[0], t)
152+
if err != nil {
153+
return 0, fmt.Errorf("Failed to parse the expr '%s': %w", fieldexpr, err)
154+
}
155+
156+
begin = low
157+
158+
// Special handling: "N/step" means "N-max/step".
159+
if len(lowAndHigh) == 1 && hasStep {
160+
end = b.max
161+
} else if len(lowAndHigh) == 1 && !hasStep {
162+
end = low
163+
} else if len(lowAndHigh) == 2 {
164+
high, err := parseInt(lowAndHigh[1], t)
165+
if err != nil {
166+
return 0, fmt.Errorf("Failed to parse the expr '%s': %w", fieldexpr, err)
167+
}
168+
169+
end = high
170+
}
171+
}
172+
173+
// Parse the step, second.
174+
step := 1
175+
if len(rangeAndStep) == 2 {
176+
var err error
177+
if step, err = strconv.Atoi(rangeAndStep[1]); err != nil {
178+
return 0, fmt.Errorf("Failed to parse the expr '%s': %w", fieldexpr, err)
179+
}
180+
}
181+
182+
return buildBitset(begin, end, step), nil
183+
}
184+
185+
func parseInt(s string, t translater) (int, error) {
186+
if i, err := strconv.Atoi(s); err == nil {
187+
return i, nil
188+
}
189+
190+
i, ok := t[strings.ToUpper(s)]
191+
if !ok {
192+
return 0, fmt.Errorf("'%s' is out of reserved words", s)
193+
}
194+
195+
return i, nil
196+
}
197+
198+
func buildBitset(min, max, step int) bitset {
199+
var b bitset
200+
201+
for i := min; i <= max; i += step {
202+
b = b | (1 << i)
203+
}
204+
205+
return b
206+
}
207+
208+
func verifyExpr(expr string) error {
209+
fields := strings.Fields(expr)
210+
if len(fields) != 5 {
211+
return fmt.Errorf("The length of fields must be five.")
212+
}
213+
214+
return nil
215+
}

parser_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package cronexpr
2+
3+
import (
4+
"reflect"
5+
"strconv"
6+
"testing"
7+
)
8+
9+
func Test_Parse(t *testing.T) {
10+
t.Run("Parse the cron expr", func(t *testing.T) {
11+
// TODO:
12+
})
13+
}
14+
15+
func Test_parseField(t *testing.T) {
16+
t.Run("Parse the field.", func(t *testing.T) {
17+
cases := []struct {
18+
value string
19+
b bound
20+
t translater
21+
want bitset
22+
}{
23+
{
24+
value: "*",
25+
b: boundMinute,
26+
want: bitsetStar,
27+
},
28+
{
29+
value: "5",
30+
b: boundMinute,
31+
want: 1 << 5,
32+
},
33+
{
34+
value: "5,10",
35+
b: boundMinute,
36+
want: 1<<5 | 1<<10,
37+
},
38+
{
39+
value: "5-20",
40+
b: boundMinute,
41+
want: buildBitset(5, 20, 1),
42+
},
43+
{
44+
value: "JAN-MAR",
45+
b: boundMonth,
46+
t: translaterMonth,
47+
want: buildBitset(1, 3, 1),
48+
},
49+
{
50+
value: "5-20/5",
51+
b: boundMinute,
52+
want: (1 << 5) | (1 << 10) | (1 << 15) | (1 << 20),
53+
},
54+
}
55+
56+
for _, c := range cases {
57+
b, err := parseField(c.value, c.b, c.t)
58+
if err != nil {
59+
t.Fatalf("parseField returns an error: %s", err)
60+
}
61+
62+
if !reflect.DeepEqual(b, c.want) {
63+
t.Fatalf("parseField(%s) = %v, wanted %v", c.value, strconv.FormatUint(uint64(b), 2), strconv.FormatUint(uint64(c.want), 2))
64+
}
65+
}
66+
})
67+
68+
t.Run("Return an error of parsing the field.", func(t *testing.T) {
69+
cases := []struct {
70+
value string
71+
b bound
72+
t translater
73+
}{
74+
{
75+
value: "1-3-5/3",
76+
b: boundMinute,
77+
},
78+
}
79+
80+
for _, c := range cases {
81+
_, err := parseField(c.value, c.b, c.t)
82+
if err == nil {
83+
t.Fatal("parseField must returns an error")
84+
}
85+
}
86+
})
87+
}

0 commit comments

Comments
 (0)