pkg.re/essentialkaos/ek.10@v12.41.0+incompatible/cron/cron.go (about) 1 // Package cron provides methods for working with cron expressions 2 package cron 3 4 // ////////////////////////////////////////////////////////////////////////////////// // 5 // // 6 // Copyright (c) 2022 ESSENTIAL KAOS // 7 // Apache License, Version 2.0 <https://www.apache.org/licenses/LICENSE-2.0> // 8 // // 9 // ////////////////////////////////////////////////////////////////////////////////// // 10 11 import ( 12 "errors" 13 "strconv" 14 "strings" 15 "time" 16 17 "pkg.re/essentialkaos/ek.v12/strutil" 18 ) 19 20 // ////////////////////////////////////////////////////////////////////////////////// // 21 22 // Aliases 23 const ( 24 YEARLY = "0 0 1 1 *" 25 ANNUALLY = "0 0 1 1 *" 26 MONTHLY = "0 0 1 * *" 27 WEEKLY = "0 0 * * 0" 28 DAILY = "0 0 * * *" 29 HOURLY = "0 * * * *" 30 ) 31 32 // ////////////////////////////////////////////////////////////////////////////////// // 33 34 const ( 35 _SYMBOL_PERIOD = "-" 36 _SYMBOL_INTERVAL = "/" 37 _SYMBOL_ENUM = "," 38 _SYMBOL_ANY = "*" 39 ) 40 41 const ( 42 _NAMES_NONE uint8 = 0 43 _NAMES_DAYS uint8 = 1 44 _NAMES_MONTHS uint8 = 2 45 ) 46 47 // ////////////////////////////////////////////////////////////////////////////////// // 48 49 // Expr cron expression struct 50 type Expr struct { 51 expression string 52 minutes []uint8 53 hours []uint8 54 doms []uint8 55 months []uint8 56 dows []uint8 57 } 58 59 // ////////////////////////////////////////////////////////////////////////////////// // 60 61 type exprInfo struct { 62 min uint8 63 max uint8 64 nt uint8 // Naming type 65 } 66 67 // ////////////////////////////////////////////////////////////////////////////////// // 68 69 var ( 70 // ErrMalformedExpression is returned by the Parse method if given cron expression has 71 // wrong or unsupported format 72 ErrMalformedExpression = errors.New("Expression must have 5 tokens") 73 74 // ErrZeroInterval is returned if interval part of expression is empty 75 ErrZeroInterval = errors.New("Interval can't be less or equals 0") 76 ) 77 78 // ////////////////////////////////////////////////////////////////////////////////// // 79 80 var info = []exprInfo{ 81 {0, 59, _NAMES_NONE}, 82 {0, 23, _NAMES_NONE}, 83 {1, 31, _NAMES_NONE}, 84 {1, 12, _NAMES_MONTHS}, 85 {0, 6, _NAMES_DAYS}, 86 } 87 88 // ////////////////////////////////////////////////////////////////////////////////// // 89 90 // codebeat:disable[LOC,ABC] 91 92 // Parse parse cron expression 93 // https://en.wikipedia.org/wiki/Cron 94 func Parse(expr string) (*Expr, error) { 95 expr = strings.Replace(expr, "\t", " ", -1) 96 expr = getAliasExpression(expr) 97 98 if strings.Count(expr, " ") < 4 { 99 return nil, ErrMalformedExpression 100 } 101 102 result := &Expr{expression: expr} 103 104 for tn, ei := range info { 105 var data []uint8 106 var err error 107 108 token := strutil.ReadField(expr, tn, true, " ") 109 110 switch { 111 case isAnyToken(token): 112 data = fillUintSlice(ei.min, ei.max, 1) 113 case isEnumToken(token): 114 data, err = parseEnumToken(token, ei) 115 case isPeriodToken(token): 116 data, err = parsePeriodToken(token, ei) 117 case isIntervalToken(token): 118 data, err = parseIntervalToken(token, ei) 119 default: 120 data, err = parseSimpleToken(token, ei) 121 } 122 123 if err != nil { 124 return nil, err 125 } 126 127 switch tn { 128 case 0: 129 result.minutes = data 130 case 1: 131 result.hours = data 132 case 2: 133 result.doms = data 134 case 3: 135 result.months = data 136 case 4: 137 result.dows = data 138 } 139 } 140 141 return result, nil 142 } 143 144 // codebeat:enable[LOC,ABC] 145 146 // ////////////////////////////////////////////////////////////////////////////////// // 147 148 // codebeat:disable[LOC] 149 150 // IsDue check if current moment is match for expression 151 func (expr *Expr) IsDue(args ...time.Time) bool { 152 var t time.Time 153 154 if len(args) >= 1 { 155 t = args[0] 156 } else { 157 t = time.Now() 158 } 159 160 if !contains(expr.minutes, uint8(t.Minute())) { 161 return false 162 } 163 164 if !contains(expr.hours, uint8(t.Hour())) { 165 return false 166 } 167 168 if !contains(expr.doms, uint8(t.Day())) { 169 return false 170 } 171 172 if !contains(expr.months, uint8(t.Month())) { 173 return false 174 } 175 176 if !contains(expr.dows, uint8(t.Weekday())) { 177 return false 178 } 179 180 return true 181 } 182 183 // I don't have an idea how we can implement this without this conditions 184 // codebeat:disable[BLOCK_NESTING,LOC,CYCLO] 185 186 // Next get time of next matched moment 187 func (expr *Expr) Next(args ...time.Time) time.Time { 188 var t time.Time 189 190 if len(args) >= 1 { 191 t = args[0] 192 } else { 193 t = time.Now() 194 } 195 196 year := t.Year() 197 198 mStart := getNearPrevIndex(expr.months, uint8(t.Month())) 199 dStart := getNearPrevIndex(expr.doms, uint8(t.Day())) 200 201 for y := year; y < year+5; y++ { 202 for i := mStart; i < len(expr.months); i++ { 203 for j := dStart; j < len(expr.doms); j++ { 204 for k := 0; k < len(expr.hours); k++ { 205 for l := 0; l < len(expr.minutes); l++ { 206 d := time.Date( 207 y, 208 time.Month(expr.months[i]), 209 int(expr.doms[j]), 210 int(expr.hours[k]), 211 int(expr.minutes[l]), 212 0, 0, t.Location(), 213 ) 214 215 if d.Unix() <= t.Unix() { 216 continue 217 } 218 219 switch { 220 case uint8(d.Month()) != expr.months[i], 221 uint8(d.Day()) != expr.doms[j], 222 uint8(d.Hour()) != expr.hours[k], 223 uint8(d.Minute()) != expr.minutes[l], 224 !contains(expr.dows, uint8(d.Weekday())): 225 continue 226 } 227 228 return d 229 } 230 } 231 } 232 233 dStart = 0 234 } 235 236 mStart = 0 237 } 238 239 return time.Unix(0, 0) 240 } 241 242 // Prev get time of prev matched moment 243 func (expr *Expr) Prev(args ...time.Time) time.Time { 244 var t time.Time 245 246 if len(args) >= 1 { 247 t = args[0] 248 } else { 249 t = time.Now() 250 } 251 252 year := t.Year() 253 254 mStart := getNearNextIndex(expr.months, uint8(t.Month())) 255 dStart := getNearNextIndex(expr.doms, uint8(t.Day())) 256 257 for y := year; y >= year-5; y-- { 258 for i := mStart; i >= 0; i-- { 259 for j := dStart; j >= 0; j-- { 260 for k := len(expr.hours) - 1; k >= 0; k-- { 261 for l := len(expr.minutes) - 1; l >= 0; l-- { 262 d := time.Date( 263 y, 264 time.Month(expr.months[i]), 265 int(expr.doms[j]), 266 int(expr.hours[k]), 267 int(expr.minutes[l]), 268 0, 0, t.Location(), 269 ) 270 271 if d.Unix() >= t.Unix() { 272 continue 273 } 274 275 switch { 276 case uint8(d.Month()) != expr.months[i], 277 uint8(d.Day()) != expr.doms[j], 278 uint8(d.Hour()) != expr.hours[k], 279 uint8(d.Minute()) != expr.minutes[l], 280 !contains(expr.dows, uint8(d.Weekday())): 281 continue 282 } 283 284 return d 285 } 286 } 287 } 288 289 dStart = len(expr.doms) - 1 290 } 291 292 mStart = len(expr.months) - 1 293 } 294 295 return time.Unix(0, 0) 296 } 297 298 // codebeat:enable[BLOCK_NESTING,LOC,CYCLO] 299 300 // String return raw expression 301 func (expr *Expr) String() string { 302 return expr.expression 303 } 304 305 // ////////////////////////////////////////////////////////////////////////////////// // 306 307 func isAnyToken(t string) bool { 308 return t == _SYMBOL_ANY 309 } 310 311 func isEnumToken(t string) bool { 312 return strings.Contains(t, _SYMBOL_ENUM) 313 } 314 315 func isPeriodToken(t string) bool { 316 return strings.Contains(t, _SYMBOL_PERIOD) 317 } 318 319 func isIntervalToken(t string) bool { 320 return strings.Contains(t, _SYMBOL_INTERVAL) 321 } 322 323 func parseEnumToken(t string, ei exprInfo) ([]uint8, error) { 324 var result []uint8 325 326 for i := 0; i <= strings.Count(t, _SYMBOL_ENUM); i++ { 327 tt := strutil.ReadField(t, i, false, _SYMBOL_ENUM) 328 329 switch { 330 case isPeriodToken(tt): 331 d, err := parsePeriodToken(tt, ei) 332 333 if err != nil { 334 return nil, err 335 } 336 337 result = append(result, d...) 338 339 default: 340 t, err := parseToken(tt, ei.nt) 341 342 if err != nil { 343 return nil, err 344 } 345 346 result = append(result, t) 347 } 348 } 349 350 return result, nil 351 } 352 353 func parsePeriodToken(t string, ei exprInfo) ([]uint8, error) { 354 t1, err := parseToken(strutil.ReadField(t, 0, false, _SYMBOL_PERIOD), ei.nt) 355 356 if err != nil { 357 return nil, err 358 } 359 360 t2, err := parseToken(strutil.ReadField(t, 1, false, _SYMBOL_PERIOD), ei.nt) 361 362 if err != nil { 363 return nil, err 364 } 365 366 return fillUintSlice( 367 between(t1, ei.min, ei.max), 368 between(t2, ei.min, ei.max), 369 1, 370 ), nil 371 } 372 373 func parseIntervalToken(t string, ei exprInfo) ([]uint8, error) { 374 i, err := str2uint(strutil.ReadField(t, 1, false, _SYMBOL_INTERVAL)) 375 376 if err != nil { 377 return nil, err 378 } 379 380 if i == 0 { 381 return nil, ErrZeroInterval 382 } 383 384 return fillUintSlice(ei.min, ei.max, i), nil 385 } 386 387 func parseSimpleToken(t string, ei exprInfo) ([]uint8, error) { 388 v, err := parseToken(t, ei.nt) 389 390 if err != nil { 391 return nil, err 392 } 393 394 return []uint8{v}, nil 395 } 396 397 func getAliasExpression(expr string) string { 398 switch expr { 399 case "@yearly": 400 return YEARLY 401 case "@annually": 402 return ANNUALLY 403 case "@monthly": 404 return MONTHLY 405 case "@weekly": 406 return WEEKLY 407 case "@daily": 408 return DAILY 409 case "@hourly": 410 return HOURLY 411 } 412 413 return expr 414 } 415 416 func parseToken(t string, nt uint8) (uint8, error) { 417 switch nt { 418 case _NAMES_DAYS: 419 tu, ok := getDayNumByName(t) 420 if ok { 421 return tu, nil 422 } 423 424 case _NAMES_MONTHS: 425 tu, ok := getMonthNumByName(t) 426 if ok { 427 return tu, nil 428 } 429 } 430 431 return str2uint(t) 432 } 433 434 func getDayNumByName(token string) (uint8, bool) { 435 switch strings.ToLower(token) { 436 case "sun": 437 return 0, true 438 case "mon": 439 return 1, true 440 case "tue": 441 return 2, true 442 case "wed": 443 return 3, true 444 case "thu": 445 return 4, true 446 case "fri": 447 return 5, true 448 case "sat": 449 return 6, true 450 } 451 452 return 0, false 453 } 454 455 func getMonthNumByName(token string) (uint8, bool) { 456 switch strings.ToLower(token) { 457 case "jan": 458 return 1, true 459 case "feb": 460 return 2, true 461 case "mar": 462 return 3, true 463 case "apr": 464 return 4, true 465 case "may": 466 return 5, true 467 case "jun": 468 return 6, true 469 case "jul": 470 return 7, true 471 case "aug": 472 return 8, true 473 case "sep": 474 return 9, true 475 case "oct": 476 return 10, true 477 case "nov": 478 return 11, true 479 case "dec": 480 return 12, true 481 } 482 483 return 0, false 484 } 485 486 func fillUintSlice(start, end, interval uint8) []uint8 { 487 var result []uint8 488 489 for i := start; i <= end; i += interval { 490 result = append(result, i) 491 } 492 493 return result 494 } 495 496 func str2uint(t string) (uint8, error) { 497 u, err := strconv.ParseUint(t, 10, 8) 498 499 if err != nil { 500 return 0, err 501 } 502 503 return uint8(u), nil 504 } 505 506 func getNearNextIndex(items []uint8, item uint8) int { 507 for i := 0; i < len(items); i++ { 508 if items[i] >= item { 509 return i 510 } 511 } 512 513 return 0 514 } 515 516 func getNearPrevIndex(items []uint8, item uint8) int { 517 for i := len(items) - 1; i >= 0; i-- { 518 if items[i] <= item { 519 return i 520 } 521 } 522 523 return len(items) - 1 524 } 525 526 func between(val, min, max uint8) uint8 { 527 switch { 528 case val < min: 529 return min 530 case val > max: 531 return max 532 default: 533 return val 534 } 535 } 536 537 func contains(data []uint8, item uint8) bool { 538 for _, v := range data { 539 if item == v { 540 return true 541 } 542 } 543 544 return false 545 }