Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hello, I changed the code again to support the complete 'L' #279

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jobs. In addition, v3 adds support for Go Modules, cleans up rough edges like
the timezone support, and fixes a number of bugs.

New features:
- Support for the "L" character at cron format.

- Support for Go modules. Callers must now import this library as
`github.com/robfig/cron/v3`, instead of `gopkg.in/...`
Expand Down
49 changes: 44 additions & 5 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,23 +120,30 @@ func (p Parser) Parse(spec string) (Schedule, error) {
return nil, err
}

field := func(field string, r bounds) uint64 {
field := func(field string, r bounds, ex ...string) uint64 {
if err != nil {
return 0
}
var bits uint64
bits, err = getField(field, r)
bits, err = getField(field, r, ex...)
return bits
}

var (
second = field(fields[0], seconds)
minute = field(fields[1], minutes)
hour = field(fields[2], hours)
dayofmonth = field(fields[3], dom)
dayofmonth = field(fields[3], dom, fields[5])
month = field(fields[4], months)
dayofweek = field(fields[5], dow)
)

matchDayofWeek := matchLastDayOfGivenMonth(fields[5])
dayofweek := func() uint64 {
if matchDayofWeek {
return 0
}
return field(fields[5], dow)
}()
if err != nil {
return nil, err
}
Expand All @@ -149,6 +156,10 @@ func (p Parser) Parse(spec string) (Schedule, error) {
Month: month,
Dow: dayofweek,
Location: loc,
Extra: Extra{
L: fields[3] == "L" || matchDayofWeek,
DayOfWeek: getNLDayOfWeek(fields[5]),
},
}, nil
}

Expand Down Expand Up @@ -230,11 +241,39 @@ func ParseStandard(standardSpec string) (Schedule, error) {
return standardParser.Parse(standardSpec)
}

// in the day-of-week field, it specifying "the last Day" of a given month,"the last Friday" of a given month,is '5L'
func matchLastDayOfGivenMonth(spec string) bool {
// 0L - 6L ,length is 2
if len(spec) != 2 || spec[1:] != "L" {
return false
}
day := spec[0:1]
if day >= "0" && day <= "6" {
return true
}
return false
}

func getNLDayOfWeek(field string) uint {
if matchLastDayOfGivenMonth(field) {
return uint([]byte(field[0:1])[0] - '0')
}
return 9
}

// getField returns an Int with the bits set representing all of the times that
// the field represents or error parsing field value. A "field" is a comma-separated
// list of "ranges".
func getField(field string, r bounds) (uint64, error) {
func getField(field string, r bounds, ex ...string) (uint64, error) {
var bits uint64
if r.min == 1 && r.max == 31 && field == "L" {
// day of month's L
field = "28,29,30,31"
}
if len(ex) > 0 && matchLastDayOfGivenMonth(ex[0]) {
// day of week's (0-6)L
field += ",21,22,23,24,25,26,27"
}
ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
for _, expr := range ranges {
bit, err := getRange(expr, r)
Expand Down
21 changes: 16 additions & 5 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ func TestParseSchedule(t *testing.T) {
Month: all(months),
Dow: all(dow),
Location: time.Local,
Extra: Extra{
L: false,
DayOfWeek: 9,
},
},
},
}
Expand Down Expand Up @@ -319,8 +323,10 @@ func TestStandardSpecSchedule(t *testing.T) {
err string
}{
{
expr: "5 * * * *",
expected: &SpecSchedule{1 << seconds.min, 1 << 5, all(hours), all(dom), all(months), all(dow), time.Local},
expr: "5 * * * *",
expected: &SpecSchedule{1 << seconds.min, 1 << 5, all(hours), all(dom), all(months), all(dow), time.Local, Extra{
L: false,
DayOfWeek: 9}},
},
{
expr: "@every 5m",
Expand Down Expand Up @@ -359,15 +365,19 @@ func TestNoDescriptorParser(t *testing.T) {
}

func every5min(loc *time.Location) *SpecSchedule {
return &SpecSchedule{1 << 0, 1 << 5, all(hours), all(dom), all(months), all(dow), loc}
return &SpecSchedule{1 << 0, 1 << 5, all(hours), all(dom), all(months), all(dow), loc, Extra{
L: false,
DayOfWeek: 9}}
}

func every5min5s(loc *time.Location) *SpecSchedule {
return &SpecSchedule{1 << 5, 1 << 5, all(hours), all(dom), all(months), all(dow), loc}
return &SpecSchedule{1 << 5, 1 << 5, all(hours), all(dom), all(months), all(dow), loc, Extra{
L: false,
DayOfWeek: 9}}
}

func midnight(loc *time.Location) *SpecSchedule {
return &SpecSchedule{1, 1, 1, all(dom), all(months), all(dow), loc}
return &SpecSchedule{1, 1, 1, all(dom), all(months), all(dow), loc, Extra{}}
}

func annual(loc *time.Location) *SpecSchedule {
Expand All @@ -379,5 +389,6 @@ func annual(loc *time.Location) *SpecSchedule {
Month: 1 << months.min,
Dow: all(dow),
Location: loc,
Extra: Extra{},
}
}
105 changes: 104 additions & 1 deletion spec.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cron

import "time"
import (
"time"
)

// SpecSchedule specifies a duty cycle (to the second granularity), based on a
// traditional crontab specification. It is computed initially and stored as bit sets.
Expand All @@ -9,6 +11,15 @@ type SpecSchedule struct {

// Override location for this schedule.
Location *time.Location

// Extra
Extra Extra
}

// Extra attributes
type Extra struct {
DayOfWeek uint // that N:0 - 6
L bool
}

// bounds provides a range of acceptable values (plus a map of name to value).
Expand Down Expand Up @@ -177,6 +188,16 @@ WRAP:
// dayMatches returns true if the schedule's day-of-week and day-of-month
// restrictions are satisfied by the given time.
func dayMatches(s *SpecSchedule, t time.Time) bool {
// If s.Extra.L means execute jobs at every last-day-of month,so need return immediately after this action scope
if s.Extra.L {
if isLastDay(t) && 1<<uint(t.Day())&s.Dom > 0 {
return true
}
if isNLastDayOfGivenMonth(t, s.Extra.DayOfWeek) {
return true
}
return false
}
var (
domMatch bool = 1<<uint(t.Day())&s.Dom > 0
dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0
Expand All @@ -186,3 +207,85 @@ func dayMatches(s *SpecSchedule, t time.Time) bool {
}
return domMatch || dowMatch
}

func matchNL(allday int, t time.Time, n uint) bool {
// is or not the last week of this month
if allday-t.Day() > 6 {
return false
}
switch t.Weekday() {
case time.Sunday:
return n == 0
case time.Monday:
return n == 1
case time.Tuesday:
return n == 2
case time.Wednesday:
return n == 3
case time.Thursday:
return n == 4
case time.Friday:
return n == 5
case time.Saturday:
return n == 6
default:
return false
}
}

// is or not the last day 'NL'of a given month
func isNLastDayOfGivenMonth(t time.Time, nl uint) bool {
year := t.Year()
leapYear := false
if (year%4 == 0 && year%100 != 0) || year%400 == 0 {
leapYear = true
}

switch t.Month() {
case time.April, time.June, time.September, time.November:
return matchNL(30, t, nl)
case time.February:
if leapYear {
return matchNL(29, t, nl)
}
return matchNL(28, t, nl)
default:
return matchNL(31, t, nl)
}
return false
}

// is or not the last day of month in this given t
func isLastDay(t time.Time) bool {
/*
January 31
February 28,29
March 31
April 30
May 31
June 30
July 31
August 31
September 30
October 31
November 30
December 31
*/
year := t.Year()
leapYear := false
if (year%4 == 0 && year%100 != 0) || year%400 == 0 {
leapYear = true
}

switch t.Month() {
case time.April, time.June, time.September, time.November:
return 30 == t.Day()
case time.February:
if leapYear {
return 29 == t.Day()
}
return 28 == t.Day()
default:
return 31 == t.Day()
}
}
5 changes: 5 additions & 0 deletions spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ func TestNext(t *testing.T) {

// Monthly job
{"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 3 3 * ?", "2012-12-03T03:00:00-0500"},
{"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 3 3 * 1L", "2012-11-26T03:00:00-0500"},
{"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 3 3 * 2L", "2012-11-27T03:00:00-0500"},

// Test the scenario of DST resulting in midnight not being a valid time.
// https://github.com/robfig/cron/issues/157
Expand Down Expand Up @@ -205,6 +207,9 @@ func TestErrors(t *testing.T) {
"60 0 * * *",
"0 60 * * *",
"0 0 * * XYZ",
"0 0 * * XYL",
"0 0 * * 12L",
"0 0 * * 8L",
}
for _, spec := range invalidSpecs {
_, err := ParseStandard(spec)
Expand Down