Procházet zdrojové kódy

Merge pull request #18 from robfig/timezone

Timezone
Rob Figueiredo před 11 roky
rodič
revize
e0f96a958f
5 změnil soubory, kde provedl 105 přidání a 67 odebrání
  1. 5 2
      doc.go
  2. 56 39
      parser.go
  3. 17 3
      parser_test.go
  4. 12 6
      spec.go
  5. 15 17
      spec_test.go

+ 5 - 2
doc.go

@@ -8,6 +8,7 @@ them in their own goroutines.
 
 	c := cron.New()
 	c.AddFunc("0 30 * * * *", func() { fmt.Println("Every hour on the half hour") })
+	c.AddFunc("TZ=Asia/Tokyo 30 04 * * * *", func() { fmt.Println("Runs at 04:30 Tokyo time every day") })
 	c.AddFunc("@hourly",      func() { fmt.Println("Every hour") })
 	c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") })
 	c.Start()
@@ -101,8 +102,10 @@ it will have only 2 minutes of idle time between each run.
 
 Time zones
 
-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!

+ 56 - 39
parser.go

@@ -23,8 +23,19 @@ func Parse(spec string) (_ Schedule, err error) {
 		}
 	}()
 
-	if spec[0] == '@' {
-		return parseDescriptor(spec), nil
+	// Extract timezone if present
+	var loc = time.Local
+	if strings.HasPrefix(spec, "TZ=") {
+		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)
+		}
+		spec = strings.TrimSpace(spec[i:])
+	}
+
+	// Handle named schedules (descriptors)
+	if strings.HasPrefix(spec, "@") {
+		return parseDescriptor(spec, loc), nil
 	}
 
 	// Split on whitespace.  We require 5 or 6 fields.
@@ -40,12 +51,13 @@ func Parse(spec string) (_ Schedule, err error) {
 	}
 
 	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),
+		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
@@ -164,56 +176,61 @@ func all(r bounds) uint64 {
 
 // parseDescriptor returns a pre-defined schedule for the expression, or panics
 // if none matches.
-func parseDescriptor(spec string) Schedule {
+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),
+			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),
+			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,
+			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),
+			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),
+			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{all(seconds), 1 << 5, all(hours), all(dom), all(months), 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}
+}

+ 12 - 6
spec.go

@@ -8,6 +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
 }
 
 // bounds provides a range of acceptable values (plus a map of name to value).
@@ -63,6 +64,11 @@ func (s *SpecSchedule) Next(t time.Time) time.Time {
 	// 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()
+	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)
 
@@ -84,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)
 
@@ -98,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)
 
@@ -110,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)
 
@@ -122,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)
 
@@ -134,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)
 
@@ -143,7 +149,7 @@ WRAP:
 		}
 	}
 
-	return t
+	return t.In(origLocation)
 }
 
 // dayMatches returns true if the schedule's day-of-week and day-of-month

+ 15 - 17
spec_test.go

@@ -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)
 }