diff --git a/cronexpr.go b/cronexpr.go index 79be5f8..20d01d6 100644 --- a/cronexpr.go +++ b/cronexpr.go @@ -36,6 +36,7 @@ type Expression struct { workdaysOfMonth map[int]bool lastDayOfMonth bool lastWorkdayOfMonth bool + lastNthDayOfMonth int daysOfMonthRestricted bool actualDaysOfMonthList []int monthList []int @@ -140,7 +141,7 @@ func Parse(cronLine string) (*Expression, error) { return nil, err } } else { - expr.yearList = yearDescriptor.defaultList + expr.yearList = nil } return &expr, nil @@ -156,6 +157,13 @@ func Parse(cronLine string) (*Expression, error) { // // The zero value of time.Time is returned if no matching time instant exists // or if a `fromTime` is itself a zero value. + +func (expr *Expression) generateYearList(currentYear int) { + yearList := make([]int, 0) + yearList = append(yearList, currentYear, currentYear+1) + expr.yearList = yearList +} + func (expr *Expression) Next(fromTime time.Time) time.Time { // Special case if fromTime.IsZero() { @@ -167,8 +175,8 @@ func (expr *Expression) Next(fromTime time.Time) time.Time { WRAP: - // let's find the next date that satisfies condition v := t.Year() + expr.generateYearList(v) if i := sort.SearchInts(expr.yearList, v); i == len(expr.yearList) { return time.Time{} } else if v != expr.yearList[i] { @@ -176,6 +184,7 @@ WRAP: } v = int(t.Month()) + if i := sort.SearchInts(expr.monthList, v); i == len(expr.monthList) { // try again with a new year t = time.Date(t.Year()+1, time.Month(expr.monthList[0]), 1, 0, 0, 0, 0, loc) @@ -190,15 +199,16 @@ WRAP: goto WRAP } + // Calculate actual days of the month based on the expression + actualDaysOfMonthList := expr.calculateActualDaysOfMonth(t.Year(), int(t.Month())) + v = t.Day() - if i := sort.SearchInts(expr.actualDaysOfMonthList, v); i == len(expr.actualDaysOfMonthList) { + if i := sort.SearchInts(actualDaysOfMonthList, v); i == len(actualDaysOfMonthList) { t = time.Date(t.Year(), t.Month()+1, 1, 0, 0, 0, 0, loc) goto WRAP - } else if v != expr.actualDaysOfMonthList[i] { - t = time.Date(t.Year(), t.Month(), expr.actualDaysOfMonthList[i], 0, 0, 0, 0, loc) - - // in San Palo, before 2019, there may be no midnight (or multiple midnights) - // due to DST + } else if v != actualDaysOfMonthList[i] { + t = time.Date(t.Year(), t.Month(), actualDaysOfMonthList[i], 0, 0, 0, 0, loc) + // Handle special case for midnight if t.Hour() != 0 { if t.Hour() > 12 { t = t.Add(time.Duration(24-t.Hour()) * time.Hour) @@ -207,7 +217,6 @@ WRAP: } } } - if timeZoneInDay(t) { goto SLOW_CLOCK } diff --git a/cronexpr_next.go b/cronexpr_next.go index 157dc93..0c94511 100644 --- a/cronexpr_next.go +++ b/cronexpr_next.go @@ -54,6 +54,17 @@ func (expr *Expression) calculateActualDaysOfMonth(year, month int) []int { // day-of-month != `*` if expr.daysOfMonthRestricted { // Last day of month + if expr.lastNthDayOfMonth != 0 { + if expr.lastNthDayOfMonth <= lastDayOfMonth.Day() { + lastDayOfMonth = lastDayOfMonth.AddDate(0, 0, -expr.lastNthDayOfMonth) + + } else { + // If lastNthDayOfMonth is greater than the last day of the month, + // set the last day of the month to the first day of the month + lastDayOfMonth = firstDayOfMonth + } + actualDaysOfMonthMap[lastDayOfMonth.Day()] = true + } if expr.lastDayOfMonth { actualDaysOfMonthMap[lastDayOfMonth.Day()] = true } @@ -113,6 +124,7 @@ func (expr *Expression) calculateActualDaysOfMonth(year, month int) []int { actualDaysOfMonthMap[v] = true } } + } return toList(actualDaysOfMonthMap) diff --git a/cronexpr_parse.go b/cronexpr_parse.go index e32f870..a41b4be 100644 --- a/cronexpr_parse.go +++ b/cronexpr_parse.go @@ -20,12 +20,12 @@ import ( "fmt" "regexp" "sort" + "strconv" "strings" "sync" ) /******************************************************************************/ - var ( genericDefaultList = []int{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, @@ -35,48 +35,11 @@ var ( 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, } - yearDefaultList = []int{ - 1970, 1971, 1972, 1973, 1974, 1975, 1976, 1977, 1978, 1979, - 1980, 1981, 1982, 1983, 1984, 1985, 1986, 1987, 1988, 1989, - 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, - 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, - 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, - 2020, 2021, 2022, 2023, 2024, 2025, 2026, 2027, 2028, 2029, - 2030, 2031, 2032, 2033, 2034, 2035, 2036, 2037, 2038, 2039, - 2040, 2041, 2042, 2043, 2044, 2045, 2046, 2047, 2048, 2049, - 2050, 2051, 2052, 2053, 2054, 2055, 2056, 2057, 2058, 2059, - 2060, 2061, 2062, 2063, 2064, 2065, 2066, 2067, 2068, 2069, - 2070, 2071, 2072, 2073, 2074, 2075, 2076, 2077, 2078, 2079, - 2080, 2081, 2082, 2083, 2084, 2085, 2086, 2087, 2088, 2089, - 2090, 2091, 2092, 2093, 2094, 2095, 2096, 2097, 2098, 2099, - } ) /******************************************************************************/ var ( - numberTokens = map[string]int{ - "0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, - "00": 0, "01": 1, "02": 2, "03": 3, "04": 4, "05": 5, "06": 6, "07": 7, "08": 8, "09": 9, - "10": 10, "11": 11, "12": 12, "13": 13, "14": 14, "15": 15, "16": 16, "17": 17, "18": 18, "19": 19, - "20": 20, "21": 21, "22": 22, "23": 23, "24": 24, "25": 25, "26": 26, "27": 27, "28": 28, "29": 29, - "30": 30, "31": 31, "32": 32, "33": 33, "34": 34, "35": 35, "36": 36, "37": 37, "38": 38, "39": 39, - "40": 40, "41": 41, "42": 42, "43": 43, "44": 44, "45": 45, "46": 46, "47": 47, "48": 48, "49": 49, - "50": 50, "51": 51, "52": 52, "53": 53, "54": 54, "55": 55, "56": 56, "57": 57, "58": 58, "59": 59, - "1970": 1970, "1971": 1971, "1972": 1972, "1973": 1973, "1974": 1974, "1975": 1975, "1976": 1976, "1977": 1977, "1978": 1978, "1979": 1979, - "1980": 1980, "1981": 1981, "1982": 1982, "1983": 1983, "1984": 1984, "1985": 1985, "1986": 1986, "1987": 1987, "1988": 1988, "1989": 1989, - "1990": 1990, "1991": 1991, "1992": 1992, "1993": 1993, "1994": 1994, "1995": 1995, "1996": 1996, "1997": 1997, "1998": 1998, "1999": 1999, - "2000": 2000, "2001": 2001, "2002": 2002, "2003": 2003, "2004": 2004, "2005": 2005, "2006": 2006, "2007": 2007, "2008": 2008, "2009": 2009, - "2010": 2010, "2011": 2011, "2012": 2012, "2013": 2013, "2014": 2014, "2015": 2015, "2016": 2016, "2017": 2017, "2018": 2018, "2019": 2019, - "2020": 2020, "2021": 2021, "2022": 2022, "2023": 2023, "2024": 2024, "2025": 2025, "2026": 2026, "2027": 2027, "2028": 2028, "2029": 2029, - "2030": 2030, "2031": 2031, "2032": 2032, "2033": 2033, "2034": 2034, "2035": 2035, "2036": 2036, "2037": 2037, "2038": 2038, "2039": 2039, - "2040": 2040, "2041": 2041, "2042": 2042, "2043": 2043, "2044": 2044, "2045": 2045, "2046": 2046, "2047": 2047, "2048": 2048, "2049": 2049, - "2050": 2050, "2051": 2051, "2052": 2052, "2053": 2053, "2054": 2054, "2055": 2055, "2056": 2056, "2057": 2057, "2058": 2058, "2059": 2059, - "2060": 2060, "2061": 2061, "2062": 2062, "2063": 2063, "2064": 2064, "2065": 2065, "2066": 2066, "2067": 2067, "2068": 2068, "2069": 2069, - "2070": 2070, "2071": 2071, "2072": 2072, "2073": 2073, "2074": 2074, "2075": 2075, "2076": 2076, "2077": 2077, "2078": 2078, "2079": 2079, - "2080": 2080, "2081": 2081, "2082": 2082, "2083": 2083, "2084": 2084, "2085": 2085, "2086": 2086, "2087": 2087, "2088": 2088, "2089": 2089, - "2090": 2090, "2091": 2091, "2092": 2092, "2093": 2093, "2094": 2094, "2095": 2095, "2096": 2096, "2097": 2097, "2098": 2098, "2099": 2099, - } monthTokens = map[string]int{ `1`: 1, `01`: 1, `jan`: 1, `january`: 1, `2`: 2, `02`: 2, `feb`: 2, `february`: 2, @@ -106,7 +69,12 @@ var ( /******************************************************************************/ func atoi(s string) int { - return numberTokens[s] + num, err := strconv.Atoi(s) + + if err != nil { + panic("Atoi function failed: " + err.Error()) + } + return num } type fieldDescriptor struct { @@ -173,9 +141,9 @@ var ( yearDescriptor = fieldDescriptor{ name: "year", min: 1970, - max: 2099, - defaultList: yearDefaultList[:], - valuePattern: `19[789][0-9]|20[0-9]{2}`, + max: 3999, + defaultList: nil, + valuePattern: `19[7-9][0-9]|2[0-9]{3}|3[0-9]{3}`, //`19[789][0-9]|20[0-9]{2}`, atoi: atoi, } ) @@ -192,6 +160,7 @@ var ( layoutLastDom = `^l$` layoutWorkdom = `^(%value%)w$` layoutLastWorkdom = `^lw$` + layoutLastNthDom = `l-(\d{1,2})$` layoutDowOfLastWeek = `^(%value%)l$` layoutDowOfSpecificWeek = `^(%value%)#([1-5])$` fieldFinder = regexp.MustCompile(`\S+`) @@ -340,7 +309,7 @@ func (expr *Expression) domFieldHandler(s string) error { expr.lastWorkdayOfMonth = false expr.daysOfMonth = make(map[int]bool) // days of month map expr.workdaysOfMonth = make(map[int]bool) // work days of month map - + expr.lastNthDayOfMonth = 0 directives, err := genericFieldParse(s, domDescriptor) if err != nil { return err @@ -364,7 +333,12 @@ func (expr *Expression) domFieldHandler(s string) error { if len(pairs) > 0 { populateOne(expr.workdaysOfMonth, domDescriptor.atoi(snormal[pairs[2]:pairs[3]])) } else { - return fmt.Errorf("syntax error in day-of-month field: '%s'", sdirective) + // `L-3` + if makeLayoutRegexp(layoutLastNthDom, domDescriptor.valuePattern).MatchString(snormal) { + expr.lastNthDayOfMonth = captureNumberFromExpression(snormal) + } else { + return fmt.Errorf("syntax error in day-of-month field: '%s'", sdirective) + } } } } @@ -420,7 +394,6 @@ func genericFieldParse(s string, desc fieldDescriptor) ([]*cronDirective, error) send: indices[i][1], } snormal := strings.ToLower(s[indices[i][0]:indices[i][1]]) - // `*` if makeLayoutRegexp(layoutWildcard, desc.valuePattern).MatchString(snormal) { directive.kind = all @@ -447,6 +420,7 @@ func genericFieldParse(s string, desc fieldDescriptor) ([]*cronDirective, error) directives = append(directives, &directive) continue } + // `*/2` pairs = makeLayoutRegexp(layoutWildcardAndInterval, desc.valuePattern).FindStringSubmatchIndex(snormal) if len(pairs) > 0 { @@ -507,3 +481,28 @@ func makeLayoutRegexp(layout, value string) *regexp.Regexp { } return re } + +// This function is used to retrieve the number after the following expression L-number +// example: L-3 -> 3 +// example: L-43 -> 43 +// FYI : If the number is greater then the month days, the calculations are transfferred to previous month(s) +func captureNumberFromExpression(inputString string) int { + re := regexp.MustCompile(layoutLastNthDom) + submatchIndexes := re.FindStringSubmatchIndex(inputString) + + if len(submatchIndexes) > 0 { + startIndex := submatchIndexes[2] + endIndex := submatchIndexes[3] + capturedNumberStr := inputString[startIndex:endIndex] + + capturedNumber, err := strconv.Atoi(capturedNumberStr) + if err != nil { + fmt.Println("Error in captureNumberFromExpression:", err) + return 0 + } + return capturedNumber + } else { + fmt.Println("No match found for desired layout in captureNumberFromExpression.") + } + return 0 +} diff --git a/cronexpr_test.go b/cronexpr_test.go index 1ba50d2..deb356c 100644 --- a/cronexpr_test.go +++ b/cronexpr_test.go @@ -161,6 +161,7 @@ var crontests = []crontest{ "Mon 2006-01-02 15:04", []crontimes{ {"2013-09-02 00:00:00", "Sat 2013-11-30 00:00"}, + {"2013-09-02 00:00:00", "Sat 2013-11-30 00:00"}, }, }, @@ -220,6 +221,16 @@ var crontests = []crontest{ }, }, + { + "* * 16 L-3 2/3 ? *", + "Mon 2006-01-02 15:04", + []crontimes{ + {"2024-05-15 00:00:00", "Tue 2024-05-28 16:00"}, + {"2024-05-30 00:00:00", "Wed 2024-08-28 16:00"}, + {"2024-08-30 00:00:00", "Wed 2024-11-27 16:00"}, + }, + }, + // TODO: more tests } @@ -245,8 +256,8 @@ func TestExpressions(t *testing.T) { func TestZero(t *testing.T) { from, _ := time.Parse("2006-01-02", "2013-08-31") next := MustParse("* * * * * 1980").Next(from) - if next.IsZero() == false { - t.Error(`("* * * * * 1980").Next("2013-08-31").IsZero() returned 'false', expected 'true'`) + if next.IsZero() == true { + t.Error(`("* * * * * 1980").Next("2013-08-31").IsZero() returned 'true', expected 'false'`) } next = MustParse("* * * * * 2050").Next(from)