1/*! 2 * Copyright 2013 Raymond Hill 3 * 4 * Project: github.com/gorhill/cronexpr 5 * File: cronexpr_parse.go 6 * Version: 1.0 7 * License: pick the one which suits you best: 8 * GPL v3 see <https://www.gnu.org/licenses/gpl.html> 9 * APL v2 see <http://www.apache.org/licenses/LICENSE-2.0> 10 * 11 */ 12 13package cronexpr 14 15/******************************************************************************/ 16 17import ( 18 "fmt" 19 "regexp" 20 "sort" 21 "strings" 22 "sync" 23) 24 25/******************************************************************************/ 26 27var ( 28 genericDefaultList = []int{ 29 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 30 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 31 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 32 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 33 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 34 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 35 } 36 yearDefaultList = []int{ 37 1970, 1971, 1972, 1973, 1974, 1975, 1976, 1977, 1978, 1979, 38 1980, 1981, 1982, 1983, 1984, 1985, 1986, 1987, 1988, 1989, 39 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 40 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 41 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 42 2020, 2021, 2022, 2023, 2024, 2025, 2026, 2027, 2028, 2029, 43 2030, 2031, 2032, 2033, 2034, 2035, 2036, 2037, 2038, 2039, 44 2040, 2041, 2042, 2043, 2044, 2045, 2046, 2047, 2048, 2049, 45 2050, 2051, 2052, 2053, 2054, 2055, 2056, 2057, 2058, 2059, 46 2060, 2061, 2062, 2063, 2064, 2065, 2066, 2067, 2068, 2069, 47 2070, 2071, 2072, 2073, 2074, 2075, 2076, 2077, 2078, 2079, 48 2080, 2081, 2082, 2083, 2084, 2085, 2086, 2087, 2088, 2089, 49 2090, 2091, 2092, 2093, 2094, 2095, 2096, 2097, 2098, 2099, 50 } 51) 52 53/******************************************************************************/ 54 55var ( 56 numberTokens = map[string]int{ 57 "0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, 58 "00": 0, "01": 1, "02": 2, "03": 3, "04": 4, "05": 5, "06": 6, "07": 7, "08": 8, "09": 9, 59 "10": 10, "11": 11, "12": 12, "13": 13, "14": 14, "15": 15, "16": 16, "17": 17, "18": 18, "19": 19, 60 "20": 20, "21": 21, "22": 22, "23": 23, "24": 24, "25": 25, "26": 26, "27": 27, "28": 28, "29": 29, 61 "30": 30, "31": 31, "32": 32, "33": 33, "34": 34, "35": 35, "36": 36, "37": 37, "38": 38, "39": 39, 62 "40": 40, "41": 41, "42": 42, "43": 43, "44": 44, "45": 45, "46": 46, "47": 47, "48": 48, "49": 49, 63 "50": 50, "51": 51, "52": 52, "53": 53, "54": 54, "55": 55, "56": 56, "57": 57, "58": 58, "59": 59, 64 "1970": 1970, "1971": 1971, "1972": 1972, "1973": 1973, "1974": 1974, "1975": 1975, "1976": 1976, "1977": 1977, "1978": 1978, "1979": 1979, 65 "1980": 1980, "1981": 1981, "1982": 1982, "1983": 1983, "1984": 1984, "1985": 1985, "1986": 1986, "1987": 1987, "1988": 1988, "1989": 1989, 66 "1990": 1990, "1991": 1991, "1992": 1992, "1993": 1993, "1994": 1994, "1995": 1995, "1996": 1996, "1997": 1997, "1998": 1998, "1999": 1999, 67 "2000": 2000, "2001": 2001, "2002": 2002, "2003": 2003, "2004": 2004, "2005": 2005, "2006": 2006, "2007": 2007, "2008": 2008, "2009": 2009, 68 "2010": 2010, "2011": 2011, "2012": 2012, "2013": 2013, "2014": 2014, "2015": 2015, "2016": 2016, "2017": 2017, "2018": 2018, "2019": 2019, 69 "2020": 2020, "2021": 2021, "2022": 2022, "2023": 2023, "2024": 2024, "2025": 2025, "2026": 2026, "2027": 2027, "2028": 2028, "2029": 2029, 70 "2030": 2030, "2031": 2031, "2032": 2032, "2033": 2033, "2034": 2034, "2035": 2035, "2036": 2036, "2037": 2037, "2038": 2038, "2039": 2039, 71 "2040": 2040, "2041": 2041, "2042": 2042, "2043": 2043, "2044": 2044, "2045": 2045, "2046": 2046, "2047": 2047, "2048": 2048, "2049": 2049, 72 "2050": 2050, "2051": 2051, "2052": 2052, "2053": 2053, "2054": 2054, "2055": 2055, "2056": 2056, "2057": 2057, "2058": 2058, "2059": 2059, 73 "2060": 2060, "2061": 2061, "2062": 2062, "2063": 2063, "2064": 2064, "2065": 2065, "2066": 2066, "2067": 2067, "2068": 2068, "2069": 2069, 74 "2070": 2070, "2071": 2071, "2072": 2072, "2073": 2073, "2074": 2074, "2075": 2075, "2076": 2076, "2077": 2077, "2078": 2078, "2079": 2079, 75 "2080": 2080, "2081": 2081, "2082": 2082, "2083": 2083, "2084": 2084, "2085": 2085, "2086": 2086, "2087": 2087, "2088": 2088, "2089": 2089, 76 "2090": 2090, "2091": 2091, "2092": 2092, "2093": 2093, "2094": 2094, "2095": 2095, "2096": 2096, "2097": 2097, "2098": 2098, "2099": 2099, 77 } 78 monthTokens = map[string]int{ 79 `1`: 1, `jan`: 1, `january`: 1, 80 `2`: 2, `feb`: 2, `february`: 2, 81 `3`: 3, `mar`: 3, `march`: 3, 82 `4`: 4, `apr`: 4, `april`: 4, 83 `5`: 5, `may`: 5, 84 `6`: 6, `jun`: 6, `june`: 6, 85 `7`: 7, `jul`: 7, `july`: 7, 86 `8`: 8, `aug`: 8, `august`: 8, 87 `9`: 9, `sep`: 9, `september`: 9, 88 `10`: 10, `oct`: 10, `october`: 10, 89 `11`: 11, `nov`: 11, `november`: 11, 90 `12`: 12, `dec`: 12, `december`: 12, 91 } 92 dowTokens = map[string]int{ 93 `0`: 0, `sun`: 0, `sunday`: 0, 94 `1`: 1, `mon`: 1, `monday`: 1, 95 `2`: 2, `tue`: 2, `tuesday`: 2, 96 `3`: 3, `wed`: 3, `wednesday`: 3, 97 `4`: 4, `thu`: 4, `thursday`: 4, 98 `5`: 5, `fri`: 5, `friday`: 5, 99 `6`: 6, `sat`: 6, `saturday`: 6, 100 `7`: 0, 101 } 102) 103 104/******************************************************************************/ 105 106func atoi(s string) int { 107 return numberTokens[s] 108} 109 110type fieldDescriptor struct { 111 name string 112 min, max int 113 defaultList []int 114 valuePattern string 115 atoi func(string) int 116} 117 118var ( 119 secondDescriptor = fieldDescriptor{ 120 name: "second", 121 min: 0, 122 max: 59, 123 defaultList: genericDefaultList[0:60], 124 valuePattern: `0?[0-9]|[1-5][0-9]`, 125 atoi: atoi, 126 } 127 minuteDescriptor = fieldDescriptor{ 128 name: "minute", 129 min: 0, 130 max: 59, 131 defaultList: genericDefaultList[0:60], 132 valuePattern: `0?[0-9]|[1-5][0-9]`, 133 atoi: atoi, 134 } 135 hourDescriptor = fieldDescriptor{ 136 name: "hour", 137 min: 0, 138 max: 23, 139 defaultList: genericDefaultList[0:24], 140 valuePattern: `0?[0-9]|1[0-9]|2[0-3]`, 141 atoi: atoi, 142 } 143 domDescriptor = fieldDescriptor{ 144 name: "day-of-month", 145 min: 1, 146 max: 31, 147 defaultList: genericDefaultList[1:32], 148 valuePattern: `0?[1-9]|[12][0-9]|3[01]`, 149 atoi: atoi, 150 } 151 monthDescriptor = fieldDescriptor{ 152 name: "month", 153 min: 1, 154 max: 12, 155 defaultList: genericDefaultList[1:13], 156 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`, 157 atoi: func(s string) int { 158 return monthTokens[s] 159 }, 160 } 161 dowDescriptor = fieldDescriptor{ 162 name: "day-of-week", 163 min: 0, 164 max: 6, 165 defaultList: genericDefaultList[0:7], 166 valuePattern: `0?[0-7]|sun|mon|tue|wed|thu|fri|sat|sunday|monday|tuesday|wednesday|thursday|friday|saturday`, 167 atoi: func(s string) int { 168 return dowTokens[s] 169 }, 170 } 171 yearDescriptor = fieldDescriptor{ 172 name: "year", 173 min: 1970, 174 max: 2099, 175 defaultList: yearDefaultList[:], 176 valuePattern: `19[789][0-9]|20[0-9]{2}`, 177 atoi: atoi, 178 } 179) 180 181/******************************************************************************/ 182 183var ( 184 layoutWildcard = `^\*$|^\?$` 185 layoutValue = `^(%value%)$` 186 layoutRange = `^(%value%)-(%value%)$` 187 layoutWildcardAndInterval = `^\*/(\d+)$` 188 layoutValueAndInterval = `^(%value%)/(\d+)$` 189 layoutRangeAndInterval = `^(%value%)-(%value%)/(\d+)$` 190 layoutLastDom = `^l$` 191 layoutWorkdom = `^(%value%)w$` 192 layoutLastWorkdom = `^lw$` 193 layoutDowOfLastWeek = `^(%value%)l$` 194 layoutDowOfSpecificWeek = `^(%value%)#([1-5])$` 195 fieldFinder = regexp.MustCompile(`\S+`) 196 entryFinder = regexp.MustCompile(`[^,]+`) 197 layoutRegexp = make(map[string]*regexp.Regexp) 198 layoutRegexpLock sync.Mutex 199) 200 201/******************************************************************************/ 202 203var cronNormalizer = strings.NewReplacer( 204 "@yearly", "0 0 0 1 1 * *", 205 "@annually", "0 0 0 1 1 * *", 206 "@monthly", "0 0 0 1 * * *", 207 "@weekly", "0 0 0 * * 0 *", 208 "@daily", "0 0 0 * * * *", 209 "@hourly", "0 0 * * * * *") 210 211/******************************************************************************/ 212 213func (expr *Expression) secondFieldHandler(s string) error { 214 var err error 215 expr.secondList, err = genericFieldHandler(s, secondDescriptor) 216 return err 217} 218 219/******************************************************************************/ 220 221func (expr *Expression) minuteFieldHandler(s string) error { 222 var err error 223 expr.minuteList, err = genericFieldHandler(s, minuteDescriptor) 224 return err 225} 226 227/******************************************************************************/ 228 229func (expr *Expression) hourFieldHandler(s string) error { 230 var err error 231 expr.hourList, err = genericFieldHandler(s, hourDescriptor) 232 return err 233} 234 235/******************************************************************************/ 236 237func (expr *Expression) monthFieldHandler(s string) error { 238 var err error 239 expr.monthList, err = genericFieldHandler(s, monthDescriptor) 240 return err 241} 242 243/******************************************************************************/ 244 245func (expr *Expression) yearFieldHandler(s string) error { 246 var err error 247 expr.yearList, err = genericFieldHandler(s, yearDescriptor) 248 return err 249} 250 251/******************************************************************************/ 252 253const ( 254 none = 0 255 one = 1 256 span = 2 257 all = 3 258) 259 260type cronDirective struct { 261 kind int 262 first int 263 last int 264 step int 265 sbeg int 266 send int 267} 268 269func genericFieldHandler(s string, desc fieldDescriptor) ([]int, error) { 270 directives, err := genericFieldParse(s, desc) 271 if err != nil { 272 return nil, err 273 } 274 values := make(map[int]bool) 275 for _, directive := range directives { 276 switch directive.kind { 277 case none: 278 return nil, fmt.Errorf("syntax error in %s field: '%s'", desc.name, s[directive.sbeg:directive.send]) 279 case one: 280 populateOne(values, directive.first) 281 case span: 282 populateMany(values, directive.first, directive.last, directive.step) 283 case all: 284 return desc.defaultList, nil 285 } 286 } 287 return toList(values), nil 288} 289 290func (expr *Expression) dowFieldHandler(s string) error { 291 expr.daysOfWeekRestricted = true 292 expr.daysOfWeek = make(map[int]bool) 293 expr.lastWeekDaysOfWeek = make(map[int]bool) 294 expr.specificWeekDaysOfWeek = make(map[int]bool) 295 296 directives, err := genericFieldParse(s, dowDescriptor) 297 if err != nil { 298 return err 299 } 300 301 for _, directive := range directives { 302 switch directive.kind { 303 case none: 304 sdirective := s[directive.sbeg:directive.send] 305 snormal := strings.ToLower(sdirective) 306 // `5L` 307 pairs := makeLayoutRegexp(layoutDowOfLastWeek, dowDescriptor.valuePattern).FindStringSubmatchIndex(snormal) 308 if len(pairs) > 0 { 309 populateOne(expr.lastWeekDaysOfWeek, dowDescriptor.atoi(snormal[pairs[2]:pairs[3]])) 310 } else { 311 // `5#3` 312 pairs := makeLayoutRegexp(layoutDowOfSpecificWeek, dowDescriptor.valuePattern).FindStringSubmatchIndex(snormal) 313 if len(pairs) > 0 { 314 populateOne(expr.specificWeekDaysOfWeek, (dowDescriptor.atoi(snormal[pairs[4]:pairs[5]])-1)*7+(dowDescriptor.atoi(snormal[pairs[2]:pairs[3]])%7)) 315 } else { 316 return fmt.Errorf("syntax error in day-of-week field: '%s'", sdirective) 317 } 318 } 319 case one: 320 populateOne(expr.daysOfWeek, directive.first) 321 case span: 322 populateMany(expr.daysOfWeek, directive.first, directive.last, directive.step) 323 case all: 324 populateMany(expr.daysOfWeek, directive.first, directive.last, directive.step) 325 expr.daysOfWeekRestricted = false 326 } 327 } 328 return nil 329} 330 331func (expr *Expression) domFieldHandler(s string) error { 332 expr.daysOfMonthRestricted = true 333 expr.lastDayOfMonth = false 334 expr.lastWorkdayOfMonth = false 335 expr.daysOfMonth = make(map[int]bool) // days of month map 336 expr.workdaysOfMonth = make(map[int]bool) // work days of month map 337 338 directives, err := genericFieldParse(s, domDescriptor) 339 if err != nil { 340 return err 341 } 342 343 for _, directive := range directives { 344 switch directive.kind { 345 case none: 346 sdirective := s[directive.sbeg:directive.send] 347 snormal := strings.ToLower(sdirective) 348 // `L` 349 if makeLayoutRegexp(layoutLastDom, domDescriptor.valuePattern).MatchString(snormal) { 350 expr.lastDayOfMonth = true 351 } else { 352 // `LW` 353 if makeLayoutRegexp(layoutLastWorkdom, domDescriptor.valuePattern).MatchString(snormal) { 354 expr.lastWorkdayOfMonth = true 355 } else { 356 // `15W` 357 pairs := makeLayoutRegexp(layoutWorkdom, domDescriptor.valuePattern).FindStringSubmatchIndex(snormal) 358 if len(pairs) > 0 { 359 populateOne(expr.workdaysOfMonth, domDescriptor.atoi(snormal[pairs[2]:pairs[3]])) 360 } else { 361 return fmt.Errorf("syntax error in day-of-month field: '%s'", sdirective) 362 } 363 } 364 } 365 case one: 366 populateOne(expr.daysOfMonth, directive.first) 367 case span: 368 populateMany(expr.daysOfMonth, directive.first, directive.last, directive.step) 369 case all: 370 populateMany(expr.daysOfMonth, directive.first, directive.last, directive.step) 371 expr.daysOfMonthRestricted = false 372 } 373 } 374 return nil 375} 376 377/******************************************************************************/ 378 379func populateOne(values map[int]bool, v int) { 380 values[v] = true 381} 382 383func populateMany(values map[int]bool, min, max, step int) { 384 for i := min; i <= max; i += step { 385 values[i] = true 386 } 387} 388 389func toList(set map[int]bool) []int { 390 list := make([]int, len(set)) 391 i := 0 392 for k := range set { 393 list[i] = k 394 i += 1 395 } 396 sort.Ints(list) 397 return list 398} 399 400/******************************************************************************/ 401 402func genericFieldParse(s string, desc fieldDescriptor) ([]*cronDirective, error) { 403 // At least one entry must be present 404 indices := entryFinder.FindAllStringIndex(s, -1) 405 if len(indices) == 0 { 406 return nil, fmt.Errorf("%s field: missing directive", desc.name) 407 } 408 409 directives := make([]*cronDirective, 0, len(indices)) 410 411 for i := range indices { 412 directive := cronDirective{ 413 sbeg: indices[i][0], 414 send: indices[i][1], 415 } 416 snormal := strings.ToLower(s[indices[i][0]:indices[i][1]]) 417 418 // `*` 419 if makeLayoutRegexp(layoutWildcard, desc.valuePattern).MatchString(snormal) { 420 directive.kind = all 421 directive.first = desc.min 422 directive.last = desc.max 423 directive.step = 1 424 directives = append(directives, &directive) 425 continue 426 } 427 // `5` 428 if makeLayoutRegexp(layoutValue, desc.valuePattern).MatchString(snormal) { 429 directive.kind = one 430 directive.first = desc.atoi(snormal) 431 directives = append(directives, &directive) 432 continue 433 } 434 // `5-20` 435 pairs := makeLayoutRegexp(layoutRange, desc.valuePattern).FindStringSubmatchIndex(snormal) 436 if len(pairs) > 0 { 437 directive.kind = span 438 directive.first = desc.atoi(snormal[pairs[2]:pairs[3]]) 439 directive.last = desc.atoi(snormal[pairs[4]:pairs[5]]) 440 directive.step = 1 441 directives = append(directives, &directive) 442 continue 443 } 444 // `*/2` 445 pairs = makeLayoutRegexp(layoutWildcardAndInterval, desc.valuePattern).FindStringSubmatchIndex(snormal) 446 if len(pairs) > 0 { 447 directive.kind = span 448 directive.first = desc.min 449 directive.last = desc.max 450 directive.step = atoi(snormal[pairs[2]:pairs[3]]) 451 if directive.step < 1 || directive.step > desc.max { 452 return nil, fmt.Errorf("invalid interval %s", snormal) 453 } 454 directives = append(directives, &directive) 455 continue 456 } 457 // `5/2` 458 pairs = makeLayoutRegexp(layoutValueAndInterval, desc.valuePattern).FindStringSubmatchIndex(snormal) 459 if len(pairs) > 0 { 460 directive.kind = span 461 directive.first = desc.atoi(snormal[pairs[2]:pairs[3]]) 462 directive.last = desc.max 463 directive.step = atoi(snormal[pairs[4]:pairs[5]]) 464 if directive.step < 1 || directive.step > desc.max { 465 return nil, fmt.Errorf("invalid interval %s", snormal) 466 } 467 directives = append(directives, &directive) 468 continue 469 } 470 // `5-20/2` 471 pairs = makeLayoutRegexp(layoutRangeAndInterval, desc.valuePattern).FindStringSubmatchIndex(snormal) 472 if len(pairs) > 0 { 473 directive.kind = span 474 directive.first = desc.atoi(snormal[pairs[2]:pairs[3]]) 475 directive.last = desc.atoi(snormal[pairs[4]:pairs[5]]) 476 directive.step = atoi(snormal[pairs[6]:pairs[7]]) 477 if directive.step < 1 || directive.step > desc.max { 478 return nil, fmt.Errorf("invalid interval %s", snormal) 479 } 480 directives = append(directives, &directive) 481 continue 482 } 483 // No behavior for this one, let caller deal with it 484 directive.kind = none 485 directives = append(directives, &directive) 486 } 487 return directives, nil 488} 489 490/******************************************************************************/ 491 492func makeLayoutRegexp(layout, value string) *regexp.Regexp { 493 layoutRegexpLock.Lock() 494 defer layoutRegexpLock.Unlock() 495 496 layout = strings.Replace(layout, `%value%`, value, -1) 497 re := layoutRegexp[layout] 498 if re == nil { 499 re = regexp.MustCompile(layout) 500 layoutRegexp[layout] = re 501 } 502 return re 503} 504