Browse Source

added Timezone support, made Dow mandatory and Seconds optional to support standard GNU/Linux crontabs

Dennis Francis 11 years ago
parent
commit
a7d924fa04
6 changed files with 92 additions and 66 deletions
  1. 1 1
      README.md
  2. 3 2
      doc.go
  3. 64 43
      parser.go
  4. 1 1
      parser_test.go
  5. 6 2
      spec.go
  6. 17 17
      spec_test.go

+ 1 - 1
README.md

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

+ 3 - 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()
@@ -28,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      | Yes        | 0-59            | * / , -
+	Seconds      | No         | 0-59            | * / , -
 	Minutes      | Yes        | 0-59            | * / , -
 	Hours        | Yes        | 0-23            | * / , -
 	Day of month | Yes        | 1-31            | * / , - ?
@@ -101,7 +102,7 @@ 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
+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).
 
 Be aware that jobs scheduled during daylight-savings leap-ahead transitions will

+ 64 - 43
parser.go

@@ -23,29 +23,45 @@ 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
+	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])
+		}
+		fields = fields[1:len(fields)]
+		spec = strings.Join(fields, " ")
+	}
+
 	if spec[0] == '@' {
-		return parseDescriptor(spec), nil
+		return parseDescriptor(spec, loc), nil
 	}
 
-	// Split on whitespace.  We require 5 or 6 fields.
-	// (second) (minute) (hour) (day of month) (month) (day of week, optional)
-	fields := strings.Fields(spec)
+	// We require 5 or 6 fields.
+	// (second optional) (minute) (hour) (day of month) (month) (day of week)
 	if len(fields) != 5 && len(fields) != 6 {
 		log.Panicf("Expected 5 or 6 fields, found %d: %s", len(fields), spec)
 	}
 
-	// If a sixth field is not provided (DayOfWeek), then it is equivalent to star.
+	// If a six fields is not provided (no seconds field), then it is equivalent to 0.
 	if len(fields) == 5 {
-		fields = append(fields, "*")
+		newfields := []string{"00"}
+		newfields  = append(newfields, fields...)
+		fields = newfields
 	}
 
 	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 +180,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,
 		}
 	}
 

+ 1 - 1
parser_test.go

@@ -101,7 +101,7 @@ func TestSpecSchedule(t *testing.T) {
 		expr     string
 		expected Schedule
 	}{
-		{"* 5 * * * *", &SpecSchedule{all(seconds), 1 << 5, all(hours), all(dom), all(months), all(dow)}},
+		{"* 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}},
 	}
 

+ 6 - 2
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).
@@ -62,7 +63,10 @@ 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)
-
+	origLocation := t.Location()
+	if s.Location != nil {
+		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)
 
@@ -143,7 +147,7 @@ WRAP:
 		}
 	}
 
-	return t
+	return t.In(origLocation)
 }
 
 // dayMatches returns true if the schedule's day-of-week and day-of-month

+ 17 - 17
spec_test.go

@@ -11,18 +11,18 @@ func TestActivation(t *testing.T) {
 		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},
+		{"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},
 
 		// 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},
+		{"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},
 
 		// Named months
-		{"Sun Jul 15 15:00 2012", "0 0/15 * * Jul", true},
-		{"Sun Jul 15 15:00 2012", "0 0/15 * * Jun", false},
+		{"Sun Jul 15 15:00 2012", "0/15 * * Jul *", true},
+		{"Sun Jul 15 15:00 2012", "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 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"},
+		{"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"},
 
 		// Wrap around hours
-		{"Mon Jul 9 15:45 2012", "0 20-35/15 * * *", "Mon Jul 9 16:20 2012"},
+		{"Mon Jul 9 15:45 2012", "20-35/15 * * * *", "Mon Jul 9 16:20 2012"},
 
 		// 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"},
+		{"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: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"},