123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- package timer
- // reference: https://github.com/robfig/cron
- import (
- "fmt"
- "math"
- "strconv"
- "strings"
- "time"
- )
- // Field name | Mandatory? | Allowed values | Allowed special characters
- // ---------- | ---------- | -------------- | --------------------------
- // Seconds | No | 0-59 | * / , -
- // Minutes | Yes | 0-59 | * / , -
- // Hours | Yes | 0-23 | * / , -
- // Day of month | Yes | 1-31 | * / , -
- // Month | Yes | 1-12 | * / , -
- // Day of week | Yes | 0-6 | * / , -
- type CronExpr struct {
- sec uint64
- min uint64
- hour uint64
- dom uint64
- month uint64
- dow uint64
- }
- // goroutine safe
- func NewCronExpr(expr string) (cronExpr *CronExpr, err error) {
- fields := strings.Fields(expr)
- if len(fields) != 5 && len(fields) != 6 {
- err = fmt.Errorf("invalid expr %v: expected 5 or 6 fields, got %v", expr, len(fields))
- return
- }
- if len(fields) == 5 {
- fields = append([]string{"0"}, fields...)
- }
- cronExpr = new(CronExpr)
- // Seconds
- cronExpr.sec, err = parseCronField(fields[0], 0, 59)
- if err != nil {
- goto onError
- }
- // Minutes
- cronExpr.min, err = parseCronField(fields[1], 0, 59)
- if err != nil {
- goto onError
- }
- // Hours
- cronExpr.hour, err = parseCronField(fields[2], 0, 23)
- if err != nil {
- goto onError
- }
- // Day of month
- cronExpr.dom, err = parseCronField(fields[3], 1, 31)
- if err != nil {
- goto onError
- }
- // Month
- cronExpr.month, err = parseCronField(fields[4], 1, 12)
- if err != nil {
- goto onError
- }
- // Day of week
- cronExpr.dow, err = parseCronField(fields[5], 0, 6)
- if err != nil {
- goto onError
- }
- return
- onError:
- err = fmt.Errorf("invalid expr %v: %v", expr, err)
- return
- }
- // 1. *
- // 2. num
- // 3. num-num
- // 4. */num
- // 5. num/num (means num-max/num)
- // 6. num-num/num
- func parseCronField(field string, min int, max int) (cronField uint64, err error) {
- fields := strings.Split(field, ",")
- for _, field := range fields {
- rangeAndIncr := strings.Split(field, "/")
- if len(rangeAndIncr) > 2 {
- err = fmt.Errorf("too many slashes: %v", field)
- return
- }
- // range
- startAndEnd := strings.Split(rangeAndIncr[0], "-")
- if len(startAndEnd) > 2 {
- err = fmt.Errorf("too many hyphens: %v", rangeAndIncr[0])
- return
- }
- var start, end int
- if startAndEnd[0] == "*" {
- if len(startAndEnd) != 1 {
- err = fmt.Errorf("invalid range: %v", rangeAndIncr[0])
- return
- }
- start = min
- end = max
- } else {
- // start
- start, err = strconv.Atoi(startAndEnd[0])
- if err != nil {
- err = fmt.Errorf("invalid range: %v", rangeAndIncr[0])
- return
- }
- // end
- if len(startAndEnd) == 1 {
- if len(rangeAndIncr) == 2 {
- end = max
- } else {
- end = start
- }
- } else {
- end, err = strconv.Atoi(startAndEnd[1])
- if err != nil {
- err = fmt.Errorf("invalid range: %v", rangeAndIncr[0])
- return
- }
- }
- }
- if start > end {
- err = fmt.Errorf("invalid range: %v", rangeAndIncr[0])
- return
- }
- if start < min {
- err = fmt.Errorf("out of range [%v, %v]: %v", min, max, rangeAndIncr[0])
- return
- }
- if end > max {
- err = fmt.Errorf("out of range [%v, %v]: %v", min, max, rangeAndIncr[0])
- return
- }
- // increment
- var incr int
- if len(rangeAndIncr) == 1 {
- incr = 1
- } else {
- incr, err = strconv.Atoi(rangeAndIncr[1])
- if err != nil {
- err = fmt.Errorf("invalid increment: %v", rangeAndIncr[1])
- return
- }
- if incr <= 0 {
- err = fmt.Errorf("invalid increment: %v", rangeAndIncr[1])
- return
- }
- }
- // cronField
- if incr == 1 {
- cronField |= ^(math.MaxUint64 << uint(end+1)) & (math.MaxUint64 << uint(start))
- } else {
- for i := start; i <= end; i += incr {
- cronField |= 1 << uint(i)
- }
- }
- }
- return
- }
- func (e *CronExpr) matchDay(t time.Time) bool {
- // day-of-month blank
- if e.dom == 0xfffffffe {
- return 1<<uint(t.Weekday())&e.dow != 0
- }
- // day-of-week blank
- if e.dow == 0x7f {
- return 1<<uint(t.Day())&e.dom != 0
- }
- return 1<<uint(t.Weekday())&e.dow != 0 ||
- 1<<uint(t.Day())&e.dom != 0
- }
- // goroutine safe
- func (e *CronExpr) Next(t time.Time) time.Time {
- // the upcoming second
- t = t.Truncate(time.Second).Add(time.Second)
- year := t.Year()
- initFlag := false
- retry:
- // Year
- if t.Year() > year+1 {
- return time.Time{}
- }
- // Month
- for 1<<uint(t.Month())&e.month == 0 {
- if !initFlag {
- initFlag = true
- t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
- }
- t = t.AddDate(0, 1, 0)
- if t.Month() == time.January {
- goto retry
- }
- }
- // Day
- for !e.matchDay(t) {
- if !initFlag {
- initFlag = true
- t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
- }
- t = t.AddDate(0, 0, 1)
- if t.Day() == 1 {
- goto retry
- }
- }
- // Hours
- for 1<<uint(t.Hour())&e.hour == 0 {
- if !initFlag {
- initFlag = true
- t = t.Truncate(time.Hour)
- }
- t = t.Add(time.Hour)
- if t.Hour() == 0 {
- goto retry
- }
- }
- // Minutes
- for 1<<uint(t.Minute())&e.min == 0 {
- if !initFlag {
- initFlag = true
- t = t.Truncate(time.Minute)
- }
- t = t.Add(time.Minute)
- if t.Minute() == 0 {
- goto retry
- }
- }
- // Seconds
- for 1<<uint(t.Second())&e.sec == 0 {
- if !initFlag {
- initFlag = true
- }
- t = t.Add(time.Second)
- if t.Second() == 0 {
- goto retry
- }
- }
- return t
- }
|