diff --git a/cronexpression.go b/cronexpression.go index 4b1e012..8c14c56 100644 --- a/cronexpression.go +++ b/cronexpression.go @@ -13,11 +13,11 @@ package cronexpression /******************************************************************************/ import ( - "regexp" - "sort" - "strconv" - "strings" - "time" + "regexp" + "sort" + "strconv" + "strings" + "time" ) /******************************************************************************/ @@ -26,21 +26,21 @@ import ( // Wikipedia: https://en.wikipedia.org/wiki/Cron#CRON_expression // type CronExpression struct { - Expression string - secondList []int - minuteList []int - hourList []int - daysOfMonth map[int]bool - workdaysOfMonth map[int]bool - lastDayOfMonth bool - daysOfMonthRestricted bool - actualDaysOfMonthList []int - monthList []int - daysOfWeek map[int]bool - specificWeekDaysOfWeek map[int]bool - lastWeekDaysOfWeek map[int]bool - daysOfWeekRestricted bool - yearList []int + Expression string + secondList []int + minuteList []int + hourList []int + daysOfMonth map[int]bool + workdaysOfMonth map[int]bool + lastDayOfMonth bool + daysOfMonthRestricted bool + actualDaysOfMonthList []int + monthList []int + daysOfWeek map[int]bool + specificWeekDaysOfWeek map[int]bool + lastWeekDaysOfWeek map[int]bool + daysOfWeekRestricted bool + yearList []int } /******************************************************************************/ @@ -53,49 +53,49 @@ var noMatchTime = time.Date(2100, 7, 1, 0, 0, 0, 0, time.UTC) // a well-formed cron expression. If a malformed cron expression is // supplied, the result is undefined. func NewCronExpression(cronLine string) *CronExpression { - cronLineNormalized := cronNormalize(cronLine) + cronLineNormalized := cronNormalize(cronLine) - // Split into fields - cronFields := regexp.MustCompile(`\s+`).Split(cronLineNormalized, -1) + // Split into fields + cronFields := regexp.MustCompile(`\s+`).Split(cronLineNormalized, -1) - // Our cron expression parser expects 7 fields: - // second minute hour dayofmonth month dayofweek year - // Standard cron is 6 fields with year field being optional - // minute hour dayofmonth month dayofweek {year} - // Thus... - // If we have 5 fields, append wildcard year field - if len(cronFields) < 6 { - cronFields = append(cronFields, "*") - } - // If we have 6 fields, prepend match-once second field - if len(cronFields) < 7 { - cronFields = append(cronFields, "") - copy(cronFields[1:], cronFields[0:]) - cronFields[0] = "0" - } - // We should have 7 fields at this point - if len(cronFields) != 7 { - panic("Malformed cron expression\n") - } + // Our cron expression parser expects 7 fields: + // second minute hour dayofmonth month dayofweek year + // Standard cron is 6 fields with year field being optional + // minute hour dayofmonth month dayofweek {year} + // Thus... + // If we have 5 fields, append wildcard year field + if len(cronFields) < 6 { + cronFields = append(cronFields, "*") + } + // If we have 6 fields, prepend match-once second field + if len(cronFields) < 7 { + cronFields = append(cronFields, "") + copy(cronFields[1:], cronFields[0:]) + cronFields[0] = "0" + } + // We should have 7 fields at this point + if len(cronFields) != 7 { + panic("Malformed cron expression\n") + } - // Generic parser can be used for most fields - cronExpr := &CronExpression{ - Expression: cronLine, - secondList: genericFieldParse(cronFields[0], 0, 59), - minuteList: genericFieldParse(cronFields[1], 0, 59), - hourList: genericFieldParse(cronFields[2], 0, 23), - monthList: genericFieldParse(cronFields[4], 1, 12), - yearList: genericFieldParse(cronFields[6], 1970, 2099), - } + // Generic parser can be used for most fields + cronExpr := &CronExpression{ + Expression: cronLine, + secondList: genericFieldParse(cronFields[0], 0, 59), + minuteList: genericFieldParse(cronFields[1], 0, 59), + hourList: genericFieldParse(cronFields[2], 0, 23), + monthList: genericFieldParse(cronFields[4], 1, 12), + yearList: genericFieldParse(cronFields[6], 1970, 2099), + } - // Days of month/days of week is a bit more complicated, due - // to their extended syntax, and the fact that days per - // month is a variable quantity, and relation between - // days of week and days of month depends on the month/year. - cronExpr.dayofmonthFieldParse(cronFields[3]) - cronExpr.dayofweekFieldParse(cronFields[5]) + // Days of month/days of week is a bit more complicated, due + // to their extended syntax, and the fact that days per + // month is a variable quantity, and relation between + // days of week and days of month depends on the month/year. + cronExpr.dayofmonthFieldParse(cronFields[3]) + cronExpr.dayofweekFieldParse(cronFields[5]) - return cronExpr + return cronExpr } /******************************************************************************/ @@ -107,8 +107,8 @@ func NewCronExpression(cronLine string) *CronExpression { // If the same cron expression must be used repeatedly, it is better to use // NewCronExpression() in order to avoid overhead of cron expression parsing. func NextTime(cronLine string, fromTime time.Time) time.Time { - cronexpr := NewCronExpression(cronLine) - return cronexpr.NextTime(fromTime) + cronexpr := NewCronExpression(cronLine) + return cronexpr.NextTime(fromTime) } /******************************************************************************/ @@ -120,8 +120,8 @@ func NextTime(cronLine string, fromTime time.Time) time.Time { // If the same cron expression must be used repeatedly, it is better to use // NewCronExpression() in order to avoid overhead of cron expression parsing. func NextTimeN(cronLine string, fromTime time.Time, n int) []time.Time { - cronexpr := NewCronExpression(cronLine) - return cronexpr.NextTimeN(fromTime, n) + cronexpr := NewCronExpression(cronLine) + return cronexpr.NextTimeN(fromTime, n) } /******************************************************************************/ @@ -130,83 +130,83 @@ func NextTimeN(cronLine string, fromTime time.Time, n int) []time.Time { // satisfies the cron expression. If no matching time stamp is found, // using NoMatch() with the returned time stamp as argument will return true. func (cronexpr *CronExpression) NextTime(fromTime time.Time) time.Time { - // Special case - if NoMatch(fromTime) { - return fromTime - } + // Special case + if NoMatch(fromTime) { + return fromTime + } - // Since cronexpr.nextSecond()-cronexpr.nextMonth() expects that the - // supplied time stamp is a perfect match to the underlying cron - // expression, and since this function is an entry point where `fromTime` - // does not necessarily matches the underlying cron expression, - // we first need to ensure supplied time stamp matches - // the cron expression. If not, this means the supplied time - // stamp falls in between matching time stamps, thus we move - // to closest future matching time stamp without changing time - // stamp + // Since cronexpr.nextSecond()-cronexpr.nextMonth() expects that the + // supplied time stamp is a perfect match to the underlying cron + // expression, and since this function is an entry point where `fromTime` + // does not necessarily matches the underlying cron expression, + // we first need to ensure supplied time stamp matches + // the cron expression. If not, this means the supplied time + // stamp falls in between matching time stamps, thus we move + // to closest future matching immediately upon encountering a mismatching + // time stamp. - // year - v := fromTime.Year() - i := sort.SearchInts(cronexpr.yearList, v) - if i == len(cronexpr.yearList) { - return noMatchTime - } - if v != cronexpr.yearList[i] { - return cronexpr.nextYear(fromTime) - } - // month - v = int(fromTime.Month()) - i = sort.SearchInts(cronexpr.monthList, v) - if i == len(cronexpr.monthList) { - return cronexpr.nextYear(fromTime) - } - if v != cronexpr.monthList[i] { - return cronexpr.nextMonth(fromTime) - } + // year + v := fromTime.Year() + i := sort.SearchInts(cronexpr.yearList, v) + if i == len(cronexpr.yearList) { + return noMatchTime + } + if v != cronexpr.yearList[i] { + return cronexpr.nextYear(fromTime) + } + // month + v = int(fromTime.Month()) + i = sort.SearchInts(cronexpr.monthList, v) + if i == len(cronexpr.monthList) { + return cronexpr.nextYear(fromTime) + } + if v != cronexpr.monthList[i] { + return cronexpr.nextMonth(fromTime) + } - cronexpr.actualDaysOfMonthList = cronexpr.calculateActualDaysOfMonth(fromTime.Year(), int(fromTime.Month())) - if len(cronexpr.actualDaysOfMonthList) == 0 { - return cronexpr.nextMonth(fromTime) - } + cronexpr.actualDaysOfMonthList = cronexpr.calculateActualDaysOfMonth(fromTime.Year(), int(fromTime.Month())) + if len(cronexpr.actualDaysOfMonthList) == 0 { + return cronexpr.nextMonth(fromTime) + } - // day of month - v = fromTime.Day() - i = sort.SearchInts(cronexpr.actualDaysOfMonthList, v) - if i == len(cronexpr.actualDaysOfMonthList) { - return cronexpr.nextMonth(fromTime) - } - if v != cronexpr.actualDaysOfMonthList[i] { - return cronexpr.nextDayOfMonth(fromTime) - } - // hour - v = fromTime.Hour() - i = sort.SearchInts(cronexpr.hourList, v) - if i == len(cronexpr.hourList) { - return cronexpr.nextDayOfMonth(fromTime) - } - if v != cronexpr.hourList[i] { - return cronexpr.nextHour(fromTime) - } - // minute - v = fromTime.Minute() - i = sort.SearchInts(cronexpr.minuteList, v) - if i == len(cronexpr.minuteList) { - return cronexpr.nextHour(fromTime) - } - if v != cronexpr.minuteList[i] { - return cronexpr.nextMinute(fromTime) - } - // second - v = fromTime.Second() - i = sort.SearchInts(cronexpr.secondList, v) - if i == len(cronexpr.secondList) { - return cronexpr.nextMinute(fromTime) - } + // day of month + v = fromTime.Day() + i = sort.SearchInts(cronexpr.actualDaysOfMonthList, v) + if i == len(cronexpr.actualDaysOfMonthList) { + return cronexpr.nextMonth(fromTime) + } + if v != cronexpr.actualDaysOfMonthList[i] { + return cronexpr.nextDayOfMonth(fromTime) + } + // hour + v = fromTime.Hour() + i = sort.SearchInts(cronexpr.hourList, v) + if i == len(cronexpr.hourList) { + return cronexpr.nextDayOfMonth(fromTime) + } + if v != cronexpr.hourList[i] { + return cronexpr.nextHour(fromTime) + } + // minute + v = fromTime.Minute() + i = sort.SearchInts(cronexpr.minuteList, v) + if i == len(cronexpr.minuteList) { + return cronexpr.nextHour(fromTime) + } + if v != cronexpr.minuteList[i] { + return cronexpr.nextMinute(fromTime) + } + // second + v = fromTime.Second() + i = sort.SearchInts(cronexpr.secondList, v) + if i == len(cronexpr.secondList) { + return cronexpr.nextMinute(fromTime) + } - // If we reach this point, there is nothing better to do - // than to move to the next second + // If we reach this point, there is nothing better to do + // than to move to the next second - return cronexpr.nextSecond(fromTime) + return cronexpr.nextSecond(fromTime) } /******************************************************************************/ @@ -215,23 +215,23 @@ func (cronexpr *CronExpression) NextTime(fromTime time.Time) time.Time { // fromTime which satisfy the cron expression. An empty list is returned if // there is no matching time stamp. func (cronexpr *CronExpression) NextTimeN(fromTime time.Time, n int) []time.Time { - if n <= 0 { - panic("CronExpression.NextTimeN(): invalid count") - } - nextTimes := make([]time.Time, 0) - fromTime = cronexpr.NextTime(fromTime) - for { - if NoMatch(fromTime) { - break - } - nextTimes = append(nextTimes, fromTime) - n -= 1 - if n == 0 { - break - } - fromTime = cronexpr.nextSecond(fromTime) - } - return nextTimes + if n <= 0 { + panic("CronExpression.NextTimeN(): invalid count") + } + nextTimes := make([]time.Time, 0) + fromTime = cronexpr.NextTime(fromTime) + for { + if NoMatch(fromTime) { + break + } + nextTimes = append(nextTimes, fromTime) + n -= 1 + if n == 0 { + break + } + fromTime = cronexpr.nextSecond(fromTime) + } + return nextTimes } /******************************************************************************/ @@ -239,449 +239,449 @@ func (cronexpr *CronExpression) NextTimeN(fromTime time.Time, n int) []time.Time // NoMatch() returns whether t is a valid time stamp, from CronExpression point // of view. func NoMatch(t time.Time) bool { - // https://en.wikipedia.org/wiki/Cron#CRON_expression: 1970–2099 - return t.Year() >= 2100 + // https://en.wikipedia.org/wiki/Cron#CRON_expression: 1970–2099 + return t.Year() >= 2100 } /******************************************************************************/ func (cronexpr *CronExpression) nextYear(t time.Time) time.Time { - // Find index at which item in list is greater or equal to - // candidate year - i := sort.SearchInts(cronexpr.yearList, t.Year()+1) - if i == len(cronexpr.yearList) { - return noMatchTime - } - // Year changed, need to recalculate actual days of month - cronexpr.actualDaysOfMonthList = cronexpr.calculateActualDaysOfMonth(cronexpr.yearList[i], cronexpr.monthList[0]) - if len(cronexpr.actualDaysOfMonthList) == 0 { - return cronexpr.nextMonth(time.Date( - cronexpr.yearList[i], - time.Month(cronexpr.monthList[0]), - 1, - cronexpr.hourList[0], - cronexpr.minuteList[0], - cronexpr.secondList[0], - 0, - time.Local)) - } - return time.Date( - cronexpr.yearList[i], - time.Month(cronexpr.monthList[0]), - cronexpr.actualDaysOfMonthList[0], - cronexpr.hourList[0], - cronexpr.minuteList[0], - cronexpr.secondList[0], - 0, - time.Local) + // Find index at which item in list is greater or equal to + // candidate year + i := sort.SearchInts(cronexpr.yearList, t.Year()+1) + if i == len(cronexpr.yearList) { + return noMatchTime + } + // Year changed, need to recalculate actual days of month + cronexpr.actualDaysOfMonthList = cronexpr.calculateActualDaysOfMonth(cronexpr.yearList[i], cronexpr.monthList[0]) + if len(cronexpr.actualDaysOfMonthList) == 0 { + return cronexpr.nextMonth(time.Date( + cronexpr.yearList[i], + time.Month(cronexpr.monthList[0]), + 1, + cronexpr.hourList[0], + cronexpr.minuteList[0], + cronexpr.secondList[0], + 0, + time.Local)) + } + return time.Date( + cronexpr.yearList[i], + time.Month(cronexpr.monthList[0]), + cronexpr.actualDaysOfMonthList[0], + cronexpr.hourList[0], + cronexpr.minuteList[0], + cronexpr.secondList[0], + 0, + time.Local) } /******************************************************************************/ func (cronexpr *CronExpression) nextMonth(t time.Time) time.Time { - // Find index at which item in list is greater or equal to - // candidate month - i := sort.SearchInts(cronexpr.monthList, int(t.Month())+1) - if i == len(cronexpr.monthList) { - return cronexpr.nextYear(t) - } - // Month changed, need to recalculate actual days of month - cronexpr.actualDaysOfMonthList = cronexpr.calculateActualDaysOfMonth(t.Year(), cronexpr.monthList[i]) - if len(cronexpr.actualDaysOfMonthList) == 0 { - return cronexpr.nextMonth(time.Date( - t.Year(), - time.Month(cronexpr.monthList[i]), - 1, - cronexpr.hourList[0], - cronexpr.minuteList[0], - cronexpr.secondList[0], - 0, - time.Local)) - } + // Find index at which item in list is greater or equal to + // candidate month + i := sort.SearchInts(cronexpr.monthList, int(t.Month())+1) + if i == len(cronexpr.monthList) { + return cronexpr.nextYear(t) + } + // Month changed, need to recalculate actual days of month + cronexpr.actualDaysOfMonthList = cronexpr.calculateActualDaysOfMonth(t.Year(), cronexpr.monthList[i]) + if len(cronexpr.actualDaysOfMonthList) == 0 { + return cronexpr.nextMonth(time.Date( + t.Year(), + time.Month(cronexpr.monthList[i]), + 1, + cronexpr.hourList[0], + cronexpr.minuteList[0], + cronexpr.secondList[0], + 0, + time.Local)) + } - return time.Date( - t.Year(), - time.Month(cronexpr.monthList[i]), - cronexpr.actualDaysOfMonthList[0], - cronexpr.hourList[0], - cronexpr.minuteList[0], - cronexpr.secondList[0], - 0, - time.Local) + return time.Date( + t.Year(), + time.Month(cronexpr.monthList[i]), + cronexpr.actualDaysOfMonthList[0], + cronexpr.hourList[0], + cronexpr.minuteList[0], + cronexpr.secondList[0], + 0, + time.Local) } /******************************************************************************/ func (cronexpr *CronExpression) calculateActualDaysOfMonth(year, month int) []int { - actualDaysOfMonthMap := make(map[int]bool) - timeOrigin := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC) - lastDayOfMonth := timeOrigin.AddDate(0, 1, -1).Day() + actualDaysOfMonthMap := make(map[int]bool) + timeOrigin := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC) + lastDayOfMonth := timeOrigin.AddDate(0, 1, -1).Day() - // As per crontab man page (http://linux.die.net/man/5/crontab#): - // "The day of a command's execution can be specified by two - // "fields - day of month, and day of week. If both fields are - // "restricted (ie, aren't *), the command will be run when - // "either field matches the current time" - if cronexpr.daysOfMonthRestricted || cronexpr.daysOfWeekRestricted == false { - // Last day of month - if cronexpr.lastDayOfMonth { - actualDaysOfMonthMap[lastDayOfMonth] = true - } - // Days of month - for v, _ := range cronexpr.daysOfMonth { - // Ignore days beyond end of month - if v <= lastDayOfMonth { - actualDaysOfMonthMap[v] = true - } - } - // Work days of month - // As per Wikipedia: month boundaries are not crossed. - for v, _ := range cronexpr.workdaysOfMonth { - // Ignore days beyond end of month - if v <= lastDayOfMonth { - // If saturday, then friday - if timeOrigin.AddDate(0, 0, v-1).Weekday() == time.Saturday { - if v > 1 { - v -= 1 - } else { - v += 2 - } - // If sunday, then monday - } else if timeOrigin.AddDate(0, 0, v-1).Weekday() == time.Sunday { - if v < lastDayOfMonth { - v += 1 - } else { - v -= 2 - } - } - actualDaysOfMonthMap[v] = true - } - } - } + // As per crontab man page (http://linux.die.net/man/5/crontab#): + // "The day of a command's execution can be specified by two + // "fields - day of month, and day of week. If both fields are + // "restricted (ie, aren't *), the command will be run when + // "either field matches the current time" + if cronexpr.daysOfMonthRestricted || cronexpr.daysOfWeekRestricted == false { + // Last day of month + if cronexpr.lastDayOfMonth { + actualDaysOfMonthMap[lastDayOfMonth] = true + } + // Days of month + for v, _ := range cronexpr.daysOfMonth { + // Ignore days beyond end of month + if v <= lastDayOfMonth { + actualDaysOfMonthMap[v] = true + } + } + // Work days of month + // As per Wikipedia: month boundaries are not crossed. + for v, _ := range cronexpr.workdaysOfMonth { + // Ignore days beyond end of month + if v <= lastDayOfMonth { + // If saturday, then friday + if timeOrigin.AddDate(0, 0, v-1).Weekday() == time.Saturday { + if v > 1 { + v -= 1 + } else { + v += 2 + } + // If sunday, then monday + } else if timeOrigin.AddDate(0, 0, v-1).Weekday() == time.Sunday { + if v < lastDayOfMonth { + v += 1 + } else { + v -= 2 + } + } + actualDaysOfMonthMap[v] = true + } + } + } - if cronexpr.daysOfWeekRestricted { - // How far first sunday is from first day of month - offset := 7 - int(timeOrigin.Weekday()) - // days of week - // offset : (7 - day_of_week_of_1st_day_of_month) - // target : 1 + (7 * week_of_month) + (offset + day_of_week) % 7 - for w := 0; w <= 4; w += 1 { - for v, _ := range cronexpr.daysOfWeek { - v := 1 + w*7 + (offset+v)%7 - if v <= lastDayOfMonth { - actualDaysOfMonthMap[v] = true - } - } - } - // days of week of specific week in the month - // offset : (7 - day_of_week_of_1st_day_of_month) - // target : 1 + (7 * week_of_month) + (offset + day_of_week) % 7 - for v, _ := range cronexpr.specificWeekDaysOfWeek { - v := 1 + 7*(v/7) + (offset+v)%7 - if v <= lastDayOfMonth { - actualDaysOfMonthMap[v] = true - } - } - // Last days of week of the month - lastWeekOrigin := timeOrigin.AddDate(0, 1, -7) - offset = 7 - int(lastWeekOrigin.Weekday()) - for v, _ := range cronexpr.lastWeekDaysOfWeek { - v := lastWeekOrigin.Day() + (offset+v)%7 - if v <= lastDayOfMonth { - actualDaysOfMonthMap[v] = true - } - } - } + if cronexpr.daysOfWeekRestricted { + // How far first sunday is from first day of month + offset := 7 - int(timeOrigin.Weekday()) + // days of week + // offset : (7 - day_of_week_of_1st_day_of_month) + // target : 1 + (7 * week_of_month) + (offset + day_of_week) % 7 + for w := 0; w <= 4; w += 1 { + for v, _ := range cronexpr.daysOfWeek { + v := 1 + w*7 + (offset+v)%7 + if v <= lastDayOfMonth { + actualDaysOfMonthMap[v] = true + } + } + } + // days of week of specific week in the month + // offset : (7 - day_of_week_of_1st_day_of_month) + // target : 1 + (7 * week_of_month) + (offset + day_of_week) % 7 + for v, _ := range cronexpr.specificWeekDaysOfWeek { + v := 1 + 7*(v/7) + (offset+v)%7 + if v <= lastDayOfMonth { + actualDaysOfMonthMap[v] = true + } + } + // Last days of week of the month + lastWeekOrigin := timeOrigin.AddDate(0, 1, -7) + offset = 7 - int(lastWeekOrigin.Weekday()) + for v, _ := range cronexpr.lastWeekDaysOfWeek { + v := lastWeekOrigin.Day() + (offset+v)%7 + if v <= lastDayOfMonth { + actualDaysOfMonthMap[v] = true + } + } + } - return toList(actualDaysOfMonthMap) + return toList(actualDaysOfMonthMap) } /******************************************************************************/ func (cronexpr *CronExpression) nextDayOfMonth(t time.Time) time.Time { - // Find index at which item in list is greater or equal to - // candidate day of month - i := sort.SearchInts(cronexpr.actualDaysOfMonthList, t.Day()+1) - if i == len(cronexpr.actualDaysOfMonthList) { - return cronexpr.nextMonth(t) - } + // Find index at which item in list is greater or equal to + // candidate day of month + i := sort.SearchInts(cronexpr.actualDaysOfMonthList, t.Day()+1) + if i == len(cronexpr.actualDaysOfMonthList) { + return cronexpr.nextMonth(t) + } - return time.Date( - t.Year(), - t.Month(), - cronexpr.actualDaysOfMonthList[i], - cronexpr.hourList[0], - cronexpr.minuteList[0], - cronexpr.secondList[0], - 0, - t.Location()) + return time.Date( + t.Year(), + t.Month(), + cronexpr.actualDaysOfMonthList[i], + cronexpr.hourList[0], + cronexpr.minuteList[0], + cronexpr.secondList[0], + 0, + t.Location()) } /******************************************************************************/ func (cronexpr *CronExpression) nextHour(t time.Time) time.Time { - // Find index at which item in list is greater or equal to - // candidate hour - i := sort.SearchInts(cronexpr.hourList, t.Hour()+1) - if i == len(cronexpr.hourList) { - return cronexpr.nextDayOfMonth(t) - } + // Find index at which item in list is greater or equal to + // candidate hour + i := sort.SearchInts(cronexpr.hourList, t.Hour()+1) + if i == len(cronexpr.hourList) { + return cronexpr.nextDayOfMonth(t) + } - return time.Date( - t.Year(), - t.Month(), - t.Day(), - cronexpr.hourList[i], - cronexpr.minuteList[0], - cronexpr.secondList[0], - 0, - t.Location()) + return time.Date( + t.Year(), + t.Month(), + t.Day(), + cronexpr.hourList[i], + cronexpr.minuteList[0], + cronexpr.secondList[0], + 0, + t.Location()) } /******************************************************************************/ func (cronexpr *CronExpression) nextMinute(t time.Time) time.Time { - // Find index at which item in list is greater or equal to - // candidate minute - i := sort.SearchInts(cronexpr.minuteList, t.Minute()+1) - if i == len(cronexpr.minuteList) { - return cronexpr.nextHour(t) - } + // Find index at which item in list is greater or equal to + // candidate minute + i := sort.SearchInts(cronexpr.minuteList, t.Minute()+1) + if i == len(cronexpr.minuteList) { + return cronexpr.nextHour(t) + } - return time.Date( - t.Year(), - t.Month(), - t.Day(), - t.Hour(), - cronexpr.minuteList[i], - cronexpr.secondList[0], - 0, - t.Location()) + return time.Date( + t.Year(), + t.Month(), + t.Day(), + t.Hour(), + cronexpr.minuteList[i], + cronexpr.secondList[0], + 0, + t.Location()) } /******************************************************************************/ func (cronexpr *CronExpression) nextSecond(t time.Time) time.Time { - // nextSecond() assumes all other fields are exactly matched - // to the cron expression + // nextSecond() assumes all other fields are exactly matched + // to the cron expression - // Find index at which item in list is greater or equal to - // candidate second - i := sort.SearchInts(cronexpr.secondList, t.Second()+1) - if i == len(cronexpr.secondList) { - return cronexpr.nextMinute(t) - } + // Find index at which item in list is greater or equal to + // candidate second + i := sort.SearchInts(cronexpr.secondList, t.Second()+1) + if i == len(cronexpr.secondList) { + return cronexpr.nextMinute(t) + } - return time.Date( - t.Year(), - t.Month(), - t.Day(), - t.Hour(), - t.Minute(), - cronexpr.secondList[i], - 0, - t.Location()) + return time.Date( + t.Year(), + t.Month(), + t.Day(), + t.Hour(), + t.Minute(), + cronexpr.secondList[i], + 0, + t.Location()) } /******************************************************************************/ var cronNormalizer = strings.NewReplacer( - // Order is important! - "@yearly", "0 0 0 1 1 * *", - "@annually", "0 0 0 1 1 * *", - "@monthly", "0 0 0 1 * * *", - "@weekly", "0 0 0 * * 0 *", - "@daily", "0 0 0 * * * *", - "@hourly", "0 0 * * * * *", - "january", "1", - "february", "2", - "march", "3", - "april", "4", - "may", "5", - "june", "6", - "july", "7", - "august", "8", - "september", "9", - "october", "0", - "november", "1", - "december", "2", - "sunday", "0", - "monday", "1", - "tuesday", "2", - "wednesday", "3", - "thursday", "4", - "friday", "5", - "saturday", "6", - "jan", "1", - "feb", "2", - "mar", "3", - "apr", "4", - "jun", "6", - "jul", "7", - "aug", "8", - "sep", "9", - "oct", "10", - "nov", "11", - "dec", "12", - "sun", "0", - "mon", "1", - "tue", "2", - "wed", "3", - "thu", "4", - "fri", "5", - "sat", "6", - "?", "*") + // Order is important! + "@yearly", "0 0 0 1 1 * *", + "@annually", "0 0 0 1 1 * *", + "@monthly", "0 0 0 1 * * *", + "@weekly", "0 0 0 * * 0 *", + "@daily", "0 0 0 * * * *", + "@hourly", "0 0 * * * * *", + "january", "1", + "february", "2", + "march", "3", + "april", "4", + "may", "5", + "june", "6", + "july", "7", + "august", "8", + "september", "9", + "october", "0", + "november", "1", + "december", "2", + "sunday", "0", + "monday", "1", + "tuesday", "2", + "wednesday", "3", + "thursday", "4", + "friday", "5", + "saturday", "6", + "jan", "1", + "feb", "2", + "mar", "3", + "apr", "4", + "jun", "6", + "jul", "7", + "aug", "8", + "sep", "9", + "oct", "10", + "nov", "11", + "dec", "12", + "sun", "0", + "mon", "1", + "tue", "2", + "wed", "3", + "thu", "4", + "fri", "5", + "sat", "6", + "?", "*") func cronNormalize(cronLine string) string { - cronLine = strings.TrimSpace(cronLine) - cronLine = strings.ToLower(cronLine) - cronLine = cronNormalizer.Replace(cronLine) - return cronLine + cronLine = strings.TrimSpace(cronLine) + cronLine = strings.ToLower(cronLine) + cronLine = cronNormalizer.Replace(cronLine) + return cronLine } /******************************************************************************/ func (cronexpr *CronExpression) dayofweekFieldParse(cronField string) error { - // Defaults - cronexpr.daysOfWeekRestricted = true - cronexpr.lastWeekDaysOfWeek = make(map[int]bool) - cronexpr.daysOfWeek = make(map[int]bool) + // Defaults + cronexpr.daysOfWeekRestricted = true + cronexpr.lastWeekDaysOfWeek = make(map[int]bool) + cronexpr.daysOfWeek = make(map[int]bool) - // "You can also mix all of the above, as in: 1-5,10,12,20-30/5" - cronList := strings.Split(cronField, ",") - for _, s := range cronList { - // "/" - step, s := extractInterval(s) - // "*" - if s == "*" { - cronexpr.daysOfWeekRestricted = (step > 1) - populateMany(cronexpr.daysOfWeek, 0, 6, step) - continue - } - // "-" - // week day interval for all weeks - i := strings.Index(s, "-") - if i >= 0 { - min := atoi(s[:i]) % 7 - max := atoi(s[i+1:]) % 7 - populateMany(cronexpr.daysOfWeek, min, max, step) - continue - } - // single value - // "l": week day for last week - i = strings.Index(s, "l") - if i >= 0 { - populateOne(cronexpr.lastWeekDaysOfWeek, atoi(s[:i])%7) - continue - } - // "#": week day for specific week - i = strings.Index(s, "#") - if i >= 0 { - // v#w - v := atoi(s[:i]) % 7 - w := atoi(s[i+1:]) - // v domain = [0,7] - // w domain = [1,5] - populateOne(cronexpr.specificWeekDaysOfWeek, (w-1)*7+(v%7)) - continue - } - // week day interval for all weeks - if step > 0 { - v := atoi(s) % 7 - populateMany(cronexpr.daysOfWeek, v, 6, step) - continue - } - // single week day for all weeks - v := atoi(s) % 7 - populateOne(cronexpr.daysOfWeek, v) - } + // "You can also mix all of the above, as in: 1-5,10,12,20-30/5" + cronList := strings.Split(cronField, ",") + for _, s := range cronList { + // "/" + step, s := extractInterval(s) + // "*" + if s == "*" { + cronexpr.daysOfWeekRestricted = (step > 1) + populateMany(cronexpr.daysOfWeek, 0, 6, step) + continue + } + // "-" + // week day interval for all weeks + i := strings.Index(s, "-") + if i >= 0 { + min := atoi(s[:i]) % 7 + max := atoi(s[i+1:]) % 7 + populateMany(cronexpr.daysOfWeek, min, max, step) + continue + } + // single value + // "l": week day for last week + i = strings.Index(s, "l") + if i >= 0 { + populateOne(cronexpr.lastWeekDaysOfWeek, atoi(s[:i])%7) + continue + } + // "#": week day for specific week + i = strings.Index(s, "#") + if i >= 0 { + // v#w + v := atoi(s[:i]) % 7 + w := atoi(s[i+1:]) + // v domain = [0,7] + // w domain = [1,5] + populateOne(cronexpr.specificWeekDaysOfWeek, (w-1)*7+(v%7)) + continue + } + // week day interval for all weeks + if step > 0 { + v := atoi(s) % 7 + populateMany(cronexpr.daysOfWeek, v, 6, step) + continue + } + // single week day for all weeks + v := atoi(s) % 7 + populateOne(cronexpr.daysOfWeek, v) + } - return nil + return nil } /******************************************************************************/ func (cronexpr *CronExpression) dayofmonthFieldParse(cronField string) error { - // Defaults - cronexpr.daysOfMonthRestricted = true - cronexpr.lastDayOfMonth = false + // Defaults + cronexpr.daysOfMonthRestricted = true + cronexpr.lastDayOfMonth = false - cronexpr.daysOfMonth = make(map[int]bool) // days of month map - cronexpr.workdaysOfMonth = make(map[int]bool) // work day of month map + cronexpr.daysOfMonth = make(map[int]bool) // days of month map + cronexpr.workdaysOfMonth = make(map[int]bool) // work day of month map - // Comma separator is used to mix different allowed syntax - cronList := strings.Split(cronField, ",") - for _, s := range cronList { - // "/" - step, s := extractInterval(s) - // "*" - if s == "*" { - cronexpr.daysOfMonthRestricted = (step > 1) - populateMany(cronexpr.daysOfMonth, 1, 31, step) - continue - } - // "-" - i := strings.Index(s, "-") - if i >= 0 { - populateMany(cronexpr.daysOfMonth, atoi(s[:i]), atoi(s[i+1:]), step) - continue - } - // single value - // "l": last day of month - if s == "l" { - cronexpr.lastDayOfMonth = true - continue - } - // "w": week day - i = strings.Index(s, "w") - if i >= 0 { - populateOne(cronexpr.workdaysOfMonth, atoi(s[:i])) - continue - } - // single value with interval - if step > 0 { - populateMany(cronexpr.daysOfMonth, atoi(s), 31, step) - continue - } - // single value - populateOne(cronexpr.daysOfMonth, atoi(s)) - } + // Comma separator is used to mix different allowed syntax + cronList := strings.Split(cronField, ",") + for _, s := range cronList { + // "/" + step, s := extractInterval(s) + // "*" + if s == "*" { + cronexpr.daysOfMonthRestricted = (step > 1) + populateMany(cronexpr.daysOfMonth, 1, 31, step) + continue + } + // "-" + i := strings.Index(s, "-") + if i >= 0 { + populateMany(cronexpr.daysOfMonth, atoi(s[:i]), atoi(s[i+1:]), step) + continue + } + // single value + // "l": last day of month + if s == "l" { + cronexpr.lastDayOfMonth = true + continue + } + // "w": week day + i = strings.Index(s, "w") + if i >= 0 { + populateOne(cronexpr.workdaysOfMonth, atoi(s[:i])) + continue + } + // single value with interval + if step > 0 { + populateMany(cronexpr.daysOfMonth, atoi(s), 31, step) + continue + } + // single value + populateOne(cronexpr.daysOfMonth, atoi(s)) + } - return nil + return nil } /******************************************************************************/ func genericFieldParse(cronField string, min, max int) []int { - // Defaults - values := make(map[int]bool) + // Defaults + values := make(map[int]bool) - // Comma separator is used to mix different allowed syntax - cronList := strings.Split(cronField, ",") - for _, s := range cronList { - // "/" - step, s := extractInterval(s) - // "*" - if s == "*" { - populateMany(values, min, max, step) - continue - } - // "-" - i := strings.Index(s, "-") - if i >= 0 { - populateMany(values, atoi(s[:i]), atoi(s[i+1:]), step) - continue - } - // single value with interval - if step > 0 { - populateMany(values, atoi(s), max, step) - continue - } - // single value - populateOne(values, atoi(s)) - } + // Comma separator is used to mix different allowed syntax + cronList := strings.Split(cronField, ",") + for _, s := range cronList { + // "/" + step, s := extractInterval(s) + // "*" + if s == "*" { + populateMany(values, min, max, step) + continue + } + // "-" + i := strings.Index(s, "-") + if i >= 0 { + populateMany(values, atoi(s[:i]), atoi(s[i+1:]), step) + continue + } + // single value with interval + if step > 0 { + populateMany(values, atoi(s), max, step) + continue + } + // single value + populateOne(values, atoi(s)) + } - return toList(values) + return toList(values) } /******************************************************************************/ @@ -689,43 +689,43 @@ func genericFieldParse(cronField string, min, max int) []int { // Local helpers func extractInterval(s string) (int, string) { - step := 0 - i := strings.Index(s, "/") - if i >= 0 { - step = atoi(s[i+1:]) - s = s[:i] - } - return step, s + step := 0 + i := strings.Index(s, "/") + if i >= 0 { + step = atoi(s[i+1:]) + s = s[:i] + } + return step, s } func atoi(s string) int { - v, err := strconv.Atoi(s) - if err != nil { - panic(err) - } - return v + v, err := strconv.Atoi(s) + if err != nil { + panic(err) + } + return v } func populateOne(values map[int]bool, v int) { - values[v] = true + values[v] = true } func populateMany(values map[int]bool, min, max, step int) { - if step == 0 { - step = 1 - } - for i := min; i <= max; i += step { - values[i] = true - } + if step == 0 { + step = 1 + } + for i := min; i <= max; i += step { + values[i] = true + } } func toList(set map[int]bool) []int { - list := make([]int, len(set)) - i := 0 - for k, _ := range set { - list[i] = k - i += 1 - } - sort.Ints(list) - return list + list := make([]int, len(set)) + i := 0 + for k, _ := range set { + list[i] = k + i += 1 + } + sort.Ints(list) + return list } diff --git a/cronexpression_test.go b/cronexpression_test.go index e3da33e..ad803db 100644 --- a/cronexpression_test.go +++ b/cronexpression_test.go @@ -21,103 +21,103 @@ import ( /******************************************************************************/ type crontimes struct { - from string - next string + from string + next string } type crontest struct { - expr string - layout string - times []crontimes + expr string + layout string + times []crontimes } var crontests = []crontest{ - // Seconds - { - "* * * * * * *", - "2006-01-02 15:04:05", - []crontimes{ - {"2013-01-01 00:00:00", "2013-01-01 00:00:01"}, - {"2013-01-01 00:00:59", "2013-01-01 00:01:00"}, - {"2013-01-01 00:59:59", "2013-01-01 01:00:00"}, - {"2013-01-01 23:59:59", "2013-01-02 00:00:00"}, - {"2013-02-28 23:59:59", "2013-03-01 00:00:00"}, - {"2016-02-28 23:59:59", "2016-02-29 00:00:00"}, - {"2012-12-31 23:59:59", "2013-01-01 00:00:00"}, - }, - }, + // Seconds + { + "* * * * * * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2013-01-01 00:00:00", "2013-01-01 00:00:01"}, + {"2013-01-01 00:00:59", "2013-01-01 00:01:00"}, + {"2013-01-01 00:59:59", "2013-01-01 01:00:00"}, + {"2013-01-01 23:59:59", "2013-01-02 00:00:00"}, + {"2013-02-28 23:59:59", "2013-03-01 00:00:00"}, + {"2016-02-28 23:59:59", "2016-02-29 00:00:00"}, + {"2012-12-31 23:59:59", "2013-01-01 00:00:00"}, + }, + }, - // Minutes - { - "* * * * *", - "2006-01-02 15:04:05", - []crontimes{ - {"2013-01-01 00:00:00", "2013-01-01 00:01:00"}, - {"2013-01-01 00:00:59", "2013-01-01 00:01:00"}, - {"2013-01-01 00:59:00", "2013-01-01 01:00:00"}, - {"2013-01-01 23:59:00", "2013-01-02 00:00:00"}, - {"2013-02-28 23:59:00", "2013-03-01 00:00:00"}, - {"2016-02-28 23:59:00", "2016-02-29 00:00:00"}, - {"2012-12-31 23:59:00", "2013-01-01 00:00:00"}, - }, - }, + // Minutes + { + "* * * * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2013-01-01 00:00:00", "2013-01-01 00:01:00"}, + {"2013-01-01 00:00:59", "2013-01-01 00:01:00"}, + {"2013-01-01 00:59:00", "2013-01-01 01:00:00"}, + {"2013-01-01 23:59:00", "2013-01-02 00:00:00"}, + {"2013-02-28 23:59:00", "2013-03-01 00:00:00"}, + {"2016-02-28 23:59:00", "2016-02-29 00:00:00"}, + {"2012-12-31 23:59:00", "2013-01-01 00:00:00"}, + }, + }, - // Minutes with interval - { - "17-43/5 * * * *", - "2006-01-02 15:04:05", - []crontimes{ - {"2013-01-01 00:00:00", "2013-01-01 00:17:00"}, - {"2013-01-01 00:16:59", "2013-01-01 00:17:00"}, - {"2013-01-01 00:30:00", "2013-01-01 00:32:00"}, - {"2013-01-01 00:50:00", "2013-01-01 01:17:00"}, - {"2013-01-01 23:50:00", "2013-01-02 00:17:00"}, - {"2013-02-28 23:50:00", "2013-03-01 00:17:00"}, - {"2016-02-28 23:50:00", "2016-02-29 00:17:00"}, - {"2012-12-31 23:50:00", "2013-01-01 00:17:00"}, - }, - }, + // Minutes with interval + { + "17-43/5 * * * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2013-01-01 00:00:00", "2013-01-01 00:17:00"}, + {"2013-01-01 00:16:59", "2013-01-01 00:17:00"}, + {"2013-01-01 00:30:00", "2013-01-01 00:32:00"}, + {"2013-01-01 00:50:00", "2013-01-01 01:17:00"}, + {"2013-01-01 23:50:00", "2013-01-02 00:17:00"}, + {"2013-02-28 23:50:00", "2013-03-01 00:17:00"}, + {"2016-02-28 23:50:00", "2016-02-29 00:17:00"}, + {"2012-12-31 23:50:00", "2013-01-01 00:17:00"}, + }, + }, - // Minutes interval, list - { - "15-30/4,55 * * * *", - "2006-01-02 15:04:05", - []crontimes{ - {"2013-01-01 00:00:00", "2013-01-01 00:15:00"}, - {"2013-01-01 00:16:00", "2013-01-01 00:19:00"}, - {"2013-01-01 00:30:00", "2013-01-01 00:55:00"}, - {"2013-01-01 00:55:00", "2013-01-01 01:15:00"}, - {"2013-01-01 23:55:00", "2013-01-02 00:15:00"}, - {"2013-02-28 23:55:00", "2013-03-01 00:15:00"}, - {"2016-02-28 23:55:00", "2016-02-29 00:15:00"}, - {"2012-12-31 23:54:00", "2012-12-31 23:55:00"}, - {"2012-12-31 23:55:00", "2013-01-01 00:15:00"}, - }, - }, + // Minutes interval, list + { + "15-30/4,55 * * * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2013-01-01 00:00:00", "2013-01-01 00:15:00"}, + {"2013-01-01 00:16:00", "2013-01-01 00:19:00"}, + {"2013-01-01 00:30:00", "2013-01-01 00:55:00"}, + {"2013-01-01 00:55:00", "2013-01-01 01:15:00"}, + {"2013-01-01 23:55:00", "2013-01-02 00:15:00"}, + {"2013-02-28 23:55:00", "2013-03-01 00:15:00"}, + {"2016-02-28 23:55:00", "2016-02-29 00:15:00"}, + {"2012-12-31 23:54:00", "2012-12-31 23:55:00"}, + {"2012-12-31 23:55:00", "2013-01-01 00:15:00"}, + }, + }, - // Days of week - { - "0 0 * * MON", - "Mon 2006-01-02 15:04", - []crontimes{ - {"2013-01-01 00:00:00", "Mon 2013-01-07 00:00"}, - {"2013-01-28 00:00:00", "Mon 2013-02-04 00:00"}, - {"2013-12-30 00:30:00", "Mon 2014-01-06 00:00"}, - }, - }, + // Days of week + { + "0 0 * * MON", + "Mon 2006-01-02 15:04", + []crontimes{ + {"2013-01-01 00:00:00", "Mon 2013-01-07 00:00"}, + {"2013-01-28 00:00:00", "Mon 2013-02-04 00:00"}, + {"2013-12-30 00:30:00", "Mon 2014-01-06 00:00"}, + }, + }, - // TODO: more tests + // TODO: more tests } func TestCronExpressions(t *testing.T) { for _, test := range crontests { - for _, times := range test.times { - from, _ := time.Parse("2006-01-02 15:04:05", times.from) - next := cronexpression.NextTime(test.expr, from) - nextstr := next.Format(test.layout) - if nextstr != times.next { - t.Errorf("(\"%s\").NextTime(\"%s\") = \"%s\", got \"%s\"", test.expr, times.from, times.next, nextstr) - } + for _, times := range test.times { + from, _ := time.Parse("2006-01-02 15:04:05", times.from) + next := cronexpression.NextTime(test.expr, from) + nextstr := next.Format(test.layout) + if nextstr != times.next { + t.Errorf("(\"%s\").NextTime(\"%s\") = \"%s\", got \"%s\"", test.expr, times.from, times.next, nextstr) + } } } }