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