├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── count.go ├── count_test.go ├── cron.go ├── cron_test.go ├── go.mod ├── parse.go └── parse.rl /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/golang:1.12.7 6 | environment: 7 | ENV: CI 8 | GO111MODULE: "on" 9 | RAGEL_VERSION: "6.10" 10 | working_directory: /go/src/github.com/influxdata/cron 11 | steps: 12 | - checkout 13 | - run: 14 | name: get ragel 15 | command: | 16 | cd ../ && 17 | curl http://www.colm.net/files/ragel/ragel-${RAGEL_VERSION}.tar.gz -O && \ 18 | tar -xzf ragel-${RAGEL_VERSION}.tar.gz && \ 19 | cd ragel-${RAGEL_VERSION}/ && \ 20 | ./configure --prefix=/usr/local && \ 21 | sudo make && \ 22 | sudo make install 23 | - run: 24 | name: check generated code 25 | command: | 26 | go generate 27 | if ! git --no-pager diff --exit-code -- go.mod go.sum; then 28 | echo generate not generated properly run go generate with ${RAGEL_VERSION} 29 | exit 1 30 | fi 31 | - run: 32 | name: check tidy 33 | command: | 34 | go mod tidy 35 | if ! git --no-pager diff --exit-code -- go.mod go.sum; then 36 | echo modules are not tidy, please run 'go mod tidy' 37 | exit 1 38 | fi 39 | - run: go get -v -t -d ./... 40 | - run: go vet -unreachable=false ./... # ragel generates unreachable code so we ignore this test 41 | - run: go get honnef.co/go/tools/cmd/staticcheck && staticcheck ./... 42 | - run: go test -v --race ./... 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jorge Landivar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cron 2 | A fast non-allocating cron parser in ragel and golang. 3 | 4 | 5 | # features 6 | - [x] standard five-position cron 7 | - [x] six-position cron with seconds 8 | - [x] seven-position cron with seconds and years 9 | - [x] case-insensitive days of the week 10 | - [x] case-insensitive month names 11 | - [x] [Quartz](http://www.quartz-scheduler.org) compatible ranges e.g.: `4/10`, `SUN-THURS/2` 12 | - [ ] timezone handling (other than UTC) 13 | - [ ] Quartz compatible # handling, i.e. `5#3` meaning the third friday of the month 14 | - [ ] Quartz compatible L handling, i.e. `5L` in the day of week field meaning the last friday of a month, or `3L` in the day of month field meaning third to last day of the month 15 | 16 | # performance 17 | On a 3.1 Ghz Core i7 MacBook Pro (Retina, 15-inch, Mid 2017): 18 | 19 | | | | | | 20 | |-|-|-|-| 21 | | `BenchmarkParse/0_*_*_*_*_*_*-8` | 20000000 | 63.5 ns/op | 0 B/op | 0 allocs/op | 22 | | `BenchmarkParse/1-6/2_*_*_*_Feb-Oct/3_*_*-8` | 10000000 | 118 ns/op | 0 B/op | 0 allocs/op | 23 | | `BenchmarkParse/1-6/2_*_*_*_Feb-Oct/3_*_2020/4-8` | 1000000 | 146 ns/op | 0 B/op | 0 allocs/op | 24 | 25 | # TODO 26 | - Once we are done adding all of the Quartz cron features, to copy and pass all of Quartz's parsing tests. 27 | -------------------------------------------------------------------------------- /count.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "math" 5 | "math/bits" 6 | "time" 7 | ) 8 | 9 | const ( 10 | sixyPositions = uint64(0xfffffffffffffff) 11 | twentyFourPositions = uint32(0xffffff) 12 | ) 13 | 14 | // Count gives us a count of the number of calls Parsed.Next would take to iterate though the time interval [from, to). 15 | // We try to be O(1) where possible. However for periods longer than a month that don't use @every, it is worst case 16 | // O(m) where m is the number of months in the interval. 17 | func (nt *Parsed) Count(from, to time.Time) (count int) { 18 | 19 | if nt.every { 20 | return nt.countEvery(from, to) 21 | } 22 | 23 | // advance to the first run on the interval [from, to) 24 | // this simplifies things a lot later on 25 | var err error 26 | from, err = nt.Next(from.Add(-1)) 27 | if err != nil { 28 | return 0 29 | } 30 | to = to.Add(-1).Truncate(time.Second) // this is to handle the fact that to is at the open interval. 31 | toY, toMo, toD := to.Date() 32 | toD-- // zero index the day so that the first of the month is day 0, and the second is day 1 and so on. This saves us calculations later. 33 | toH, toM, toS := to.Clock() 34 | for from.Before(to) { 35 | fromY, fromMo, fromD := from.Date() 36 | fromD-- // zero index the day. 37 | fromH, fromM, fromS := from.Clock() 38 | weekDayOfFirstOfMonth := time.Date(fromY, fromMo, 1, 0, 0, 0, 0, from.Location()).Weekday() 39 | days := nt.prepDays(weekDayOfFirstOfMonth, int(fromMo-1), fromY) 40 | 41 | // some masks 42 | toDMask := uint64(math.MaxUint64) >> (63 - uint64(toD)) 43 | fromDMask := uint64(math.MaxUint64) << uint64(fromD) 44 | fromHMask := twentyFourPositions << uint(fromH) 45 | toHMask := twentyFourPositions >> (23 - uint(toH)) 46 | fromSMask := sixyPositions << uint(fromS) 47 | toSMask := sixyPositions >> (59 - uint(toS)) 48 | fromMMask := sixyPositions << uint(fromM) 49 | toMMask := sixyPositions >> (59 - uint(toM)) 50 | 51 | fromDBit := uint64(1) << uint(fromD) 52 | if fromMo == toMo && fromY == toY { 53 | toMIn := int(nt.minute & (1 << uint(toM)) >> uint(toM)) 54 | if !nt.monthIn(toMo) { 55 | return count 56 | } 57 | d := days & fromDMask & toDMask 58 | 59 | toDIn := ((int(1) << uint(toD)) & int(d)) >> uint(toD) 60 | // we can assume here that toS > fromS, because we know that timeRange > 0 61 | toHIn := nt.hourIn(toH) 62 | 63 | toMBit := uint64(1) << uint(toM) 64 | fromMBit := uint64(1) << uint(fromM) 65 | 66 | toHBit := uint32(1 << uint(toH)) 67 | fromHBit := uint32(1) << uint(fromH) 68 | toDBit := uint64(1 << uint(toD)) 69 | 70 | if fromD == toD { 71 | if toDIn == 0 { 72 | return count 73 | } 74 | 75 | if fromH == toH { 76 | if toHIn == 0 { 77 | return count 78 | } 79 | if fromM == toM { 80 | if !nt.minuteIn(toM) { 81 | return 0 82 | } 83 | count += bits.OnesCount64(nt.second & fromSMask & toSMask) 84 | return count 85 | } 86 | 87 | // we can assume here that toM > fromM and we know that fromM is in nt 88 | // count for first minute 89 | count += bits.OnesCount64(nt.second & fromSMask) 90 | // count for last minute 91 | count += bits.OnesCount64(nt.second&toSMask) * toMIn 92 | // count for inbetween minutes 93 | mCount := bits.OnesCount64(nt.minute & toMMask & fromMMask &^ fromMBit &^ toMBit) 94 | if mCount > 0 { 95 | count += bits.OnesCount64(nt.second) * mCount 96 | } 97 | count += 0 98 | return count 99 | } 100 | // count for first minute 101 | count += bits.OnesCount64(nt.second & fromSMask) 102 | // count for first hour except first minute 103 | count += (bits.OnesCount64(nt.minute&fromMMask) - 1) * bits.OnesCount64(nt.second) 104 | // count for the between hours 105 | count += bits.OnesCount64(nt.second) * bits.OnesCount64(nt.minute) * bits.OnesCount32(nt.hour&fromHMask&toHMask&^toHBit&^fromHBit) 106 | 107 | // count for the last minute 108 | count += bits.OnesCount64(nt.second&toSMask) * toMIn 109 | // count for last hour except last minute 110 | count += bits.OnesCount64(nt.minute&toMMask&^toMBit) * bits.OnesCount64(nt.second) * toHIn 111 | return count 112 | } 113 | // count for first minute 114 | count += bits.OnesCount64(nt.second & fromSMask) 115 | // count for first hour except first minute 116 | count += (bits.OnesCount64(nt.minute&fromMMask) - 1) * bits.OnesCount64(nt.second) 117 | // count for first day except first hour 118 | count += (bits.OnesCount32(nt.hour&fromHMask) - 1) * bits.OnesCount64(nt.minute) * bits.OnesCount64(nt.second) 119 | 120 | // count for last minute 121 | count += bits.OnesCount64(nt.second&toSMask) * toHIn * toDIn * toMIn 122 | // count for last hour except last minute 123 | count += bits.OnesCount64(nt.minute&toMMask&^toMBit) * bits.OnesCount64(nt.second) * toHIn * toDIn 124 | 125 | // count for the last day except the last hour 126 | count += bits.OnesCount64(nt.second) * bits.OnesCount64(nt.minute) * bits.OnesCount32(nt.hour&toHMask&^toHBit) * toDIn 127 | // count for the between days 128 | count += bits.OnesCount64(nt.second) * bits.OnesCount64(nt.minute) * bits.OnesCount32(nt.hour) * bits.OnesCount64(d&^toDBit&^fromDBit) 129 | return count 130 | } 131 | // count for first minute 132 | count += bits.OnesCount64(nt.second & fromSMask) 133 | // count for first hour except first minute 134 | count += (bits.OnesCount64(nt.minute&fromMMask) - 1) * bits.OnesCount64(nt.second) 135 | // count for first day except first hour 136 | count += (bits.OnesCount32(nt.hour&fromHMask) - 1) * bits.OnesCount64(nt.minute) * bits.OnesCount64(nt.second) 137 | //count for first month except first day 138 | count += bits.OnesCount64(days&fromDMask&^fromDBit) * bits.OnesCount64(nt.second) * bits.OnesCount64(nt.minute) * bits.OnesCount32(nt.hour) 139 | 140 | if fromMo == 12 { 141 | fromMo = time.January 142 | fromY++ 143 | } else { 144 | fromMo++ 145 | } 146 | 147 | from = time.Date(fromY, fromMo, 1, 0, 0, 0, 0, from.Location()) 148 | from, err = nt.Next(from.Add(-1)) 149 | if err != nil { 150 | return count 151 | } 152 | 153 | } 154 | 155 | return count 156 | } 157 | 158 | func (nt *Parsed) countEvery(from, to time.Time) int { 159 | if to.Sub(from) <= time.Second { 160 | return 0 161 | } 162 | 163 | fromY, fromMo, fromD := from.Date() 164 | fromH, fromM, fromS := from.Clock() 165 | 166 | d0 := nt.everyDay() 167 | m0 := nt.everyMonth() 168 | y0 := nt.everyYear() 169 | s := nt.everySeconds() * time.Second 170 | 171 | if d0 != 0 || m0 != 0 || y0 != 0 { 172 | // TODO(docmerlin): picking a better start would probably speed things up. 173 | n0, n1, n := 0, 1, 1 174 | 175 | // because date is a discontinuous but monotonically increasing function we can use the bisection method 176 | // This should be pretty fast, because the space we are working over is pretty small. 177 | // TODO(docmerlin): update to a method that uses slope, as this is a good candidate for it, we just need to make sure that it is one that is guaranteed to result in a stable solution for integers 178 | // first find an n s.t. the the time is after or equal to to 179 | for ; time.Date(fromY+y0*n1, fromMo+time.Month(m0*n1), fromD+d0*n1, fromH, fromM, fromS, 0, time.UTC).Add(time.Duration(n1) * time.Duration(s)).Before(to); n1 = n1 << 1 { 180 | // n0 is the previous n1 181 | n0 = n1 182 | } 183 | 184 | var t0, t1 time.Time 185 | t0 = time.Date(fromY+y0*n0, fromMo+time.Month(m0*n0), fromD+d0*n0, fromH, fromM, fromS, 0, time.UTC).Add(time.Duration(n0) * time.Duration(s)) 186 | t1 = time.Date(fromY+y0*n1, fromMo+time.Month(m0*n1), fromD+d0*n1, fromH, fromM, fromS, 0, time.UTC).Add(time.Duration(n1) * time.Duration(s)) 187 | 188 | for n1 != n0 { 189 | n = (n1 + n0) >> 1 190 | t := time.Date(fromY+y0*n, fromMo+time.Month(m0*n), fromD+d0*n, fromH, fromM, fromS, 0, time.UTC).Add(time.Duration(n) * time.Duration(s)) 191 | if to.Sub(t0) > t1.Sub(to) { 192 | if t0 == t { 193 | n1 = n 194 | t1 = t 195 | } 196 | n0 = n 197 | t0 = t 198 | } else { 199 | if t1 == t { 200 | n0 = n 201 | t0 = t 202 | } 203 | n1 = n 204 | t1 = t 205 | } 206 | } 207 | return n 208 | } 209 | return int(to.Sub(from).Truncate(time.Second) / (time.Duration(nt.everySeconds()) * time.Second)) 210 | } 211 | -------------------------------------------------------------------------------- /count_test.go: -------------------------------------------------------------------------------- 1 | package cron_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/influxdata/cron" 9 | ) 10 | 11 | func BenchmarkCount(b *testing.B) { 12 | cases := []struct { 13 | cron string 14 | from time.Time 15 | to time.Time 16 | }{ 17 | { 18 | cron: "20,44 7 5 * * * *", 19 | from: mustParseTime("2019-12-02T00:00:00Z"), 20 | to: mustParseTime("2020-03-30T00:00:00Z"), 21 | }, 22 | { 23 | cron: "20,44 7 * * * * 2018", 24 | from: mustParseTime("2019-12-02T00:00:00Z"), 25 | to: mustParseTime("2019-12-03T00:00:00Z"), 26 | }, 27 | { 28 | cron: "* */2 * * * * 2023", 29 | from: mustParseTime("2013-02-03T01:02:04Z"), 30 | to: mustParseTime("2013-02-03T01:20:55Z"), 31 | }, 32 | { 33 | cron: "20,44 7 * * * *", 34 | from: mustParseTime("2019-12-02T00:00:00Z"), 35 | to: mustParseTime("2019-12-03T00:00:00Z"), 36 | }, 37 | { 38 | cron: "20,44 * * * * *", 39 | from: mustParseTime("2019-12-02T00:00:00Z"), 40 | to: mustParseTime("2019-12-03T00:00:00Z"), 41 | }, 42 | { 43 | cron: "*/7 * * * * Thu", 44 | from: mustParseTime("2019-12-02T00:00:00Z"), 45 | to: mustParseTime("2019-12-09T05:01:03Z"), 46 | }, 47 | { 48 | cron: "* * * * Thu", 49 | from: mustParseTime("2019-12-02T00:00:00Z"), 50 | to: mustParseTime("2019-12-09T05:01:03Z"), 51 | }, 52 | { 53 | cron: "* * * * * *", 54 | from: mustParseTime("2013-02-03T01:02:04Z"), 55 | to: mustParseTime("2013-03-15T05:01:03Z"), 56 | }, 57 | 58 | { 59 | cron: "* */2 * * * * 2023", 60 | from: mustParseTime("2013-02-03T01:02:04Z"), 61 | to: mustParseTime("2013-02-03T01:20:55Z"), 62 | }, 63 | { 64 | cron: "@every 1s", 65 | from: mustParseTime("2013-02-03T01:02:04Z"), 66 | to: mustParseTime("2013-02-04T01:02:04Z"), 67 | }, 68 | { 69 | cron: "@every 1d1h", 70 | from: mustParseTime("2013-02-03T01:02:04Z"), 71 | to: mustParseTime("2013-02-04T01:02:04Z"), 72 | }, 73 | { 74 | cron: "@every 1d100ms", 75 | from: mustParseTime("2013-02-03T00:00:00Z"), 76 | to: mustParseTime("2013-02-04T01:02:04Z"), 77 | }, 78 | { 79 | cron: "@every 1mo100d", 80 | from: mustParseTime("2013-01-03T00:00:00Z"), 81 | to: mustParseTime("2013-11-20T01:02:04Z"), 82 | }, 83 | { 84 | cron: "@every 2y", 85 | from: mustParseTime("2013-01-03T00:00:00Z"), 86 | to: mustParseTime("2019-11-20T01:02:04Z"), 87 | }, 88 | } 89 | 90 | for _, tt := range cases { 91 | b.Run(fmt.Sprintf("%s, [%s, %s)", tt.cron, tt.from, tt.to), func(b *testing.B) { 92 | nt, err := cron.ParseUTC(tt.cron) 93 | 94 | if err != nil { 95 | b.Fatal(err) 96 | } 97 | 98 | b.ResetTimer() 99 | for i := 0; i < b.N; i++ { 100 | nt.Count(tt.from, tt.to) 101 | } 102 | }) 103 | } 104 | } 105 | 106 | func TestParsed_Count(t *testing.T) { 107 | tests := []struct { 108 | cron string 109 | from time.Time 110 | to time.Time 111 | wantCount int 112 | }{ 113 | { 114 | cron: "20,44 7 5 * * * *", 115 | from: mustParseTime("2020-01-01T00:00:00Z"), 116 | to: mustParseTime("2020-01-03T00:00:00Z"), 117 | wantCount: int(mustParseTime("2020-01-03T00:00:00Z").Sub(mustParseTime("2020-01-01T00:00:00Z")) / time.Hour / 12), 118 | }, 119 | { 120 | cron: "20,44 7 5 * * * *", 121 | from: mustParseTime("2019-12-20T00:00:00Z"), 122 | to: mustParseTime("2020-01-03T00:00:00Z"), 123 | wantCount: int(mustParseTime("2020-01-03T00:00:00Z").Sub(mustParseTime("2019-12-20T00:00:00Z")) / time.Hour / 12), 124 | }, 125 | { 126 | cron: "20,44 7 * * * * 2018", 127 | from: mustParseTime("2019-12-02T00:00:00Z"), 128 | to: mustParseTime("2019-12-03T00:00:00Z"), 129 | wantCount: 0, 130 | }, 131 | { 132 | cron: "20,44 7 * * * *", 133 | from: mustParseTime("2019-12-02T00:00:00Z"), 134 | to: mustParseTime("2019-12-03T00:00:00Z"), 135 | wantCount: 24 * 2, 136 | }, 137 | { 138 | cron: "20,44 * * * * *", 139 | from: mustParseTime("2019-12-02T00:00:00Z"), 140 | to: mustParseTime("2019-12-03T00:00:00Z"), 141 | wantCount: 24 * 60 * 2, 142 | }, 143 | { 144 | cron: "*/7 * * * * Thu", 145 | from: mustParseTime("2019-12-02T00:00:00Z"), 146 | to: mustParseTime("2019-12-09T05:01:03Z"), 147 | wantCount: 24 * 60 * 9, 148 | }, 149 | { 150 | cron: "* * * * Thu", 151 | from: mustParseTime("2019-12-02T00:00:00Z"), 152 | to: mustParseTime("2019-12-09T05:01:03Z"), 153 | wantCount: 24 * 60, 154 | }, 155 | 156 | { 157 | cron: "* * * * * Thu", 158 | from: mustParseTime("2019-12-02T00:00:00Z"), 159 | to: mustParseTime("2019-12-09T05:01:03Z"), 160 | wantCount: 24 * 60 * 60, 161 | }, 162 | 163 | { 164 | cron: "* * * * * *", 165 | from: mustParseTime("2013-02-03T01:02:04Z"), 166 | to: mustParseTime("2013-03-15T05:01:03Z"), 167 | wantCount: int(mustParseTime("2013-03-15T05:01:03Z").Sub(mustParseTime("2013-02-03T01:02:04Z").Add(-1)).Truncate(time.Second) / time.Second), 168 | }, 169 | { 170 | cron: "* * * * * *", 171 | from: mustParseTime("2013-02-03T01:02:04Z"), 172 | to: mustParseTime("2013-02-15T05:01:03Z"), 173 | wantCount: int(mustParseTime("2013-02-15T05:01:03Z").Sub(mustParseTime("2013-02-03T01:02:04Z").Add(-1)).Truncate(time.Second) / time.Second), 174 | }, 175 | 176 | { 177 | cron: "* * * * * *", 178 | from: mustParseTime("2013-02-03T01:02:04Z"), 179 | to: mustParseTime("2013-02-03T05:01:03Z"), 180 | wantCount: int(mustParseTime("2013-02-03T05:01:03Z").Sub(mustParseTime("2013-02-03T01:02:04Z").Add(-1)).Truncate(time.Second) / time.Second), 181 | }, 182 | 183 | { 184 | cron: "* * * * * *", 185 | from: mustParseTime("2013-02-03T01:02:04Z"), 186 | to: mustParseTime("2013-02-03T05:01:03Z"), 187 | wantCount: int(mustParseTime("2013-02-03T05:01:03Z").Sub(mustParseTime("2013-02-03T01:02:04Z").Add(-1)).Truncate(time.Second) / time.Second), 188 | }, 189 | 190 | { 191 | cron: "* * * * * *", 192 | from: mustParseTime("2013-02-03T01:02:04Z"), 193 | to: mustParseTime("2013-02-03T05:20:55Z"), 194 | wantCount: int(mustParseTime("2013-02-03T05:20:55Z").Sub(mustParseTime("2013-02-03T01:02:04Z").Add(-1)).Truncate(time.Second) / time.Second), 195 | }, 196 | 197 | { 198 | cron: "* * * * * *", 199 | from: mustParseTime("2013-02-03T01:02:04Z"), 200 | to: mustParseTime("2013-02-03T02:20:55Z"), 201 | wantCount: int(mustParseTime("2013-02-03T02:20:55Z").Sub(mustParseTime("2013-02-03T01:02:04Z").Add(-1)).Truncate(time.Second) / time.Second), 202 | }, 203 | { 204 | cron: "* */2 * * * * 2023", 205 | from: mustParseTime("2013-02-03T01:02:04Z"), 206 | to: mustParseTime("2013-02-03T01:20:55Z"), 207 | wantCount: 0, 208 | }, 209 | { 210 | cron: "*/2 * * * *", 211 | from: mustParseTime("2013-02-03T01:02:04Z"), 212 | to: mustParseTime("2013-02-03T01:20:55Z"), 213 | wantCount: 9, 214 | }, 215 | 216 | { 217 | cron: "* * * * *", 218 | from: mustParseTime("2013-02-03T01:02:04Z"), 219 | to: mustParseTime("2013-02-03T01:10:55Z"), 220 | wantCount: 8, 221 | }, 222 | { 223 | cron: "* * * * *", 224 | from: mustParseTime("2013-02-03T01:02:04Z"), 225 | to: mustParseTime("2013-02-03T01:03:55Z"), 226 | wantCount: 1, 227 | }, 228 | { 229 | cron: "* * * * * *", 230 | from: mustParseTime("2013-02-03T01:02:04Z"), 231 | to: mustParseTime("2013-02-03T01:03:55Z"), 232 | wantCount: int(mustParseTime("2013-02-03T01:03:55Z").Sub(mustParseTime("2013-02-03T01:02:04Z").Add(-1)).Truncate(time.Second) / time.Second), 233 | }, 234 | { 235 | cron: "* * * * * *", 236 | from: mustParseTime("2013-02-03T01:02:04Z"), 237 | to: mustParseTime("2013-02-03T01:02:55Z"), 238 | wantCount: int(mustParseTime("2013-02-03T01:02:55Z").Sub(mustParseTime("2013-02-03T01:02:04Z").Add(-1)).Truncate(time.Second) / time.Second), 239 | }, 240 | { 241 | cron: "@every 1s", 242 | from: mustParseTime("2013-02-03T01:02:04Z"), 243 | to: mustParseTime("2013-02-04T01:02:04Z"), 244 | wantCount: int(mustParseTime("2013-02-04T01:02:04Z").Sub(mustParseTime("2013-02-03T01:02:04Z").Add(-1)).Truncate(time.Second) / time.Second), 245 | }, 246 | { 247 | cron: "@every 1d", 248 | from: mustParseTime("2013-02-03T01:02:04Z"), 249 | to: mustParseTime("2013-02-04T01:02:04Z"), 250 | wantCount: 0, 251 | }, 252 | { 253 | cron: "@every 1d1h", 254 | from: mustParseTime("2013-02-03T01:02:04Z"), 255 | to: mustParseTime("2013-02-04T01:02:04Z"), 256 | wantCount: 0, 257 | }, 258 | { 259 | cron: "@every 1d", 260 | from: mustParseTime("2013-02-03T01:02:04Z"), 261 | to: mustParseTime("2013-02-04T01:02:04Z"), 262 | wantCount: 0, 263 | }, 264 | { 265 | cron: "@every 2d4s", 266 | from: mustParseTime("2013-02-03T01:02:04Z"), 267 | to: mustParseTime("2013-02-04T01:02:04Z"), 268 | wantCount: 0, 269 | }, 270 | { 271 | cron: "@every 1d100ms", 272 | from: mustParseTime("2013-02-03T00:00:00Z"), 273 | to: mustParseTime("2013-02-04T01:02:04Z"), 274 | wantCount: 1, 275 | }, 276 | { 277 | cron: "@every 3d", 278 | from: mustParseTime("2013-02-03T00:00:00Z"), 279 | to: mustParseTime("2013-05-20T01:02:04Z"), 280 | wantCount: int(mustParseTime("2013-05-20T01:02:04Z").Sub(mustParseTime("2013-02-03T00:00:00Z")).Truncate(time.Second) / (72 * time.Hour)), 281 | }, 282 | { 283 | cron: "@every 4d", 284 | from: mustParseTime("2013-02-03T00:00:00Z"), 285 | to: mustParseTime("2013-05-20T01:02:04Z"), 286 | wantCount: int(mustParseTime("2013-05-20T01:02:04Z").Sub(mustParseTime("2013-02-03T00:00:00Z")).Truncate(time.Second) / (96 * time.Hour)), 287 | }, 288 | { 289 | cron: "@every 4d12h", 290 | from: mustParseTime("2013-02-03T00:00:00Z"), 291 | to: mustParseTime("2013-05-20T01:02:04Z"), 292 | wantCount: int(mustParseTime("2013-05-20T01:02:04Z").Sub(mustParseTime("2013-02-03T00:00:00Z")).Truncate(time.Second) / (108 * time.Hour)), 293 | }, 294 | { 295 | cron: "@every 1mo", 296 | from: mustParseTime("2013-02-03T00:00:00Z"), 297 | to: mustParseTime("2013-05-20T01:02:04Z"), 298 | wantCount: 3, 299 | }, 300 | { 301 | cron: "@every 1mo100d", 302 | from: mustParseTime("2013-01-03T00:00:00Z"), 303 | to: mustParseTime("2013-11-20T01:02:04Z"), 304 | wantCount: 2, 305 | }, 306 | { 307 | cron: "@every 1y", 308 | from: mustParseTime("2013-01-03T00:00:00Z"), 309 | to: mustParseTime("2019-11-20T01:02:04Z"), 310 | wantCount: 6, 311 | }, 312 | } 313 | for _, tt := range tests { 314 | t.Run(fmt.Sprintf("%s, [%s, %s)", tt.cron, tt.from, tt.to), func(t *testing.T) { 315 | nt, err := cron.ParseUTC(tt.cron) 316 | if err != nil { 317 | t.Fatal(err) 318 | } 319 | if gotCount := nt.Count(tt.from, tt.to); gotCount != tt.wantCount { 320 | t.Fatalf("Parsed.Count() = %v, want %v", gotCount, tt.wantCount) 321 | } 322 | }) 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /cron.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | //go:generate ragel -G2 -Z parse.rl 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "math/bits" 9 | "time" 10 | ) 11 | 12 | const magicDOW2DOM = 0x8102040810204081 // multiplying by this number converts to a dow bitmap to dom bitmap 13 | 14 | // ParseUTC parses a cron string in UTC timezone. 15 | func ParseUTC(s string) (Parsed, error) { 16 | return parse(s) 17 | } 18 | 19 | func (p *Parsed) yearIsZero() bool { 20 | return p.low|p.high|uint64(p.end) == 0 21 | } 22 | 23 | func (ys *Parsed) setYear(year int) { 24 | y := uint64(year - 1970) 25 | switch { 26 | case y > 128: 27 | ys.end |= 1 << (y - 128) 28 | case y > 64: 29 | ys.high |= 1 << (y - 64) 30 | default: 31 | ys.low |= 1 << y 32 | } 33 | } 34 | 35 | // this is undefined if the year is above 2070 or below 1970 36 | func (ys *Parsed) yearIn(year int) bool { 37 | y := uint64(year - 1970) 38 | if y >= 128 { 39 | return ys.end&(1<<(y-128)) > 0 40 | } 41 | if y >= 64 { 42 | return ys.high&(1<<(y-64)) > 0 43 | } 44 | return ys.low&(1< 0 45 | } 46 | 47 | func isLeap(y int) bool { 48 | return (y&3 == 0 && y%100 != 0) || y%400 == 0 49 | } 50 | 51 | // wanted to keep this a private method useful for debugging 52 | func (p *Parsed) string() string { 53 | return fmt.Sprintf(`s: %b 54 | m:%16b 55 | h:%24b 56 | dom:%8x 57 | dow:%8b 58 | year:%01x%016x%16x 59 | month:%016b`, 60 | p.second, 61 | p.minute, 62 | p.hour, 63 | p.dom, 64 | p.dow, 65 | p.end, 66 | p.low, 67 | p.high, 68 | p.month) 69 | } 70 | 71 | // to keep staticcheck happy 72 | var _ = (&Parsed{}).string 73 | 74 | // Parsed is a parsed cron string. It can be used to find the next instance of a cron task. 75 | type Parsed struct { 76 | // some fields are overloaded, because @every behaves very different from a normal cron string, 77 | // and we want to keep this struct under 64 bytes, so it fits in a cache line, on most machines 78 | second uint64 // also serves as the total time-like duration (hours, minutes, seconds) for every 79 | minute uint64 // also serves as the month count for every 80 | low uint64 // also serves as the year count for every 81 | high uint64 82 | hour uint32 83 | dom uint32 // also serves as the day count for every queries 84 | end uint8 // this is here so we can support 2098 and 2099 85 | ldow uint8 //lint:ignore U1000 we plan on using this field once we add L crons 86 | month uint16 87 | ldom uint32 //lint:ignore U1000 we plan on using this field once we add L crons 88 | dow uint8 89 | every bool 90 | //TODO(docmerlin): add location once we support location 91 | } 92 | 93 | func (p *Parsed) everyYear() int { 94 | if !p.every { 95 | return 0 96 | } 97 | return int(p.low) // we overload this field to also store year counts in the every case 98 | } 99 | 100 | func (p *Parsed) setEveryYear(d int) { 101 | p.low = uint64(d) // we overload this field to also store seconds for every 102 | } 103 | 104 | func (p *Parsed) everyMonth() int { 105 | if !p.every { 106 | return 0 107 | } 108 | return int(p.minute) // we overload this field to also store months in the every case 109 | } 110 | 111 | func (p *Parsed) setEveryMonth(m int) { 112 | p.minute = uint64(m) // we overload this field to also store seconds for every 113 | } 114 | 115 | func (p *Parsed) everyDay() int { 116 | if !p.every { 117 | return 0 118 | } 119 | return int(p.dom) 120 | } 121 | 122 | func (p *Parsed) addEveryDay(d int) { 123 | p.dom += uint32(d) // we overload this field to also store days for every 124 | } 125 | 126 | func (p *Parsed) everySeconds() time.Duration { 127 | if !p.every { 128 | return 0 129 | } 130 | return time.Duration(p.second) / time.Second // we overload this field to also store seconds for every 131 | } 132 | 133 | func (p *Parsed) addEveryDur(s time.Duration) { 134 | p.second += uint64(s) // we overload this field to also store time-like duration (hour minutes seconds, in nanosecond count) for every 135 | } 136 | 137 | func (p *Parsed) everyZero() bool { 138 | return p.every && (p.low == 0 && p.minute == 0 && p.second == 0 && p.dom == 0) 139 | } 140 | 141 | func (p *Parsed) monthIn(m time.Month) bool { 142 | if m < 1 || m > 12 { 143 | return false 144 | } 145 | m-- //to change to zero indexed month from 1 indexed month, since the formula below requires zero indexing 146 | return (1<> uint(d)) 152 | } 153 | 154 | func (p *Parsed) minuteIn(m int) bool { 155 | return (1<= 128 { 168 | y += uint64(bits.TrailingZeros8(nt.end >> (yBits - 128))) 169 | } else if yBits >= 64 { 170 | addToY += uint64(bits.TrailingZeros64(nt.high >> (yBits - 64))) 171 | if addToY == 64 { 172 | addToY -= yBits - 64 173 | addToY += uint64(bits.TrailingZeros8(nt.end)) 174 | } 175 | } else { 176 | addToY = uint64(bits.TrailingZeros64(nt.low >> yBits)) 177 | var addToYHigh uint64 178 | if addToY == 64 { 179 | addToY -= yBits 180 | addToYHigh = uint64(bits.TrailingZeros64(nt.high)) 181 | if addToYHigh == 128 { 182 | addToY += uint64(bits.TrailingZeros8(nt.end)) 183 | } 184 | addToY += addToYHigh 185 | } 186 | } 187 | y += addToY 188 | 189 | if y > 2099 { 190 | return -1 191 | } 192 | //Feb 29th special casing 193 | // if we only allow feb 29th, or then the next year must be a leap year 194 | // remember we zero index months here 195 | if m == 1 && d == 28 { 196 | if isLeap(int(y)) { 197 | return int(y) 198 | } 199 | if y%100 == 0 { 200 | return int(y + 4) 201 | } 202 | if y%3 != 0 { // is multiple of 4? 203 | y = ((y + 4) >> 2) << 2 // next multiple of 4 204 | if y%100 != 0 { 205 | return int(y) 206 | } 207 | return int(y + 4) 208 | } 209 | } 210 | 211 | return int(y) 212 | } 213 | 214 | // returns the index for the month 215 | func (nt *Parsed) nextMonth(m, d uint64) int { 216 | m = uint64(bits.TrailingZeros16(nt.month>>m)) + m 217 | d = uint64(bits.TrailingZeros32(nt.dom>>d)) + d 218 | if m > 11 { // if there is no next avaliable months 219 | return -1 220 | } 221 | if maxMonthLengths[m] <= d { 222 | m++ 223 | } 224 | return int(m) 225 | } 226 | 227 | func (nt *Parsed) nextDay(y int, m int, d uint64) int { 228 | firstOfMonth := time.Date(y, time.Month(m+1), 1, 0, 0, 0, 0, time.UTC) // TODO(docmerlin): location support 229 | days := nt.prepDays(firstOfMonth.Weekday(), m, y) 230 | d++ 231 | d = uint64(bits.TrailingZeros64(days>>d)) + d 232 | if m >= 12 { 233 | return -1 234 | } 235 | if d >= uint64(maxMonthLengths[m]) { 236 | return -1 237 | } 238 | return int(d) 239 | } 240 | 241 | func (nt *Parsed) nextHour(h uint64) int { 242 | h++ 243 | h = uint64(bits.TrailingZeros32(nt.hour>>h)) + h 244 | if h >= 24 { 245 | return -1 246 | } 247 | return int(h) 248 | } 249 | 250 | func (nt *Parsed) nextMinute(m uint64) int { 251 | m++ 252 | m = uint64(bits.TrailingZeros64(nt.minute>>m)) + m 253 | if m >= 60 { 254 | return -1 255 | } 256 | return int(m) 257 | } 258 | 259 | func (nt *Parsed) nextSecond(s uint64) int { 260 | s++ 261 | s = uint64(bits.TrailingZeros64(nt.second>>s)) + s 262 | if s >= 60 { 263 | return -1 264 | } 265 | return int(s) 266 | } 267 | 268 | func (nt *Parsed) valid() bool { 269 | return !(nt.everyZero() || (!nt.every) && (nt.minute == 0 || nt.hour == 0 || nt.dom == 0 || nt.month == 0 || nt.dow == 0 || nt.yearIsZero())) 270 | } 271 | 272 | // undefined for shifts larger than 7 273 | // firstDayOfWeek is the 0 indexed day of first day of the month 274 | // month is zero indexed instead of 1 indexed. (i.e.: Jan is month 0) 275 | func (nt *Parsed) prepDays(firstDayOfWeek time.Weekday, month int, year int) uint64 { 276 | doms := uint64(1<> uint64(firstDayOfWeek) & (doms & uint64(nt.dom)) 281 | } 282 | 283 | // Next returns the next time a cron task should run given a Parsed cron string. 284 | // It will error if the Parsed is not from a zero value. 285 | func (nt Parsed) Next(from time.Time) (time.Time, error) { 286 | // handle case where we have an @every query 287 | if nt.every { 288 | ts := from.AddDate(nt.everyYear(), nt.everyMonth(), nt.everyDay()) 289 | ts = ts.Add(time.Duration(nt.everySeconds()) * time.Second) 290 | if !ts.After(from) { 291 | return time.Time{}, errors.New("next time must be later than from time") 292 | } 293 | return ts, nil 294 | } 295 | // handle the non @every case 296 | y, MTime, dTime := from.Date() 297 | d := int(dTime - 1) //time's day is 1 indexed but our day is zero indexed 298 | M := int(MTime - 1) //time's month is 1 indexed in time but ours is 0 indexed 299 | h := from.Hour() 300 | m := from.Minute() 301 | s := from.Second() 302 | 303 | updateHour := nt.hour&(1< 31 days", 567 | nt: parsit(" * * * 30,32 * * * "), 568 | from: ts, 569 | wantParseErr: true, 570 | }, 571 | { 572 | name: "seconds = 60", 573 | nt: parsit(" 60 * * * * * * "), 574 | from: ts, 575 | wantParseErr: true, 576 | }, 577 | { 578 | name: "minutes = 60", 579 | nt: parsit("* 60 * * * * * "), 580 | from: ts, 581 | wantParseErr: true, 582 | }, 583 | { 584 | name: "hours = 24", 585 | nt: parsit(" * * 24 * * * * "), 586 | from: ts, 587 | wantParseErr: true, 588 | }, 589 | { 590 | name: "seconds > 60", 591 | nt: parsit(" 61 * * 30 * * * "), 592 | from: ts, 593 | wantParseErr: true, 594 | }, 595 | { 596 | name: "minutes > 60", 597 | nt: parsit(" * 61 * 30 * * * "), 598 | from: ts, 599 | wantParseErr: true, 600 | }, 601 | { 602 | name: "month = 0", 603 | nt: parsit(" * * * * 0 * * "), 604 | from: ts, 605 | wantParseErr: true, 606 | }, 607 | { 608 | name: "month = 13", 609 | nt: parsit(" * * * * 13 * * "), 610 | from: ts, 611 | wantParseErr: true, 612 | }, 613 | { 614 | name: "all stars", 615 | nt: parsit("* * * * * * *"), 616 | from: ts, 617 | want: ceilTime(ts, time.Second), 618 | }, 619 | { 620 | name: "all stars wed though sun pos", 621 | nt: parsit("* * * * * 4-6 *"), 622 | from: ts, 623 | want: ceilTime(ts, time.Second), 624 | }, 625 | { 626 | name: "2023", 627 | nt: parsit(" * * * * * * 2023 "), 628 | from: ts, 629 | want: mustParseTime("2023-01-01T00:00:00Z"), 630 | }, 631 | { 632 | name: "all seconds starting from ten by 7", 633 | nt: parsit("10/4 * * * * * *"), 634 | from: ts, 635 | want: mustParseTime("2019-06-27T06:05:14Z"), 636 | }, 637 | { 638 | name: "all seconds between 3 and 12 by 3", 639 | nt: parsit("3-12/2 * * * * * *"), 640 | from: ts, 641 | want: mustParseTime("2019-06-27T06:06:03Z"), 642 | }, 643 | { 644 | name: "all minutes between 4 and 22 by 3", 645 | nt: parsit("* 3-22/3 * * * * *"), 646 | from: ts, 647 | want: mustParseTime("2019-06-27T06:06:00Z"), 648 | }, 649 | { 650 | name: "at every 5nd minute starting at minute 3.", 651 | nt: parsit("* 3/5 * * * * *"), 652 | from: ts, 653 | want: mustParseTime("2019-06-27T06:08:00Z"), 654 | }, 655 | { 656 | name: "at every 2nd minute starting at minute 3 for even seconds.", 657 | nt: parsit("*/2 3/2 * * * * *"), 658 | from: ts, 659 | want: mustParseTime("2019-06-27T06:05:14Z"), 660 | }, 661 | { 662 | name: "once a minute at second 1", 663 | nt: parsit("1 * * * * * *"), 664 | from: ts, 665 | want: mustParseTime("2019-06-27T06:06:01Z"), 666 | }, 667 | { 668 | name: "all stars extra space at end", 669 | nt: parsit("* * * * * * * "), 670 | from: ts, 671 | want: ceilTime(ts, time.Second), 672 | }, 673 | { 674 | name: "all stars extra space at beginning", 675 | nt: parsit(" * * * * * * *"), 676 | from: ts, 677 | want: ceilTime(ts, time.Second), 678 | }, 679 | { 680 | name: "all stars extra spaces randomly ", 681 | nt: parsit(" * * * * \t*\t* * "), 682 | from: ts, 683 | want: ceilTime(ts, time.Second), 684 | }, 685 | { 686 | name: "fourth second", 687 | nt: parsit("4 * * * * * *"), 688 | from: ts, 689 | want: ceilTime(ts, time.Minute).Add(4 * time.Second), 690 | }, 691 | { 692 | name: "every fourth second", 693 | nt: parsit("*/4 * * * * * *"), 694 | from: ts, 695 | want: mustParseTime("2019-06-27T06:05:16Z"), 696 | }, 697 | { 698 | name: "every fourth second and every 2nd hour", 699 | nt: parsit("*/4 * */2 * * * *"), 700 | from: ts, 701 | want: mustParseTime("2019-06-27T06:05:16Z"), 702 | }, 703 | { 704 | name: "every fourth second and every 4th hour between 4 and 16", 705 | nt: parsit("*/4 * 4-16/4 * * * *"), 706 | from: ts, 707 | want: mustParseTime("2019-06-27T08:00:00Z"), 708 | }, 709 | { 710 | name: "every second except on thursday", 711 | nt: parsit("* * * * * FRI,SAT,SUN,MON,TUE,WED *"), 712 | from: ts, 713 | want: mustParseTime("2019-06-28T00:00:00Z"), 714 | }, 715 | { 716 | name: "every second on saturday", 717 | nt: parsit("* * * * * SAT *"), 718 | from: ts, 719 | want: mustParseTime("2019-06-29T00:00:00Z"), 720 | }, 721 | { 722 | name: "fourth second, 7th minute", 723 | nt: parsit("4 7 * * * * *"), 724 | from: ts, 725 | want: mustParseTime("2019-06-27T06:07:04Z"), 726 | }, 727 | { 728 | name: "fourth second, 7th minute on saturday", 729 | nt: parsit("4 7 * * * 6 *"), 730 | from: ts, 731 | want: mustParseTime("2019-06-29T00:07:04Z"), 732 | }, 733 | { 734 | name: "fourth second, 5-10th minute on saturday", 735 | nt: parsit("4 5-10 * * * 6 *"), 736 | from: ts, 737 | want: mustParseTime("2019-06-29T00:05:04Z"), 738 | }, 739 | { 740 | name: "noon on dec 3rd", 741 | nt: parsit("* * 12 3 12 * *"), 742 | from: ts, 743 | want: mustParseTime("2019-12-03T12:00:00Z"), 744 | }, 745 | { 746 | name: "noon on leap day", 747 | nt: parsit("* * 12 29 FEB * *"), 748 | from: ts, 749 | want: mustParseTime("2020-02-29T12:00:00Z"), 750 | }, 751 | { 752 | name: "noon on leap day on day on wed", 753 | nt: parsit("* * 12 29 FEB WED *"), 754 | from: ts, 755 | want: mustParseTime("2040-02-29T12:00:00Z"), 756 | }, 757 | { 758 | name: "noon on leap day on day on tuesday,wednesday, or thursday", 759 | nt: parsit("* * 12 29 FEB tue-thu *"), 760 | from: ts, 761 | want: mustParseTime("2024-02-29T12:00:00Z"), 762 | }, 763 | { 764 | name: "noon on leap day on day on Monday or sunday", 765 | nt: parsit("* * 12 29 2 MON,0 *"), 766 | from: ts, 767 | want: mustParseTime("2032-02-29T12:00:00Z"), 768 | }, 769 | { 770 | name: "when july 4 is a weekday", 771 | nt: parsit("* * * 4 7 MON,TUE,WED,4,5 *"), 772 | from: ts, 773 | want: mustParseTime("2019-07-04T00:00:00Z"), 774 | }, 775 | { 776 | name: "when leap day is a monday and the year is a multiple of 3 after 1970", 777 | nt: parsit("* * * 29 2 MON */3"), 778 | from: ts, 779 | want: mustParseTime("2072-02-29T00:00:00Z"), 780 | }, 781 | { 782 | name: "jan 1 falls on a monday and year is a multiple of 3 after 1970", 783 | nt: parsit("* * * 1 1 MON */3"), 784 | from: ts, 785 | want: mustParseTime("2024-01-01T00:00:00Z"), 786 | }, 787 | { 788 | name: "jan 1 on year is divisible by 2", 789 | nt: parsit("* * * * * * */2"), 790 | from: ts, 791 | want: mustParseTime("2020-01-01T00:00:00Z"), 792 | }, 793 | { 794 | name: "6 position (no year)", 795 | nt: parsit("* * * * * *"), 796 | from: ts, 797 | want: mustParseTime("2019-06-27T06:05:13Z"), 798 | }, 799 | { 800 | name: "5 position (no year, no sec) extra spaces", 801 | nt: parsit(" * * * * * "), 802 | from: ts, 803 | want: mustParseTime("2019-06-27T06:06:00Z"), 804 | }, 805 | { 806 | name: "1970", 807 | nt: parsit("* * * * * * 1970"), 808 | from: time.Time{}, 809 | want: mustParseTime("1970-01-01T00:00:00Z"), 810 | }, 811 | { 812 | name: "2097", 813 | nt: parsit("* * * * * * 2097"), 814 | from: time.Time{}, 815 | want: mustParseTime("2097-01-01T00:00:00Z"), 816 | }, 817 | { 818 | name: "yearly", 819 | nt: parsit("@yearly"), 820 | from: ts, 821 | want: mustParseTime("2020-01-01T00:00:00Z"), 822 | }, 823 | { 824 | name: "annually", 825 | nt: parsit("@annually"), 826 | from: ts, 827 | want: mustParseTime("2020-01-01T00:00:00Z"), 828 | }, 829 | { 830 | name: "fred likes snickerdoodles", 831 | nt: parsit("fred likes snickerdoodles"), 832 | from: ts, 833 | wantParseErr: true, 834 | }, 835 | { 836 | name: "@notanemailaddress", 837 | nt: parsit("@notanemailaddress"), 838 | from: ts, 839 | wantParseErr: true, 840 | }, 841 | { 842 | name: "@yearlyplusextraletters", 843 | nt: parsit("@yearlyplusextraletters"), 844 | from: ts, 845 | wantParseErr: true, 846 | }, 847 | { 848 | name: "too many fields", 849 | nt: parsit("* * 8 8 1 1 0"), 850 | from: ts, 851 | wantParseErr: true, 852 | }, 853 | { 854 | name: "too many fields", 855 | nt: parsit("* * 8 8 1 1 0"), 856 | from: ts, 857 | wantParseErr: true, 858 | }, 859 | { 860 | name: "error instead of summoning the old ones", 861 | nt: parsit("c̴̡͈̪̪̬͙͈̼̞̥̩̜̪̳̜̰̬̱͇̮̼̳̟̩̳̫̾̃͐̽̒̎̈́̽͠͝͝͠t̴͍̫͓͉̥̤̺͇͇̦̮̞͈̬̮͕͖̼̺̭̥͓̬̣͉̻̭̣̓̐̿̇͊̈́̉̏͐̆͑͂͛̎͒͌̈́͋̃̾̔̕͝h̸̜̭͕͎̰̮̭͙̙͇͎̺̤̙͖̒͌̂̌̍̿̒̀̊̿̾̚̕̕͝͠ͅū̷̲̝̂̑̈́͊͋̓̆̈́́́̄̎̄̃͐̈́̇̚͝ͅl̷̢̡̛͉̠̞̮̝͙̻̜͓̹̻̩̯̙̹̯͓̝͇̫̫̠̿̑̇̓̀̂̀͗̒͋̀͌͒̕̕͜͠ͅh̵̡̛̖͉̟̞̣̖̬͓̜̄̀̂̽̄͒̅̀͋̓̀͌̆̂̀͌͌̉̀̐̈́͋̓͂̚̕͠͠ų̶̛͎̖͖̜̹̬͙̖̜̲̺͐̀̎̎̽͛͌͆̂̌͒̋̆̿̎̎̽̑̏̏̈̾͘̕̕̕̕̕̚͠ͅ ̸̢̨̡̛̙̺̹͖͙͔̩̮͙̝̳̘̰̣̪̦̥̰̠̝̼͕͔͌̑̃́̐̈͗̍̅̽̌̈́̑̉̋̃͘͜͝͠͠f̶̹̹͉̉ä̸̲͈̺͉̮̹̘̦̦̟̘̼̪͚̯͓̻͉̣̬͓̰̟͇͕̳̟̗̉̈̔̉̿̔̇̑̕͜g̴̢̧̡̱̞̭͉̼̤̟̗͚͖̩̖̤͉͉̤̪͖̣̠̜̟̟̜̜̺̯̭̋̐̌̆͒͌̓̒͛́̌͒̀̓̆͋͐̈́̐̚͝ň̴̨̢̨̡͓̥̪̬͍̙̳̫̰̲̗̰̥̖̫̹̱̟̰̒̿̓̀̒̊́͛̊̈́̒̌̂͐́̕͝"), 862 | from: ts, 863 | wantParseErr: true, 864 | }, 865 | { 866 | name: "empty string", 867 | nt: parsit(""), 868 | from: ts, 869 | wantParseErr: true, 870 | }, 871 | { 872 | name: "white space", 873 | nt: parsit(" "), 874 | from: ts, 875 | wantParseErr: true, 876 | }, 877 | { 878 | name: "white space", 879 | nt: parsit(" "), 880 | from: ts, 881 | wantParseErr: true, 882 | }, 883 | { 884 | name: "incorrectly formatted range", 885 | nt: parsit("a-7/7 1 1 1 1 "), 886 | from: ts, 887 | wantParseErr: true, 888 | }, 889 | { 890 | name: "correctly formatted range", 891 | nt: parsit("2-7/7 1 1 1 0-7 "), 892 | from: ts, 893 | want: mustParseTime("2020-01-01T01:02:00Z"), 894 | }, 895 | { 896 | name: "month ranges", 897 | nt: parsit("* * * * 8-10/3 * *"), 898 | from: ts, 899 | want: mustParseTime("2019-08-01T00:00:00Z"), 900 | }, 901 | { 902 | name: "month ranges while inside the range", 903 | nt: parsit("* * * * 3-10/3 * *"), 904 | from: ts, 905 | want: mustParseTime("2019-06-27T06:05:13Z"), 906 | }, 907 | { 908 | name: "more ranges", 909 | nt: parsit("2-20/2 * * * 8-10/3 * *"), 910 | from: ts, 911 | want: mustParseTime("2019-08-01T00:00:02Z"), 912 | }, 913 | { 914 | name: "more ranges while inside them", 915 | nt: parsit("2-20/2 * * * 3-10/3 * *"), 916 | from: ts, 917 | want: mustParseTime("2019-06-27T06:05:14Z"), 918 | }, 919 | { 920 | name: "zeros in months", 921 | nt: parsit("2-20/2 * * * 0-2 * *"), 922 | from: ts, 923 | wantParseErr: true, 924 | }, 925 | { 926 | name: "zeros in month ranges", 927 | nt: parsit("2-20/2 * * * 0-2 * *"), 928 | from: ts, 929 | wantParseErr: true, 930 | }, 931 | { 932 | name: "1970", 933 | nt: parsit("* * * * * * 1970"), 934 | from: mustParseTime("1960-01-01T00:00:02Z"), 935 | want: mustParseTime("1970-01-01T00:00:00Z"), 936 | }, 937 | { 938 | name: "2099", 939 | nt: parsit("* * * * * * 2099"), 940 | from: mustParseTime("1960-01-01T00:00:02Z"), 941 | want: mustParseTime("2099-01-01T00:00:00Z"), 942 | }, 943 | } 944 | 945 | for _, tt := range tests { 946 | t.Run(tt.name, func(t *testing.T) { 947 | nt, err := tt.nt(t) 948 | if (!tt.wantParseErr) && (err != nil) { 949 | t.Errorf("expected no parse error but got %v", err) 950 | return // if parse errors we can't schedule. 951 | } 952 | if tt.wantParseErr && (err == nil) { 953 | t.Errorf("expected parse error but got nil") 954 | return 955 | } 956 | if tt.wantParseErr && err != nil { 957 | return 958 | } 959 | if got, err := nt.Next(tt.from); !reflect.DeepEqual(got, tt.want) || tt.wanterr == (err == nil) { 960 | if (!tt.wanterr) && (err != nil) { 961 | t.Errorf("expected no error but got %v", err) 962 | } 963 | if tt.wanterr && (err == nil) { 964 | t.Errorf("expected error but got nil") 965 | } 966 | t.Errorf("from = %v,cron.Parsed.Next() = %v, want %v", tt.from, got, tt.want) 967 | } 968 | }) 969 | } 970 | } 971 | 972 | func BenchmarkParse(b *testing.B) { 973 | nt := cron.Parsed{} 974 | var err error 975 | b.Run("@every 3y4mo2d1s", func(b *testing.B) { 976 | for n := 0; n < b.N; n++ { 977 | nt, err = cron.ParseUTC("0 * * * * * *") 978 | } 979 | }) 980 | _, _ = nt, err 981 | b.Run("0 * * * * * *", func(b *testing.B) { 982 | for n := 0; n < b.N; n++ { 983 | nt, err = cron.ParseUTC("0 * * * * * *") 984 | } 985 | }) 986 | _, _ = nt, err 987 | b.Run("1-6/2 * * * Feb-Oct/3 * *", func(b *testing.B) { 988 | for n := 0; n < b.N; n++ { 989 | nt, err = cron.ParseUTC("1-6/2 * * * Feb-Oct/3 * *") 990 | } 991 | }) 992 | _, _ = nt, err 993 | b.Run("1-6/2 * * * Feb-Oct/3 * 2020/4", func(b *testing.B) { 994 | for n := 0; n < b.N; n++ { 995 | nt, err = cron.ParseUTC("1-6/2 * * * Feb-Oct/3 * 2020/4") 996 | } 997 | }) 998 | _, _ = nt, err 999 | } 1000 | 1001 | func BenchmarkNext(b *testing.B) { 1002 | nt := cron.Parsed{} 1003 | var err error 1004 | ts := time.Now() 1005 | b.ResetTimer() 1006 | for n := 0; n < b.N; n++ { 1007 | nt, err = cron.ParseUTC("* * * * * * *") 1008 | nt.Next(ts) 1009 | } 1010 | _, _, _ = nt, err, ts 1011 | } 1012 | 1013 | func ExampleParseUTC() { 1014 | p, err := cron.ParseUTC("10 * * * * *") 1015 | if err != nil { 1016 | fmt.Println(err) 1017 | } 1018 | ts, err := p.Next(time.Date(1999, 12, 31, 23, 59, 59, 0, time.UTC)) 1019 | if err != nil { 1020 | fmt.Println(err) 1021 | } 1022 | fmt.Println(ts) 1023 | // Output: 1024 | // 2000-01-01 00:00:10 +0000 UTC 1025 | } 1026 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/influxdata/cron 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /parse.rl: -------------------------------------------------------------------------------- 1 | /* 2 | cron is a fast, zero-allocation cron parsing library. We aim to be Quartz cron compatible eventually. 3 | Currently we support: 4 | - standard five-position cron 5 | - six-position cron with seconds 6 | - seven-position cron with seconds and years 7 | - case-insensitive names of the days of the week 8 | - case-insensitive month names 9 | */ 10 | 11 | package cron 12 | 13 | import ( 14 | "fmt" 15 | "errors" 16 | "math/bits" 17 | "time" 18 | ) 19 | 20 | // the following comment is there so it will end up, in the generated code. 21 | // the blank line below this needs to be here. 22 | 23 | // Code generated by ragel DO NOT EDIT. 24 | 25 | //lint:file-ignore SA4006 Ignore unused, its generated 26 | //lint:file-ignore U1000 Ignore all unused code, its generated 27 | 28 | 29 | %% machine parse; 30 | %% variable data s; 31 | %% write data; 32 | 33 | 34 | var skips = [...]uint64{ 35 | ^uint64(0), 36 | 1|1<<2|1<<4|1<<6|1<<8|1<<10|1<<12|1<<14|1<<16|1<<18|1<<20|1<<22|1<<24|1<<26|1<<28|1<<30|1<<32|1<<34|1<<36|1<<38|1<<40|1<<42|1<<44|1<<46|1<<48|1<<50|1<<52|1<<54|1<<56|1<<58|1<<60|1<<62, 37 | 1|1<<3|1<<6|1<<9|1<<12|1<<15|1<<18|1<<21|1<<24|1<<27|1<<30|1<<33|1<<36|1<<39|1<<42|1<<45|1<<48|1<<51|1<<54|1<<57|1<<60|1<<63, 38 | 1|1<<4|1<<8|1<<12|1<<16|1<<20|1<<24|1<<28|1<<32|1<<36|1<<40|1<<44|1<<48|1<<52|1<<56|1<<60, 39 | 1|1<<5|1<<10|1<<15|1<<20|1<<25|1<<30|1<<35|1<<40|1<<45|1<<50|1<<55|1<<60, 40 | 1|1<<6|1<<12|1<<18|1<<24|1<<30|1<<36|1<<42|1<<48|1<<54|1<<60, 41 | 1|1<<7|1<<14|1<<21|1<<28|1<<35|1<<42|1<<49|1<<56|1<<63, 42 | 1|1<<8|1<<16|1<<24|1<<32|1<<40|1<<48|1<<56, 43 | 1|1<<9|1<<18|1<<27|1<<36|1<<45|1<<54|1<<63, 44 | 1|1<<10|1<<20|1<<30|1<<40|1<<50|1<<60, 45 | 1|1<<11|1<<22|1<<33|1<<44|1<<55, 46 | 1|1<<12|1<<24|1<<36|1<<48, 47 | 1|1<<13|1<<26|1<<39|1<<52, 48 | 1|1<<14|1<<28|1<<42|1<<56, 49 | 1|1<<15|1<<30|1<<45, 50 | 1|1<<16|1<<32|1<<48, 51 | 1|1<<17|1<<34|1<<51, 52 | 1|1<<18|1<<36|1<<54, 53 | 1|1<<19|1<<38|1<<57, 54 | 1|1<<20|1<<40, 55 | 1|1<<21|1<<42, 56 | 1|1<<22|1<<44, 57 | 1|1<<23|1<<46, 58 | 1|1<<24|1<<48, 59 | 1|1<<25|1<<50, 60 | 1|1<<26|1<<52, 61 | 1|1<<27|1<<54, 62 | 1|1<<28|1<<56, 63 | 1|1<<29|1<<58, 64 | 1|1<<30|1<<59, 65 | 1|1<<31|1<<61, 66 | 1|1<<32|1<<63, 67 | 1|1<<33, 68 | 1|1<<34, 69 | 1|1<<35, 70 | 1|1<<36, 71 | 1|1<<37, 72 | 1|1<<38, 73 | 1|1<<39, 74 | 1|1<<40, 75 | 1|1<<41, 76 | 1|1<<42, 77 | 1|1<<43, 78 | 1|1<<44, 79 | 1|1<<45, 80 | 1|1<<46, 81 | 1|1<<47, 82 | 1|1<<48, 83 | 1|1<<49, 84 | 1|1<<50, 85 | 1|1<<51, 86 | 1|1<<52, 87 | 1|1<<53, 88 | 1|1<<54, 89 | 1|1<<55, 90 | 1|1<<56, 91 | 1|1<<57, 92 | 1|1<<58, 93 | 1|1<<59, 94 | 1|1<<60, 95 | 1|1<<61, 96 | 1|1<<62, 97 | 1|1<<63, 98 | } 99 | 100 | const ( 101 | mask60 = (^uint64(0))>>(64-60) 102 | mask31 = (^uint64(0))>>(64-31) 103 | mask7 = (^uint64(0))>>(64-7) 104 | mask24 = (^uint64(0))>>(64-24) 105 | mask12 = (^uint64(0))>>(64-12) 106 | ) 107 | 108 | func parse(s string)(Parsed, error){ 109 | nt:=Parsed{} 110 | cs, p, pe, eof:= 0, 0,len(s), len(s) 111 | 112 | // init scanner vars 113 | act, ts, te := 0, 0, 0 114 | _, _, _ = act, ts, te // we have to do this. 115 | 116 | // for fcall 117 | top := 0 118 | _ = top 119 | stack := [8]int{} 120 | mark, backtrack := 0,0 121 | _ = mark 122 | _ = backtrack 123 | m, d, start, end, dec, befDec, sign:=uint64(0), uint64(0), uint64(0), uint64(0), int64(0), int64(0), int64(0) 124 | _ = d 125 | var dur time.Duration 126 | _, _, _, _ = dec, sign, dur, befDec 127 | 128 | // TODO(docmerlin): handle ranges 129 | %% write init; 130 | //m,h := 1<<0,1<<0 131 | %%{ 132 | action mark { 133 | mark = p; 134 | } 135 | 136 | action appendSeconds { 137 | { 138 | if d>=60 { 139 | return nt, fmt.Errorf("invalid second */%d", d) 140 | } 141 | if start>=60 { 142 | return nt, fmt.Errorf("invalid start second %d", start) 143 | } 144 | if end>=60 { 145 | return nt, fmt.Errorf("invalid end second %d", end) 146 | } 147 | // handle the case that isn't a 148 | endOp := 64-end-1 149 | if end==0{ 150 | endOp = 0 151 | } 152 | endMask := (^uint64(0))<>endOp 153 | if d==0{ 154 | nt.second |= 1<=60 { 164 | return nt, fmt.Errorf("invalid minute */%d", d) 165 | } 166 | if start>=60 { 167 | return nt, fmt.Errorf("invalid start minute %d", start) 168 | } 169 | if end>=60 { 170 | return nt, fmt.Errorf("invalid end minute %d", start) 171 | } 172 | // handle the case that isn't a 173 | endOp := 64-end-1 174 | if end==0{ 175 | endOp = 0 176 | } 177 | endMask := (^uint64(0))<>endOp 178 | if d==0{ 179 | nt.minute |= 1<=24{ 189 | return nt, fmt.Errorf("invalid hour */%d", d) 190 | } 191 | if start>=24 { 192 | return nt, fmt.Errorf("invalid start hour %d", start) 193 | } 194 | if end>=24 { 195 | return nt, fmt.Errorf("invalid end hour %d", start) 196 | } 197 | // handle the case that isn't a 198 | endOp := 64-end-1 199 | if end==0{ 200 | endOp = 0 201 | } 202 | endMask := (^uint64(0))<>endOp 203 | if d==0{ 204 | nt.hour |= 1<12{ 214 | return nt, fmt.Errorf("invalid month */%d", d) 215 | } 216 | if start>12 { 217 | return nt, fmt.Errorf("invalid start month %d", start) 218 | } 219 | if end>12 { 220 | return nt, fmt.Errorf("invalid end month %d", start) 221 | } 222 | // handle the case that isn't an error 223 | endOp := 16-end 224 | if end==0{ 225 | endOp = 0 226 | } 227 | endMask := (^uint16(0))<>endOp 228 | if d==0{ 229 | nt.month |= 1<<(start-1) 230 | }else{ 231 | nt.month |= (uint16(skips[d-1]&mask12)<7{ 240 | return nt, fmt.Errorf("invalid day of week */%d", d) 241 | } 242 | if start>7 { 243 | return nt, fmt.Errorf("invalid start day of week %d", start) 244 | } 245 | if end==7{ 246 | end=6 // for compatibility with older crons 247 | } 248 | if end>6 { 249 | return nt, fmt.Errorf("invalid end day of week %d", start) 250 | } 251 | if start>end { 252 | return nt, errors.New("invalid day of week range start must be before end") 253 | } 254 | 255 | // handle the case that isn't a 256 | dayRange := (^uint64(0))<<(64 - (end-start+1))>>(64 - end-1) 257 | if d==0{ 258 | //nt.dow |= uint32(sundaysAtFirst<128 { 270 | return nt, fmt.Errorf("invalid year */%d", d) 271 | } 272 | if start<1970 || start>2099 { 273 | return nt, fmt.Errorf("invalid start year %d", start) 274 | } 275 | if end<1970 || end>2099 { 276 | return nt, fmt.Errorf("invalid end year %d", end) 277 | } 278 | if d==0{ 279 | nt.setYear(int(start)) 280 | }else if d==1&&start==end{ 281 | nt.low=^uint64(0) 282 | nt.high=^uint64(0) 283 | nt.end=^uint8(0) 284 | }else if d >=64 { 285 | for i:=start;i<=end;i+=d{ 286 | nt.setYear(int(i)) 287 | } 288 | } else { 289 | s := start - 1970 290 | e := end - 1970 291 | repeat:=d-1 292 | sk := skips[repeat] 293 | switch{ 294 | case end<=start: 295 | nt.setYear(int(start)) 296 | case s < 64: 297 | switch{ 298 | case e < 64: 299 | nt.low |= sk<> (63-e)) 300 | case e < 128: 301 | nt.low |= sk<> ( 127 - e )) 303 | default: 304 | nt.low |= sk<> ( 191 - e ))) 307 | } 308 | case s < 128: 309 | switch{ 310 | case e < 128: 311 | nt.high |= sk<<( s - 64 ) & ((^uint64(0)) >> ( 127 - e )) 312 | default: 313 | nt.high |= sk<<( s - 64) 314 | nt.end |= uint8((sk << (repeat - uint64(bits.LeadingZeros64(nt.high)))) & ((^uint64(0)) >> ( 191 - e ))) 315 | } 316 | case s < 192: 317 | nt.end |= uint8(sk<<( s - 128 ) & ((^uint64(0)) >> ( 191 - e ))) 318 | } 319 | } 320 | } 321 | 322 | action appendDoM { 323 | { 324 | if d>=31{ 325 | return nt, fmt.Errorf("invalid day month */%d", d) 326 | } 327 | if start>30 { 328 | return nt, fmt.Errorf("invalid start month %d", start) 329 | } 330 | if end>31 { 331 | return nt, fmt.Errorf("invalid end month %d", start) 332 | } 333 | // handle the case that isn't a 334 | endOp := 64-end-1 335 | if end==0{ 336 | endOp = 0 337 | } 338 | endMask := (^uint64(0))<>endOp 339 | if d==0{ 340 | nt.dom |= 1<<(m-1) 341 | }else{ 342 | nt.dom |= uint32(((skips[d-1]&mask31)<mark %{ 357 | m=0 358 | for _, x := range s[mark:p] { 359 | m*=10 360 | m+=uint64(x-'0') // since we know that x is a numerical digit we can subtract the rune '0' to convert to a number from 0 to 9 361 | } 362 | }; 363 | 364 | allowedNonSpace = alnum|"/"|"*"|","|"-"; 365 | slash = "/"; 366 | comma = ","; 367 | hypen = "-"; 368 | 369 | dowName = ( /SUN/i @{m=0} | /MON/i @{m=1} | /TUE/i @{m=2} | /WED/i @{m=3} | /THU/i @{m=4} | /FRI/i @{m=5} | /SAT/i @{m=6} ); 370 | monthName = ( /JAN/i @{m=1} | /FEB/i @{m=2} | /MAR/i @{m=3} | /APR/i @{m=4} | /MAY/i @{m=5} | /JUN/i @{m=6} | /JUL/i @{m=7} | /AUG/i @{m=8} | /SEP/i @{m=9} | /OCT/i @{m=10} | /NOV/i @{m=11} | /DEC/i @{m=12} ) ; 371 | digitlist = digits ("," space* digits)*; 372 | starSlashDigits = ( ("*" @{m=1})( slash %mark digits )? ); 373 | digitsAndSlashList = ( starSlashDigits | digits ) ( ',' ( starSlashDigits | digits ) )*; 374 | 375 | secminrange = ( ( "*" %{ start=0;end=59;m=1;d=1; } ) | (digits %{ start=m; end=0;d=0;} ( "-" digits %{ end=m; d=1;} )? )) >{d=0;}; 376 | second = ( secminrange ("/" digits %{d=m})? ) %appendSeconds; 377 | seconds = second ("," second) *; 378 | 379 | minute = ( secminrange ("/" digits %{d=m})? ) %appendMinutes; 380 | minutes = minute ("," minute) *; 381 | 382 | hourrange = ( ( "*" %{ start=0;end=23;m=1;d=1; } ) | (digits %{ start=m; end=0;d=0;} ( "-" digits %{ end=m; d=1;} )? )) >{d=0;}; 383 | hour = ( hourrange ("/" digits %{d=m})? ) %appendHours; 384 | hours = hour ("," hour) *; 385 | 386 | domrange = ( ( "*" %{ start=0;end=30;m=0;d=1; } ) | (( digits | dowName ) %{ start=m-1; end=0;d=0;} ( "-" ( digits | dowName ) %{ end=m-1; d=1;} )? )) >{d=0;}; 387 | dom = ( domrange ("/" digits %{d=m})? ) %appendDoM; 388 | doms = dom ("," dom) *; 389 | 390 | monthrange = ( ( "*" %{ start=1;end=12;m=0;d=1; } ) | (( digits | monthName ) %{ start=m; end=0;d=0;} ( "-" ( digits | monthName ) %{ end=m; d=1;} )? )) >{d=0;}; 391 | month = ( monthrange ("/" digits %{d=m})? ) %appendMonths; 392 | months = month ("," month) *; 393 | 394 | dowrange = ( ( "*" %{ start=0;end=6;m=0;d=1; } ) | (( digits | dowName ) %{ start=m; end=6;d=0;} ( "-" ( digits | dowName ) %{ end=m; d=1;} )? )) >{d=0;}; 395 | dow = ( dowrange ("/" digits %{d=m})? ) %appendStarSlDoW; 396 | dows = dow ("," dow) *; 397 | 398 | yearrange = ( ( "*" %{ start=1970;end=2099;m=0;d=1; } ) | (( digits ) %{ start=m; end=m; d=0;} ( "-" ( digits ) %{ end=m; d=1;} )? )) >{d=0;}; 399 | year = ( yearrange ("/" digits %{d=m})? ) %appendYears; 400 | years = year ("," year) *; 401 | sixPos:= (seconds space+ minutes space+ hours space+ doms space+ months space+ dows) space*; 402 | sevenPos:= (seconds space+ minutes space+ hours space+ doms space+ months space+ dows space+ years) space*; 403 | fivePos := (minutes space+ hours space+ doms space+ months space+ dows) space*; 404 | durationMacro := |* 405 | digits . ( 406 | (/y/i %{ nt.setEveryYear(int(m));}) 407 | | (/ms/i %{ nt.addEveryDur(time.Duration(m)*time.Millisecond); }) 408 | | (/mo/i %{ nt.setEveryMonth(int(m)); }) 409 | | ((/[µu]/i./s/i?) %{ nt.addEveryDur(time.Duration(m)*time.Microsecond); }) 410 | | (/ns/i %{ nt.addEveryDur( time.Duration(m)) ;}) 411 | | (/s/i %{nt.addEveryDur(time.Duration(m)*time.Second); }) 412 | | ((/h/i./r/i?) %{ nt.addEveryDur(time.Duration(m)*time.Hour); }) 413 | | (/m/i %{ nt.addEveryDur(time.Duration(m)*time.Minute); }) 414 | | (/d/i %{ nt.addEveryDay(int(m)); }) 415 | | (/w/i %{ nt.addEveryDay(int(m*7)); }) 416 | ) => {}; 417 | space+ => {}; 418 | [0-9]| ^("y"|"m"|"µ"|"u"|"n"|"h"|"m"|"d") => parse_err; 419 | *|; 420 | atMacro := |* 421 | ("yearly"|"annually") space* => { 422 | nt.second=1 423 | nt.minute=1 424 | nt.hour=1 425 | nt.dom=1 426 | nt.month=1 427 | nt.dow=^uint8(0) 428 | nt.high=^uint64(0) 429 | nt.low=^uint64(0) 430 | nt.end=^uint8(0) 431 | if p!=pe-1{return nt, fmt.Errorf("error in parsing at char %d, '%s'", p, s[p:p+1])} 432 | }; 433 | "monthly" space* => { 434 | nt.second=1 435 | nt.minute=1 436 | nt.hour=1 437 | nt.dom=1 438 | nt.month=((1<<13) - 1) // every month 439 | nt.dow=^uint8(0) 440 | nt.high=^uint64(0) 441 | nt.low=^uint64(0) 442 | nt.end =^uint8(0) 443 | if p!=pe-1{return nt, fmt.Errorf("error in parsing at char %d, '%s'", p, s[p:p+1])} 444 | }; 445 | "weekly" space* => { 446 | nt.second=1 447 | nt.minute=1 448 | nt.hour=1 449 | nt.dom=^uint32(0) 450 | nt.month=((1<<13) - 1) // every month 451 | nt.dow=1 452 | nt.high=^uint64(0) 453 | nt.low=^uint64(0) 454 | nt.end =^uint8(0) 455 | if p!=pe-1{return nt, fmt.Errorf("error in parsing at char %d, '%s'", p, s[p:p+1])} 456 | }; 457 | ("daily"|"midnight") space* => { 458 | nt.second=1 459 | nt.minute=1 460 | nt.hour=1 461 | nt.dom=^uint32(0) 462 | nt.month=((1<<13) - 1) // every month 463 | nt.dow=^uint8(0) 464 | nt.high=^uint64(0) 465 | nt.low=^uint64(0) 466 | nt.end =^uint8(0) 467 | if p!=pe-1{return nt, fmt.Errorf("error in parsing at char %d, '%s'", p, s[p:p+1])} 468 | }; 469 | "hourly" space* => { 470 | nt.second=1 471 | nt.minute=1 472 | nt.hour=^uint32(0) 473 | nt.dom=^uint32(0) 474 | nt.month=((1<<13) - 1) // every month 475 | nt.dow=^uint8(0) 476 | nt.high=^uint64(0) 477 | nt.low=^uint64(0) 478 | nt.end =^uint8(0) 479 | if p!=pe-1{return nt, fmt.Errorf("error in parsing at char %d, '%s'", p, s[p:p+1])} 480 | }; 481 | "every_minute" space* => { 482 | nt.second=1 483 | nt.minute=(1<<60)-1 484 | nt.hour=^uint32(0) 485 | nt.dom=^uint32(0) 486 | nt.month=((1<<13) - 1) // every month 487 | nt.dow=^uint8(0) 488 | nt.high=^uint64(0) 489 | nt.low=^uint64(0) 490 | nt.end =^uint8(0) 491 | if p!=pe-1{return nt, fmt.Errorf("error in parsing at char %d, '%s'", p, s[p:p+1])} 492 | }; 493 | "every_second" space* => { 494 | nt.second=(1<<60)-1 495 | nt.minute=(1<<60)-1 496 | nt.hour=^uint32(0) 497 | nt.dom=^uint32(0) 498 | nt.month=((1<<13) - 1) // every month 499 | nt.dow=^uint8(0) 500 | nt.high=^uint64(0) 501 | nt.low=^uint64(0) 502 | nt.end =^uint8(0) 503 | if p!=pe-1{return nt, fmt.Errorf("error in parsing at char %d, '%s'", p, s[p:p+1])} 504 | }; 505 | # this parser can also parse golang duration format 506 | "every" space+ >mark=> { 507 | nt.every = true; 508 | p = mark 509 | fcall durationMacro; 510 | }; 511 | /.+/ => parse_err; 512 | *|; 513 | main := |* 514 | space* => mark; # to get rid of extra leading white space 515 | # 5 position cron 516 | ((allowedNonSpace+ space+){4} allowedNonSpace+) >mark => { 517 | _ = p // this is to make staticcheck happy 518 | // set seconds to 0 second of minute 519 | nt.second=1 520 | // set year to be permissive 521 | nt.high=^uint64(0) 522 | nt.low=^uint64(0) 523 | fexec mark; 524 | fcall fivePos; 525 | }; # 6 position cron with seconds but no year 526 | ((allowedNonSpace+ space+){5} allowedNonSpace+) >mark => { 527 | _ = p // this is to make staticcheck happy 528 | nt.high=^uint64(0) // set year to be permissive 529 | nt.low=^uint64(0) 530 | fexec mark; 531 | fgoto sixPos; 532 | }; # 7 position cron 533 | ((allowedNonSpace+ space+){6} allowedNonSpace+) >mark => { 534 | _ = p // this is to make staticcheck happy 535 | fexec mark; 536 | fcall sevenPos; 537 | }; 538 | "@" => {fcall atMacro;}; 539 | ((allowedNonSpace+ space+){7} (allowedNonSpace+ space?)+) => parse_err; 540 | /.+/ => parse_err; 541 | 542 | *|; 543 | }%% 544 | 545 | %% write exec; 546 | if !nt.valid() { 547 | return nt, fmt.Errorf("failed to parse cron string '%s' %v %b", s, nt, mask12) 548 | } 549 | return nt, nil 550 | } 551 | 552 | 553 | --------------------------------------------------------------------------------