summaryrefslogtreecommitdiff
path: root/schedule
diff options
context:
space:
mode:
authorKevin Chabowski <kevin@kch42.de>2013-09-15 12:53:11 +0200
committerKevin Chabowski <kevin@kch42.de>2013-09-15 12:53:11 +0200
commit819a2e2c9b15e5167f8af3736905569949306c94 (patch)
treec359490b71fd090c9a3033905e6f32640f67ced3 /schedule
parentc0a06609919d09fbcb6326ff69cc455dd13506ef (diff)
downloadmailremind-819a2e2c9b15e5167f8af3736905569949306c94.tar.gz
mailremind-819a2e2c9b15e5167f8af3736905569949306c94.tar.bz2
mailremind-819a2e2c9b15e5167f8af3736905569949306c94.zip
Changed weird internal terminology
Diffstat (limited to 'schedule')
-rw-r--r--schedule/chronos.go280
-rw-r--r--schedule/chronos_test.go45
2 files changed, 325 insertions, 0 deletions
diff --git a/schedule/chronos.go b/schedule/chronos.go
new file mode 100644
index 0000000..417f3d8
--- /dev/null
+++ b/schedule/chronos.go
@@ -0,0 +1,280 @@
+package schedule
+
+import (
+ "errors"
+ "fmt"
+ "math"
+ "strconv"
+ "strings"
+ "time"
+)
+
+type TimeUnit int
+
+const timefmt = "2006-01-02 15:04:05"
+
+const (
+ Minute TimeUnit = iota
+ Hour
+ Day
+ Week
+ Month
+ Year
+)
+
+var nilTime time.Time
+
+var tuLookup = map[string]TimeUnit{
+ "Minute": Minute,
+ "Hour": Hour,
+ "Day": Day,
+ "Week": Week,
+ "Month": Month,
+ "Year": Year,
+}
+
+func (tu TimeUnit) String() string {
+ switch tu {
+ case Minute:
+ return "Minute"
+ case Hour:
+ return "Hour"
+ case Day:
+ return "Day"
+ case Week:
+ return "Week"
+ case Month:
+ return "Month"
+ case Year:
+ return "Year"
+ default:
+ return "(Unknown TimeUnit)"
+ }
+}
+
+func (tu TimeUnit) minApprox() time.Duration {
+ const (
+ maMinute = time.Minute
+ maHour = time.Hour
+ maDay = 24*time.Hour - time.Second
+ maWeek = 7 * maDay
+ maMonth = 28 * maDay
+ maYear = 365 * maDay
+ )
+
+ switch tu {
+ case Minute:
+ return maMinute
+ case Hour:
+ return maHour
+ case Day:
+ return maDay
+ case Week:
+ return maWeek
+ case Month:
+ return maMonth
+ case Year:
+ return maYear
+ default:
+ return 0
+ }
+}
+
+func (tu TimeUnit) maxApprox() time.Duration {
+ const (
+ maMinute = time.Minute
+ maHour = time.Hour
+ maDay = 24*time.Hour + time.Second
+ maWeek = 7 * maDay
+ maMonth = 31 * maDay
+ maYear = 366 * maDay
+ )
+
+ switch tu {
+ case Minute:
+ return maMinute
+ case Hour:
+ return maHour
+ case Day:
+ return maDay
+ case Week:
+ return maWeek
+ case Month:
+ return maMonth
+ case Year:
+ return maYear
+ default:
+ return 0
+ }
+}
+
+type Frequency struct {
+ Unit TimeUnit
+ Count uint
+}
+
+func (f Frequency) String() string {
+ return fmt.Sprintf("%d %s", f.Count, f.Unit)
+}
+
+func (f Frequency) addTo(t time.Time, mul uint) time.Time {
+ sec := t.Second()
+ min := t.Minute()
+ hour := t.Hour()
+ day := t.Day()
+ month := t.Month()
+ year := t.Year()
+ loc := t.Location()
+
+ fq := int(f.Count * mul)
+
+ switch f.Unit {
+ case Minute:
+ return t.Add(time.Minute * time.Duration(fq))
+ case Hour:
+ return t.Add(time.Hour * time.Duration(fq))
+ case Day:
+ return time.Date(year, month, day+fq, hour, min, sec, 0, loc)
+ case Week:
+ return time.Date(year, month, day+fq*7, hour, min, sec, 0, loc)
+ case Month:
+ return time.Date(year, month+time.Month(fq), day, hour, min, sec, 0, loc)
+ case Year:
+ return time.Date(year+fq, month, day, hour, min, sec, 0, loc)
+ default:
+ return nilTime
+ }
+}
+
+func (f Frequency) minApprox() time.Duration { return time.Duration(f.Count) * f.Unit.minApprox() }
+func (f Frequency) maxApprox() time.Duration { return time.Duration(f.Count) * f.Unit.maxApprox() }
+
+// Schedule describes a time schedule. It has a start and optional end point and an optional frequency.
+type Schedule struct {
+ Start, End time.Time
+ Freq Frequency
+}
+
+// NextAfter calculates the next time in the schedule after t. If no such time exists, nil is returned (test with Time.IsZero()).
+func (c Schedule) NextAfter(t time.Time) time.Time {
+ if !t.After(c.Start) {
+ return c.Start
+ }
+ if c.Freq.Count == 0 {
+ return nilTime
+ }
+
+ d := t.Sub(c.Start)
+
+ fmin := uint(math.Floor(float64(d) / float64(c.Freq.maxApprox())))
+ fmax := uint(math.Ceil(float64(d) / float64(c.Freq.minApprox())))
+
+ for f := fmin; f <= fmax; f++ {
+ t2 := c.Freq.addTo(c.Start, f)
+ if t2.Before(c.Start) || t2.Before(t) {
+ continue
+ }
+ if (!c.End.IsZero()) && t2.After(c.End) {
+ return nilTime
+ }
+ return t2
+ }
+
+ return nilTime // Should actually never happen...
+}
+
+func (c Schedule) String() string {
+ s := c.Start.UTC().Format(timefmt)
+ if c.Freq.Count > 0 {
+ s += " +" + c.Freq.String()
+ if !c.End.IsZero() {
+ s += " !" + c.End.UTC().Format(timefmt)
+ }
+ }
+ return s
+}
+
+func ParseSchedule(s string) (c Schedule, err error) {
+ elems := strings.Split(s, " ")
+
+ switch len(elems) {
+ case 6: // Everything specified
+ _end := elems[4] + " " + elems[5]
+ if c.End, err = time.ParseInLocation(timefmt, _end[1:], time.UTC); err != nil {
+ return
+ }
+ fallthrough
+ case 4: // start time and frequency
+ var count uint64
+ if count, err = strconv.ParseUint(elems[2][1:], 10, 32); err != nil {
+ return
+ }
+ c.Freq.Count = uint(count)
+
+ var ok bool
+ if c.Freq.Unit, ok = tuLookup[elems[3]]; !ok {
+ err = fmt.Errorf("Unknown timeunit %s", elems[3])
+ return
+ }
+ fallthrough
+ case 2: // Only start time
+ if c.Start, err = time.ParseInLocation(timefmt, elems[0]+" "+elems[1], time.UTC); err != nil {
+ return
+ }
+ default:
+ err = errors.New("Unknown schedule format")
+ }
+
+ return
+}
+
+type MultiSchedule []Schedule
+
+func (mc MultiSchedule) NextAfter(t time.Time) time.Time {
+ var nearest time.Time
+
+ for _, c := range mc {
+ next := c.NextAfter(t)
+ if next.IsZero() {
+ continue
+ }
+
+ if nearest.IsZero() {
+ nearest = next
+ } else if next.Before(nearest) {
+ nearest = next
+ }
+ }
+
+ return nearest
+}
+
+func (mc MultiSchedule) String() (s string) {
+ sep := ""
+
+ for _, c := range mc {
+ s += sep + c.String()
+ sep = "\n"
+ }
+
+ return
+}
+
+func ParseMultiSchedule(s string) (mc MultiSchedule, err error) {
+ parts := strings.Split(s, "\n")
+ for l, _part := range parts {
+ part := strings.TrimSpace(_part)
+ if part == "" {
+ continue
+ }
+
+ c, err := ParseSchedule(part)
+ if err != nil {
+ return nil, fmt.Errorf("Line %d: %s", l+1, err)
+ }
+
+ mc = append(mc, c)
+ }
+
+ return
+}
diff --git a/schedule/chronos_test.go b/schedule/chronos_test.go
new file mode 100644
index 0000000..484039e
--- /dev/null
+++ b/schedule/chronos_test.go
@@ -0,0 +1,45 @@
+package schedule
+
+import (
+ "testing"
+ "time"
+)
+
+func mktime(y int, month time.Month, d, h, min int) time.Time {
+ return time.Date(y, month, d, h, min, 0, 0, time.UTC)
+}
+
+func TestSchedule(t *testing.T) {
+ tbl := []struct {
+ schedule string
+ now time.Time
+ want time.Time
+ }{
+ {"1991-04-30 00:00:00 +1 Year", mktime(2013, 8, 26, 13, 37), mktime(2014, 4, 30, 0, 0)},
+ {"2013-01-01 00:00:00", mktime(2013, 8, 26, 13, 37), nilTime},
+ {"2013-01-01 00:00:00", mktime(2012, 1, 1, 0, 0), mktime(2013, 1, 1, 0, 0)},
+ {"1900-12-24 12:34:00 +5 Year", mktime(2013, 8, 26, 13, 37), mktime(2015, 12, 24, 12, 34)},
+ {"1900-12-24 12:34:00 +5 Year !2010-01-01 01:01:00", mktime(2013, 8, 26, 13, 37), nilTime},
+ {"2013-08-01 04:02:00 +3 Week", mktime(2013, 8, 26, 13, 37), mktime(2013, 9, 12, 4, 2)},
+ {"2013-08-26 13:37:00", mktime(2013, 8, 26, 13, 37), mktime(2013, 8, 26, 13, 37)},
+ {"2013-08-25 13:37:00 +1 Day", mktime(2013, 8, 26, 13, 37), mktime(2013, 8, 26, 13, 37)},
+ {"2012-12-31 23:59:00 +100 Minute", mktime(2013, 1, 1, 0, 0), mktime(2013, 1, 1, 1, 39)},
+ }
+
+ for i, e := range tbl {
+ c, err := ParseSchedule(e.schedule)
+ if err != nil {
+ t.Errorf("#%d: Failed parsing \"%s\": %s", i, e.schedule, err)
+ continue
+ }
+ have := c.NextAfter(e.now)
+ if !have.Equal(e.want) {
+ t.Errorf("#%d: Want: %s, Have: %s", i, e.want, have)
+ }
+
+ if s := c.String(); s != e.schedule {
+ t.Errorf("#%d: String() failed: \"%s\" != \"%s\"", i, e.schedule, s)
+ }
+
+ }
+}