浏览代码

Update docs and tests, and clean up implementation a bit.

Rob Figueiredo 11 年之前
父节点
当前提交
c0173a8cbd
共有 6 个文件被更改,包括 121 次插入109 次删除
  1. 1 1
      README.md
  2. 5 3
      doc.go
  3. 55 59
      parser.go
  4. 17 3
      parser_test.go
  5. 11 9
      spec.go
  6. 32 34
      spec_test.go

+ 1 - 1
README.md

@@ -1 +1 @@
-[![GoDoc](http://godoc.org/github.com/dennisfrancis/cron?status.png)](http://godoc.org/github.com/dennisfrancis/cron)
+[![GoDoc](http://godoc.org/github.com/robfig/cron?status.png)](http://godoc.org/github.com/robfig/cron)

+ 5 - 3
doc.go

@@ -29,7 +29,7 @@ A cron expression represents a set of times, using 6 space-separated fields.
 
 	Field name   | Mandatory? | Allowed values  | Allowed special characters
 	----------   | ---------- | --------------  | --------------------------
-	Seconds      | No         | 0-59            | * / , -
+	Seconds      | Yes        | 0-59            | * / , -
 	Minutes      | Yes        | 0-59            | * / , -
 	Hours        | Yes        | 0-23            | * / , -
 	Day of month | Yes        | 1-31            | * / , - ?
@@ -102,8 +102,10 @@ it will have only 2 minutes of idle time between each run.
 
 Time zones
 
-If TZ= field is not provided, all interpretation and scheduling is done in the machine's local time zone (as
-provided by the Go time package (http://www.golang.org/pkg/time).
+By default, all interpretation and scheduling is done in the machine's local
+time zone (as provided by the Go time package http://www.golang.org/pkg/time).
+The time zone may be overridden by providing an additional space-separated field
+at the beginning of the cron spec, of the form "TZ=Asia/Tokyo"
 
 Be aware that jobs scheduled during daylight-savings leap-ahead transitions will
 not be run!

+ 55 - 59
parser.go

@@ -23,45 +23,41 @@ func Parse(spec string) (_ Schedule, err error) {
 		}
 	}()
 
-	var loc *time.Location = nil
-	// Split on whitespace.
-	fields := strings.Fields(spec)
-
-	// Check if timezone field is present
+	// Extract timezone if present
+	var loc = time.Local
 	if strings.HasPrefix(spec, "TZ=") {
-		var err error
-		if loc, err = time.LoadLocation(fields[0][3:len(fields[0])]); err != nil {
-			log.Panicf("Provided bad location %s", fields[0])
+		i := strings.Index(spec, " ")
+		if loc, err = time.LoadLocation(spec[3:i]); err != nil {
+			log.Panicf("Provided bad location %s: %v", spec[3:i], err)
 		}
-		fields = fields[1:len(fields)]
-		spec = strings.Join(fields, " ")
+		spec = strings.TrimSpace(spec[i:])
 	}
 
-	if spec[0] == '@' {
+	// Handle named schedules (descriptors)
+	if strings.HasPrefix(spec, "@") {
 		return parseDescriptor(spec, loc), nil
 	}
 
-	// We require 5 or 6 fields.
-	// (second optional) (minute) (hour) (day of month) (month) (day of week)
+	// Split on whitespace.  We require 5 or 6 fields.
+	// (second) (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 six fields is not provided (no seconds field), then it is equivalent to 0.
+	// If a sixth field is not provided (DayOfWeek), then it is equivalent to star.
 	if len(fields) == 5 {
-		newfields := []string{"00"}
-		newfields  = append(newfields, fields...)
-		fields = newfields
+		fields = append(fields, "*")
 	}
 
 	schedule := &SpecSchedule{
-		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),
-	        Location:  loc,
+		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),
+		Location: loc,
 	}
 
 	return schedule, nil
@@ -184,57 +180,57 @@ func parseDescriptor(spec string, loc *time.Location) Schedule {
 	switch spec {
 	case "@yearly", "@annually":
 		return &SpecSchedule{
-			Second:    1 << seconds.min,
-			Minute:    1 << minutes.min,
-			Hour:      1 << hours.min,
-			Dom:       1 << dom.min,
-			Month:     1 << months.min,
-			Dow:       all(dow),
-		        Location:  loc,
+			Second:   1 << seconds.min,
+			Minute:   1 << minutes.min,
+			Hour:     1 << hours.min,
+			Dom:      1 << dom.min,
+			Month:    1 << months.min,
+			Dow:      all(dow),
+			Location: loc,
 		}
 
 	case "@monthly":
 		return &SpecSchedule{
-			Second:    1 << seconds.min,
-			Minute:    1 << minutes.min,
-			Hour:      1 << hours.min,
-			Dom:       1 << dom.min,
-			Month:     all(months),
-			Dow:       all(dow),
-		        Location:  loc,
+			Second:   1 << seconds.min,
+			Minute:   1 << minutes.min,
+			Hour:     1 << hours.min,
+			Dom:      1 << dom.min,
+			Month:    all(months),
+			Dow:      all(dow),
+			Location: loc,
 		}
 
 	case "@weekly":
 		return &SpecSchedule{
-			Second:    1 << seconds.min,
-			Minute:    1 << minutes.min,
-			Hour:      1 << hours.min,
-			Dom:       all(dom),
-			Month:     all(months),
-			Dow:       1 << dow.min,
-			Location:  loc,
+			Second:   1 << seconds.min,
+			Minute:   1 << minutes.min,
+			Hour:     1 << hours.min,
+			Dom:      all(dom),
+			Month:    all(months),
+			Dow:      1 << dow.min,
+			Location: loc,
 		}
 
 	case "@daily", "@midnight":
 		return &SpecSchedule{
-			Second:    1 << seconds.min,
-			Minute:    1 << minutes.min,
-			Hour:      1 << hours.min,
-			Dom:       all(dom),
-			Month:     all(months),
-			Dow:       all(dow),
-			Location:  loc,
+			Second:   1 << seconds.min,
+			Minute:   1 << minutes.min,
+			Hour:     1 << hours.min,
+			Dom:      all(dom),
+			Month:    all(months),
+			Dow:      all(dow),
+			Location: loc,
 		}
 
 	case "@hourly":
 		return &SpecSchedule{
-			Second:    1 << seconds.min,
-			Minute:    1 << minutes.min,
-			Hour:      all(hours),
-			Dom:       all(dom),
-			Month:     all(months),
-			Dow:       all(dow),
-			Location:  loc,
+			Second:   1 << seconds.min,
+			Minute:   1 << minutes.min,
+			Hour:     all(hours),
+			Dom:      all(dom),
+			Month:    all(months),
+			Dow:      all(dow),
+			Location: loc,
 		}
 	}
 

+ 17 - 3
parser_test.go

@@ -96,13 +96,19 @@ func TestBits(t *testing.T) {
 	}
 }
 
-func TestSpecSchedule(t *testing.T) {
+func TestParseSchedule(t *testing.T) {
+	tokyo, _ := time.LoadLocation("Asia/Tokyo")
 	entries := []struct {
 		expr     string
 		expected Schedule
 	}{
-		{"* 5 * * * *", &SpecSchedule{Second: all(seconds), Minute: 1 << 5, Hour: all(hours), Dom: all(dom), Month: all(months), Dow: all(dow)}},
-		{"@every 5m", ConstantDelaySchedule{time.Duration(5) * time.Minute}},
+		{"* 5 * * * *", every5min(time.Local)},
+		{"TZ=UTC  * 5 * * * *", every5min(time.UTC)},
+		{"TZ=Asia/Tokyo * 5 * * * *", every5min(tokyo)},
+		{"@every 5m", ConstantDelaySchedule{5 * time.Minute}},
+		{"@midnight", midnight(time.Local)},
+		{"TZ=UTC  @midnight", midnight(time.UTC)},
+		{"TZ=Asia/Tokyo @midnight", midnight(tokyo)},
 	}
 
 	for _, c := range entries {
@@ -115,3 +121,11 @@ func TestSpecSchedule(t *testing.T) {
 		}
 	}
 }
+
+func every5min(loc *time.Location) *SpecSchedule {
+	return &SpecSchedule{all(seconds), 1 << 5, all(hours), all(dom), all(months), all(dow), loc}
+}
+
+func midnight(loc *time.Location) *SpecSchedule {
+	return &SpecSchedule{1, 1, 1, all(dom), all(months), all(dow), loc}
+}

+ 11 - 9
spec.go

@@ -8,7 +8,7 @@ import (
 // traditional crontab specification. It is computed initially and stored as bit sets.
 type SpecSchedule struct {
 	Second, Minute, Hour, Dom, Month, Dow uint64
-	Location *time.Location
+	Location                              *time.Location
 }
 
 // bounds provides a range of acceptable values (plus a map of name to value).
@@ -63,10 +63,12 @@ func (s *SpecSchedule) Next(t time.Time) time.Time {
 	// 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)
+
+	// Convert the given time into the schedule's timezone.
+	// Save the original timezone so we can convert back after we find a time.
 	origLocation := t.Location()
-	if s.Location != nil {
-		t = t.In(s.Location)
-	}
+	t = t.In(s.Location)
+
 	// Start at the earliest possible time (the upcoming second).
 	t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)
 
@@ -88,7 +90,7 @@ WRAP:
 		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 = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, s.Location)
 		}
 		t = t.AddDate(0, 1, 0)
 
@@ -102,7 +104,7 @@ WRAP:
 	for !dayMatches(s, t) {
 		if !added {
 			added = true
-			t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
+			t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, s.Location)
 		}
 		t = t.AddDate(0, 0, 1)
 
@@ -114,7 +116,7 @@ WRAP:
 	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())
+			t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, s.Location)
 		}
 		t = t.Add(1 * time.Hour)
 
@@ -126,7 +128,7 @@ WRAP:
 	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 = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), 0, 0, s.Location)
 		}
 		t = t.Add(1 * time.Minute)
 
@@ -138,7 +140,7 @@ WRAP:
 	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 = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, s.Location)
 		}
 		t = t.Add(1 * time.Second)
 

+ 32 - 34
spec_test.go

@@ -11,18 +11,18 @@ func TestActivation(t *testing.T) {
 		expected   bool
 	}{
 		// Every fifteen minutes.
-		{"Mon Jul 9 15:00 2012", "0/15 * * * *", true},
-		{"Mon Jul 9 15:45 2012", "0/15 * * * *", true},
-		{"Mon Jul 9 15:40 2012", "0/15 * * * *", false},
+		{"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", "5/15 * * * *", true},
-		{"Mon Jul 9 15:20 2012", "5/15 * * * *", true},
-		{"Mon Jul 9 15:50 2012", "5/15 * * * *", true},
+		{"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/15 * * Jul *", true},
-		{"Sun Jul 15 15:00 2012", "0/15 * * Jun *", false},
+		{"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},
@@ -76,19 +76,19 @@ func TestNext(t *testing.T) {
 		expected   string
 	}{
 		// Simple cases
-		{"Mon Jul 9 14:45 2012", "0/15 * * * *", "Mon Jul 9 15:00 2012"},
-		{"Mon Jul 9 14:59 2012", "0/15 * * * *", "Mon Jul 9 15:00 2012"},
-		{"Mon Jul 9 14:59:59 2012", "0/15 * * * *", "Mon Jul 9 15:00 2012"},
+		{"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"},
 
 		// Wrap around hours
-		{"Mon Jul 9 15:45 2012", "20-35/15 * * * *", "Mon Jul 9 16:20 2012"},
+		{"Mon Jul 9 15:45 2012", "0 20-35/15 * * *", "Mon Jul 9 16:20 2012"},
 
 		// Wrap around days
-		{"Mon Jul 9 23:46 2012", "*/15 * * * *", "Tue Jul 10 00:00 2012"},
-		{"Mon Jul 9 23:45 2012", "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"},
+		{"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"},
 
 		{"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"},
@@ -110,11 +110,11 @@ func TestNext(t *testing.T) {
 		{"Mon Jul 9 23:35 2012", "0 0 0 29 Feb ?", "Mon Feb 29 00:00 2016"},
 
 		// Daylight savings time EST -> EDT
-		{"2012-03-11T00:00:00-0500", "0 30 2 11 Mar ?", "2013-03-11T02:30:00-0400"},
+		{"2012-03-11T00:00:00-0500", "TZ=America/New_York 0 30 2 11 Mar ?", "2013-03-11T02:30:00-0400"},
 
 		// Daylight savings time EDT -> EST
-		{"2012-11-04T00:00:00-0400", "0 30 2 04 Nov ?", "2012-11-04T02:30:00-0500"},
-		{"2012-11-04T01:45:00-0400", "0 30 1 04 Nov ?", "2012-11-04T01:30:00-0500"},
+		{"2012-11-04T00:00:00-0400", "TZ=America/New_York 0 30 2 04 Nov ?", "2012-11-04T02:30:00-0500"},
+		{"2012-11-04T01:45:00-0400", "TZ=America/New_York 0 30 1 04 Nov ?", "2012-11-04T01:30:00-0500"},
 
 		// Unsatisfiable
 		{"Mon Jul 9 23:35 2012", "0 0 0 30 Feb ?", ""},
@@ -154,20 +154,18 @@ func getTime(value string) time.Time {
 	if value == "" {
 		return time.Time{}
 	}
-	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("2006-01-02T15:04:05-0700", value)
-			if err != nil {
-				panic(err)
-			}
-			// Daylight savings time tests require location
-			if ny, err := time.LoadLocation("America/New_York"); err == nil {
-				t = t.In(ny)
-			}
+
+	var layouts = []string{
+		"Mon Jan 2 15:04 2006",
+		"Mon Jan 2 15:04:05 2006",
+	}
+	for _, layout := range layouts {
+		if t, err := time.ParseInLocation(layout, value, time.Local); err == nil {
+			return t
 		}
 	}
-
-	return t
+	if t, err := time.Parse("2006-01-02T15:04:05-0700", value); err == nil {
+		return t
+	}
+	panic("could not parse time value " + value)
 }