diff --git a/README.md b/README.md index f3d4a9b..48c8662 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,13 @@ Hyphens define ranges. For example, 2000-2010 indicates every year between 2000 `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. +The `W` character is allowed for the day-of-month field. This character is used to specify the business day (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 business day 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. + +The `W` character can also be combined with `L` to mean "the last business day of the month." #### 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. diff --git a/cronexpr.go b/cronexpr.go index 5189e0f..bad9553 100644 --- a/cronexpr.go +++ b/cronexpr.go @@ -70,16 +70,22 @@ func Parse(cronLine string) (*Expression, error) { cron := cronNormalizer.Replace(cronLine) indices := fieldFinder.FindAllStringIndex(cron, -1) - if len(indices) < 5 { + fieldCount := len(indices) + if fieldCount < 5 { return nil, fmt.Errorf("missing field(s)") } + // ignore fields beyond 7th + if fieldCount > 7 { + fieldCount = 7 + } - expr := Expression{} - field := 0 + var expr = Expression{} + var field = 0 + var err error // second field (optional) - if len(indices) >= 7 { - err := expr.secondFieldHandler(cron[indices[field][0]:indices[field][1]]) + if fieldCount == 7 { + err = expr.secondFieldHandler(cron[indices[field][0]:indices[field][1]]) if err != nil { return nil, err } @@ -89,7 +95,7 @@ func Parse(cronLine string) (*Expression, error) { } // minute field - err := expr.minuteFieldHandler(cron[indices[field][0]:indices[field][1]]) + err = expr.minuteFieldHandler(cron[indices[field][0]:indices[field][1]]) if err != nil { return nil, err } @@ -124,7 +130,7 @@ func Parse(cronLine string) (*Expression, error) { field += 1 // year field - if field < len(indices) { + if field < fieldCount { err = expr.yearFieldHandler(cron[indices[field][0]:indices[field][1]]) if err != nil { return nil, err diff --git a/cronexpr_next.go b/cronexpr_next.go index 819df19..19f0ac2 100644 --- a/cronexpr_next.go +++ b/cronexpr_next.go @@ -86,102 +86,6 @@ func (expr *Expression) nextMonth(t time.Time) time.Time { /******************************************************************************/ -func workdayOfMonth(targetDom, lastDom time.Time) int { - dom := targetDom.Day() - dow := targetDom.Weekday() - // If saturday, then friday - if dow == time.Saturday { - if dom > 1 { - dom -= 1 - } else { - dom += 2 - } - // If sunday, then monday - } else if dow == time.Sunday { - if dom < lastDom.Day() { - dom += 1 - } else { - dom -= 2 - } - } - return dom -} - -func (expr *Expression) calculateActualDaysOfMonth(year, month int) []int { - actualDaysOfMonthMap := make(map[int]bool) - firstDayOfMonth := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC) - lastDayOfMonth := firstDayOfMonth.AddDate(0, 1, -1) - - // 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 expr.daysOfMonthRestricted || expr.daysOfWeekRestricted == false { - // Last day of month - if expr.lastDayOfMonth { - actualDaysOfMonthMap[lastDayOfMonth.Day()] = true - } - // Last work day of month - if expr.lastWorkdayOfMonth { - actualDaysOfMonthMap[workdayOfMonth(lastDayOfMonth, lastDayOfMonth)] = true - } - // Days of month - for v := range expr.daysOfMonth { - // Ignore days beyond end of month - if v <= lastDayOfMonth.Day() { - actualDaysOfMonthMap[v] = true - } - } - // Work days of month - // As per Wikipedia: month boundaries are not crossed. - for v := range expr.workdaysOfMonth { - // Ignore days beyond end of month - if v <= lastDayOfMonth.Day() { - actualDaysOfMonthMap[workdayOfMonth(firstDayOfMonth.AddDate(0, 0, v-1), lastDayOfMonth)] = true - } - } - } - - if expr.daysOfWeekRestricted { - // How far first sunday is from first day of month - offset := 7 - int(firstDayOfMonth.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 expr.daysOfWeek { - v := 1 + w*7 + (offset+v)%7 - if v <= lastDayOfMonth.Day() { - 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 expr.specificWeekDaysOfWeek { - v := 1 + 7*(v/7) + (offset+v)%7 - if v <= lastDayOfMonth.Day() { - actualDaysOfMonthMap[v] = true - } - } - // Last days of week of the month - lastWeekOrigin := firstDayOfMonth.AddDate(0, 1, -7) - offset = 7 - int(lastWeekOrigin.Weekday()) - for v := range expr.lastWeekDaysOfWeek { - v := lastWeekOrigin.Day() + (offset+v)%7 - if v <= lastDayOfMonth.Day() { - actualDaysOfMonthMap[v] = true - } - } - } - - return toList(actualDaysOfMonthMap) -} - -/******************************************************************************/ - func (expr *Expression) nextDayOfMonth(t time.Time) time.Time { // Find index at which item in list is greater or equal to // candidate day of month @@ -266,3 +170,99 @@ func (expr *Expression) nextSecond(t time.Time) time.Time { 0, t.Location()) } + +/******************************************************************************/ + +func (expr *Expression) calculateActualDaysOfMonth(year, month int) []int { + actualDaysOfMonthMap := make(map[int]bool) + firstDayOfMonth := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC) + lastDayOfMonth := firstDayOfMonth.AddDate(0, 1, -1) + + // 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 expr.daysOfMonthRestricted || expr.daysOfWeekRestricted == false { + // Last day of month + if expr.lastDayOfMonth { + actualDaysOfMonthMap[lastDayOfMonth.Day()] = true + } + // Last work day of month + if expr.lastWorkdayOfMonth { + actualDaysOfMonthMap[workdayOfMonth(lastDayOfMonth, lastDayOfMonth)] = true + } + // Days of month + for v := range expr.daysOfMonth { + // Ignore days beyond end of month + if v <= lastDayOfMonth.Day() { + actualDaysOfMonthMap[v] = true + } + } + // Work days of month + // As per Wikipedia: month boundaries are not crossed. + for v := range expr.workdaysOfMonth { + // Ignore days beyond end of month + if v <= lastDayOfMonth.Day() { + actualDaysOfMonthMap[workdayOfMonth(firstDayOfMonth.AddDate(0, 0, v-1), lastDayOfMonth)] = true + } + } + } + + if expr.daysOfWeekRestricted { + // How far first sunday is from first day of month + offset := 7 - int(firstDayOfMonth.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 expr.daysOfWeek { + v := 1 + w*7 + (offset+v)%7 + if v <= lastDayOfMonth.Day() { + 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 expr.specificWeekDaysOfWeek { + v := 1 + 7*(v/7) + (offset+v)%7 + if v <= lastDayOfMonth.Day() { + actualDaysOfMonthMap[v] = true + } + } + // Last days of week of the month + lastWeekOrigin := firstDayOfMonth.AddDate(0, 1, -7) + offset = 7 - int(lastWeekOrigin.Weekday()) + for v := range expr.lastWeekDaysOfWeek { + v := lastWeekOrigin.Day() + (offset+v)%7 + if v <= lastDayOfMonth.Day() { + actualDaysOfMonthMap[v] = true + } + } + } + + return toList(actualDaysOfMonthMap) +} + +func workdayOfMonth(targetDom, lastDom time.Time) int { + dom := targetDom.Day() + dow := targetDom.Weekday() + // If saturday, then friday + if dow == time.Saturday { + if dom > 1 { + dom -= 1 + } else { + dom += 2 + } + // If sunday, then monday + } else if dow == time.Sunday { + if dom < lastDom.Day() { + dom += 1 + } else { + dom -= 2 + } + } + return dom +} diff --git a/cronexpr_parse.go b/cronexpr_parse.go index fd02482..f7df2b5 100644 --- a/cronexpr_parse.go +++ b/cronexpr_parse.go @@ -371,6 +371,29 @@ func (expr *Expression) domFieldHandler(s string) error { /******************************************************************************/ +func populateOne(values map[int]bool, v int) { + values[v] = true +} + +func populateMany(values map[int]bool, min, max, step int) { + 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 +} + +/******************************************************************************/ + func genericFieldParse(s string, desc fieldDescriptor) ([]*cronDirective, error) { // At least one entry must be present indices := entryFinder.FindAllStringIndex(s, -1) @@ -451,29 +474,6 @@ func genericFieldParse(s string, desc fieldDescriptor) ([]*cronDirective, error) /******************************************************************************/ -func populateOne(values map[int]bool, v int) { - values[v] = true -} - -func populateMany(values map[int]bool, min, max, step int) { - 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 -} - -/******************************************************************************/ - func makeLayoutRegexp(layout, value string) *regexp.Regexp { layout = strings.Replace(layout, `%value%`, value, -1) re := layoutRegexp[layout]