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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 55 additions & 4 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,25 +133,76 @@ func (p Parser) Parse(spec string) (Schedule, error) {
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)
month = field(fields[4], months)
dayofweek = field(fields[5], dow)
)
if err != nil {
return nil, err
}
dayNum, weekNumInt, isLastWeek, ok := parseDowInNthWeekFormat(fields[5])
dayOfWeek := func() uint64 {
if ok {
return 0
}
return field(fields[5], dow)
}()
if err != nil {
return nil, err
}

return &SpecSchedule{
Second: second,
Minute: minute,
Hour: hour,
Dom: dayofmonth,
Dom: dayOfMonth,
Month: month,
Dow: dayofweek,
Dow: dayOfWeek,
Location: loc,
Extra: Extra{
DayOfWeek: dayNum,
WeekNumber: weekNumInt,
LastWeek: isLastWeek,
Valid: ok,
},
}, nil
}

// parseDowInNthWeekFormat parse the dow spec of the cron spec
// It returns dayOfTheWeek, occurrence or the week number in month, true if the week number is last week, ok
// For example:
// 6#3 => 6, 3, false, true
// 6#L => 6, 0, true, true
// XYZ => 0, 0, false, false
// 8#9 => 0, 0, false, false
func parseDowInNthWeekFormat(spec string) (uint8, uint8, bool, bool) {
var dayOfWeek uint8 = 0
var weekNumber uint8 = 0
if len(spec) != 3 {
return 0, 0, false, false
}
day := spec[0:1]
if day >= "0" && day <= "6" {
dayOfWeek = day[0] - '0'
} else {
return 0, 0, false, false
}

shebang := spec[1:2]
if shebang != "#" {
return 0, 0, false, false
}

week := spec[2:]
if week >= "1" && week <= "4" {
weekNumber = week[0] - '0'
} else if week == "L" {
return dayOfWeek, 0, true, true
} else {
return 0, 0, false, false
}
return dayOfWeek, weekNumber, false, true
}

// normalizeFields takes a subset set of the time fields and returns the full set
// with defaults (zeroes) populated for unset fields.
//
Expand Down
160 changes: 156 additions & 4 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,83 @@ func TestParseSchedule(t *testing.T) {
}
}

func TestParseScheduleWithDOWAndWeekNum(t *testing.T) {
tokyo, _ := time.LoadLocation("Asia/Tokyo")
entries := []struct {
parser Parser
expr string
expected Schedule
}{
{
parser: standardParser,
expr: "5 10 10 * 3#2",
expected: &SpecSchedule{
Second: 1 << 0,
Minute: 1 << 5,
Hour: 1 << 10,
Dom: 1 << 10,
Month: all(months),
Dow: 0,
Location: time.Local,
Extra: Extra{
DayOfWeek: 3,
WeekNumber: 2,
LastWeek: false,
Valid: true,
},
},
},
{
parser: standardParser,
expr: "CRON_TZ=Asia/Tokyo 5 10 10 * 3#2",
expected: &SpecSchedule{
Second: 1 << 0,
Minute: 1 << 5,
Hour: 1 << 10,
Dom: 1 << 10,
Month: all(months),
Dow: 0,
Location: tokyo,
Extra: Extra{
DayOfWeek: 3,
WeekNumber: 2,
LastWeek: false,
Valid: true,
},
},
},
{
parser: standardParser,
expr: "CRON_TZ=Asia/Tokyo 5 10 10 * 4#L",
expected: &SpecSchedule{
Second: 1 << 0,
Minute: 1 << 5,
Hour: 1 << 10,
Dom: 1 << 10,
Month: all(months),
Dow: 0,
Location: tokyo,
Extra: Extra{
DayOfWeek: 4,
WeekNumber: 0,
LastWeek: true,
Valid: true,
},
},
},
}

for _, c := range entries {
actual, err := c.parser.Parse(c.expr)
if err != nil {
t.Errorf("%s => unexpected error %v", c.expr, err)
}
if !reflect.DeepEqual(actual, c.expected) {
t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual)
}
}
}

func TestOptionalSecondSchedule(t *testing.T) {
parser := NewParser(SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor)
entries := []struct {
Expand Down Expand Up @@ -320,7 +397,7 @@ func TestStandardSpecSchedule(t *testing.T) {
}{
{
expr: "5 * * * *",
expected: &SpecSchedule{1 << seconds.min, 1 << 5, all(hours), all(dom), all(months), all(dow), time.Local},
expected: &SpecSchedule{1 << seconds.min, 1 << 5, all(hours), all(dom), all(months), all(dow), time.Local, Extra{}},
},
{
expr: "@every 5m",
Expand Down Expand Up @@ -358,16 +435,91 @@ func TestNoDescriptorParser(t *testing.T) {
}
}

func TestParseDowInNthWeekFormat(t *testing.T) {
entries := []struct {
spec string
dayOfWeek uint8
weekNumber uint8
lastWeek bool
ok bool
}{
{
spec: "3#3",
dayOfWeek: 3,
weekNumber: 3,
lastWeek: false,
ok: true,
},
{
spec: "3#5",
dayOfWeek: 0,
weekNumber: 0,
lastWeek: false,
ok: false,
},
{
spec: "7#5",
dayOfWeek: 0,
weekNumber: 0,
lastWeek: false,
ok: false,
},
{
spec: "1#L",
dayOfWeek: 1,
weekNumber: 0,
lastWeek: true,
ok: true,
},
{
spec: "##L",
dayOfWeek: 0,
weekNumber: 0,
lastWeek: false,
ok: false,
},
{
spec: "#L",
dayOfWeek: 0,
weekNumber: 0,
lastWeek: false,
ok: false,
}, {
spec: "2KL",
dayOfWeek: 0,
weekNumber: 0,
lastWeek: false,
ok: false,
},
}

for _, c := range entries {
dayOfWeek, weekNumber, lastWeek, ok := parseDowInNthWeekFormat(c.spec)
if dayOfWeek != c.dayOfWeek {
t.Errorf("%s => expected %v, got %v", c.spec, c.dayOfWeek, dayOfWeek)
}
if weekNumber != c.weekNumber {
t.Errorf("%s => expected %v, got %v", c.spec, c.weekNumber, weekNumber)
}
if lastWeek != c.lastWeek {
t.Errorf("%s => expected %v, got %v", c.spec, c.lastWeek, lastWeek)
}
if ok != c.ok {
t.Errorf("%s => expected %v, got %v", c.spec, c.ok, ok)
}
}
}

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{}}
}

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{}}
}

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 Down
110 changes: 108 additions & 2 deletions spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ type SpecSchedule struct {

// Override location for this schedule.
Location *time.Location
// Extra for nth Day of the Week
Extra Extra
}

// Extra attributes is currently storing the spec config for nth Day of the Week
type Extra struct {
DayOfWeek uint8 // 0 - 6, same as, time.Weekday
WeekNumber uint8 // Week of the month, value ranges from 1 - 4
LastWeek bool // true, if the last week
Valid bool // true, if the Object is the valid
}

// bounds provides a range of acceptable values (plus a map of name to value).
Expand Down Expand Up @@ -177,12 +187,108 @@ 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.Valid {
if s.Extra.LastWeek {
if matchDoWForTheLastWeek(t, s.Extra.DayOfWeek) {
return true
}
} else {
if matchDayOfTheWeekAndWeekInMonth(t, s.Extra.WeekNumber, s.Extra.DayOfWeek) {
return true
}
}
}
var (
domMatch bool = 1<<uint(t.Day())&s.Dom > 0
dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0
domMatch = 1<<uint(t.Day())&s.Dom > 0
dowMatch = 1<<uint(t.Weekday())&s.Dow > 0
)
if s.Dom&starBit > 0 || s.Dow&starBit > 0 {
return domMatch && dowMatch
}
return domMatch || dowMatch
}

// matchDayOfTheWeekAndWeekInMonth returns true if the time, t, has week day = dayOfTheWeek
// and the dayOfTheWeek is occurring (weekInTheMonth)th time
// for example, it will return true if
// t = 8th June 2020, weekInTheMonth = 2nd(2), dayOfTheWeek = Monday(0)
func matchDayOfTheWeekAndWeekInMonth(t time.Time, weekInTheMonth, dayOfTheWeek uint8) bool {
valid := false
switch weekInTheMonth {
case 1:
valid = t.Day() <= 7 && t.Day() >= 1
case 2:
valid = t.Day() <= 14 && t.Day() >= 8
case 3:
valid = t.Day() <= 21 && t.Day() >= 15
case 4:
valid = t.Day() <= 28 && t.Day() >= 22
}
if !valid {
return false
}
switch t.Weekday() {
case time.Sunday:
return dayOfTheWeek == 0
case time.Monday:
return dayOfTheWeek == 1
case time.Tuesday:
return dayOfTheWeek == 2
case time.Wednesday:
return dayOfTheWeek == 3
case time.Thursday:
return dayOfTheWeek == 4
case time.Friday:
return dayOfTheWeek == 5
case time.Saturday:
return dayOfTheWeek == 6
default:
return false
}
}

func matchLastWeekDOW(numberOfDaysInMonth int, t time.Time, dow uint8) bool {
if numberOfDaysInMonth-t.Day() > 6 {
return false
}
switch t.Weekday() {
case time.Sunday:
return dow == 0
case time.Monday:
return dow == 1
case time.Tuesday:
return dow == 2
case time.Wednesday:
return dow == 3
case time.Thursday:
return dow == 4
case time.Friday:
return dow == 5
case time.Saturday:
return dow == 6
default:
return false
}
}

// matchDoWForTheLastWeek returns true if the time, t, is of the last week of the month
// and day of the week is dow
func matchDoWForTheLastWeek(t time.Time, dow uint8) 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 matchLastWeekDOW(30, t, dow)
case time.February:
if leapYear {
return matchLastWeekDOW(29, t, dow)
}
return matchLastWeekDOW(28, t, dow)
default:
return matchLastWeekDOW(31, t, dow)
}
}
Loading