Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions cronexpr.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type Expression struct {
workdaysOfMonth map[int]bool
lastDayOfMonth bool
lastWorkdayOfMonth bool
lastNthDayOfMonth int
daysOfMonthRestricted bool
actualDaysOfMonthList []int
monthList []int
Expand Down Expand Up @@ -140,7 +141,7 @@ func Parse(cronLine string) (*Expression, error) {
return nil, err
}
} else {
expr.yearList = yearDescriptor.defaultList
expr.yearList = nil
}

return &expr, nil
Expand All @@ -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() {
Expand All @@ -167,15 +175,16 @@ 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] {
t = time.Date(expr.yearList[i], time.Month(expr.monthList[0]), 1, 0, 0, 0, 0, loc)
}

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)
Expand All @@ -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)
Expand All @@ -207,7 +217,6 @@ WRAP:
}
}
}

if timeZoneInDay(t) {
goto SLOW_CLOCK
}
Expand Down
12 changes: 12 additions & 0 deletions cronexpr_next.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -113,6 +124,7 @@ func (expr *Expression) calculateActualDaysOfMonth(year, month int) []int {
actualDaysOfMonthMap[v] = true
}
}

}

return toList(actualDaysOfMonthMap)
Expand Down
89 changes: 44 additions & 45 deletions cronexpr_parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ import (
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"sync"
)

/******************************************************************************/

var (
genericDefaultList = []int{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
Expand All @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
}
)
Expand All @@ -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+`)
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
}
}
}
Expand Down Expand Up @@ -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
Expand 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 {
Expand Down Expand Up @@ -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
}
15 changes: 13 additions & 2 deletions cronexpr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
},
},

Expand Down Expand Up @@ -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
}

Expand All @@ -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)
Expand Down