github.com/sandwich-go/boost@v1.3.29/xtime/cron/cronexpr.go (about) 1 package cron 2 3 // reference: https://github.com/gorhill/cronexpr 4 import ( 5 "github.com/sandwich-go/boost/xerror" 6 "math" 7 "strconv" 8 "strings" 9 "time" 10 ) 11 12 // Expression 13 // 5列会自动在头添加秒标记 14 // Field name | Mandatory? | Allowed values | Allowed special characters 15 // ---------- | ---------- | -------------- | -------------------------- 16 // Seconds | No | 0-59 | * / , - 17 // Minutes | Yes | 0-59 | * / , - 18 // Hours | Yes | 0-23 | * / , - 19 // Day | Yes | 1-31 | * / , - 20 // Month | Yes | 1-12 | * / , - 21 // Day of week | Yes | 0-6 | * / , - 22 type Expression struct { 23 sec uint64 24 min uint64 25 hour uint64 26 dom uint64 27 month uint64 28 dow uint64 29 } 30 31 // MustParse returns a new Expression pointer. It expects a well-formed cron 32 // expression. If a malformed cron expression is supplied, it will `panic`. 33 func MustParse(cronLine string) *Expression { 34 expr, err := Parse(cronLine) 35 if err != nil { 36 panic(err) 37 } 38 return expr 39 } 40 41 // Parse returns a new Expression pointer. An error is returned if a malformed 42 // cron expression is supplied. 43 func Parse(expr string) (cronExpr *Expression, err error) { 44 fields := strings.Fields(expr) 45 if len(fields) != 5 && len(fields) != 6 { 46 err = xerror.NewText("invalid expr %v: expected 5 or 6 fields, got %v", expr, len(fields)) 47 return 48 } 49 50 if len(fields) == 5 { 51 fields = append([]string{"0"}, fields...) 52 } 53 54 cronExpr = new(Expression) 55 // Seconds 56 cronExpr.sec, err = parseCronField(fields[0], 0, 59) 57 if err != nil { 58 goto onError 59 } 60 // Minutes 61 cronExpr.min, err = parseCronField(fields[1], 0, 59) 62 if err != nil { 63 goto onError 64 } 65 // Hours 66 cronExpr.hour, err = parseCronField(fields[2], 0, 23) 67 if err != nil { 68 goto onError 69 } 70 // Day of month 71 cronExpr.dom, err = parseCronField(fields[3], 1, 31) 72 if err != nil { 73 goto onError 74 } 75 // Month 76 cronExpr.month, err = parseCronField(fields[4], 1, 12) 77 if err != nil { 78 goto onError 79 } 80 // Day of week 81 cronExpr.dow, err = parseCronField(fields[5], 0, 6) 82 if err != nil { 83 goto onError 84 } 85 return 86 87 onError: 88 err = xerror.NewText("invalid expr %v: %v", expr, err) 89 return 90 } 91 92 // 1. * 93 // 2. num 94 // 3. num-num 95 // 4. */num 96 // 5. num/num (means num-max/num) 97 // 6. num-num/num 98 func parseCronField(field string, min int, max int) (cronField uint64, err error) { 99 fields := strings.Split(field, ",") 100 for _, field := range fields { 101 rangeAndIncr := strings.Split(field, "/") 102 if len(rangeAndIncr) > 2 { 103 err = xerror.NewText("too many slashes: %v", field) 104 return 105 } 106 107 // range 108 startAndEnd := strings.Split(rangeAndIncr[0], "-") 109 if len(startAndEnd) > 2 { 110 err = xerror.NewText("too many hyphens: %v", rangeAndIncr[0]) 111 return 112 } 113 114 var start, end int 115 if startAndEnd[0] == "*" { 116 if len(startAndEnd) != 1 { 117 err = xerror.NewText("invalid range p1: %v", rangeAndIncr[0]) 118 return 119 } 120 start = min 121 end = max 122 } else { 123 // start 124 start, err = strconv.Atoi(startAndEnd[0]) 125 if err != nil { 126 err = xerror.NewText("invalid range p2: %v", rangeAndIncr[0]) 127 return 128 } 129 // end 130 if len(startAndEnd) == 1 { 131 if len(rangeAndIncr) == 2 { 132 end = max 133 } else { 134 end = start 135 } 136 } else { 137 end, err = strconv.Atoi(startAndEnd[1]) 138 if err != nil { 139 err = xerror.NewText("invalid range p3: %v", rangeAndIncr[0]) 140 return 141 } 142 } 143 } 144 145 if start > end { 146 err = xerror.NewText("invalid range p4: %v", rangeAndIncr[0]) 147 return 148 } 149 if start < min { 150 err = xerror.NewText("start out of range [%v, %v]: %v", min, max, rangeAndIncr[0]) 151 return 152 } 153 if end > max { 154 err = xerror.NewText("end out of range [%v, %v]: %v", min, max, rangeAndIncr[0]) 155 return 156 } 157 158 // increment 159 var incr int 160 if len(rangeAndIncr) == 1 { 161 incr = 1 162 } else { 163 incr, err = strconv.Atoi(rangeAndIncr[1]) 164 if err != nil { 165 err = xerror.NewText("invalid increment: %v", rangeAndIncr[1]) 166 return 167 } 168 if incr <= 0 { 169 err = xerror.NewText("invalid increment: %v", rangeAndIncr[1]) 170 return 171 } 172 } 173 174 // cronField 175 if incr == 1 { 176 cronField |= ^(math.MaxUint64 << uint(end+1)) & (math.MaxUint64 << uint(start)) 177 } else { 178 for i := start; i <= end; i += incr { 179 cronField |= 1 << uint(i) 180 } 181 } 182 } 183 184 return 185 } 186 187 func (e *Expression) matchDay(t time.Time) bool { 188 // day-of-month blank 189 if e.dom == 0xfffffffe { 190 return 1<<uint(t.Weekday())&e.dow != 0 191 } 192 193 // day-of-week blank 194 if e.dow == 0x7f { 195 return 1<<uint(t.Day())&e.dom != 0 196 } 197 198 return 1<<uint(t.Weekday())&e.dow != 0 || 199 1<<uint(t.Day())&e.dom != 0 200 } 201 202 // goroutine safe 203 func (e *Expression) Next(t time.Time) time.Time { 204 // the upcoming second 205 t = t.Truncate(time.Second).Add(time.Second) 206 207 year := t.Year() 208 initFlag := false 209 210 retry: 211 // Year 212 if t.Year() > year+1 { 213 return time.Time{} 214 } 215 216 // Month 217 for 1<<uint(t.Month())&e.month == 0 { 218 if !initFlag { 219 initFlag = true 220 t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location()) 221 } 222 223 t = t.AddDate(0, 1, 0) 224 if t.Month() == time.January { 225 goto retry 226 } 227 } 228 229 // Day 230 for !e.matchDay(t) { 231 if !initFlag { 232 initFlag = true 233 t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) 234 } 235 236 t = t.AddDate(0, 0, 1) 237 if t.Day() == 1 { 238 goto retry 239 } 240 } 241 242 // Hours 243 for 1<<uint(t.Hour())&e.hour == 0 { 244 if !initFlag { 245 initFlag = true 246 t = t.Truncate(time.Hour) 247 } 248 249 t = t.Add(time.Hour) 250 if t.Hour() == 0 { 251 goto retry 252 } 253 } 254 255 // Minutes 256 for 1<<uint(t.Minute())&e.min == 0 { 257 if !initFlag { 258 initFlag = true 259 t = t.Truncate(time.Minute) 260 } 261 262 t = t.Add(time.Minute) 263 if t.Minute() == 0 { 264 goto retry 265 } 266 } 267 268 // Seconds 269 for 1<<uint(t.Second())&e.sec == 0 { 270 if !initFlag { 271 initFlag = true 272 } 273 274 t = t.Add(time.Second) 275 if t.Second() == 0 { 276 goto retry 277 } 278 } 279 280 return t 281 }