-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit e6917c7
Showing
2 changed files
with
149 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
package cron | ||
|
||
import( | ||
"fmt" | ||
"regexp" | ||
"strconv" | ||
"strings" | ||
"time" | ||
) | ||
|
||
type UnitMatcher interface { | ||
Matches(timePart int) bool | ||
} | ||
|
||
type Wildcard struct { | ||
subpattern string | ||
} | ||
|
||
func (_ *Wildcard) Matches(timePart int) bool { | ||
return true | ||
} | ||
|
||
type Constant struct { | ||
subpattern string | ||
value int | ||
} | ||
|
||
func (constant *Constant) Matches(timePart int) bool { | ||
return constant.value == timePart | ||
} | ||
|
||
type Range struct { | ||
subpattern string | ||
min, max int | ||
} | ||
|
||
func (r *Range) Matches(timePart int) bool { | ||
return timePart >= r.min && timePart <= r.max | ||
} | ||
|
||
type CronSchedule struct { | ||
minutes UnitMatcher | ||
hours UnitMatcher | ||
dayOfMonth UnitMatcher | ||
monthOfYear UnitMatcher | ||
dayOfWeek UnitMatcher | ||
year UnitMatcher | ||
} | ||
|
||
type Schedule interface { | ||
ShouldRun(t time.Time) bool | ||
} | ||
|
||
func (schedule CronSchedule) ShouldRun(t time.Time) bool { | ||
return schedule.minutes.Matches(t.Minute()) && schedule.hours.Matches(t.Hour()) && schedule.dayOfMonth.Matches(t.Day()) && schedule.monthOfYear.Matches(int(t.Month())) && schedule.dayOfWeek.Matches(int(t.Weekday())) && schedule.year.Matches(t.Year()) | ||
|
||
} | ||
|
||
func parsePart(subpattern string) UnitMatcher { | ||
constantRegexp, err := regexp.Compile(`\A\d+\z`) | ||
if err != nil { | ||
panic("invalid hardcoded regexp!") | ||
} | ||
rangeRegexp, err := regexp.Compile(`\A\d+\-\d+\z`) | ||
if err != nil { | ||
panic("invalid hardcoded regexp!") | ||
} | ||
|
||
if "*" == subpattern { | ||
return &Wildcard{subpattern} | ||
} else if constantRegexp.MatchString(subpattern) { | ||
number, err := strconv.Atoi(subpattern) | ||
if err != nil { | ||
panic(fmt.Sprintf("regexp matched integer, but couldn't convert %q to int", subpattern)) | ||
} | ||
return &Constant{subpattern, number} | ||
} else if rangeRegexp.MatchString(subpattern) { | ||
rangeParts := strings.Split(subpattern, "-") | ||
min, err := strconv.Atoi(rangeParts[0]) | ||
if err != nil { | ||
panic(fmt.Sprintf("regexp matched integer, but couldn't convert %q to int", rangeParts[0])) | ||
} | ||
max, err := strconv.Atoi(rangeParts[1]) | ||
if err != nil { | ||
panic(fmt.Sprintf("regexp matched integer, but couldn't convert %q to int", rangeParts[1])) | ||
} | ||
return &Range{subpattern, min, max} | ||
} | ||
return nil | ||
} | ||
|
||
func Parse(pattern string) Schedule { | ||
parts := strings.Split(pattern, " ") | ||
|
||
return &CronSchedule{ | ||
minutes: parsePart(parts[0]), | ||
hours: parsePart(parts[1]), | ||
dayOfMonth: parsePart(parts[2]), | ||
monthOfYear: parsePart(parts[3]), | ||
dayOfWeek: parsePart(parts[4]), | ||
year: parsePart(parts[4]), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package cron | ||
|
||
import( | ||
"testing" | ||
"time" | ||
) | ||
|
||
type TimeTestCase struct { | ||
when time.Time | ||
runs bool | ||
} | ||
|
||
func IsScheduled(t *testing.T, pattern string, tests []TimeTestCase) { | ||
schedule := Parse(pattern) | ||
|
||
for _, test := range(tests) { | ||
if test.runs != schedule.ShouldRun(test.when) { | ||
should := "" | ||
if !test.runs { | ||
should = "not " | ||
} | ||
|
||
t.Errorf("pattern %q should %vrun at time %v", pattern, should, test.when) | ||
} | ||
} | ||
} | ||
|
||
func TestAllStars(t *testing.T) { | ||
IsScheduled(t, "* * * * * *", []TimeTestCase{{time.Now(), true}}) | ||
} | ||
|
||
func TestFiveMinutesAfterTheHour(t *testing.T) { | ||
IsScheduled(t, "5 * * * * *", []TimeTestCase{ | ||
{time.Date(2016, time.January, 5, 10, 15, 0, 0, time.UTC), false}, | ||
{time.Date(2016, time.January, 5, 10, 5, 0, 0, time.UTC), true}, | ||
}) | ||
} | ||
|
||
func TestRanges(t *testing.T) { | ||
IsScheduled(t, "4-8 * * * * *", []TimeTestCase{ | ||
{time.Date(2016, time.January, 5, 10, 15, 0, 0, time.UTC), false}, | ||
{time.Date(2016, time.January, 5, 10, 5, 0, 0, time.UTC), true}, | ||
{time.Date(2016, time.January, 5, 10, 4, 0, 0, time.UTC), true}, | ||
{time.Date(2016, time.January, 5, 10, 8, 0, 0, time.UTC), true}, | ||
}) | ||
} |