浏览代码

Add Schedule.Next() to find the next actiation time. Separate schedule methods from spec parsing functions. Change cron activation tests to a test of Next()

Rob Figueiredo 13 年之前
父节点
当前提交
78f5232d84
共有 6 个文件被更改,包括 556 次插入378 次删除
  1. 46 49
      cron.go
  2. 1 66
      cron_test.go
  3. 205 0
      parser.go
  4. 112 0
      parser_test.go
  5. 84 183
      schedule.go
  6. 108 80
      schedule_test.go

+ 46 - 49
cron.go

@@ -3,6 +3,7 @@
 package cron
 
 import (
+	_ "sort"
 	"time"
 )
 
@@ -17,70 +18,66 @@ type Cron struct {
 // A cron entry consists of a schedule and the func to execute on that schedule.
 type Entry struct {
 	*Schedule
+	Next time.Time
 	Func func()
 }
 
+type byTime []*Entry
+
+func (s byTime) Len() int           { return len(s) }
+func (s byTime) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
+func (s byTime) Less(i, j int) bool { return s[i].Next.Before(s[j].Next) }
+
 func New() *Cron {
-	return new(Cron)
+	return &Cron{
+		Entries: nil,
+		stop:    make(chan struct{}),
+	}
 }
 
 func (c *Cron) Add(spec string, cmd func()) {
-	c.Entries = append(c.Entries, &Entry{Parse(spec), cmd})
+	c.Entries = append(c.Entries, &Entry{Parse(spec), time.Time{}, cmd})
 }
 
-func (c *Cron) Run() {
-	ticker := time.Tick(1 * time.Minute)
-	for {
-		select {
-		case now := <-ticker:
-			for _, entry := range c.Entries {
-				if matches(now, entry.Schedule) {
-					go entry.Func()
-				}
-			}
-
-		case <-c.stop:
-			return
-		}
-	}
-}
+// func (c *Cron) Run() {
+// 	if len(c.Entries) == 0 {
+// 		return
+// 	}
 
-func (c Cron) Stop() {
-	c.stop <- struct{}{}
-}
+// 	var (
+// 		now = time.Now()
+// 		effective = now
+// 	)
 
-// Return true if the given entries overlap.
-func matches(t time.Time, sched *Schedule) bool {
-	var (
-		domMatch bool = 1<<uint(t.Day())&sched.Dom > 0
-		dowMatch bool = 1<<uint(t.Weekday())&sched.Dow > 0
-		dayMatch bool
-	)
+// 	// Figure out the next activation times for each entry.
+// 	for _, entry := range c.Entries {
+// 		entry.Next = entry.Schedule.Next(now)
+// 	}
+// 	sort.Sort(byTime(c.Entries))
 
-	if sched.Dom&STAR_BIT > 0 || sched.Dow&STAR_BIT > 0 {
-		dayMatch = domMatch && dowMatch
-	} else {
-		dayMatch = domMatch || dowMatch
-	}
+// 	for {
+// 		// Sleep until the next job needs to get run.
+// 		effective = c.Entries[0].Next
+// 		time.Sleep(effective.Sub(now))
 
-	return 1<<uint(t.Minute())&sched.Minute > 0 &&
-		1<<uint(t.Hour())&sched.Hour > 0 &&
-		1<<uint(t.Month())&sched.Month > 0 &&
-		dayMatch
-}
+// 		now = time.Now()
 
-// // Return the number of units betwee now and then.
-// func difference(then, now uint64, r bounds) uint {
-// 	// Shift the current time fields left (and around) until & is non-zero.
-// 	i := 0
-// 	for then & now << ((i - r.min) % (r.max - r.min + 1) + r.min) == 0 {
-// 		// A guard against no units selected.
-// 		if i > r.max {
-// 			panic("Entry had no minute/hour selected.")
+// 		// Run every entry whose next time was this effective time.
+// 		// Find how long until the next entry needs to get run.
+// 		for _, e := range c.Entries {
+// 			if e.Next != effective {
+// 				break
+// 			}
+// 			// TODO: Check that it's at least one
+// 			go c.Func()
 // 		}
 
-// 		i++
+// 		case <-c.stop:
+// 			return
+// 		}
 // 	}
-
-// 	return i
 // }
+
+func (c Cron) Stop() {
+	c.stop <- struct{}{}
+}

+ 1 - 66
cron_test.go

@@ -2,72 +2,7 @@ package cron
 
 import (
 	"testing"
-	"time"
 )
 
-func TestActivation(t *testing.T) {
-	tests := []struct {
-		time, spec string
-		expected   bool
-	}{
-		// Every fifteen minutes.
-		{"Mon Jul 9 15:00 2012", "0 0/15 * * *", true},
-		{"Mon Jul 9 15:45 2012", "0 0/15 * * *", true},
-		{"Mon Jul 9 15:40 2012", "0 0/15 * * *", false},
-
-		// Every fifteen minutes, starting at 5 minutes.
-		{"Mon Jul 9 15:05 2012", "0 5/15 * * *", true},
-		{"Mon Jul 9 15:20 2012", "0 5/15 * * *", true},
-		{"Mon Jul 9 15:50 2012", "0 5/15 * * *", true},
-
-		// Named months
-		{"Sun Jul 15 15:00 2012", "0 0/15 * * Jul", true},
-		{"Sun Jul 15 15:00 2012", "0 0/15 * * Jun", false},
-
-		// Everything set.
-		{"Sun Jul 15 08:30 2012", "0 30 08 ? Jul Sun", true},
-		{"Sun Jul 15 08:30 2012", "0 30 08 15 Jul ?", true},
-		{"Mon Jul 16 08:30 2012", "0 30 08 ? Jul Sun", false},
-		{"Mon Jul 16 08:30 2012", "0 30 08 15 Jul ?", false},
-
-		// Predefined schedules
-		{"Mon Jul 9 15:00 2012", "@hourly", true},
-		{"Mon Jul 9 15:04 2012", "@hourly", false},
-		{"Mon Jul 9 15:00 2012", "@daily", false},
-		{"Mon Jul 9 00:00 2012", "@daily", true},
-		{"Mon Jul 9 00:00 2012", "@weekly", false},
-		{"Sun Jul 8 00:00 2012", "@weekly", true},
-		{"Sun Jul 8 01:00 2012", "@weekly", false},
-		{"Sun Jul 8 00:00 2012", "@monthly", false},
-		{"Sun Jul 1 00:00 2012", "@monthly", true},
-
-		// Test interaction of DOW and DOM.
-		// If both are specified, then only one needs to match.
-		{"Sun Jul 15 00:00 2012", "0 * * 1,15 * Sun", true},
-		{"Fri Jun 15 00:00 2012", "0 * * 1,15 * Sun", true},
-		{"Wed Aug 1 00:00 2012", "0 * * 1,15 * Sun", true},
-
-		// However, if one has a star, then both need to match.
-		{"Sun Jul 15 00:00 2012", "0 * * * * Mon", false},
-		{"Sun Jul 15 00:00 2012", "0 * * */10 * Sun", false},
-		{"Mon Jul 9 00:00 2012", "0 * * 1,15 * *", false},
-		{"Sun Jul 15 00:00 2012", "0 * * 1,15 * *", true},
-	}
-
-	for _, test := range tests {
-		actual := matches(getTime(test.time), Parse(test.spec))
-		if test.expected != actual {
-			t.Logf("Actual Minutes mask: %b", Parse(test.spec).Minute)
-			t.Errorf("Fail evaluating %s on %s: (expected) %t != %t (actual)",
-				test.spec, test.time, test.expected, actual)
-		}
-	}
-}
-
-func getTime(value string) time.Time {
-	t, err := time.Parse("Mon Jan 2 15:04 2006", value)
-	if err != nil {
-		panic(err)
-	}
-	return t
+func TestActivation2(t *testing.T) {
 }

+ 205 - 0
parser.go

@@ -0,0 +1,205 @@
+package cron
+
+import (
+	"log"
+	"math"
+	"strconv"
+	"strings"
+)
+
+// Returns a new crontab schedule representing the given spec.
+// Panics with a descriptive error if the spec is not valid.
+func Parse(spec string) *Schedule {
+	if spec[0] == '@' {
+		return parseDescriptor(spec)
+	}
+
+	// Split on whitespace.  We require 4 or 5 fields.
+	// (minute) (hour) (day of month) (month) (day of week, optional)
+	fields := strings.Fields(spec)
+	if len(fields) != 5 && len(fields) != 6 {
+		log.Panicf("Expected 5 or 6 fields, found %d: %s", len(fields), spec)
+	}
+
+	// If a fifth field is not provided (DayOfWeek), then it is equivalent to star.
+	if len(fields) == 5 {
+		fields = append(fields, "*")
+	}
+
+	schedule := &Schedule{
+		Second: getField(fields[0], seconds),
+		Minute: getField(fields[1], minutes),
+		Hour:   getField(fields[2], hours),
+		Dom:    getField(fields[3], dom),
+		Month:  getField(fields[4], months),
+		Dow:    getField(fields[5], dow),
+	}
+
+	return schedule
+}
+
+// Return an Int with the bits set representing all of the times that the field represents.
+// A "field" is a comma-separated list of "ranges".
+func getField(field string, r bounds) uint64 {
+	// list = range {"," range}
+	var bits uint64
+	ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
+	for _, expr := range ranges {
+		bits |= getRange(expr, r)
+	}
+	return bits
+}
+
+func getRange(expr string, r bounds) uint64 {
+	// number | number "-" number [ "/" number ]
+	var (
+		start, end, step uint
+		rangeAndStep     = strings.Split(expr, "/")
+		lowAndHigh       = strings.Split(rangeAndStep[0], "-")
+		singleDigit      = len(lowAndHigh) == 1
+	)
+
+	var extra_star uint64
+	if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
+		start = r.min
+		end = r.max
+		extra_star = STAR_BIT
+	} else {
+		start = parseIntOrName(lowAndHigh[0], r.names)
+		switch len(lowAndHigh) {
+		case 1:
+			end = start
+		case 2:
+			end = parseIntOrName(lowAndHigh[1], r.names)
+		default:
+			log.Panicf("Too many hyphens: %s", expr)
+		}
+	}
+
+	switch len(rangeAndStep) {
+	case 1:
+		step = 1
+	case 2:
+		step = mustParseInt(rangeAndStep[1])
+
+		// Special handling: "N/step" means "N-max/step".
+		if singleDigit {
+			end = r.max
+		}
+	default:
+		log.Panicf("Too many slashes: %s", expr)
+	}
+
+	if start < r.min {
+		log.Panicf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
+	}
+	if end > r.max {
+		log.Panicf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
+	}
+	if start > end {
+		log.Panicf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
+	}
+
+	return getBits(start, end, step) | extra_star
+}
+
+func parseIntOrName(expr string, names map[string]uint) uint {
+	if names != nil {
+		if namedInt, ok := names[strings.ToLower(expr)]; ok {
+			return namedInt
+		}
+	}
+	return mustParseInt(expr)
+}
+
+func mustParseInt(expr string) uint {
+	num, err := strconv.Atoi(expr)
+	if err != nil {
+		log.Panicf("Failed to parse int from %s: %s", expr, err)
+	}
+	if num < 0 {
+		log.Panicf("Negative number (%d) not allowed: %s", num, expr)
+	}
+
+	return uint(num)
+}
+
+func getBits(min, max, step uint) uint64 {
+	var bits uint64
+
+	// If step is 1, use shifts.
+	if step == 1 {
+		return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
+	}
+
+	// Else, use a simple loop.
+	for i := min; i <= max; i += step {
+		bits |= 1 << i
+	}
+	return bits
+}
+
+func all(r bounds) uint64 {
+	return getBits(r.min, r.max, 1) | STAR_BIT
+}
+
+func first(r bounds) uint64 {
+	return getBits(r.min, r.min, 1)
+}
+
+func parseDescriptor(spec string) *Schedule {
+	switch spec {
+	case "@yearly", "@annually":
+		return &Schedule{
+			Second: 1 << seconds.min,
+			Minute: 1 << minutes.min,
+			Hour:   1 << hours.min,
+			Dom:    1 << dom.min,
+			Month:  1 << months.min,
+			Dow:    all(dow),
+		}
+
+	case "@monthly":
+		return &Schedule{
+			Second: 1 << seconds.min,
+			Minute: 1 << minutes.min,
+			Hour:   1 << hours.min,
+			Dom:    1 << dom.min,
+			Month:  all(months),
+			Dow:    all(dow),
+		}
+
+	case "@weekly":
+		return &Schedule{
+			Second: 1 << seconds.min,
+			Minute: 1 << minutes.min,
+			Hour:   1 << hours.min,
+			Dom:    all(dom),
+			Month:  all(months),
+			Dow:    1 << dow.min,
+		}
+
+	case "@daily", "@midnight":
+		return &Schedule{
+			Second: 1 << seconds.min,
+			Minute: 1 << minutes.min,
+			Hour:   1 << hours.min,
+			Dom:    all(dom),
+			Month:  all(months),
+			Dow:    all(dow),
+		}
+
+	case "@hourly":
+		return &Schedule{
+			Second: 1 << seconds.min,
+			Minute: 1 << minutes.min,
+			Hour:   all(hours),
+			Dom:    all(dom),
+			Month:  all(months),
+			Dow:    all(dow),
+		}
+	}
+
+	log.Panicf("Unrecognized descriptor: %s", spec)
+	return nil
+}

+ 112 - 0
parser_test.go

@@ -0,0 +1,112 @@
+package cron
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestRange(t *testing.T) {
+	ranges := []struct {
+		expr     string
+		min, max uint
+		expected uint64
+	}{
+		{"5", 0, 7, 1 << 5},
+		{"0", 0, 7, 1 << 0},
+		{"7", 0, 7, 1 << 7},
+
+		{"5-5", 0, 7, 1 << 5},
+		{"5-6", 0, 7, 1<<5 | 1<<6},
+		{"5-7", 0, 7, 1<<5 | 1<<6 | 1<<7},
+
+		{"5-6/2", 0, 7, 1 << 5},
+		{"5-7/2", 0, 7, 1<<5 | 1<<7},
+		{"5-7/1", 0, 7, 1<<5 | 1<<6 | 1<<7},
+
+		{"*", 1, 3, 1<<1 | 1<<2 | 1<<3 | STAR_BIT},
+		{"*/2", 1, 3, 1<<1 | 1<<3 | STAR_BIT},
+	}
+
+	for _, c := range ranges {
+		actual := getRange(c.expr, bounds{c.min, c.max, nil})
+		if actual != c.expected {
+			t.Errorf("%s => (expected) %d != %d (actual)", c.expr, c.expected, actual)
+		}
+	}
+}
+
+func TestField(t *testing.T) {
+	fields := []struct {
+		expr     string
+		min, max uint
+		expected uint64
+	}{
+		{"5", 1, 7, 1 << 5},
+		{"5,6", 1, 7, 1<<5 | 1<<6},
+		{"5,6,7", 1, 7, 1<<5 | 1<<6 | 1<<7},
+		{"1,5-7/2,3", 1, 7, 1<<1 | 1<<5 | 1<<7 | 1<<3},
+	}
+
+	for _, c := range fields {
+		actual := getField(c.expr, bounds{c.min, c.max, nil})
+		if actual != c.expected {
+			t.Errorf("%s => (expected) %d != %d (actual)", c.expr, c.expected, actual)
+		}
+	}
+}
+
+func TestBits(t *testing.T) {
+	allBits := []struct {
+		r        bounds
+		expected uint64
+	}{
+		{minutes, 0xfffffffffffffff}, // 0-59: 60 ones
+		{hours, 0xffffff},            // 0-23: 24 ones
+		{dom, 0xfffffffe},            // 1-31: 31 ones, 1 zero
+		{months, 0x1ffe},             // 1-12: 12 ones, 1 zero
+		{dow, 0x7f},                  // 0-6: 7 ones
+	}
+
+	for _, c := range allBits {
+		actual := all(c.r) // all() adds the STAR_BIT, so compensate for that..
+		if c.expected|STAR_BIT != actual {
+			t.Errorf("%d-%d/%d => (expected) %b != %b (actual)",
+				c.r.min, c.r.max, 1, c.expected|STAR_BIT, actual)
+		}
+	}
+
+	bits := []struct {
+		min, max, step uint
+		expected       uint64
+	}{
+
+		{0, 0, 1, 0x1},
+		{1, 1, 1, 0x2},
+		{1, 5, 2, 0x2a}, // 101010
+		{1, 4, 2, 0xa},  // 1010
+	}
+
+	for _, c := range bits {
+		actual := getBits(c.min, c.max, c.step)
+		if c.expected != actual {
+			t.Errorf("%d-%d/%d => (expected) %b != %b (actual)",
+				c.min, c.max, c.step, c.expected, actual)
+		}
+	}
+}
+
+func TestSchedule(t *testing.T) {
+	entries := []struct {
+		expr     string
+		expected Schedule
+	}{
+		{"* 5 * * * *", Schedule{all(seconds), 1 << 5, all(hours), all(dom), all(months), all(dow)}},
+	}
+
+	for _, c := range entries {
+		actual := *Parse(c.expr)
+		if !reflect.DeepEqual(actual, c.expected) {
+			t.Errorf("%s => (expected) %b != %b (actual)", c.expr, c.expected, actual)
+		}
+	}
+}

+ 84 - 183
schedule.go

@@ -1,10 +1,7 @@
 package cron
 
 import (
-	"log"
-	"math"
-	"strconv"
-	"strings"
+	"time"
 )
 
 // A cron schedule that specifies a duty cycle (to the second granularity).
@@ -39,7 +36,7 @@ var (
 		"nov": 11,
 		"dec": 12,
 	}}
-	dow = bounds{0, 7, map[string]uint{
+	dow = bounds{0, 6, map[string]uint{
 		"sun": 0,
 		"mon": 1,
 		"tue": 2,
@@ -55,204 +52,108 @@ const (
 	STAR_BIT = 1 << 63
 )
 
-// Returns a new crontab schedule representing the given spec.
-// Panics with a descriptive error if the spec is not valid.
-func Parse(spec string) *Schedule {
-	if spec[0] == '@' {
-		return parseDescriptor(spec)
-	}
-
-	// Split on whitespace.  We require 4 or 5 fields.
-	// (minute) (hour) (day of month) (month) (day of week, optional)
-	fields := strings.Fields(spec)
-	if len(fields) != 5 && len(fields) != 6 {
-		log.Panicf("Expected 5 or 6 fields, found %d: %s", len(fields), spec)
-	}
-
-	// If a fifth field is not provided (DayOfWeek), then it is equivalent to star.
-	if len(fields) == 5 {
-		fields = append(fields, "*")
-	}
-
-	schedule := &Schedule{
-		Second: getField(fields[0], seconds),
-		Minute: getField(fields[1], minutes),
-		Hour:   getField(fields[2], hours),
-		Dom:    getField(fields[3], dom),
-		Month:  getField(fields[4], months),
-		Dow:    getField(fields[5], dow),
-	}
-
-	// If either bit 0 or 7 are set, set both.  (both accepted as Sunday)
-	if 1&schedule.Dow|1<<7&schedule.Dow > 0 {
-		schedule.Dow = schedule.Dow | 1 | 1<<7
-	}
-
-	return schedule
-}
-
-// Return an Int with the bits set representing all of the times that the field represents.
-// A "field" is a comma-separated list of "ranges".
-func getField(field string, r bounds) uint64 {
-	// list = range {"," range}
-	var bits uint64
-	ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
-	for _, expr := range ranges {
-		bits |= getRange(expr, r)
-	}
-	return bits
-}
-
-func getRange(expr string, r bounds) uint64 {
-	// number | number "-" number [ "/" number ]
-	var (
-		start, end, step uint
-		rangeAndStep     = strings.Split(expr, "/")
-		lowAndHigh       = strings.Split(rangeAndStep[0], "-")
-		singleDigit      = len(lowAndHigh) == 1
-	)
+// Return the next time this schedule is activated, greater than the
+// given time.  If no time can be found to satisfy the schedule, return the
+// zero time.
+func (s *Schedule) Next(t time.Time) time.Time {
+	// For Month, Day, Hour, Minute, Second:
+	// Check if the current value matches.  If yes, do nothing for that field.
+	// If the field doesn't match the schedule, then increment the field until it matches.
+	// While incrementing the field, a wrap-around brings it back to the beginning
+	// of the field list (since it is necessary to re-verify previous field
+	// values)
+
+	// Start at the earliest possible time.
+	t = t.Add(1 * time.Second)
+
+	// This flag indicates whether a field has been incremented.
+	added := false
+
+	// If no time is found within five years, return zero.
+	yearLimit := t.Year() + 5
+
+WRAP:
+	if t.Year() > yearLimit {
+		return time.Time{}
+	}
+
+	// Find the first applicable month.
+	// If it's this month, then do nothing.
+	for 1<<uint(t.Month())&s.Month == 0 {
+		// If we have to add a month, reset the other parts to 0.
+		if !added {
+			added = true
+			// Otherwise, set the date at the beginning (since the current time is irrelevant).
+			t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
+		}
+		t = t.AddDate(0, 1, 0)
 
-	var extra_star uint64
-	if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
-		start = r.min
-		end = r.max
-		extra_star = STAR_BIT
-	} else {
-		start = parseIntOrName(lowAndHigh[0], r.names)
-		switch len(lowAndHigh) {
-		case 1:
-			end = start
-		case 2:
-			end = parseIntOrName(lowAndHigh[1], r.names)
-		default:
-			log.Panicf("Too many hyphens: %s", expr)
+		// Wrapped around.
+		if t.Month() == time.January {
+			goto WRAP
 		}
 	}
 
-	switch len(rangeAndStep) {
-	case 1:
-		step = 1
-	case 2:
-		step = mustParseInt(rangeAndStep[1])
-
-		// Special handling: "N/step" means "N-max/step".
-		if singleDigit {
-			end = r.max
+	// Now get a day in that month.
+	for !dayMatches(s, t) {
+		if !added {
+			added = true
+			t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
 		}
-	default:
-		log.Panicf("Too many slashes: %s", expr)
-	}
+		t = t.AddDate(0, 0, 1)
 
-	if start < r.min {
-		log.Panicf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
-	}
-	if end > r.max {
-		log.Panicf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
-	}
-	if start > end {
-		log.Panicf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
+		if t.Day() == 1 {
+			goto WRAP
+		}
 	}
 
-	return getBits(start, end, step) | extra_star
-}
-
-func parseIntOrName(expr string, names map[string]uint) uint {
-	if names != nil {
-		if namedInt, ok := names[strings.ToLower(expr)]; ok {
-			return namedInt
+	for 1<<uint(t.Hour())&s.Hour == 0 {
+		if !added {
+			added = true
+			t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, t.Location())
 		}
-	}
-	return mustParseInt(expr)
-}
+		t = t.Add(1 * time.Hour)
 
-func mustParseInt(expr string) uint {
-	num, err := strconv.Atoi(expr)
-	if err != nil {
-		log.Panicf("Failed to parse int from %s: %s", expr, err)
-	}
-	if num < 0 {
-		log.Panicf("Negative number (%d) not allowed: %s", num, expr)
+		if t.Hour() == 0 {
+			goto WRAP
+		}
 	}
 
-	return uint(num)
-}
-
-func getBits(min, max, step uint) uint64 {
-	var bits uint64
-
-	// If step is 1, use shifts.
-	if step == 1 {
-		return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
-	}
+	for 1<<uint(t.Minute())&s.Minute == 0 {
+		if !added {
+			added = true
+			t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), 0, 0, t.Location())
+		}
+		t = t.Add(1 * time.Minute)
 
-	// Else, use a simple loop.
-	for i := min; i <= max; i += step {
-		bits |= 1 << i
+		if t.Minute() == 0 {
+			goto WRAP
+		}
 	}
-	return bits
-}
 
-func all(r bounds) uint64 {
-	return getBits(r.min, r.max, 1) | STAR_BIT
-}
-
-func first(r bounds) uint64 {
-	return getBits(r.min, r.min, 1)
-}
-
-func parseDescriptor(spec string) *Schedule {
-	switch spec {
-	case "@yearly", "@annually":
-		return &Schedule{
-			Second: 1 << seconds.min,
-			Minute: 1 << minutes.min,
-			Hour:   1 << hours.min,
-			Dom:    1 << dom.min,
-			Month:  1 << months.min,
-			Dow:    all(dow),
+	for 1<<uint(t.Second())&s.Second == 0 {
+		if !added {
+			added = true
+			t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, t.Location())
 		}
+		t = t.Add(1 * time.Second)
 
-	case "@monthly":
-		return &Schedule{
-			Second: 1 << seconds.min,
-			Minute: 1 << minutes.min,
-			Hour:   1 << hours.min,
-			Dom:    1 << dom.min,
-			Month:  all(months),
-			Dow:    all(dow),
+		if t.Second() == 0 {
+			goto WRAP
 		}
+	}
 
-	case "@weekly":
-		return &Schedule{
-			Second: 1 << seconds.min,
-			Minute: 1 << minutes.min,
-			Hour:   1 << hours.min,
-			Dom:    all(dom),
-			Month:  all(months),
-			Dow:    1 << dow.min,
-		}
+	return t
+}
 
-	case "@daily", "@midnight":
-		return &Schedule{
-			Second: 1 << seconds.min,
-			Minute: 1 << minutes.min,
-			Hour:   1 << hours.min,
-			Dom:    all(dom),
-			Month:  all(months),
-			Dow:    all(dow),
-		}
+func dayMatches(s *Schedule, t time.Time) bool {
+	var (
+		domMatch bool = 1<<uint(t.Day())&s.Dom > 0
+		dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0
+	)
 
-	case "@hourly":
-		return &Schedule{
-			Second: 1 << seconds.min,
-			Minute: 1 << minutes.min,
-			Hour:   all(hours),
-			Dom:    all(dom),
-			Month:  all(months),
-			Dow:    all(dow),
-		}
+	if s.Dom&STAR_BIT > 0 || s.Dow&STAR_BIT > 0 {
+		return domMatch && dowMatch
 	}
-
-	log.Panicf("Unrecognized descriptor: %s", spec)
-	return nil
+	return domMatch || dowMatch
 }

+ 108 - 80
schedule_test.go

@@ -1,112 +1,140 @@
 package cron
 
 import (
-	"reflect"
 	"testing"
+	"time"
 )
 
-func TestRange(t *testing.T) {
-	ranges := []struct {
-		expr     string
-		min, max uint
-		expected uint64
+func TestActivation(t *testing.T) {
+	tests := []struct {
+		time, spec string
+		expected   bool
 	}{
-		{"5", 0, 7, 1 << 5},
-		{"0", 0, 7, 1 << 0},
-		{"7", 0, 7, 1 << 7},
+		// Every fifteen minutes.
+		{"Mon Jul 9 15:00 2012", "0 0/15 * * *", true},
+		{"Mon Jul 9 15:45 2012", "0 0/15 * * *", true},
+		{"Mon Jul 9 15:40 2012", "0 0/15 * * *", false},
 
-		{"5-5", 0, 7, 1 << 5},
-		{"5-6", 0, 7, 1<<5 | 1<<6},
-		{"5-7", 0, 7, 1<<5 | 1<<6 | 1<<7},
+		// Every fifteen minutes, starting at 5 minutes.
+		{"Mon Jul 9 15:05 2012", "0 5/15 * * *", true},
+		{"Mon Jul 9 15:20 2012", "0 5/15 * * *", true},
+		{"Mon Jul 9 15:50 2012", "0 5/15 * * *", true},
 
-		{"5-6/2", 0, 7, 1 << 5},
-		{"5-7/2", 0, 7, 1<<5 | 1<<7},
-		{"5-7/1", 0, 7, 1<<5 | 1<<6 | 1<<7},
+		// Named months
+		{"Sun Jul 15 15:00 2012", "0 0/15 * * Jul", true},
+		{"Sun Jul 15 15:00 2012", "0 0/15 * * Jun", false},
 
-		{"*", 1, 3, 1<<1 | 1<<2 | 1<<3 | STAR_BIT},
-		{"*/2", 1, 3, 1<<1 | 1<<3 | STAR_BIT},
-	}
+		// Everything set.
+		{"Sun Jul 15 08:30 2012", "0 30 08 ? Jul Sun", true},
+		{"Sun Jul 15 08:30 2012", "0 30 08 15 Jul ?", true},
+		{"Mon Jul 16 08:30 2012", "0 30 08 ? Jul Sun", false},
+		{"Mon Jul 16 08:30 2012", "0 30 08 15 Jul ?", false},
 
-	for _, c := range ranges {
-		actual := getRange(c.expr, bounds{c.min, c.max, nil})
-		if actual != c.expected {
-			t.Errorf("%s => (expected) %d != %d (actual)", c.expr, c.expected, actual)
-		}
-	}
-}
+		// Predefined schedules
+		{"Mon Jul 9 15:00 2012", "@hourly", true},
+		{"Mon Jul 9 15:04 2012", "@hourly", false},
+		{"Mon Jul 9 15:00 2012", "@daily", false},
+		{"Mon Jul 9 00:00 2012", "@daily", true},
+		{"Mon Jul 9 00:00 2012", "@weekly", false},
+		{"Sun Jul 8 00:00 2012", "@weekly", true},
+		{"Sun Jul 8 01:00 2012", "@weekly", false},
+		{"Sun Jul 8 00:00 2012", "@monthly", false},
+		{"Sun Jul 1 00:00 2012", "@monthly", true},
 
-func TestField(t *testing.T) {
-	fields := []struct {
-		expr     string
-		min, max uint
-		expected uint64
-	}{
-		{"5", 1, 7, 1 << 5},
-		{"5,6", 1, 7, 1<<5 | 1<<6},
-		{"5,6,7", 1, 7, 1<<5 | 1<<6 | 1<<7},
-		{"1,5-7/2,3", 1, 7, 1<<1 | 1<<5 | 1<<7 | 1<<3},
+		// Test interaction of DOW and DOM.
+		// If both are specified, then only one needs to match.
+		{"Sun Jul 15 00:00 2012", "0 * * 1,15 * Sun", true},
+		{"Fri Jun 15 00:00 2012", "0 * * 1,15 * Sun", true},
+		{"Wed Aug 1 00:00 2012", "0 * * 1,15 * Sun", true},
+
+		// However, if one has a star, then both need to match.
+		{"Sun Jul 15 00:00 2012", "0 * * * * Mon", false},
+		{"Sun Jul 15 00:00 2012", "0 * * */10 * Sun", false},
+		{"Mon Jul 9 00:00 2012", "0 * * 1,15 * *", false},
+		{"Sun Jul 15 00:00 2012", "0 * * 1,15 * *", true},
+		{"Sun Jul 15 00:00 2012", "0 * * */2 * Sun", true},
 	}
 
-	for _, c := range fields {
-		actual := getField(c.expr, bounds{c.min, c.max, nil})
-		if actual != c.expected {
-			t.Errorf("%s => (expected) %d != %d (actual)", c.expr, c.expected, actual)
+	for _, test := range tests {
+		actual := Parse(test.spec).Next(getTime(test.time).Add(-1 * time.Second))
+		expected := getTime(test.time)
+		if test.expected && expected != actual || !test.expected && expected == actual {
+			t.Errorf("Fail evaluating %s on %s: (expected) %s != %s (actual)",
+				test.spec, test.time, expected, actual)
 		}
 	}
 }
 
-func TestBits(t *testing.T) {
-	allBits := []struct {
-		r        bounds
-		expected uint64
+func TestNext(t *testing.T) {
+	runs := []struct {
+		time, spec string
+		expected   string
 	}{
-		{minutes, 0xfffffffffffffff}, // 0-59: 60 ones
-		{hours, 0xffffff},            // 0-23: 24 ones
-		{dom, 0xfffffffe},            // 1-31: 31 ones, 1 zero
-		{months, 0x1ffe},             // 1-12: 12 ones, 1 zero
-		{dow, 0xff},                  // 0-7: 8 ones
-	}
+		// Simple cases
+		{"Mon Jul 9 14:45 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"},
+		{"Mon Jul 9 14:59 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"},
+		{"Mon Jul 9 14:59:59 2012", "0 0/15 * * *", "Mon Jul 9 15:00 2012"},
 
-	for _, c := range allBits {
-		actual := all(c.r) // all() adds the STAR_BIT, so compensate for that..
-		if c.expected|STAR_BIT != actual {
-			t.Errorf("%d-%d/%d => (expected) %b != %b (actual)",
-				c.r.min, c.r.max, 1, c.expected, actual)
-		}
-	}
+		// Wrap around hours
+		{"Mon Jul 9 15:45 2012", "0 20-35/15 * * *", "Mon Jul 9 16:20 2012"},
 
-	bits := []struct {
-		min, max, step uint
-		expected       uint64
-	}{
+		// Wrap around days
+		{"Mon Jul 9 23:46 2012", "0 */15 * * *", "Tue Jul 10 00:00 2012"},
+		{"Mon Jul 9 23:45 2012", "0 20-35/15 * * *", "Tue Jul 10 00:20 2012"},
+		{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * * *", "Tue Jul 10 00:20:15 2012"},
+		{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 * *", "Tue Jul 10 01:20:15 2012"},
+		{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 10-12 * *", "Tue Jul 10 10:20:15 2012"},
 
-		{0, 0, 1, 0x1},
-		{1, 1, 1, 0x2},
-		{1, 5, 2, 0x2a}, // 101010
-		{1, 4, 2, 0xa},  // 1010
+		{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 */2 * *", "Thu Jul 11 01:20:15 2012"},
+		{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 * *", "Wed Jul 10 00:20:15 2012"},
+		{"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 Jul *", "Wed Jul 10 00:20:15 2012"},
+
+		// Wrap around months
+		{"Mon Jul 9 23:35 2012", "0 0 0 9 Apr-Oct ?", "Thu Aug 9 00:00 2012"},
+		{"Mon Jul 9 23:35 2012", "0 0 0 */5 Apr,Aug,Oct Mon", "Mon Aug 6 00:00 2012"},
+		{"Mon Jul 9 23:35 2012", "0 0 0 */5 Oct Mon", "Mon Oct 1 00:00 2012"},
+
+		// Wrap around years
+		{"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon", "Mon Feb 4 00:00 2013"},
+		{"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon/2", "Fri Feb 1 00:00 2013"},
+
+		// Wrap around minute, hour, day, month, and year
+		{"Mon Dec 31 23:59:45 2012", "0 * * * * *", "Tue Jan 1 00:00:00 2013"},
+
+		// Leap year
+		{"Mon Jul 9 23:35 2012", "0 0 0 29 Feb ?", "Mon Feb 29 00:00 2016"},
+
+		// Daylight savings time
+		{"Sun Mar 11 00:00 2012 EST", "0 30 2 11 Mar ?", "Mon Mar 11 02:30 2013 EDT"},
+
+		// Unsatisfiable
+		{"Mon Jul 9 23:35 2012", "0 0 0 30 Feb ?", ""},
+		{"Mon Jul 9 23:35 2012", "0 0 0 31 Apr ?", ""},
 	}
 
-	for _, c := range bits {
-		actual := getBits(c.min, c.max, c.step)
-		if c.expected != actual {
-			t.Errorf("%d-%d/%d => (expected) %b != %b (actual)",
-				c.min, c.max, c.step, c.expected, actual)
+	for _, c := range runs {
+		actual := Parse(c.spec).Next(getTime(c.time))
+		expected := getTime(c.expected)
+		if actual != expected {
+			t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.spec, expected, actual)
 		}
 	}
 }
 
-func TestSchedule(t *testing.T) {
-	entries := []struct {
-		expr     string
-		expected Schedule
-	}{
-		{"* 5 * * * *", Schedule{all(seconds), 1 << 5, all(hours), all(dom), all(months), all(dow)}},
+func getTime(value string) time.Time {
+	if value == "" {
+		return time.Time{}
 	}
-
-	for _, c := range entries {
-		actual := *Parse(c.expr)
-		if !reflect.DeepEqual(actual, c.expected) {
-			t.Errorf("%s => (expected) %b != %b (actual)", c.expr, c.expected, actual)
+	t, err := time.Parse("Mon Jan 2 15:04 2006", value)
+	if err != nil {
+		t, err = time.Parse("Mon Jan 2 15:04:05 2006", value)
+		if err != nil {
+			t, err = time.Parse("Mon Jan 2 15:04 2006 MST", value)
+			if err != nil {
+				panic(err)
+			}
 		}
 	}
+
+	return t
 }