From e1f3594bd5a2fdfd1cbdb3ce44ef03c9946fb61e Mon Sep 17 00:00:00 2001 From: "pallav.jha" Date: Mon, 15 Jun 2020 20:41:32 +0530 Subject: [PATCH 1/3] add extras, adjustments in the spec and parser --- parser.go | 51 ++++++++++++++++++----- parser_test.go | 8 ++-- spec.go | 107 ++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 151 insertions(+), 15 deletions(-) diff --git a/parser.go b/parser.go index 8da6547a..36348e38 100644 --- a/parser.go +++ b/parser.go @@ -121,11 +121,11 @@ func (p Parser) Parse(spec string) (Schedule, error) { } field := func(field string, r bounds) uint64 { + var bits uint64 + bits, err = getField(field, r) if err != nil { return 0 } - var bits uint64 - bits, err = getField(field, r) return bits } @@ -133,25 +133,58 @@ 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 := dowInNthWeekFormat(fields[5]) + dayOfWeek := func() uint64 { + if ok { + return 0 + } + return field(fields[5], dow) + }() 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 } +func dowInNthWeekFormat(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 + } + + 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. // diff --git a/parser_test.go b/parser_test.go index 41c8c520..b14d8728 100644 --- a/parser_test.go +++ b/parser_test.go @@ -320,7 +320,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", @@ -359,15 +359,15 @@ 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{}} } 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 { diff --git a/spec.go b/spec.go index fa1e241e..cc279ee7 100644 --- a/spec.go +++ b/spec.go @@ -9,6 +9,16 @@ type SpecSchedule struct { // Override location for this schedule. Location *time.Location + // Extra + Extra Extra +} + +// Extra attributes +type Extra struct { + DayOfWeek uint8 // that N:0 - 6 + WeekNumber uint8 // Week of the month + LastWeek bool // if that's a last week + Valid bool } // bounds provides a range of acceptable values (plus a map of name to value). @@ -177,12 +187,105 @@ 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.LastWeek means execute jobs at every last-day-of month,so need return immediately after this action scope + if s.Extra.Valid { + if s.Extra.LastWeek { + if isNLastDayOfGivenMonth(t, s.Extra.DayOfWeek) { + return true + } + } else { + if matchDayOfTheWeekAndWeekInMonth(t, s.Extra.WeekNumber, s.Extra.DayOfWeek) { + return true + } + } + } var ( - domMatch bool = 1< 0 - dowMatch bool = 1< 0 + domMatch = 1< 0 + dowMatch = 1< 0 ) if s.Dom&starBit > 0 || s.Dow&starBit > 0 { return domMatch && dowMatch } return domMatch || dowMatch } + +func matchDayOfTheWeekAndWeekInMonth(t time.Time, weekInTheMonth uint8, 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 == false { + 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 matchNL(allday int, t time.Time, n uint8) 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 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 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) + } +} From 0335986b022a82169951b4e0111e499ee2958f94 Mon Sep 17 00:00:00 2001 From: "pallav.jha" Date: Mon, 15 Jun 2020 20:54:41 +0530 Subject: [PATCH 2/3] adding changes from the master that were removed --- parser.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/parser.go b/parser.go index 36348e38..bf26dda9 100644 --- a/parser.go +++ b/parser.go @@ -121,11 +121,11 @@ func (p Parser) Parse(spec string) (Schedule, error) { } field := func(field string, r bounds) uint64 { - var bits uint64 - bits, err = getField(field, r) if err != nil { return 0 } + var bits uint64 + bits, err = getField(field, r) return bits } @@ -136,6 +136,9 @@ func (p Parser) Parse(spec string) (Schedule, error) { dayOfMonth = field(fields[3], dom) month = field(fields[4], months) ) + if err != nil { + return nil, err + } dayNum, weekNumInt, isLastWeek, ok := dowInNthWeekFormat(fields[5]) dayOfWeek := func() uint64 { if ok { From 70187ce1d1a1916a168e07df10cff0814fe1e991 Mon Sep 17 00:00:00 2001 From: "pallav.jha" Date: Tue, 16 Jun 2020 16:44:26 +0530 Subject: [PATCH 3/3] add testcases for the nth dow --- parser.go | 19 ++++- parser_test.go | 152 +++++++++++++++++++++++++++++++++ spec.go | 55 ++++++------ spec_test.go | 225 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 423 insertions(+), 28 deletions(-) diff --git a/parser.go b/parser.go index bf26dda9..ac4e7d32 100644 --- a/parser.go +++ b/parser.go @@ -139,13 +139,16 @@ func (p Parser) Parse(spec string) (Schedule, error) { if err != nil { return nil, err } - dayNum, weekNumInt, isLastWeek, ok := dowInNthWeekFormat(fields[5]) + 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, @@ -164,7 +167,14 @@ func (p Parser) Parse(spec string) (Schedule, error) { }, nil } -func dowInNthWeekFormat(spec string) (uint8, uint8, bool, bool) { +// 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 { @@ -177,6 +187,11 @@ func dowInNthWeekFormat(spec string) (uint8, uint8, bool, bool) { 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' diff --git a/parser_test.go b/parser_test.go index b14d8728..762f3c21 100644 --- a/parser_test.go +++ b/parser_test.go @@ -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 { @@ -358,6 +435,81 @@ 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, Extra{}} } diff --git a/spec.go b/spec.go index cc279ee7..67d9513d 100644 --- a/spec.go +++ b/spec.go @@ -9,16 +9,16 @@ type SpecSchedule struct { // Override location for this schedule. Location *time.Location - // Extra + // Extra for nth Day of the Week Extra Extra } -// Extra attributes +// Extra attributes is currently storing the spec config for nth Day of the Week type Extra struct { - DayOfWeek uint8 // that N:0 - 6 - WeekNumber uint8 // Week of the month - LastWeek bool // if that's a last week - Valid bool + 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). @@ -187,10 +187,9 @@ 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.LastWeek means execute jobs at every last-day-of month,so need return immediately after this action scope if s.Extra.Valid { if s.Extra.LastWeek { - if isNLastDayOfGivenMonth(t, s.Extra.DayOfWeek) { + if matchDoWForTheLastWeek(t, s.Extra.DayOfWeek) { return true } } else { @@ -209,7 +208,11 @@ func dayMatches(s *SpecSchedule, t time.Time) bool { return domMatch || dowMatch } -func matchDayOfTheWeekAndWeekInMonth(t time.Time, weekInTheMonth uint8, dayOfTheWeek uint8) bool { +// 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: @@ -221,7 +224,7 @@ func matchDayOfTheWeekAndWeekInMonth(t time.Time, weekInTheMonth uint8, dayOfThe case 4: valid = t.Day() <= 28 && t.Day() >= 22 } - if valid == false { + if !valid { return false } switch t.Weekday() { @@ -244,33 +247,33 @@ func matchDayOfTheWeekAndWeekInMonth(t time.Time, weekInTheMonth uint8, dayOfThe } } -func matchNL(allday int, t time.Time, n uint8) bool { - // is or not the last week of this month - if allday-t.Day() > 6 { +func matchLastWeekDOW(numberOfDaysInMonth int, t time.Time, dow uint8) bool { + if numberOfDaysInMonth-t.Day() > 6 { return false } switch t.Weekday() { case time.Sunday: - return n == 0 + return dow == 0 case time.Monday: - return n == 1 + return dow == 1 case time.Tuesday: - return n == 2 + return dow == 2 case time.Wednesday: - return n == 3 + return dow == 3 case time.Thursday: - return n == 4 + return dow == 4 case time.Friday: - return n == 5 + return dow == 5 case time.Saturday: - return n == 6 + return dow == 6 default: return false } } -// is or not the last day 'NL'of a given month -func isNLastDayOfGivenMonth(t time.Time, nl uint8) bool { +// 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 { @@ -279,13 +282,13 @@ func isNLastDayOfGivenMonth(t time.Time, nl uint8) bool { switch t.Month() { case time.April, time.June, time.September, time.November: - return matchNL(30, t, nl) + return matchLastWeekDOW(30, t, dow) case time.February: if leapYear { - return matchNL(29, t, nl) + return matchLastWeekDOW(29, t, dow) } - return matchNL(28, t, nl) + return matchLastWeekDOW(28, t, dow) default: - return matchNL(31, t, nl) + return matchLastWeekDOW(31, t, dow) } } diff --git a/spec_test.go b/spec_test.go index 1b8a503e..0705d2a8 100644 --- a/spec_test.go +++ b/spec_test.go @@ -1,6 +1,7 @@ package cron import ( + "fmt" "strings" "testing" "time" @@ -199,6 +200,57 @@ func TestNext(t *testing.T) { } } +func TestNextWithNthDayOfMthWeek(t *testing.T) { + runs := []struct { + time, spec string + expected string + }{ + // Simple cases - For Monday in June 2020 + {"Mon Jun 1 01:00 2020", "1 1 * 6 1#1", "Mon Jun 1 01:01 2020"}, + {"Mon Jun 1 01:00 2020", "1 1 * 6 1#2", "Mon Jun 8 01:01 2020"}, + {"Mon Jun 1 01:00 2020", "1 1 * 6 1#3", "Mon Jun 15 01:01 2020"}, + {"Mon Jun 1 01:00 2020", "1 1 * 6 1#4", "Mon Jun 22 01:01 2020"}, + {"Mon Jun 1 01:00 2020", "1 1 * 6 1#L", "Mon Jun 29 01:01 2020"}, + + // Simple cases - For Thursday in June 2020 + {"Mon Jun 1 01:00 2020", "1 1 * 6 4#1", "Mon Jun 4 01:01 2020"}, + {"Mon Jun 1 01:00 2020", "1 1 * 6 4#2", "Mon Jun 11 01:01 2020"}, + {"Mon Jun 1 01:00 2020", "1 1 * 6 4#3", "Mon Jun 18 01:01 2020"}, + {"Mon Jun 1 01:00 2020", "1 1 * 6 4#4", "Mon Jun 25 01:01 2020"}, + {"Mon Jun 1 01:00 2020", "1 1 * 6 4#L", "Mon Jun 25 01:01 2020"}, + + + {"Mon Jun 1 01:00 2020", "1 1 10 6 1#2", "Mon Jun 8 01:01 2020"}, + {"Mon Jun 8 02:00 2020", "1 1 10 6 1#2", "Mon Jun 10 01:01 2020"}, + {"Mon Jun 10 02:00 2020", "1 1 10 6 1#2", "Mon Jun 10 01:01 2021"}, + {"Mon Jun 10 02:00 2021", "1 1 10 6 1#2", "Mon Jun 14 01:01 2021"}, + {"Mon Jun 10 01:00 2021", "1 1 10 6 1#2", "Mon Jun 10 01:01 2021"}, + } + + for _, c := range runs { + sched, err := standardParser.Parse(c.spec) + if err != nil { + t.Error(err) + continue + } + actual := sched.Next(getTime(c.time)) + expected := getTime(c.expected) + if !actual.Equal(expected) { + t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.spec, expected, actual) + } + } + sched, err := standardParser.Parse("1 1 * * 4#1") + if err != nil { + t.Error(err) + } + startTime := getTime("Mon Jun 1 01:00 2020") + for i := 0; i < 10; i++ { + nextTime := sched.Next(startTime) + fmt.Println(nextTime) + startTime = nextTime + } +} + func TestErrors(t *testing.T) { invalidSpecs := []string{ "xyz", @@ -272,6 +324,179 @@ func TestNextWithTz(t *testing.T) { } } +// TODO: add the description for each of the tests +func TestMatchDoWForTheLastWeek(t *testing.T) { + runs := []struct { + desc string + time time.Time + dow uint8 + expected bool + }{ + { + desc: "21 Feb 2021 is not the last Sunday of the Month", + time: time.Date(2021, 2, 21, 0, 0, 0, 0, time.Local), + dow: 0, + expected: false, + }, + { + desc: "28 Feb 2021 is the last Sunday of the Month", + time: time.Date(2021, 2, 28, 0, 0, 0, 0, time.Local), + dow: 0, + expected: true, + }, + { + desc: "24 June 2020 is the last Wednesday of the Month", + time: time.Date(2020, 6, 24, 0, 0, 0, 0, time.Local), + dow: 3, + expected: true, + }, + { + desc: "25 June 2020 is the last Thursday of the Month", + time: time.Date(2020, 6, 25, 0, 0, 0, 0, time.Local), + dow: 4, + expected: true, + }, + { + desc: "26 June 2020 is the last Friday of the Month", + time: time.Date(2020, 6, 26, 0, 0, 0, 0, time.Local), + dow: 5, + expected: true, + }, + { + desc: "27 June 2020 is the last Saturday of the Month", + time: time.Date(2020, 6, 27, 0, 0, 0, 0, time.Local), + dow: 6, + expected: true, + }, + { + desc: "28 June 2020 is the last Sunday of the Month", + time: time.Date(2020, 6, 28, 0, 0, 0, 0, time.Local), + dow: 0, + expected: true, + }, + { + desc: "29 June 2020 is the last Monday of the Month", + time: time.Date(2020, 6, 29, 0, 0, 0, 0, time.Local), + dow: 1, + expected: true, + }, + { + desc: "30 June 2020 is the last Tuesday of the Month", + time: time.Date(2020, 6, 30, 0, 0, 0, 0, time.Local), + dow: 2, + expected: true, + }, + { + desc: "30 June 2020 is not the last Monday of the Month", + time: time.Date(2020, 6, 30, 0, 0, 0, 0, time.Local), + dow: 1, + expected: false, + }, + { + desc: "15 June 2020 is not the last Monday of the Month", + time: time.Date(2020, 6, 15, 0, 0, 0, 0, time.Local), + dow: 1, + expected: false, + }, + { + desc: "29 Feb 2020 is the last Saturday of the Month", + time: time.Date(2020, 2, 29, 0, 0, 0, 0, time.Local), + dow: 6, + expected: true, + }, + { + desc: "22 Feb 2020 is the not last Saturday of the Month", + time: time.Date(2020, 2, 22, 0, 0, 0, 0, time.Local), + dow: 6, + expected: false, + }, + { + desc: "21 Aug 2020 is not the last Friday of the Month", + time: time.Date(2020, 8, 21, 0, 0, 0, 0, time.Local), + dow: 5, + expected: false, + }, + { + desc: "28 Aug 2020 is the last Friday of the Month", + time: time.Date(2020, 8, 28, 0, 0, 0, 0, time.Local), + dow: 5, + expected: true, + }, + } + + for _, c := range runs { + ok := matchDoWForTheLastWeek(c.time, c.dow) + if c.expected != ok { + t.Errorf("%s, %d : (expected) %v != %v (actual)", c.time, c.dow, c.expected, ok) + } + } +} + +func TestMatchDayOfTheWeekAndWeekInMonth(t *testing.T) { + runs := []struct { + time time.Time + dow uint8 + weekOfTheMonth uint8 + expected bool + }{ + { + time: time.Date(2020, 6, 1, 0, 0, 0, 0, time.Local), + dow: 1, + weekOfTheMonth: 1, + expected: true, + }, + { + time: time.Date(2020, 6, 13, 0, 0, 0, 0, time.Local), + dow: 6, + weekOfTheMonth: 2, + expected: true, + }, + { + time: time.Date(2020, 6, 13, 0, 0, 0, 0, time.Local), + dow: 4, + weekOfTheMonth: 3, + expected: false, + }, + { + time: time.Date(2020, 6, 28, 0, 0, 0, 0, time.Local), + dow: 0, + weekOfTheMonth: 4, + expected: true, + }, + { + time: time.Date(2020, 6, 23, 0, 0, 0, 0, time.Local), + dow: 2, + weekOfTheMonth: 4, + expected: true, + }, + { + time: time.Date(2020, 6, 24, 0, 0, 0, 0, time.Local), + dow: 3, + weekOfTheMonth: 4, + expected: true, + }, + { + time: time.Date(2020, 6, 25, 0, 0, 0, 0, time.Local), + dow: 4, + weekOfTheMonth: 4, + expected: true, + }, + { + time: time.Date(2020, 6, 26, 0, 0, 0, 0, time.Local), + dow: 5, + weekOfTheMonth: 4, + expected: true, + }, + } + + for _, c := range runs { + ok := matchDayOfTheWeekAndWeekInMonth(c.time, c.weekOfTheMonth, c.dow) + if c.expected != ok { + t.Errorf("%s, %d : (expected) %v != %v (actual)", c.time, c.dow, c.expected, ok) + } + } +} + func getTimeTZ(value string) time.Time { if value == "" { return time.Time{}