From 40271bc5f487ed32767196db683c6771df66cf09 Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 29 Aug 2013 09:03:10 -0400 Subject: [PATCH 01/17] first commit --- cronexpression.go | 726 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 726 insertions(+) create mode 100644 cronexpression.go diff --git a/cronexpression.go b/cronexpression.go new file mode 100644 index 0000000..6b8fa6b --- /dev/null +++ b/cronexpression.go @@ -0,0 +1,726 @@ +/*! + * Copyright 2013 Raymond Hill + * + * Project: github.com/gorhill/gocronexpression + * File: cronexpression.go + * Version: 1.0 + * License: GPL v3 see + * + */ + +package cronexpression + +/******************************************************************************/ + +import ( + "regexp" + "sort" + "strconv" + "strings" + "time" +) + +/******************************************************************************/ + +// A CronExpression represents a specific cron expression as defined on +// 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 +} + +/******************************************************************************/ + +var noMatchTime = time.Date(2100, 7, 1, 0, 0, 0, 0, time.UTC) + +/******************************************************************************/ + +// NewCronExpression() returns a new CronExpression pointer. It expects +// a well-formed cron expression. If a malformed cron expression is +// supplied, the result is undefined. +func NewCronExpression(cronLine string) *CronExpression { + cronLineNormalized := cronNormalize(cronLine) + + // 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") + } + + // 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]) + + return cronExpr +} + +/******************************************************************************/ + +// NextTimeFromCronString() returns the time stamp following fromTime which +// satisfies the cron expression cronLine. If no matching time stamp is found, +// using NoMatch() with the returned time stamp as argument will return true. +// +// 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 NextTimeFromCronString(cronLine string, fromTime time.Time) time.Time { + cronexpr := NewCronExpression(cronLine) + return cronexpr.NextTime(fromTime) +} + +/******************************************************************************/ + +// NextTimeNFromCronString() returns the n time stamps following fromTime which +// satisfy the cron expression cronLine. An empty list is returned if +// there is no matching time stamp. +// +// 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 NextTimeNFromCronString(cronLine string, fromTime time.Time, n int) []time.Time { + cronexpr := NewCronExpression(cronLine) + return cronexpr.NextTimeN(fromTime, n) +} + +/******************************************************************************/ + +// CronExpression.NextTime() returns the time stamp following fromTime which +// 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 + } + + // First we need to ensure supplied time stamp matches + // the cron expression. If not, this means the supplied time + // stamp might be between matching time stamps, thus we move + // to closest matching time stamp without changing 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) + } + + 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) + } + + // If we reach this point, there is nothing better to do + // than to move to the next second + + return cronexpr.nextSecond(fromTime) +} + +/******************************************************************************/ + +// CronExpression.NextTimeN() returns an array of n time stamps following +// 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 +} + +/******************************************************************************/ + +// 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 +} + +/******************************************************************************/ + +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) +} + +/******************************************************************************/ + +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)) + } + + 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() + + // 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 : (7 * week_of_month) + (offset + day_of_week) % 7 + 1 + 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 : (7 * week_of_month) + (offset + day_of_week) % 7 + 1 + 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) +} + +/******************************************************************************/ + +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) + } + + 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) + } + + 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) + } + + 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 + + // 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()) +} + +/******************************************************************************/ + +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", "0", + "nov", "1", + "dec", "2", + "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 +} + +/******************************************************************************/ + +func (cronexpr *CronExpression) dayofweekFieldParse(cronField string) error { + // 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) + } + + return nil +} + +/******************************************************************************/ + +func (cronexpr *CronExpression) dayofmonthFieldParse(cronField string) error { + // 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 + + // "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.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 +} + +/******************************************************************************/ + +func genericFieldParse(cronField string, min, max int) []int { + // Defaults + values := 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 == "*" { + 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) +} + +/******************************************************************************/ + +// 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 +} + +func atoi(s string) int { + v, err := strconv.Atoi(s) + if err != nil { + panic(err) + } + return v +} + +func populateOne(values map[int]bool, v int) { + 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 + } +} + +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 +} From 612c9d3d632fd64d97dcf87ac4b2af71b2d19c86 Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 29 Aug 2013 15:05:08 -0400 Subject: [PATCH 02/17] README.md --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9055cb3..b1ae6e2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,24 @@ gocronexpression ================ -Go language (golang) cron expression parser. Given a cron expression and a time stamp, you can get the next time stamp which satisfy the cron expression +Go language (golang) cron expression parser. Given a cron expression and a time stamp, you can get the next time stamp which satisfy the cron expression. + +Install +------- + + go get github.com/gorhill/cronexpression + +Usage +----- + +Import the library: + + import "github.com/gorhill/cronexpression" + import "time" + +Simplest way: + + ... + nextTime := cronexpression.NextTimeFromCronString("* * 29 2 *", time.Now()) + +Assuming *time.Now()* is "2013-08-29 09:28:00", then *nextTime* will be "Monday, February 29, 2016 00:00:00". From 8d401e72b1d77ff14b1644716d4c09d50053de8d Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 29 Aug 2013 15:15:59 -0400 Subject: [PATCH 03/17] added more details --- README.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b1ae6e2..f8b6fd4 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,9 @@ gocronexpression Go language (golang) cron expression parser. Given a cron expression and a time stamp, you can get the next time stamp which satisfy the cron expression. +The reference documentation for this implementation is found at +https://en.wikipedia.org/wiki/Cron#CRON_expression + Install ------- @@ -18,7 +21,14 @@ Import the library: Simplest way: - ... - nextTime := cronexpression.NextTimeFromCronString("* * 29 2 *", time.Now()) + nextTime := cronexpression.NextTimeFromCronString("0 0 29 2 *", time.Now()) + +Assuming `time.Now()` is "2013-08-29 09:28:00", then `nextTime` will be "2016-02-29 00:00:00". + +If you need to reuse many times a cron expression in your code, it is more efficient +to create a `CronExpression` object once and keep a copy of it for reuse: + + cronexpr := cronexpression.NewCronExpression("0 0 29 2 *") + nextTime := cronexpr.NextTime(time.Now()) + -Assuming *time.Now()* is "2013-08-29 09:28:00", then *nextTime* will be "Monday, February 29, 2016 00:00:00". From e0efb2872b2364c3b2729648b7de3494a6da646f Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 29 Aug 2013 15:30:54 -0400 Subject: [PATCH 04/17] added more details --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f8b6fd4..cc8b77a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,18 @@ -gocronexpression -================ +cronexpression for Go +===================== Go language (golang) cron expression parser. Given a cron expression and a time stamp, you can get the next time stamp which satisfy the cron expression. The reference documentation for this implementation is found at -https://en.wikipedia.org/wiki/Cron#CRON_expression +https://en.wikipedia.org/wiki/Cron#CRON_expression, with the following +difference: + +* Supports the second field (before minute field) +* If five fields are present, a wildcard year field is appended +* If six field are present, "0" is prepended as second field +* Domain for day-of-week field is [0-7] instead of [0-6], 7 being Sunday, like zero. +* `@reboot` is not supported, as it is meaningless for a cron expression parser library +* As of now, the behavior of the code is undetermined if a malformed cron expression is supplied (most likely, code will panic) Install ------- From e15b9a67dee3a64dd77636c1a3936dcc401586b7 Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 29 Aug 2013 18:58:01 -0400 Subject: [PATCH 05/17] fleshing out --- README.md | 2 +- cronexpression.go | 2 +- cronexpression_test.go | 109 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 cronexpression_test.go diff --git a/README.md b/README.md index cc8b77a..2d67405 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Go language (golang) cron expression parser. Given a cron expression and a time The reference documentation for this implementation is found at https://en.wikipedia.org/wiki/Cron#CRON_expression, with the following -difference: +differences: * Supports the second field (before minute field) * If five fields are present, a wildcard year field is appended diff --git a/cronexpression.go b/cronexpression.go index 6b8fa6b..2cb6574 100644 --- a/cronexpression.go +++ b/cronexpression.go @@ -1,7 +1,7 @@ /*! * Copyright 2013 Raymond Hill * - * Project: github.com/gorhill/gocronexpression + * Project: github.com/gorhill/cronexpression * File: cronexpression.go * Version: 1.0 * License: GPL v3 see diff --git a/cronexpression_test.go b/cronexpression_test.go new file mode 100644 index 0000000..002e0db --- /dev/null +++ b/cronexpression_test.go @@ -0,0 +1,109 @@ +/*! + * Copyright 2013 Raymond Hill + * + * Project: github.com/gorhill/cronexpression + * File: cronexpression_test.go + * Version: 1.0 + * License: GPL v3 see + * + */ + +package cronexpression_test + +/******************************************************************************/ + +import ( + "cronexpression" + "testing" + "time" +) + +/******************************************************************************/ + +type crontimes struct { + from string + next string +} + +type crontest struct { + 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 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 with intervals + { + "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:55:00", "2013-01-01 00:15:00"}, + }, + }, + + // 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.NextTimeFromCronString(test.expr, from) + if next.Format(test.layout) != times.next { + t.Errorf("(\"%s\").NextTime(\"%s\") = \"%s\", got \"%s\"", test.expr, times.from, times.next, next.Format(test.layout)) + } + } + } +} From db81c0f41dd91b85d4c0557a2c267e550f8a1bf7 Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 29 Aug 2013 19:37:04 -0400 Subject: [PATCH 06/17] fleshing out --- README.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 2d67405..90b8ecb 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,70 @@ cronexpression for Go ===================== +Cron expression parser in Go language (golang). -Go language (golang) cron expression parser. Given a cron expression and a time stamp, you can get the next time stamp which satisfy the cron expression. +Given a cron expression and a time stamp, you can get the next time stamp which satisfy the cron expression. +Implementation +-------------- The reference documentation for this implementation is found at -https://en.wikipedia.org/wiki/Cron#CRON_expression, with the following -differences: +https://en.wikipedia.org/wiki/Cron#CRON_expression, which I copy/pasted here (laziness) with modifications where this implementation differs: -* Supports the second field (before minute field) + Field name Mandatory? Allowed values Allowed special characters + ---------- ---------- -------------- -------------------------- + Minutes Yes 0-59 * / , - + Hours Yes 0-23 * / , - + Day of month Yes 1-31 * / , - ? L W + Month Yes 1-12 or JAN-DEC * / , - + Day of week Yes 0-6 or SUN-SAT * / , - ? L # + Year No 1970–2099 * / , - + +Asterisk ( * ) +-------------- +The asterisk indicates that the cron expression matches for all values of the field. E.g., using an asterisk in the 4th field (month) indicates every month. + +Slash ( / ) +----------- +Slashes describe increments of ranges. For example `3-59/15` in the minute field indicate the third minute of the hour and every 15 minutes thereafter. The form `*/...` is equivalent to the form "first-last/...", that is, an increment over the largest possible range of the field. + +Comma ( , ) +----------- +Commas are used to separate items of a list. For example, using `MON,WED,FRI` in the 5th field (day of week) means Mondays, Wednesdays and Fridays. + +Hyphen ( - ) +------------ +Hyphens define ranges. For example, 2000-2010 indicates every year between 2000 and 2010 AD, inclusive. + +L +- +`L` stands for "last". When used in the day-of-week field, it allows you to specify constructs such as "the last Friday" (`5L`) of a given month. In the day-of-month field, it specifies the last day of the month. + +W +- +The `W` character is allowed for the day-of-month field. This character is used to specify the weekday (Monday-Friday) nearest the given day. As an example, if you were to specify `15W` as the value for the day-of-month field, the meaning is: "the nearest weekday to the 15th of the month." So, if the 15th is a Saturday, the trigger fires on Friday the 14th. If the 15th is a Sunday, the trigger fires on Monday the 16th. If the 15th is a Tuesday, then it fires on Tuesday the 15th. However if you specify `1W` as the value for day-of-month, and the 1st is a Saturday, the trigger fires on Monday the 3rd, as it does not 'jump' over the boundary of a month's days. The `W` character can be specified only when the day-of-month is a single day, not a range or list of days. + +Hash ( # ) +---------- +`#` is allowed for the day-of-week field, and must be followed by a number between one and five. It allows you to specify constructs such as "the second Friday" of a given month. + +Question mark ( ? ) +------------------- +Note: Question mark is a non-standard character and exists only in some cron implementations. It is used instead of `*` for leaving either day-of-month or day-of-week blank. + +With the following differences: + +* Supports optional second field (before minute field) * If five fields are present, a wildcard year field is appended -* If six field are present, "0" is prepended as second field -* Domain for day-of-week field is [0-7] instead of [0-6], 7 being Sunday, like zero. -* `@reboot` is not supported, as it is meaningless for a cron expression parser library -* As of now, the behavior of the code is undetermined if a malformed cron expression is supplied (most likely, code will panic) +* If six field are present, `0` is prepended as second field, that is, `* * * * * *` internally become `0 * * * * * *`. +* Domain for day-of-week field is [0-7] instead of [0-6], 7 being Sunday (like 0). +* `@reboot` is not supported +* As of now, the behavior of the code is undetermined if a malformed cron expression is supplied Install ------- - go get github.com/gorhill/cronexpression Usage ----- - Import the library: import "github.com/gorhill/cronexpression" From af4a7c4204d291f064d3cf8b8aa1f4ef36b3dfde Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 29 Aug 2013 19:39:08 -0400 Subject: [PATCH 07/17] fleshing out --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 90b8ecb..0276218 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Cron expression parser in Go language (golang). Given a cron expression and a time stamp, you can get the next time stamp which satisfy the cron expression. +In another project, I decided to use Cron syntax to encode scheduling information. Thus this standalone library to parse and execute cron expressions. + Implementation -------------- The reference documentation for this implementation is found at From 664335f344d1fee5f72becd98fa37ee147bd1967 Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 29 Aug 2013 19:41:35 -0400 Subject: [PATCH 08/17] fleshing out --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0276218..2a5c0d8 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ https://en.wikipedia.org/wiki/Cron#CRON_expression, which I copy/pasted here (la Field name Mandatory? Allowed values Allowed special characters ---------- ---------- -------------- -------------------------- + Seconds No 0-59 * / , - Minutes Yes 0-59 * / , - Hours Yes 0-23 * / , - Day of month Yes 1-31 * / , - ? L W From 584af0dd083c644a8be53f2b4f03a9aaffb96315 Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 29 Aug 2013 19:48:59 -0400 Subject: [PATCH 09/17] fleshing out --- README.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2a5c0d8..5653a4b 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ https://en.wikipedia.org/wiki/Cron#CRON_expression, which I copy/pasted here (la Seconds No 0-59 * / , - Minutes Yes 0-59 * / , - Hours Yes 0-23 * / , - - Day of month Yes 1-31 * / , - ? L W + Day of month Yes 1-31 * / , - L W Month Yes 1-12 or JAN-DEC * / , - - Day of week Yes 0-6 or SUN-SAT * / , - ? L # + Day of week Yes 0-6 or SUN-SAT * / , - L # Year No 1970–2099 * / , - Asterisk ( * ) @@ -49,13 +49,20 @@ Hash ( # ) ---------- `#` is allowed for the day-of-week field, and must be followed by a number between one and five. It allows you to specify constructs such as "the second Friday" of a given month. -Question mark ( ? ) -------------------- -Note: Question mark is a non-standard character and exists only in some cron implementations. It is used instead of `*` for leaving either day-of-month or day-of-week blank. +Predefined cron expressions +--------------------------- +(Copied from https://en.wikipedia.org/wiki/Cron#Predefined_scheduling_definitions, with text modified according to this implementation) -With the following differences: + Entry Description Equivalent to + @annually Run once a year at midnight in the morning of January 1 0 0 0 1 1 * * + @yearly Run once a year at midnight in the morning of January 1 0 0 0 1 1 * * + @monthly Run once a month at midnight in the morning of the first of the month 0 0 0 1 * * * + @weekly Run once a week at midnight in the morning of Sunday 0 0 0 * * 0 * + @daily Run once a day at midnight 0 0 0 * * * * + @hourly Run once an hour at the beginning of the hour 0 0 * * * * * -* Supports optional second field (before minute field) +Other details +------------- * If five fields are present, a wildcard year field is appended * If six field are present, `0` is prepended as second field, that is, `* * * * * *` internally become `0 * * * * * *`. * Domain for day-of-week field is [0-7] instead of [0-6], 7 being Sunday (like 0). From ffde273e504cb1000e3278d423aa6cd5582d20e0 Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 29 Aug 2013 19:53:58 -0400 Subject: [PATCH 10/17] fleshing out --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5653a4b..b9b8cb2 100644 --- a/README.md +++ b/README.md @@ -60,13 +60,13 @@ Predefined cron expressions @weekly Run once a week at midnight in the morning of Sunday 0 0 0 * * 0 * @daily Run once a day at midnight 0 0 0 * * * * @hourly Run once an hour at the beginning of the hour 0 0 * * * * * + @reboot Not supported Other details ------------- -* If five fields are present, a wildcard year field is appended -* If six field are present, `0` is prepended as second field, that is, `* * * * * *` internally become `0 * * * * * *`. -* Domain for day-of-week field is [0-7] instead of [0-6], 7 being Sunday (like 0). -* `@reboot` is not supported +* If only five fields are present, a wildcard year field is appended, that is, `* * * * *` internally become `0 * * * * * *`. +* If only six field are present, `0` is prepended as second field, that is, `* * * * * 2013` internally become `0 * * * * * 2013`. +* Domain for day-of-week field is [0-7] instead of [0-6], 7 being Sunday (like 0). This to comply with http://linux.die.net/man/5/crontab#. * As of now, the behavior of the code is undetermined if a malformed cron expression is supplied Install From 9774af70eac13fd5d38ce4980c6432a8644eaf93 Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 29 Aug 2013 19:55:54 -0400 Subject: [PATCH 11/17] fleshing out --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b9b8cb2..0d23b1b 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,8 @@ Predefined cron expressions Other details ------------- -* If only five fields are present, a wildcard year field is appended, that is, `* * * * *` internally become `0 * * * * * *`. -* If only six field are present, `0` is prepended as second field, that is, `* * * * * 2013` internally become `0 * * * * * 2013`. +* If only six field are present, a `0` second field is prepended, that is, `* * * * * 2013` internally become `0 * * * * * 2013`. +* If only five fields are present, a `0` second field is prepended and a wildcard year field is appended, that is, `* * * * *` internally become `0 * * * * * *`. * Domain for day-of-week field is [0-7] instead of [0-6], 7 being Sunday (like 0). This to comply with http://linux.die.net/man/5/crontab#. * As of now, the behavior of the code is undetermined if a malformed cron expression is supplied From a8244603b572a665a5ea427d99ad94b843adaf94 Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 29 Aug 2013 20:06:45 -0400 Subject: [PATCH 12/17] fleshing out --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0d23b1b..ffb3917 100644 --- a/README.md +++ b/README.md @@ -91,5 +91,7 @@ to create a `CronExpression` object once and keep a copy of it for reuse: cronexpr := cronexpression.NewCronExpression("0 0 29 2 *") nextTime := cronexpr.NextTime(time.Now()) + +`Use cronexpression.NoMatch(nextTime)` to find out whether a valid time was returned. For exemple, `cronexpression.NoMatch(cronexpression.NextTimeFromCronString("* * * * * 1980", time.Now()))` will return `true`, whereas `cronexpression.NoMatch(cronexpression.NextTimeFromCronString("* * * * * 2050", time.Now()))` will return false (as of 2013-08-29...) From 6d87e60fc4602e3f037cca84d6657acd415cdf08 Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 29 Aug 2013 20:07:47 -0400 Subject: [PATCH 13/17] fleshing out --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ffb3917..ff2cfe7 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,10 @@ to create a `CronExpression` object once and keep a copy of it for reuse: cronexpr := cronexpression.NewCronExpression("0 0 29 2 *") nextTime := cronexpr.NextTime(time.Now()) -`Use cronexpression.NoMatch(nextTime)` to find out whether a valid time was returned. For exemple, `cronexpression.NoMatch(cronexpression.NextTimeFromCronString("* * * * * 1980", time.Now()))` will return `true`, whereas `cronexpression.NoMatch(cronexpression.NextTimeFromCronString("* * * * * 2050", time.Now()))` will return false (as of 2013-08-29...) +`Use cronexpression.NoMatch(nextTime)` to find out whether a valid time was returned. For exemple, + cronexpression.NoMatch(cronexpression.NextTimeFromCronString("* * * * * 1980", time.Now())) +will return `true`, whereas + cronexpression.NoMatch(cronexpression.NextTimeFromCronString("* * * * * 2050", time.Now())) +will return false (as of 2013-08-29...) From 536d6c6cd241936e7f9ca5e9495df8e69090c90a Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 29 Aug 2013 20:08:22 -0400 Subject: [PATCH 14/17] fleshing out --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ff2cfe7..55c28aa 100644 --- a/README.md +++ b/README.md @@ -93,9 +93,12 @@ to create a `CronExpression` object once and keep a copy of it for reuse: nextTime := cronexpr.NextTime(time.Now()) `Use cronexpression.NoMatch(nextTime)` to find out whether a valid time was returned. For exemple, - cronexpression.NoMatch(cronexpression.NextTimeFromCronString("* * * * * 1980", time.Now())) -will return `true`, whereas - cronexpression.NoMatch(cronexpression.NextTimeFromCronString("* * * * * 2050", time.Now())) -will return false (as of 2013-08-29...) - + + cronexpression.NoMatch(cronexpression.NextTimeFromCronString("* * * * * 1980", time.Now())) + +will return `true`, whereas + + cronexpression.NoMatch(cronexpression.NextTimeFromCronString("* * * * * 2050", time.Now())) + +will return false (as of 2013-08-29...) From 3ce59d04d0d459911f8e8b2ce03714a19a26d826 Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 29 Aug 2013 20:09:16 -0400 Subject: [PATCH 15/17] fleshing out --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 55c28aa..8968226 100644 --- a/README.md +++ b/README.md @@ -100,5 +100,5 @@ will return `true`, whereas cronexpression.NoMatch(cronexpression.NextTimeFromCronString("* * * * * 2050", time.Now())) -will return false (as of 2013-08-29...) +will return `false` (as of 2013-08-29...) From 16d55f80353f26accac9836bc9a0b58b44713601 Mon Sep 17 00:00:00 2001 From: gorhill Date: Thu, 29 Aug 2013 20:10:42 -0400 Subject: [PATCH 16/17] fleshing out --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8968226..94b69aa 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ to create a `CronExpression` object once and keep a copy of it for reuse: cronexpr := cronexpression.NewCronExpression("0 0 29 2 *") nextTime := cronexpr.NextTime(time.Now()) -`Use cronexpression.NoMatch(nextTime)` to find out whether a valid time was returned. For exemple, +Use `cronexpression.NoMatch()` to find out whether a valid time was returned. For exemple, cronexpression.NoMatch(cronexpression.NextTimeFromCronString("* * * * * 1980", time.Now())) From 1c56081e0027a04fff73faab025dbb4f96dd2dc4 Mon Sep 17 00:00:00 2001 From: gorhill Date: Fri, 30 Aug 2013 07:36:30 -0400 Subject: [PATCH 17/17] fleshing out --- cronexpression.go | 4 ++-- cronexpression_test.go | 20 +++++++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/cronexpression.go b/cronexpression.go index 2cb6574..b64eb00 100644 --- a/cronexpression.go +++ b/cronexpression.go @@ -604,7 +604,7 @@ func (cronexpr *CronExpression) dayofmonthFieldParse(cronField string) error { cronexpr.daysOfMonth = make(map[int]bool) // days of month map cronexpr.workdaysOfMonth = make(map[int]bool) // work day of month map - // "You can also mix all of the above, as in: 1-5,10,12,20-30/5" + // Comma separator is used to mix different allowed syntax cronList := strings.Split(cronField, ",") for _, s := range cronList { // "/" @@ -651,7 +651,7 @@ func genericFieldParse(cronField string, min, max int) []int { // Defaults values := make(map[int]bool) - // "You can also mix all of the above, as in: 1-5,10,12,20-30/5" + // Comma separator is used to mix different allowed syntax cronList := strings.Split(cronField, ",") for _, s := range cronList { // "/" diff --git a/cronexpression_test.go b/cronexpression_test.go index 002e0db..5501baa 100644 --- a/cronexpression_test.go +++ b/cronexpression_test.go @@ -39,6 +39,7 @@ var crontests = []crontest{ []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"}, @@ -61,7 +62,7 @@ var crontests = []crontest{ }, }, - // Minutes with intervals + // Minutes with interval { "17-43/5 * * * *", "2006-01-02 15:04:05", @@ -89,10 +90,22 @@ var crontests = []crontest{ {"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"}, + }, + }, + // TODO: more tests } @@ -101,8 +114,9 @@ func TestCronExpressions(t *testing.T) { for _, times := range test.times { from, _ := time.Parse("2006-01-02 15:04:05", times.from) next := cronexpression.NextTimeFromCronString(test.expr, from) - if next.Format(test.layout) != times.next { - t.Errorf("(\"%s\").NextTime(\"%s\") = \"%s\", got \"%s\"", test.expr, times.from, times.next, next.Format(test.layout)) + nextstr := next.Format(test.layout) + if nextstr != times.next { + t.Errorf("(\"%s\").NextTime(\"%s\") = \"%s\", got \"%s\"", test.expr, times.from, times.next, nextstr) } } }