From 819a2e2c9b15e5167f8af3736905569949306c94 Mon Sep 17 00:00:00 2001 From: Kevin Chabowski Date: Sun, 15 Sep 2013 12:53:11 +0200 Subject: Changed weird internal terminology --- checkjobs.go | 2 +- chronos/chronos.go | 280 ----------------------------------------------- chronos/chronos_test.go | 45 -------- jobedit.go | 80 +++++++------- model/dbmodel.go | 8 +- model/mysql/jobs.go | 34 +++--- model/mysql/queries.go | 8 +- schedule/chronos.go | 280 +++++++++++++++++++++++++++++++++++++++++++++++ schedule/chronos_test.go | 45 ++++++++ 9 files changed, 391 insertions(+), 391 deletions(-) delete mode 100644 chronos/chronos.go delete mode 100644 chronos/chronos_test.go create mode 100644 schedule/chronos.go create mode 100644 schedule/chronos_test.go diff --git a/checkjobs.go b/checkjobs.go index c053dd5..3974c9b 100644 --- a/checkjobs.go +++ b/checkjobs.go @@ -42,7 +42,7 @@ func checkjobsOnce(t time.Time) { for _, job := range jobs { if sendjob(job, t) { - next := job.Chronos().NextAfter(t) + next := job.Schedule().NextAfter(t) if next.IsZero() { if err := job.Delete(); err != nil { log.Printf("Failed deleting job %s after job was done: %s", job.ID(), err) diff --git a/chronos/chronos.go b/chronos/chronos.go deleted file mode 100644 index 74fec19..0000000 --- a/chronos/chronos.go +++ /dev/null @@ -1,280 +0,0 @@ -package chronos - -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() } - -// Chronos describes a time schedule. It has a start and optional end point and an optional frequency. -type Chronos 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 Chronos) 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 Chronos) 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 ParseChronos(s string) (c Chronos, 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 chronos format") - } - - return -} - -type MultiChronos []Chronos - -func (mc MultiChronos) 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 MultiChronos) String() (s string) { - sep := "" - - for _, c := range mc { - s += sep + c.String() - sep = "\n" - } - - return -} - -func ParseMultiChronos(s string) (mc MultiChronos, err error) { - parts := strings.Split(s, "\n") - for l, _part := range parts { - part := strings.TrimSpace(_part) - if part == "" { - continue - } - - c, err := ParseChronos(part) - if err != nil { - return nil, fmt.Errorf("Line %d: %s", l+1, err) - } - - mc = append(mc, c) - } - - return -} diff --git a/chronos/chronos_test.go b/chronos/chronos_test.go deleted file mode 100644 index 1798b4c..0000000 --- a/chronos/chronos_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package chronos - -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 TestChronos(t *testing.T) { - tbl := []struct { - chronos 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 := ParseChronos(e.chronos) - if err != nil { - t.Errorf("#%d: Failed parsing \"%s\": %s", i, e.chronos, 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.chronos { - t.Errorf("#%d: String() failed: \"%s\" != \"%s\"", i, e.chronos, s) - } - - } -} diff --git a/jobedit.go b/jobedit.go index cbe9225..6057767 100644 --- a/jobedit.go +++ b/jobedit.go @@ -3,9 +3,9 @@ package main import ( "github.com/gorilla/mux" "github.com/gorilla/sessions" - "kch42.de/gostuff/mailremind/chronos" "kch42.de/gostuff/mailremind/confhelper" "kch42.de/gostuff/mailremind/model" + "kch42.de/gostuff/mailremind/schedule" "log" "net/http" "net/url" @@ -20,38 +20,38 @@ type scheduleTpldata struct { RepetitionEnabled, EndEnabled bool } -func chronToSchedTL(chron chronos.Chronos, u model.User) scheduleTpldata { +func schedToSchedTL(sched schedule.Schedule, u model.User) scheduleTpldata { loc := u.Location() - schedule := scheduleTpldata{ - Start: chron.Start.In(loc).Format(bestTimeFmtEver), + schedtl := scheduleTpldata{ + Start: sched.Start.In(loc).Format(bestTimeFmtEver), } - if f := chron.Freq; f.Count > 0 { - schedule.RepetitionEnabled = true - schedule.Count = int(f.Count) + if f := sched.Freq; f.Count > 0 { + schedtl.RepetitionEnabled = true + schedtl.Count = int(f.Count) switch f.Unit { - case chronos.Minute: - schedule.UnitIsMinute = true - case chronos.Hour: - schedule.UnitIsHour = true - case chronos.Day: - schedule.UnitIsDay = true - case chronos.Week: - schedule.UnitIsWeek = true - case chronos.Month: - schedule.UnitIsMonth = true - case chronos.Year: - schedule.UnitIsYear = true + case schedule.Minute: + schedtl.UnitIsMinute = true + case schedule.Hour: + schedtl.UnitIsHour = true + case schedule.Day: + schedtl.UnitIsDay = true + case schedule.Week: + schedtl.UnitIsWeek = true + case schedule.Month: + schedtl.UnitIsMonth = true + case schedule.Year: + schedtl.UnitIsYear = true } } - if end := chron.End; !end.IsZero() { - schedule.EndEnabled = true - schedule.End = end.In(loc).Format(bestTimeFmtEver) + if end := sched.End; !end.IsZero() { + schedtl.EndEnabled = true + schedtl.End = end.In(loc).Format(bestTimeFmtEver) } - return schedule + return schedtl } var maxSchedules, jobsLimit int @@ -76,17 +76,17 @@ func (jt *jobeditTpldata) fillFromJob(job model.Job, u model.User) { jt.Content = string(job.Content()) jt.Schedules = make([]scheduleTpldata, maxSchedules) - for i, chron := range job.Chronos() { + for i, sched := range job.Schedule() { if i == maxSchedules { - log.Printf("Job %s has more than %d Chronos entries!", job.ID(), maxSchedules) + log.Printf("Job %s has more than %d schedule entries!", job.ID(), maxSchedules) break } - jt.Schedules[i] = chronToSchedTL(chron, u) + jt.Schedules[i] = schedToSchedTL(sched, u) } } -func (jt *jobeditTpldata) interpretForm(form url.Values, u model.User) (subject string, content []byte, mc chronos.MultiChronos, ok bool) { +func (jt *jobeditTpldata) interpretForm(form url.Values, u model.User) (subject string, content []byte, ms schedule.MultiSchedule, ok bool) { loc := u.Location() l1 := len(form["Start"]) @@ -124,7 +124,7 @@ func (jt *jobeditTpldata) interpretForm(form url.Values, u model.User) (subject } count := uint64(0) - var unit chronos.TimeUnit + var unit schedule.TimeUnit var end time.Time if form["RepetitionEnabled"][i] == "yes" { if count, err = strconv.ParseUint(form["Count"][i], 10, 64); err != nil { @@ -134,17 +134,17 @@ func (jt *jobeditTpldata) interpretForm(form url.Values, u model.User) (subject switch form["Unit"][i] { case "Minute": - unit = chronos.Minute + unit = schedule.Minute case "Hour": - unit = chronos.Hour + unit = schedule.Hour case "Day": - unit = chronos.Day + unit = schedule.Day case "Week": - unit = chronos.Week + unit = schedule.Week case "Month": - unit = chronos.Month + unit = schedule.Month case "Year": - unit = chronos.Year + unit = schedule.Year default: ok = false continue @@ -158,16 +158,16 @@ func (jt *jobeditTpldata) interpretForm(form url.Values, u model.User) (subject } } - chron := chronos.Chronos{ + sched := schedule.Schedule{ Start: start, - Freq: chronos.Frequency{ + Freq: schedule.Frequency{ Count: uint(count), Unit: unit, }, End: end, } - mc = append(mc, chron) - jt.Schedules[i] = chronToSchedTL(chron, u) + ms = append(ms, sched) + jt.Schedules[i] = schedToSchedTL(sched, u) } if !ok { @@ -175,7 +175,7 @@ func (jt *jobeditTpldata) interpretForm(form url.Values, u model.User) (subject return } - if len(mc) == 0 { + if len(ms) == 0 { jt.Error = "No schedule." ok = false } @@ -236,7 +236,7 @@ func jobedit(user model.User, sess *sessions.Session, req *http.Request) (interf } else if job != nil { if logfail("setting subject", job.SetSubject(subject)) && logfail("setting content", job.SetContent(content)) && - logfail("setting chronos", job.SetChronos(mc)) && + logfail("setting schedule", job.SetSchedule(mc)) && logfail("setting next", job.SetNext(next)) { outdata.Success = "Changes saved" } else { diff --git a/model/dbmodel.go b/model/dbmodel.go index 2889908..d4036d1 100644 --- a/model/dbmodel.go +++ b/model/dbmodel.go @@ -3,7 +3,7 @@ package model import ( "errors" "fmt" - "kch42.de/gostuff/mailremind/chronos" + "kch42.de/gostuff/mailremind/schedule" "sync" "time" ) @@ -23,7 +23,7 @@ type User interface { PWHash() []byte SetPWHash([]byte) error - AddJob(subject string, content []byte, chron chronos.MultiChronos, next time.Time) (Job, error) + AddJob(subject string, content []byte, sched schedule.MultiSchedule, next time.Time) (Job, error) Jobs() []Job JobByID(DBID) (Job, error) CountJobs() int @@ -50,8 +50,8 @@ type Job interface { Content() []byte SetContent([]byte) error - Chronos() chronos.MultiChronos - SetChronos(chronos.MultiChronos) error + Schedule() schedule.MultiSchedule + SetSchedule(schedule.MultiSchedule) error Next() time.Time SetNext(time.Time) error diff --git a/model/mysql/jobs.go b/model/mysql/jobs.go index 1b9598b..eef5667 100644 --- a/model/mysql/jobs.go +++ b/model/mysql/jobs.go @@ -3,8 +3,8 @@ package mysql import ( "database/sql" "fmt" - "kch42.de/gostuff/mailremind/chronos" "kch42.de/gostuff/mailremind/model" + "kch42.de/gostuff/mailremind/schedule" "log" "time" ) @@ -17,7 +17,7 @@ type Job struct { subject string content []byte next time.Time - chron []chronos.Chronos + sched schedule.MultiSchedule } func jobFromSQL(con *MySQLDBCon, s scanner) (*Job, error) { @@ -25,13 +25,13 @@ func jobFromSQL(con *MySQLDBCon, s scanner) (*Job, error) { var subject string var content []byte var _next int64 - var _mchron string + var _msched string - if err := s.Scan(&_id, &_user, &subject, &content, &_next, &_mchron); err != nil { + if err := s.Scan(&_id, &_user, &subject, &content, &_next, &_msched); err != nil { return nil, err } - chron, err := chronos.ParseMultiChronos(_mchron) + sched, err := schedule.ParseMultiSchedule(_msched) if err != nil { return nil, err } @@ -43,7 +43,7 @@ func jobFromSQL(con *MySQLDBCon, s scanner) (*Job, error) { subject: subject, content: content, next: time.Unix(_next, 0), - chron: chron, + sched: sched, }, nil } @@ -90,7 +90,7 @@ func (u *User) JobByID(_id model.DBID) (model.Job, error) { } } -func (u *User) AddJob(subject string, content []byte, chron chronos.MultiChronos, next time.Time) (model.Job, error) { +func (u *User) AddJob(subject string, content []byte, sched schedule.MultiSchedule, next time.Time) (model.Job, error) { tx, err := u.con.con.Begin() if err != nil { return nil, err @@ -98,7 +98,7 @@ func (u *User) AddJob(subject string, content []byte, chron chronos.MultiChronos insjob := tx.Stmt(u.con.stmt[qInsertJob]) - res, err := insjob.Exec(uint64(u.id), subject, content, next.Unix(), chron.String()) + res, err := insjob.Exec(uint64(u.id), subject, content, next.Unix(), sched.String()) if err != nil { tx.Rollback() return nil, err @@ -121,15 +121,15 @@ func (u *User) AddJob(subject string, content []byte, chron chronos.MultiChronos subject: subject, content: content, next: next, - chron: chron, + sched: sched, }, nil } -func (j *Job) ID() model.DBID { return j.id } -func (j *Job) Subject() string { return j.subject } -func (j *Job) Content() []byte { return j.content } -func (j *Job) Chronos() chronos.MultiChronos { return j.chron } -func (j *Job) Next() time.Time { return j.next } +func (j *Job) ID() model.DBID { return j.id } +func (j *Job) Subject() string { return j.subject } +func (j *Job) Content() []byte { return j.content } +func (j *Job) Schedule() schedule.MultiSchedule { return j.sched } +func (j *Job) Next() time.Time { return j.next } func (j *Job) User() model.User { u, err := j.con.UserByID(j.user) @@ -160,12 +160,12 @@ func (j *Job) SetContent(cont []byte) error { return nil } -func (j *Job) SetChronos(chron chronos.MultiChronos) error { - if _, err := j.con.stmt[qSetChronos].Exec(chron.String(), uint64(j.id)); err != nil { +func (j *Job) SetSchedule(sched schedule.MultiSchedule) error { + if _, err := j.con.stmt[qSetSchedule].Exec(sched.String(), uint64(j.id)); err != nil { return err } - j.chron = chron + j.sched = sched return nil } diff --git a/model/mysql/queries.go b/model/mysql/queries.go index e579df7..1aa6853 100644 --- a/model/mysql/queries.go +++ b/model/mysql/queries.go @@ -19,14 +19,14 @@ const ( qJobsBefore qInsertJob qInsertUser - qSetChronos + qSetSchedule qSetLocation qEnd ) const ( qfragSelUser = "SELECT `id`, `email`, `passwd`, `location`, `active`, `activationcode`, `added` FROM `users` " - qfragSelJob = "SELECT `id`, `user`, `subject`, `content`, `next`, `chronos` FROM `jobs` " + qfragSelJob = "SELECT `id`, `user`, `subject`, `content`, `next`, `schedule` FROM `jobs` " ) var queries = map[int]string{ @@ -46,8 +46,8 @@ var queries = map[int]string{ qSetNext: "UPDATE `jobs` SET `next` = ? WHERE `id` = ?", qDelJob: "DELETE FROM `jobs` WHERE `id` = ?", qJobsBefore: qfragSelJob + "WHERE `next` <= ?", - qInsertJob: "INSERT INTO `jobs` (`user`, `subject`, `content`, `next`, `chronos`) VALUES (?, ?, ?, ?, ?)", + qInsertJob: "INSERT INTO `jobs` (`user`, `subject`, `content`, `next`, `schedule`) VALUES (?, ?, ?, ?, ?)", qInsertUser: "INSERT INTO `users` (`email`, `passwd`, `location`, `active`, `activationcode`, `added`) VALUES (?, ?, ?, ?, ?, ?)", - qSetChronos: "UPDATE `jobs` SET `chronos` = ? WHERE `id` = ?", + qSetSchedule: "UPDATE `jobs` SET `schedule` = ? WHERE `id` = ?", qSetLocation: "UPDATE `users` SET `location` = ? WHERE `id` = ?", } 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) + } + + } +} -- cgit v1.2.3-54-g00ecf