github.com/sohaha/zlsgo@v1.7.13-0.20240501141223-10dd1a906f76/ztime/cron/cronexpr.go (about) 1 package cron 2 3 import ( 4 "fmt" 5 "regexp" 6 "sort" 7 "strings" 8 "sync" 9 "time" 10 ) 11 12 // copyright https://github.com/gorhill/cronexpr 13 14 type ( 15 Expression struct { 16 daysOfWeek map[int]bool 17 lastWeekDaysOfWeek map[int]bool 18 specificWeekDaysOfWeek map[int]bool 19 daysOfMonth map[int]bool 20 workdaysOfMonth map[int]bool 21 monthList []int 22 actualDaysOfMonthList []int 23 secondList []int 24 hourList []int 25 minuteList []int 26 yearList []int 27 lastWorkdayOfMonth bool 28 daysOfMonthRestricted bool 29 lastDayOfMonth bool 30 daysOfWeekRestricted bool 31 } 32 cronDirective struct { 33 kind int 34 first int 35 last int 36 step int 37 sbeg int 38 send int 39 } 40 ) 41 42 const ( 43 none = 0 44 one = 1 45 span = 2 46 all = 3 47 ) 48 49 var ( 50 dowNormalizedOffsets = [][]int{ 51 {1, 8, 15, 22, 29}, 52 {2, 9, 16, 23, 30}, 53 {3, 10, 17, 24, 31}, 54 {4, 11, 18, 25}, 55 {5, 12, 19, 26}, 56 {6, 13, 20, 27}, 57 {7, 14, 21, 28}, 58 } 59 genericDefaultList = []int{ 60 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 61 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 62 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 63 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 64 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 65 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 66 } 67 yearDefaultList = []int{ 68 1970, 1971, 1972, 1973, 1974, 1975, 1976, 1977, 1978, 1979, 69 1980, 1981, 1982, 1983, 1984, 1985, 1986, 1987, 1988, 1989, 70 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 71 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 72 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 73 2020, 2021, 2022, 2023, 2024, 2025, 2026, 2027, 2028, 2029, 74 2030, 2031, 2032, 2033, 2034, 2035, 2036, 2037, 2038, 2039, 75 2040, 2041, 2042, 2043, 2044, 2045, 2046, 2047, 2048, 2049, 76 2050, 2051, 2052, 2053, 2054, 2055, 2056, 2057, 2058, 2059, 77 2060, 2061, 2062, 2063, 2064, 2065, 2066, 2067, 2068, 2069, 78 2070, 2071, 2072, 2073, 2074, 2075, 2076, 2077, 2078, 2079, 79 2080, 2081, 2082, 2083, 2084, 2085, 2086, 2087, 2088, 2089, 80 2090, 2091, 2092, 2093, 2094, 2095, 2096, 2097, 2098, 2099, 81 } 82 numberTokens = map[string]int{ 83 "0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, 84 "00": 0, "01": 1, "02": 2, "03": 3, "04": 4, "05": 5, "06": 6, "07": 7, "08": 8, "09": 9, 85 "10": 10, "11": 11, "12": 12, "13": 13, "14": 14, "15": 15, "16": 16, "17": 17, "18": 18, "19": 19, 86 "20": 20, "21": 21, "22": 22, "23": 23, "24": 24, "25": 25, "26": 26, "27": 27, "28": 28, "29": 29, 87 "30": 30, "31": 31, "32": 32, "33": 33, "34": 34, "35": 35, "36": 36, "37": 37, "38": 38, "39": 39, 88 "40": 40, "41": 41, "42": 42, "43": 43, "44": 44, "45": 45, "46": 46, "47": 47, "48": 48, "49": 49, 89 "50": 50, "51": 51, "52": 52, "53": 53, "54": 54, "55": 55, "56": 56, "57": 57, "58": 58, "59": 59, 90 "1970": 1970, "1971": 1971, "1972": 1972, "1973": 1973, "1974": 1974, "1975": 1975, "1976": 1976, "1977": 1977, "1978": 1978, "1979": 1979, 91 "1980": 1980, "1981": 1981, "1982": 1982, "1983": 1983, "1984": 1984, "1985": 1985, "1986": 1986, "1987": 1987, "1988": 1988, "1989": 1989, 92 "1990": 1990, "1991": 1991, "1992": 1992, "1993": 1993, "1994": 1994, "1995": 1995, "1996": 1996, "1997": 1997, "1998": 1998, "1999": 1999, 93 "2000": 2000, "2001": 2001, "2002": 2002, "2003": 2003, "2004": 2004, "2005": 2005, "2006": 2006, "2007": 2007, "2008": 2008, "2009": 2009, 94 "2010": 2010, "2011": 2011, "2012": 2012, "2013": 2013, "2014": 2014, "2015": 2015, "2016": 2016, "2017": 2017, "2018": 2018, "2019": 2019, 95 "2020": 2020, "2021": 2021, "2022": 2022, "2023": 2023, "2024": 2024, "2025": 2025, "2026": 2026, "2027": 2027, "2028": 2028, "2029": 2029, 96 "2030": 2030, "2031": 2031, "2032": 2032, "2033": 2033, "2034": 2034, "2035": 2035, "2036": 2036, "2037": 2037, "2038": 2038, "2039": 2039, 97 "2040": 2040, "2041": 2041, "2042": 2042, "2043": 2043, "2044": 2044, "2045": 2045, "2046": 2046, "2047": 2047, "2048": 2048, "2049": 2049, 98 "2050": 2050, "2051": 2051, "2052": 2052, "2053": 2053, "2054": 2054, "2055": 2055, "2056": 2056, "2057": 2057, "2058": 2058, "2059": 2059, 99 "2060": 2060, "2061": 2061, "2062": 2062, "2063": 2063, "2064": 2064, "2065": 2065, "2066": 2066, "2067": 2067, "2068": 2068, "2069": 2069, 100 "2070": 2070, "2071": 2071, "2072": 2072, "2073": 2073, "2074": 2074, "2075": 2075, "2076": 2076, "2077": 2077, "2078": 2078, "2079": 2079, 101 "2080": 2080, "2081": 2081, "2082": 2082, "2083": 2083, "2084": 2084, "2085": 2085, "2086": 2086, "2087": 2087, "2088": 2088, "2089": 2089, 102 "2090": 2090, "2091": 2091, "2092": 2092, "2093": 2093, "2094": 2094, "2095": 2095, "2096": 2096, "2097": 2097, "2098": 2098, "2099": 2099, 103 } 104 monthTokens = map[string]int{ 105 `1`: 1, `jan`: 1, `january`: 1, 106 `2`: 2, `feb`: 2, `february`: 2, 107 `3`: 3, `mar`: 3, `march`: 3, 108 `4`: 4, `apr`: 4, `april`: 4, 109 `5`: 5, `may`: 5, 110 `6`: 6, `jun`: 6, `june`: 6, 111 `7`: 7, `jul`: 7, `july`: 7, 112 `8`: 8, `aug`: 8, `august`: 8, 113 `9`: 9, `sep`: 9, `september`: 9, 114 `10`: 10, `oct`: 10, `october`: 10, 115 `11`: 11, `nov`: 11, `november`: 11, 116 `12`: 12, `dec`: 12, `december`: 12, 117 } 118 dowTokens = map[string]int{ 119 `0`: 0, `sun`: 0, `sunday`: 0, 120 `1`: 1, `mon`: 1, `monday`: 1, 121 `2`: 2, `tue`: 2, `tuesday`: 2, 122 `3`: 3, `wed`: 3, `wednesday`: 3, 123 `4`: 4, `thu`: 4, `thursday`: 4, 124 `5`: 5, `fri`: 5, `friday`: 5, 125 `6`: 6, `sat`: 6, `saturday`: 6, 126 `7`: 0, 127 } 128 ) 129 130 func atoi(s string) int { 131 return numberTokens[s] 132 } 133 134 type fieldDescriptor struct { 135 atoi func(string) int 136 name string 137 valuePattern string 138 defaultList []int 139 min int 140 max int 141 } 142 143 var ( 144 secondDescriptor = fieldDescriptor{ 145 name: "second", 146 min: 0, 147 max: 59, 148 defaultList: genericDefaultList[0:60], 149 valuePattern: `0?[0-9]|[1-5][0-9]`, 150 atoi: atoi, 151 } 152 minuteDescriptor = fieldDescriptor{ 153 name: "minute", 154 min: 0, 155 max: 59, 156 defaultList: genericDefaultList[0:60], 157 valuePattern: `0?[0-9]|[1-5][0-9]`, 158 atoi: atoi, 159 } 160 hourDescriptor = fieldDescriptor{ 161 name: "hour", 162 min: 0, 163 max: 23, 164 defaultList: genericDefaultList[0:24], 165 valuePattern: `0?[0-9]|1[0-9]|2[0-3]`, 166 atoi: atoi, 167 } 168 domDescriptor = fieldDescriptor{ 169 name: "day-of-month", 170 min: 1, 171 max: 31, 172 defaultList: genericDefaultList[1:32], 173 valuePattern: `0?[1-9]|[12][0-9]|3[01]`, 174 atoi: atoi, 175 } 176 monthDescriptor = fieldDescriptor{ 177 name: "month", 178 min: 1, 179 max: 12, 180 defaultList: genericDefaultList[1:13], 181 valuePattern: `0?[1-9]|1[012]|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|march|april|june|july|august|september|october|november|december`, 182 atoi: func(s string) int { 183 return monthTokens[s] 184 }, 185 } 186 dowDescriptor = fieldDescriptor{ 187 name: "day-of-week", 188 min: 0, 189 max: 6, 190 defaultList: genericDefaultList[0:7], 191 valuePattern: `0?[0-7]|sun|mon|tue|wed|thu|fri|sat|sunday|monday|tuesday|wednesday|thursday|friday|saturday`, 192 atoi: func(s string) int { 193 return dowTokens[s] 194 }, 195 } 196 yearDescriptor = fieldDescriptor{ 197 name: "year", 198 min: 1970, 199 max: 2099, 200 defaultList: yearDefaultList[:], 201 valuePattern: `19[789][0-9]|20[0-9]{2}`, 202 atoi: atoi, 203 } 204 layoutWildcard = `^\*$|^\?$` 205 layoutValue = `^(%value%)$` 206 layoutRange = `^(%value%)-(%value%)$` 207 layoutWildcardAndInterval = `^\*/(\d+)$` 208 layoutValueAndInterval = `^(%value%)/(\d+)$` 209 layoutRangeAndInterval = `^(%value%)-(%value%)/(\d+)$` 210 layoutLastDom = `^l$` 211 layoutWorkdom = `^(%value%)w$` 212 layoutLastWorkdom = `^lw$` 213 layoutDowOfLastWeek = `^(%value%)l$` 214 layoutDowOfSpecificWeek = `^(%value%)#([1-5])$` 215 fieldFinder = regexp.MustCompile(`\S+`) 216 entryFinder = regexp.MustCompile(`[^,]+`) 217 layoutRegexp = make(map[string]*regexp.Regexp) 218 layoutRegexpLock sync.Mutex 219 cronNormalizer = strings.NewReplacer( 220 "@yearly", "0 0 0 1 1 * *", 221 "@annually", "0 0 0 1 1 * *", 222 "@monthly", "0 0 0 1 * * *", 223 "@weekly", "0 0 0 * * 0 *", 224 "@daily", "0 0 0 * * * *", 225 "@hourly", "0 0 * * * * *") 226 ) 227 228 func (expr *Expression) secondFieldHandler(s string) error { 229 var err error 230 expr.secondList, err = genericFieldHandler(s, secondDescriptor) 231 return err 232 } 233 234 func (expr *Expression) minuteFieldHandler(s string) error { 235 var err error 236 expr.minuteList, err = genericFieldHandler(s, minuteDescriptor) 237 return err 238 } 239 func (expr *Expression) hourFieldHandler(s string) error { 240 var err error 241 expr.hourList, err = genericFieldHandler(s, hourDescriptor) 242 return err 243 } 244 245 func (expr *Expression) monthFieldHandler(s string) error { 246 var err error 247 expr.monthList, err = genericFieldHandler(s, monthDescriptor) 248 return err 249 } 250 func (expr *Expression) yearFieldHandler(s string) error { 251 var err error 252 expr.yearList, err = genericFieldHandler(s, yearDescriptor) 253 return err 254 } 255 256 func genericFieldHandler(s string, desc fieldDescriptor) ([]int, error) { 257 directives, err := genericFieldParse(s, desc) 258 if err != nil { 259 return nil, err 260 } 261 values := make(map[int]bool) 262 for _, directive := range directives { 263 switch directive.kind { 264 case none: 265 return nil, fmt.Errorf("syntax error in %s field: '%s'", desc.name, s[directive.sbeg:directive.send]) 266 case one: 267 populateOne(values, directive.first) 268 case span: 269 populateMany(values, directive.first, directive.last, directive.step) 270 case all: 271 return desc.defaultList, nil 272 } 273 } 274 return toList(values), nil 275 } 276 277 func (expr *Expression) dowFieldHandler(s string) error { 278 expr.daysOfWeekRestricted = true 279 expr.daysOfWeek = make(map[int]bool) 280 expr.lastWeekDaysOfWeek = make(map[int]bool) 281 expr.specificWeekDaysOfWeek = make(map[int]bool) 282 283 directives, err := genericFieldParse(s, dowDescriptor) 284 if err != nil { 285 return err 286 } 287 288 for _, directive := range directives { 289 switch directive.kind { 290 case none: 291 sdirective := s[directive.sbeg:directive.send] 292 snormal := strings.ToLower(sdirective) 293 // `5L` 294 pairs := makeLayoutRegexp(layoutDowOfLastWeek, dowDescriptor.valuePattern).FindStringSubmatchIndex(snormal) 295 if len(pairs) > 0 { 296 populateOne(expr.lastWeekDaysOfWeek, dowDescriptor.atoi(snormal[pairs[2]:pairs[3]])) 297 } else { 298 // `5#3` 299 pairs := makeLayoutRegexp(layoutDowOfSpecificWeek, dowDescriptor.valuePattern).FindStringSubmatchIndex(snormal) 300 if len(pairs) > 0 { 301 populateOne(expr.specificWeekDaysOfWeek, (dowDescriptor.atoi(snormal[pairs[4]:pairs[5]])-1)*7+(dowDescriptor.atoi(snormal[pairs[2]:pairs[3]])%7)) 302 } else { 303 return fmt.Errorf("syntax error in day-of-week field: '%s'", sdirective) 304 } 305 } 306 case one: 307 populateOne(expr.daysOfWeek, directive.first) 308 case span: 309 populateMany(expr.daysOfWeek, directive.first, directive.last, directive.step) 310 case all: 311 populateMany(expr.daysOfWeek, directive.first, directive.last, directive.step) 312 expr.daysOfWeekRestricted = false 313 } 314 } 315 return nil 316 } 317 318 func (expr *Expression) domFieldHandler(s string) error { 319 expr.daysOfMonthRestricted = true 320 expr.lastDayOfMonth = false 321 expr.lastWorkdayOfMonth = false 322 expr.daysOfMonth = make(map[int]bool) // days of month map 323 expr.workdaysOfMonth = make(map[int]bool) // work days of month map 324 325 directives, err := genericFieldParse(s, domDescriptor) 326 if err != nil { 327 return err 328 } 329 330 for _, directive := range directives { 331 switch directive.kind { 332 case none: 333 sdirective := s[directive.sbeg:directive.send] 334 snormal := strings.ToLower(sdirective) 335 // `L` 336 if makeLayoutRegexp(layoutLastDom, domDescriptor.valuePattern).MatchString(snormal) { 337 expr.lastDayOfMonth = true 338 } else { 339 // `LW` 340 if makeLayoutRegexp(layoutLastWorkdom, domDescriptor.valuePattern).MatchString(snormal) { 341 expr.lastWorkdayOfMonth = true 342 } else { 343 // `15W` 344 pairs := makeLayoutRegexp(layoutWorkdom, domDescriptor.valuePattern).FindStringSubmatchIndex(snormal) 345 if len(pairs) > 0 { 346 populateOne(expr.workdaysOfMonth, domDescriptor.atoi(snormal[pairs[2]:pairs[3]])) 347 } else { 348 return fmt.Errorf("syntax error in day-of-month field: '%s'", sdirective) 349 } 350 } 351 } 352 case one: 353 populateOne(expr.daysOfMonth, directive.first) 354 case span: 355 populateMany(expr.daysOfMonth, directive.first, directive.last, directive.step) 356 case all: 357 populateMany(expr.daysOfMonth, directive.first, directive.last, directive.step) 358 expr.daysOfMonthRestricted = false 359 } 360 } 361 return nil 362 } 363 364 /******************************************************************************/ 365 366 func populateOne(values map[int]bool, v int) { 367 values[v] = true 368 } 369 370 func populateMany(values map[int]bool, min, max, step int) { 371 for i := min; i <= max; i += step { 372 values[i] = true 373 } 374 } 375 376 func toList(set map[int]bool) []int { 377 list := make([]int, len(set)) 378 i := 0 379 for k := range set { 380 list[i] = k 381 i += 1 382 } 383 sort.Ints(list) 384 return list 385 } 386 func genericFieldParse(s string, desc fieldDescriptor) ([]*cronDirective, error) { 387 // At least one entry must be present 388 indices := entryFinder.FindAllStringIndex(s, -1) 389 if len(indices) == 0 { 390 return nil, fmt.Errorf("%s field: missing directive", desc.name) 391 } 392 393 directives := make([]*cronDirective, 0, len(indices)) 394 395 for i := range indices { 396 directive := cronDirective{ 397 sbeg: indices[i][0], 398 send: indices[i][1], 399 } 400 snormal := strings.ToLower(s[indices[i][0]:indices[i][1]]) 401 402 // `*` 403 if makeLayoutRegexp(layoutWildcard, desc.valuePattern).MatchString(snormal) { 404 directive.kind = all 405 directive.first = desc.min 406 directive.last = desc.max 407 directive.step = 1 408 directives = append(directives, &directive) 409 continue 410 } 411 // `5` 412 if makeLayoutRegexp(layoutValue, desc.valuePattern).MatchString(snormal) { 413 directive.kind = one 414 directive.first = desc.atoi(snormal) 415 directives = append(directives, &directive) 416 continue 417 } 418 // `5-20` 419 pairs := makeLayoutRegexp(layoutRange, desc.valuePattern).FindStringSubmatchIndex(snormal) 420 if len(pairs) > 0 { 421 directive.kind = span 422 directive.first = desc.atoi(snormal[pairs[2]:pairs[3]]) 423 directive.last = desc.atoi(snormal[pairs[4]:pairs[5]]) 424 directive.step = 1 425 directives = append(directives, &directive) 426 continue 427 } 428 // `*/2` 429 pairs = makeLayoutRegexp(layoutWildcardAndInterval, desc.valuePattern).FindStringSubmatchIndex(snormal) 430 if len(pairs) > 0 { 431 directive.kind = span 432 directive.first = desc.min 433 directive.last = desc.max 434 directive.step = atoi(snormal[pairs[2]:pairs[3]]) 435 if directive.step < 1 || directive.step > desc.max { 436 return nil, fmt.Errorf("invalid interval %s", snormal) 437 } 438 directives = append(directives, &directive) 439 continue 440 } 441 // `5/2` 442 pairs = makeLayoutRegexp(layoutValueAndInterval, desc.valuePattern).FindStringSubmatchIndex(snormal) 443 if len(pairs) > 0 { 444 directive.kind = span 445 directive.first = desc.atoi(snormal[pairs[2]:pairs[3]]) 446 directive.last = desc.max 447 directive.step = atoi(snormal[pairs[4]:pairs[5]]) 448 if directive.step < 1 || directive.step > desc.max { 449 return nil, fmt.Errorf("invalid interval %s", snormal) 450 } 451 directives = append(directives, &directive) 452 continue 453 } 454 // `5-20/2` 455 pairs = makeLayoutRegexp(layoutRangeAndInterval, desc.valuePattern).FindStringSubmatchIndex(snormal) 456 if len(pairs) > 0 { 457 directive.kind = span 458 directive.first = desc.atoi(snormal[pairs[2]:pairs[3]]) 459 directive.last = desc.atoi(snormal[pairs[4]:pairs[5]]) 460 directive.step = atoi(snormal[pairs[6]:pairs[7]]) 461 if directive.step < 1 || directive.step > desc.max { 462 return nil, fmt.Errorf("invalid interval %s", snormal) 463 } 464 directives = append(directives, &directive) 465 continue 466 } 467 // No behavior for this one, let caller deal with it 468 directive.kind = none 469 directives = append(directives, &directive) 470 } 471 return directives, nil 472 } 473 474 func makeLayoutRegexp(layout, value string) *regexp.Regexp { 475 layoutRegexpLock.Lock() 476 defer layoutRegexpLock.Unlock() 477 478 layout = strings.Replace(layout, `%value%`, value, -1) 479 re := layoutRegexp[layout] 480 if re == nil { 481 re = regexp.MustCompile(layout) 482 layoutRegexp[layout] = re 483 } 484 return re 485 } 486 487 func ParseNextTime(cronLine string) (nextTime time.Time, err error) { 488 expr, err := Parse(cronLine) 489 if err == nil { 490 nextTime = expr.Next(time.Now()) 491 } 492 493 return 494 } 495 496 func Parse(cronLine string) (*Expression, error) { 497 cron := cronNormalizer.Replace(cronLine) 498 499 indices := fieldFinder.FindAllStringIndex(cron, -1) 500 fieldCount := len(indices) 501 if fieldCount < 5 { 502 return nil, fmt.Errorf("missing field(s)") 503 } 504 // ignore fields beyond 7th 505 if fieldCount > 7 { 506 fieldCount = 7 507 } 508 509 var expr = Expression{} 510 var field = 0 511 var err error 512 513 // second field (optional) 514 if fieldCount == 7 { 515 err = expr.secondFieldHandler(cron[indices[field][0]:indices[field][1]]) 516 if err != nil { 517 return nil, err 518 } 519 field += 1 520 } else { 521 expr.secondList = []int{0} 522 } 523 524 // minute field 525 err = expr.minuteFieldHandler(cron[indices[field][0]:indices[field][1]]) 526 if err != nil { 527 return nil, err 528 } 529 field += 1 530 531 // hour field 532 err = expr.hourFieldHandler(cron[indices[field][0]:indices[field][1]]) 533 if err != nil { 534 return nil, err 535 } 536 field += 1 537 538 // day of month field 539 err = expr.domFieldHandler(cron[indices[field][0]:indices[field][1]]) 540 if err != nil { 541 return nil, err 542 } 543 field += 1 544 545 // month field 546 err = expr.monthFieldHandler(cron[indices[field][0]:indices[field][1]]) 547 if err != nil { 548 return nil, err 549 } 550 field += 1 551 552 // day of week field 553 err = expr.dowFieldHandler(cron[indices[field][0]:indices[field][1]]) 554 if err != nil { 555 return nil, err 556 } 557 field += 1 558 559 // year field 560 if field < fieldCount { 561 err = expr.yearFieldHandler(cron[indices[field][0]:indices[field][1]]) 562 if err != nil { 563 return nil, err 564 } 565 } else { 566 expr.yearList = yearDescriptor.defaultList 567 } 568 569 return &expr, nil 570 } 571 572 // Next returns the closest time instant immediately following `fromTime` which matches the cron expression `expr` 573 func (expr *Expression) Next(fromTime time.Time) time.Time { 574 // Special case 575 if fromTime.IsZero() { 576 return fromTime 577 } 578 579 // year 580 v := fromTime.Year() 581 i := sort.SearchInts(expr.yearList, v) 582 if i == len(expr.yearList) { 583 return time.Time{} 584 } 585 if v != expr.yearList[i] { 586 return expr.nextYear(fromTime) 587 } 588 // month 589 v = int(fromTime.Month()) 590 i = sort.SearchInts(expr.monthList, v) 591 if i == len(expr.monthList) { 592 return expr.nextYear(fromTime) 593 } 594 if v != expr.monthList[i] { 595 return expr.nextMonth(fromTime) 596 } 597 598 expr.actualDaysOfMonthList = expr.calculateActualDaysOfMonth(fromTime.Year(), int(fromTime.Month())) 599 if len(expr.actualDaysOfMonthList) == 0 { 600 return expr.nextMonth(fromTime) 601 } 602 603 // day of month 604 v = fromTime.Day() 605 i = sort.SearchInts(expr.actualDaysOfMonthList, v) 606 if i == len(expr.actualDaysOfMonthList) { 607 return expr.nextMonth(fromTime) 608 } 609 if v != expr.actualDaysOfMonthList[i] { 610 return expr.nextDayOfMonth(fromTime) 611 } 612 // hour 613 v = fromTime.Hour() 614 i = sort.SearchInts(expr.hourList, v) 615 if i == len(expr.hourList) { 616 return expr.nextDayOfMonth(fromTime) 617 } 618 if v != expr.hourList[i] { 619 return expr.nextHour(fromTime) 620 } 621 // minute 622 v = fromTime.Minute() 623 i = sort.SearchInts(expr.minuteList, v) 624 if i == len(expr.minuteList) { 625 return expr.nextHour(fromTime) 626 } 627 if v != expr.minuteList[i] { 628 return expr.nextMinute(fromTime) 629 } 630 // second 631 v = fromTime.Second() 632 i = sort.SearchInts(expr.secondList, v) 633 if i == len(expr.secondList) { 634 return expr.nextMinute(fromTime) 635 } 636 637 // If we reach this point, there is nothing better to do 638 // than to move to the next second 639 640 return expr.nextSecond(fromTime) 641 } 642 643 // NextN returns a slice of `n` closest time instants immediately following `fromTime` which match the cron expression `expr` 644 func (expr *Expression) NextN(fromTime time.Time, n uint) []time.Time { 645 nextTimes := make([]time.Time, 0, n) 646 if n > 0 { 647 fromTime = expr.Next(fromTime) 648 for { 649 if fromTime.IsZero() { 650 break 651 } 652 nextTimes = append(nextTimes, fromTime) 653 n -= 1 654 if n == 0 { 655 break 656 } 657 fromTime = expr.nextSecond(fromTime) 658 } 659 } 660 return nextTimes 661 } 662 663 func (expr *Expression) nextYear(t time.Time) time.Time { 664 // Find index at which item in list is greater or equal to 665 // candidate year 666 i := sort.SearchInts(expr.yearList, t.Year()+1) 667 if i == len(expr.yearList) { 668 return time.Time{} 669 } 670 // Year changed, need to recalculate actual days of month 671 expr.actualDaysOfMonthList = expr.calculateActualDaysOfMonth(expr.yearList[i], expr.monthList[0]) 672 if len(expr.actualDaysOfMonthList) == 0 { 673 return expr.nextMonth(time.Date( 674 expr.yearList[i], 675 time.Month(expr.monthList[0]), 676 1, 677 expr.hourList[0], 678 expr.minuteList[0], 679 expr.secondList[0], 680 0, 681 t.Location())) 682 } 683 return time.Date( 684 expr.yearList[i], 685 time.Month(expr.monthList[0]), 686 expr.actualDaysOfMonthList[0], 687 expr.hourList[0], 688 expr.minuteList[0], 689 expr.secondList[0], 690 0, 691 t.Location()) 692 } 693 694 func (expr *Expression) nextMonth(t time.Time) time.Time { 695 // Find index at which item in list is greater or equal to 696 // candidate month 697 i := sort.SearchInts(expr.monthList, int(t.Month())+1) 698 if i == len(expr.monthList) { 699 return expr.nextYear(t) 700 } 701 // Month changed, need to recalculate actual days of month 702 expr.actualDaysOfMonthList = expr.calculateActualDaysOfMonth(t.Year(), expr.monthList[i]) 703 if len(expr.actualDaysOfMonthList) == 0 { 704 return expr.nextMonth(time.Date( 705 t.Year(), 706 time.Month(expr.monthList[i]), 707 1, 708 expr.hourList[0], 709 expr.minuteList[0], 710 expr.secondList[0], 711 0, 712 t.Location())) 713 } 714 715 return time.Date( 716 t.Year(), 717 time.Month(expr.monthList[i]), 718 expr.actualDaysOfMonthList[0], 719 expr.hourList[0], 720 expr.minuteList[0], 721 expr.secondList[0], 722 0, 723 t.Location()) 724 } 725 726 func (expr *Expression) nextDayOfMonth(t time.Time) time.Time { 727 // Find index at which item in list is greater or equal to 728 // candidate day of month 729 i := sort.SearchInts(expr.actualDaysOfMonthList, t.Day()+1) 730 if i == len(expr.actualDaysOfMonthList) { 731 return expr.nextMonth(t) 732 } 733 734 return time.Date( 735 t.Year(), 736 t.Month(), 737 expr.actualDaysOfMonthList[i], 738 expr.hourList[0], 739 expr.minuteList[0], 740 expr.secondList[0], 741 0, 742 t.Location()) 743 } 744 745 func (expr *Expression) nextHour(t time.Time) time.Time { 746 // Find index at which item in list is greater or equal to 747 // candidate hour 748 i := sort.SearchInts(expr.hourList, t.Hour()+1) 749 if i == len(expr.hourList) { 750 return expr.nextDayOfMonth(t) 751 } 752 753 return time.Date( 754 t.Year(), 755 t.Month(), 756 t.Day(), 757 expr.hourList[i], 758 expr.minuteList[0], 759 expr.secondList[0], 760 0, 761 t.Location()) 762 } 763 764 func (expr *Expression) nextMinute(t time.Time) time.Time { 765 // Find index at which item in list is greater or equal to 766 // candidate minute 767 i := sort.SearchInts(expr.minuteList, t.Minute()+1) 768 if i == len(expr.minuteList) { 769 return expr.nextHour(t) 770 } 771 772 return time.Date( 773 t.Year(), 774 t.Month(), 775 t.Day(), 776 t.Hour(), 777 expr.minuteList[i], 778 expr.secondList[0], 779 0, 780 t.Location()) 781 } 782 783 func (expr *Expression) nextSecond(t time.Time) time.Time { 784 // nextSecond() assumes all other fields are exactly matched 785 // to the cron expression 786 787 // Find index at which item in list is greater or equal to 788 // candidate second 789 i := sort.SearchInts(expr.secondList, t.Second()+1) 790 if i == len(expr.secondList) { 791 return expr.nextMinute(t) 792 } 793 794 return time.Date( 795 t.Year(), 796 t.Month(), 797 t.Day(), 798 t.Hour(), 799 t.Minute(), 800 expr.secondList[i], 801 0, 802 t.Location()) 803 } 804 805 func (expr *Expression) calculateActualDaysOfMonth(year, month int) []int { 806 actualDaysOfMonthMap := make(map[int]bool) 807 firstDayOfMonth := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC) 808 lastDayOfMonth := firstDayOfMonth.AddDate(0, 1, -1) 809 810 // As per crontab man page (http://linux.die.net/man/5/crontab#): 811 // "The day of a command's execution can be specified by two 812 // "fields - day of month, and day of week. If both fields are 813 // "restricted (ie, aren't *), the command will be run when 814 // "either field matches the current time" 815 816 // If both fields are not restricted, all days of the month are a hit 817 if !expr.daysOfMonthRestricted && !expr.daysOfWeekRestricted { 818 return genericDefaultList[1 : lastDayOfMonth.Day()+1] 819 } 820 821 // day-of-month != `*` 822 if expr.daysOfMonthRestricted { 823 // Last day of month 824 if expr.lastDayOfMonth { 825 actualDaysOfMonthMap[lastDayOfMonth.Day()] = true 826 } 827 // Last work day of month 828 if expr.lastWorkdayOfMonth { 829 actualDaysOfMonthMap[workdayOfMonth(lastDayOfMonth, lastDayOfMonth)] = true 830 } 831 // Days of month 832 for v := range expr.daysOfMonth { 833 // Ignore days beyond end of month 834 if v <= lastDayOfMonth.Day() { 835 actualDaysOfMonthMap[v] = true 836 } 837 } 838 // Work days of month 839 // As per Wikipedia: month boundaries are not crossed. 840 for v := range expr.workdaysOfMonth { 841 // Ignore days beyond end of month 842 if v <= lastDayOfMonth.Day() { 843 actualDaysOfMonthMap[workdayOfMonth(firstDayOfMonth.AddDate(0, 0, v-1), lastDayOfMonth)] = true 844 } 845 } 846 } 847 848 // day-of-week != `*` 849 if expr.daysOfWeekRestricted { 850 // How far first sunday is from first day of month 851 offset := 7 - int(firstDayOfMonth.Weekday()) 852 // days of week 853 // offset : (7 - day_of_week_of_1st_day_of_month) 854 // target : 1 + (7 * week_of_month) + (offset + day_of_week) % 7 855 for v := range expr.daysOfWeek { 856 w := dowNormalizedOffsets[(offset+v)%7] 857 actualDaysOfMonthMap[w[0]] = true 858 actualDaysOfMonthMap[w[1]] = true 859 actualDaysOfMonthMap[w[2]] = true 860 actualDaysOfMonthMap[w[3]] = true 861 if len(w) > 4 && w[4] <= lastDayOfMonth.Day() { 862 actualDaysOfMonthMap[w[4]] = true 863 } 864 } 865 // days of week of specific week in the month 866 // offset : (7 - day_of_week_of_1st_day_of_month) 867 // target : 1 + (7 * week_of_month) + (offset + day_of_week) % 7 868 for v := range expr.specificWeekDaysOfWeek { 869 v = 1 + 7*(v/7) + (offset+v)%7 870 if v <= lastDayOfMonth.Day() { 871 actualDaysOfMonthMap[v] = true 872 } 873 } 874 // Last days of week of the month 875 lastWeekOrigin := firstDayOfMonth.AddDate(0, 1, -7) 876 offset = 7 - int(lastWeekOrigin.Weekday()) 877 for v := range expr.lastWeekDaysOfWeek { 878 v = lastWeekOrigin.Day() + (offset+v)%7 879 if v <= lastDayOfMonth.Day() { 880 actualDaysOfMonthMap[v] = true 881 } 882 } 883 } 884 885 return toList(actualDaysOfMonthMap) 886 } 887 888 func workdayOfMonth(targetDom, lastDom time.Time) int { 889 // If saturday, then friday 890 // If sunday, then monday 891 dom := targetDom.Day() 892 dow := targetDom.Weekday() 893 if dow == time.Saturday { 894 if dom > 1 { 895 dom -= 1 896 } else { 897 dom += 2 898 } 899 } else if dow == time.Sunday { 900 if dom < lastDom.Day() { 901 dom += 1 902 } else { 903 dom -= 2 904 } 905 } 906 return dom 907 }