diff --git a/README.md b/README.md index f0eaeeb3..7805fc0a 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,14 @@ s.Every(5).Days().Do(func(){ ... }) s.Every(1).Month(1, 2, 3).Do(func(){ ... }) +// set time +s.Every(1).Day().At("10:30").Do(func(){ ... }) + +// set multiple times +s.Every(1).Day().At("10:30;08:00").Do(func(){ ... }) + +s.Every(1).Day().At("10:30").At("08:00").Do(func(){ ... }) + // Schedule each last day of the month s.Every(1).MonthLastDay().Do(func(){ ... }) diff --git a/example_test.go b/example_test.go index 62af879d..9378de17 100644 --- a/example_test.go +++ b/example_test.go @@ -88,8 +88,22 @@ func ExampleJob_ScheduledAtTime() { job, _ := s.Every(1).Day().At("10:30").Do(task) s.StartAsync() fmt.Println(job.ScheduledAtTime()) + + // if multiple times are set, the earliest time will be returned + job1, _ := s.Every(1).Day().At("10:30;08:00").Do(task) + fmt.Println(job1.ScheduledAtTime()) // Output: // 10:30 + // 8:0 +} + +func ExampleJob_ScheduledAtTimes() { + s := gocron.NewScheduler(time.UTC) + job, _ := s.Every(1).Day().At("10:30;08:00").Do(task) + s.StartAsync() + fmt.Println(job.ScheduledAtTimes()) + // Output: + // [8:0 10:30] } func ExampleJob_ScheduledTime() { @@ -171,6 +185,9 @@ func ExampleScheduler_At() { s := gocron.NewScheduler(time.UTC) _, _ = s.Every(1).Day().At("10:30").Do(task) _, _ = s.Every(1).Monday().At("10:30:01").Do(task) + // multiple + _, _ = s.Every(1).Monday().At("10:30;18:00").Do(task) + _, _ = s.Every(1).Monday().At("10:30").At("18:00").Do(task) } func ExampleScheduler_ChangeLocation() { diff --git a/go.mod b/go.mod index e41e9944..7ea126e0 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,13 @@ module github.com/go-co-op/gocron go 1.17 require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/robfig/cron/v3 v3.0.1 github.com/stretchr/testify v1.7.0 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/job.go b/job.go index 32cd9652..e52a0e10 100644 --- a/job.go +++ b/job.go @@ -3,6 +3,7 @@ package gocron import ( "context" "fmt" + "sort" "sync" "sync/atomic" "time" @@ -15,21 +16,21 @@ import ( type Job struct { mu sync.RWMutex jobFunction - interval int // pause interval * unit between runs - duration time.Duration // time duration between runs - unit schedulingUnit // time units, e.g. 'minutes', 'hours'... - startsImmediately bool // if the Job should run upon scheduler start - atTime time.Duration // optional time at which this Job runs when interval is day - startAtTime time.Time // optional time at which the Job starts - error error // error related to Job - lastRun time.Time // datetime of last run - nextRun time.Time // datetime of next run - scheduledWeekdays []time.Weekday // Specific days of the week to start on - daysOfTheMonth []int // Specific days of the month to run the job - tags []string // allow the user to tag Jobs with certain labels - runCount int // number of times the job ran - timer *time.Timer // handles running tasks at specific time - cronSchedule cron.Schedule // stores the schedule when a task uses cron + interval int // pause interval * unit between runs + duration time.Duration // time duration between runs + unit schedulingUnit // time units, e.g. 'minutes', 'hours'... + startsImmediately bool // if the Job should run upon scheduler start + atTimes []time.Duration // optional time(s) at which this Job runs when interval is day + startAtTime time.Time // optional time at which the Job starts + error error // error related to Job + lastRun time.Time // datetime of last run + nextRun time.Time // datetime of next run + scheduledWeekdays []time.Weekday // Specific days of the week to start on + daysOfTheMonth []int // Specific days of the month to run the job + tags []string // allow the user to tag Jobs with certain labels + runCount int // number of times the job ran + timer *time.Timer // handles running tasks at specific time + cronSchedule cron.Schedule // stores the schedule when a task uses cron } type jobFunction struct { @@ -113,12 +114,63 @@ func (j *Job) setTimer(t *time.Timer) { j.timer = t } -func (j *Job) getAtTime() time.Duration { - return j.atTime +func (j *Job) getFirstAtTime() time.Duration { + var t time.Duration + if len(j.atTimes) > 0 { + t = j.atTimes[0] + } + + return t +} + +func (j *Job) getAtTime(lastRun time.Time) time.Duration { + var r time.Duration + if len(j.atTimes) == 0 { + return r + } + + if len(j.atTimes) == 1 { + return j.atTimes[0] + } + + if lastRun.IsZero() { + r = j.atTimes[0] + } else { + for _, d := range j.atTimes { + nt := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, lastRun.Location()).Add(d) + if nt.After(lastRun) { + r = d + break + } + } + } + + return r } -func (j *Job) setAtTime(t time.Duration) { - j.atTime = t +func (j *Job) addAtTime(t time.Duration) { + if len(j.atTimes) == 0 { + j.atTimes = append(j.atTimes, t) + return + } + exist := false + index := sort.Search(len(j.atTimes), func(i int) bool { + atTime := j.atTimes[i] + b := atTime >= t + if b { + exist = atTime == t + } + return b + }) + + // ignore if present + if exist { + return + } + + j.atTimes = append(j.atTimes, time.Duration(0)) + copy(j.atTimes[index+1:], j.atTimes[index:]) + j.atTimes[index] = t } func (j *Job) getStartAtTime() time.Time { @@ -208,9 +260,24 @@ func (j *Job) ScheduledTime() time.Time { return j.nextRun } -// ScheduledAtTime returns the specific time of day the Job will run at +// ScheduledAtTime returns the specific time of day the Job will run at. +// If multiple times are set, the earliest time will be returned. func (j *Job) ScheduledAtTime() string { - return fmt.Sprintf("%d:%d", j.atTime/time.Hour, (j.atTime%time.Hour)/time.Minute) + if len(j.atTimes) == 0 { + return "0:0" + } + + return fmt.Sprintf("%d:%d", j.getFirstAtTime()/time.Hour, (j.getFirstAtTime()%time.Hour)/time.Minute) +} + +// ScheduledAtTimes returns the specific times of day the Job will run at +func (j *Job) ScheduledAtTimes() []string { + r := make([]string, len(j.atTimes)) + for i, t := range j.atTimes { + r[i] = fmt.Sprintf("%d:%d", t/time.Hour, (t%time.Hour)/time.Minute) + } + + return r } // Weekday returns which day of the week the Job will run on and diff --git a/scheduler.go b/scheduler.go index 64dc061c..b92da946 100644 --- a/scheduler.go +++ b/scheduler.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" "sort" + "strings" "sync" "time" @@ -266,8 +267,8 @@ func (s *Scheduler) calculateMonths(job *Job, lastRun time.Time) nextRun { return nextRunResult } - next := lastRunRoundedMidnight.Add(job.getAtTime()).AddDate(0, job.interval, 0) - return nextRun{duration: until(lastRunRoundedMidnight, next), dateTime: next} + next := lastRunRoundedMidnight.Add(job.getFirstAtTime()).AddDate(0, job.interval, 0) + return nextRun{duration: until(lastRun, next), dateTime: next} } func calculateNextRunForLastDayOfMonth(s *Scheduler, job *Job, lastRun time.Time) nextRun { @@ -275,47 +276,60 @@ func calculateNextRunForLastDayOfMonth(s *Scheduler, job *Job, lastRun time.Time // first day of the month after the next month), and subtracting one day, unless the // last run occurred before the end of the month. addMonth := job.interval + atTime := job.getAtTime(lastRun) if testDate := lastRun.AddDate(0, 0, 1); testDate.Month() != lastRun.Month() && - !s.roundToMidnight(lastRun).Add(job.getAtTime()).After(lastRun) { + !s.roundToMidnight(lastRun).Add(atTime).After(lastRun) { // Our last run was on the last day of this month. addMonth++ + atTime = job.getFirstAtTime() } + next := time.Date(lastRun.Year(), lastRun.Month(), 1, 0, 0, 0, 0, s.Location()). - Add(job.getAtTime()). + Add(atTime). AddDate(0, addMonth, 0). AddDate(0, 0, -1) return nextRun{duration: until(lastRun, next), dateTime: next} } func calculateNextRunForMonth(s *Scheduler, job *Job, lastRun time.Time, dayOfMonth int) nextRun { - - jobDay := time.Date(lastRun.Year(), lastRun.Month(), dayOfMonth, 0, 0, 0, 0, s.Location()).Add(job.getAtTime()) + atTime := job.getAtTime(lastRun) + natTime := atTime + jobDay := time.Date(lastRun.Year(), lastRun.Month(), dayOfMonth, 0, 0, 0, 0, s.Location()).Add(atTime) difference := absDuration(lastRun.Sub(jobDay)) next := lastRun if jobDay.Before(lastRun) { // shouldn't run this month; schedule for next interval minus day difference next = next.AddDate(0, job.interval, -0) next = next.Add(-difference) + natTime = job.getFirstAtTime() } else { if job.interval == 1 && !jobDay.Equal(lastRun) { // every month counts current month next = next.AddDate(0, job.interval-1, 0) } else { // should run next month interval next = next.AddDate(0, job.interval, 0) + natTime = job.getFirstAtTime() } next = next.Add(difference) } + if atTime != natTime { + next = next.Add(-atTime).Add(natTime) + } return nextRun{duration: until(lastRun, next), dateTime: next} } func (s *Scheduler) calculateWeekday(job *Job, lastRun time.Time) nextRun { daysToWeekday := s.remainingDaysToWeekday(lastRun, job) totalDaysDifference := s.calculateTotalDaysDifference(lastRun, daysToWeekday, job) - next := s.roundToMidnight(lastRun).Add(job.getAtTime()).AddDate(0, 0, totalDaysDifference) + acTime := job.getAtTime(lastRun) + if totalDaysDifference > 0 { + acTime = job.getFirstAtTime() + } + next := s.roundToMidnight(lastRun).Add(acTime).AddDate(0, 0, totalDaysDifference) return nextRun{duration: until(lastRun, next), dateTime: next} } func (s *Scheduler) calculateWeeks(job *Job, lastRun time.Time) nextRun { totalDaysDifference := int(job.interval) * 7 - next := s.roundToMidnight(lastRun).Add(job.getAtTime()).AddDate(0, 0, totalDaysDifference) + next := s.roundToMidnight(lastRun).Add(job.getFirstAtTime()).AddDate(0, 0, totalDaysDifference) return nextRun{duration: until(lastRun, next), dateTime: next} } @@ -332,7 +346,7 @@ func (s *Scheduler) calculateTotalDaysDifference(lastRun time.Time, daysToWeekda } if daysToWeekday == 0 { // today, at future time or already passed - lastRunAtTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.Location()).Add(job.getAtTime()) + lastRunAtTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.Location()).Add(job.getAtTime(lastRun)) if lastRun.Before(lastRunAtTime) { return 0 } @@ -344,7 +358,7 @@ func (s *Scheduler) calculateTotalDaysDifference(lastRun time.Time, daysToWeekda func (s *Scheduler) calculateDays(job *Job, lastRun time.Time) nextRun { if job.interval == 1 { - lastRunDayPlusJobAtTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.Location()).Add(job.getAtTime()) + lastRunDayPlusJobAtTime := s.roundToMidnight(lastRun).Add(job.getAtTime(lastRun)) // handle occasional occurrence of job running to quickly / too early such that last run was within a second of now lastRunUnix, nowUnix := job.LastRun().Unix(), s.now().Unix() @@ -353,11 +367,11 @@ func (s *Scheduler) calculateDays(job *Job, lastRun time.Time) nextRun { } if shouldRunToday(lastRun, lastRunDayPlusJobAtTime) { - return nextRun{duration: until(lastRun, s.roundToMidnight(lastRun).Add(job.getAtTime())), dateTime: s.roundToMidnight(lastRun).Add(job.getAtTime())} + return nextRun{duration: until(lastRun, lastRunDayPlusJobAtTime), dateTime: lastRunDayPlusJobAtTime} } } - nextRunAtTime := s.roundToMidnight(lastRun).Add(job.getAtTime()).AddDate(0, 0, job.interval).In(s.Location()) + nextRunAtTime := s.roundToMidnight(lastRun).Add(job.getFirstAtTime()).AddDate(0, 0, job.interval).In(s.Location()) return nextRun{duration: until(lastRun, nextRunAtTime), dateTime: nextRunAtTime} } @@ -382,11 +396,11 @@ func in(scheduleWeekdays []time.Weekday, weekday time.Weekday) bool { } func (s *Scheduler) calculateDuration(job *Job) time.Duration { - lastRun := job.LastRun() if job.neverRan() && shouldRunAtSpecificTime(job) { // ugly. in order to avoid this we could prohibit setting .At() and allowing only .StartAt() when dealing with Duration types - atTime := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, s.Location()).Add(job.getAtTime()) - if lastRun.Before(atTime) || lastRun.Equal(atTime) { - return time.Until(s.roundToMidnight(lastRun).Add(job.getAtTime())) + now := s.time.Now(s.location) + next := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, s.Location()).Add(job.getFirstAtTime()) + if now.Before(next) || now.Equal(next) { + return next.Sub(now) } } @@ -404,7 +418,7 @@ func (s *Scheduler) calculateDuration(job *Job) time.Duration { } func shouldRunAtSpecificTime(job *Job) bool { - return job.getAtTime() != 0 + return job.getAtTime(job.lastRun) != 0 } func (s *Scheduler) remainingDaysToWeekday(lastRun time.Time, job *Job) int { @@ -424,7 +438,7 @@ func (s *Scheduler) remainingDaysToWeekday(lastRun time.Time, job *Job) int { }) // check atTime if equals { - if s.roundToMidnight(lastRun).Add(job.getAtTime()).After(lastRun) { + if s.roundToMidnight(lastRun).Add(job.getAtTime(lastRun)).After(lastRun) { return 0 } index++ @@ -764,7 +778,7 @@ func (s *Scheduler) Do(jobFun interface{}, params ...interface{}) (*Job, error) job := s.getCurrentJob() jobUnit := job.getUnit() - if job.atTime != 0 && (jobUnit <= hours || jobUnit >= duration) { + if job.getAtTime(job.lastRun) != 0 && (jobUnit <= hours || jobUnit >= duration) { job.error = wrapOrError(job.error, ErrAtTimeNotSupported) } @@ -821,15 +835,17 @@ func (s *Scheduler) At(i interface{}) *Scheduler { switch t := i.(type) { case string: - hour, min, sec, err := parseTime(t) - if err != nil { - job.error = wrapOrError(job.error, err) - return s + for _, tt := range strings.Split(t, ";") { + hour, min, sec, err := parseTime(tt) + if err != nil { + job.error = wrapOrError(job.error, err) + return s + } + // save atTime start as duration from midnight + job.addAtTime(time.Duration(hour)*time.Hour + time.Duration(min)*time.Minute + time.Duration(sec)*time.Second) } - // save atTime start as duration from midnight - job.setAtTime(time.Duration(hour)*time.Hour + time.Duration(min)*time.Minute + time.Duration(sec)*time.Second) case time.Time: - job.setAtTime(time.Duration(t.Hour())*time.Hour + time.Duration(t.Minute())*time.Minute + time.Duration(t.Second())*time.Second + time.Duration(t.Nanosecond())*time.Nanosecond) + job.addAtTime(time.Duration(t.Hour())*time.Hour + time.Duration(t.Minute())*time.Minute + time.Duration(t.Second())*time.Second + time.Duration(t.Nanosecond())*time.Nanosecond) default: job.error = wrapOrError(job.error, ErrUnsupportedTimeFormat) } diff --git a/scheduler_test.go b/scheduler_test.go index 4c5c9e06..fe5d3d4e 100644 --- a/scheduler_test.go +++ b/scheduler_test.go @@ -243,6 +243,48 @@ func TestAt(t *testing.T) { assert.EqualError(t, err, ErrUnsupportedTimeFormat.Error()) assert.Zero(t, s.Len()) }) + + exp := []time.Duration{_getHours(1), _getHours(3), _getHours(4), _getHours(7), _getHours(15)} + // multiple times + testCases := []struct { + name string + params []interface{} + result []time.Duration + }{ + { + name: "test1", + params: []interface{}{"03:00", "15:00", "01:00", "07:00", "04:00"}, + result: exp, + }, + { + name: "test2", + params: []interface{}{"03:00;15:00;01:00;07:00;04:00"}, + result: exp, + }, + { + name: "test3", + params: []interface{}{"03:00;15:00;01:00", time.Date(0, 0, 0, 7, 0, 0, 0, time.UTC), "04:00"}, + result: exp, + }, + { + name: "test4", + params: []interface{}{"03:00;15:00;01:00;07:00;04:00;01:00"}, + result: exp, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + s := NewScheduler(time.UTC) + for _, p := range tc.params { + s.At(p) + } + + got := s.getCurrentJob().atTimes + assert.Equalf(t, tc.result, got, fmt.Sprintf("expected %v / got %v", tc.result, got)) + }) + } + } func schedulerForNextOrPreviousWeekdayEveryNTimes(weekday time.Weekday, next bool, n int, s *Scheduler) *Scheduler { @@ -869,17 +911,17 @@ func TestScheduler_CalculateNextRun(t *testing.T) { {name: "every 25 hours test", job: &Job{interval: 25, unit: hours, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: _getHours(25)}, // DAYS {name: "every day at midnight", job: &Job{interval: 1, unit: days, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: 1 * day}, - {name: "every day at 09:30AM with scheduler starting before 09:30AM should run at same day at time", job: &Job{interval: 1, unit: days, atTime: _getHours(9) + _getMinutes(30), lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: _getHours(9) + _getMinutes(30)}, - {name: "every day at 09:30AM which just ran should run tomorrow at 09:30AM", job: &Job{interval: 1, unit: days, atTime: _getHours(9) + _getMinutes(30), lastRun: januaryFirst2020At(9, 30, 0)}, wantTimeUntilNextRun: 1 * day}, + {name: "every day at 09:30AM with scheduler starting before 09:30AM should run at same day at time", job: &Job{interval: 1, unit: days, atTimes: []time.Duration{_getHours(9) + _getMinutes(30)}, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: _getHours(9) + _getMinutes(30)}, + {name: "every day at 09:30AM which just ran should run tomorrow at 09:30AM", job: &Job{interval: 1, unit: days, atTimes: []time.Duration{_getHours(9) + _getMinutes(30)}, lastRun: januaryFirst2020At(9, 30, 0)}, wantTimeUntilNextRun: 1 * day}, {name: "every 31 days at midnight should run 31 days later", job: &Job{interval: 31, unit: days, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: 31 * day}, - {name: "daily job just ran at 8:30AM and should be scheduled for next day's 8:30AM", job: &Job{interval: 1, unit: days, atTime: 8*time.Hour + 30*time.Minute, lastRun: januaryFirst2020At(8, 30, 0)}, wantTimeUntilNextRun: 24 * time.Hour}, - {name: "daily job just ran at 5:30AM and should be scheduled for today at 8:30AM", job: &Job{interval: 1, unit: days, atTime: 8*time.Hour + 30*time.Minute, lastRun: januaryFirst2020At(5, 30, 0)}, wantTimeUntilNextRun: 3 * time.Hour}, - {name: "job runs every 2 days, just ran at 5:30AM and should be scheduled for 2 days at 8:30AM", job: &Job{interval: 2, unit: days, atTime: 8*time.Hour + 30*time.Minute, lastRun: januaryFirst2020At(5, 30, 0)}, wantTimeUntilNextRun: (2 * day) + 3*time.Hour}, - {name: "job runs every 2 days, just ran at 8:30AM and should be scheduled for 2 days at 8:30AM", job: &Job{interval: 2, unit: days, atTime: 8*time.Hour + 30*time.Minute, lastRun: januaryFirst2020At(8, 30, 0)}, wantTimeUntilNextRun: 2 * day}, - {name: "daily, last run was 1 second ago", job: &Job{interval: 1, unit: days, atTime: 12 * time.Hour, lastRun: ft.Now(time.UTC).Add(-time.Second)}, wantTimeUntilNextRun: 1 * day}, + {name: "daily job just ran at 8:30AM and should be scheduled for next day's 8:30AM", job: &Job{interval: 1, unit: days, atTimes: []time.Duration{8*time.Hour + 30*time.Minute}, lastRun: januaryFirst2020At(8, 30, 0)}, wantTimeUntilNextRun: 24 * time.Hour}, + {name: "daily job just ran at 5:30AM and should be scheduled for today at 8:30AM", job: &Job{interval: 1, unit: days, atTimes: []time.Duration{8*time.Hour + 30*time.Minute}, lastRun: januaryFirst2020At(5, 30, 0)}, wantTimeUntilNextRun: 3 * time.Hour}, + {name: "job runs every 2 days, just ran at 5:30AM and should be scheduled for 2 days at 8:30AM", job: &Job{interval: 2, unit: days, atTimes: []time.Duration{8*time.Hour + 30*time.Minute}, lastRun: januaryFirst2020At(5, 30, 0)}, wantTimeUntilNextRun: (2 * day) + 3*time.Hour}, + {name: "job runs every 2 days, just ran at 8:30AM and should be scheduled for 2 days at 8:30AM", job: &Job{interval: 2, unit: days, atTimes: []time.Duration{8*time.Hour + 30*time.Minute}, lastRun: januaryFirst2020At(8, 30, 0)}, wantTimeUntilNextRun: 2 * day}, + {name: "daily, last run was 1 second ago", job: &Job{interval: 1, unit: days, atTimes: []time.Duration{12 * time.Hour}, lastRun: ft.Now(time.UTC).Add(-time.Second)}, wantTimeUntilNextRun: 1 * day}, //// WEEKS {name: "every week should run in 7 days", job: &Job{interval: 1, unit: weeks, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: 7 * day}, - {name: "every week with .At time rule should run respect .At time rule", job: &Job{interval: 1, atTime: _getHours(9) + _getMinutes(30), unit: weeks, lastRun: januaryFirst2020At(9, 30, 0)}, wantTimeUntilNextRun: 7 * day}, + {name: "every week with .At time rule should run respect .At time rule", job: &Job{interval: 1, atTimes: []time.Duration{_getHours(9) + _getMinutes(30)}, unit: weeks, lastRun: januaryFirst2020At(9, 30, 0)}, wantTimeUntilNextRun: 7 * day}, {name: "every two weeks at 09:30AM should run in 14 days at 09:30AM", job: &Job{interval: 2, unit: weeks, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: 14 * day}, {name: "every 31 weeks ran at jan 1st at midnight should run at August 5, 2020", job: &Job{interval: 31, unit: weeks, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: 31 * 7 * day}, // MONTHS @@ -887,9 +929,9 @@ func TestScheduler_CalculateNextRun(t *testing.T) { {name: "every month in a 30 days month should be scheduled for 30 days ahead", job: &Job{interval: 1, unit: months, lastRun: time.Date(2020, time.April, 1, 0, 0, 0, 0, time.UTC)}, wantTimeUntilNextRun: 30 * day}, {name: "every month at february on leap year should count 29 days", job: &Job{interval: 1, unit: months, lastRun: time.Date(2020, time.February, 1, 0, 0, 0, 0, time.UTC)}, wantTimeUntilNextRun: 29 * day}, {name: "every month at february on non leap year should count 28 days", job: &Job{interval: 1, unit: months, lastRun: time.Date(2019, time.February, 1, 0, 0, 0, 0, time.UTC)}, wantTimeUntilNextRun: 28 * day}, - {name: "every month at first day at time should run next month + at time", job: &Job{interval: 1, unit: months, atTime: _getHours(9) + _getMinutes(30), lastRun: januaryFirst2020At(9, 30, 0)}, wantTimeUntilNextRun: 31*day + _getHours(9) + _getMinutes(30)}, + {name: "every month at first day at time should run next month", job: &Job{interval: 1, unit: months, atTimes: []time.Duration{_getHours(9) + _getMinutes(30)}, lastRun: januaryFirst2020At(9, 30, 0)}, wantTimeUntilNextRun: 31 * day}, {name: "every month at day should consider at days", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{2}, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: 1 * day}, - {name: "every month at day should consider at hours", job: &Job{interval: 1, unit: months, atTime: _getHours(9) + _getMinutes(30), lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: 31*day + _getHours(9) + _getMinutes(30)}, + {name: "every month at day should consider at hours", job: &Job{interval: 1, unit: months, atTimes: []time.Duration{_getHours(9) + _getMinutes(30)}, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: 31*day + _getHours(9) + _getMinutes(30)}, {name: "every month on the first day, but started on january 8th, should run February 1st", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{1}, lastRun: januaryFirst2020At(0, 0, 0).AddDate(0, 0, 7)}, wantTimeUntilNextRun: 24 * day}, {name: "every month same as lastRun, should run February 1st", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{1}, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: 31 * day}, {name: "every 2 months at day 1, starting at day 1, should run in 2 months", job: &Job{interval: 2, unit: months, daysOfTheMonth: []int{1}, lastRun: januaryFirst2020At(0, 0, 0)}, wantTimeUntilNextRun: 31*day + 29*day}, // 2020 january and february @@ -914,8 +956,8 @@ func TestScheduler_CalculateNextRun(t *testing.T) { {name: "every weekday starting on same weekday should run in 7 days", job: &Job{interval: 1, unit: weeks, scheduledWeekdays: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(0, 0, 0).AddDate(0, 0, 1)}, wantTimeUntilNextRun: 7 * day}, {name: "every 2 weekdays counting this week's weekday should run next weekday", job: &Job{interval: 2, unit: weeks, scheduledWeekdays: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(0, 0, 0)}, wantTimeUntilNextRun: day}, {name: "every weekday starting on one day after should count days remaining", job: &Job{interval: 1, unit: weeks, scheduledWeekdays: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(0, 0, 0).AddDate(0, 0, 2)}, wantTimeUntilNextRun: 6 * day}, - {name: "every weekday starting before jobs .At() time should run at same day at time", job: &Job{interval: 1, unit: weeks, atTime: _getHours(9) + _getMinutes(30), scheduledWeekdays: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(0, 0, 0).AddDate(0, 0, 1)}, wantTimeUntilNextRun: _getHours(9) + _getMinutes(30)}, - {name: "every weekday starting at same day at time that already passed should run at next week at time", job: &Job{interval: 1, unit: weeks, atTime: _getHours(9) + _getMinutes(30), scheduledWeekdays: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(10, 30, 0).AddDate(0, 0, 1)}, wantTimeUntilNextRun: 6*day + _getHours(23) + _getMinutes(0)}, + {name: "every weekday starting before jobs .At() time should run at same day at time", job: &Job{interval: 1, unit: weeks, atTimes: []time.Duration{_getHours(9) + _getMinutes(30)}, scheduledWeekdays: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(0, 0, 0).AddDate(0, 0, 1)}, wantTimeUntilNextRun: _getHours(9) + _getMinutes(30)}, + {name: "every weekday starting at same day at time that already passed should run at next week at time", job: &Job{interval: 1, unit: weeks, atTimes: []time.Duration{_getHours(9) + _getMinutes(30)}, scheduledWeekdays: []time.Weekday{*_tuesdayWeekday()}, lastRun: mondayAt(10, 30, 0).AddDate(0, 0, 1)}, wantTimeUntilNextRun: 6*day + _getHours(23) + _getMinutes(0)}, } for _, tc := range testCases { @@ -934,6 +976,11 @@ func _tuesdayWeekday() *time.Weekday { return &tuesday } +// helper test method +func _getDays(i int) time.Duration { + return time.Duration(i) * time.Hour * 24 +} + // helper test method func _getSeconds(i int) time.Duration { return time.Duration(i) * time.Second @@ -1134,12 +1181,12 @@ func TestCalculateMonths(t *testing.T) { job *Job wantTimeUntilNextRun time.Duration }{ - {description: "day before current and before current time, should run next month", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{2}, atTime: _getHours(2), lastRun: maySixth2021At0500.Now(time.UTC)}, wantTimeUntilNextRun: (31 * day) - maySixth2021At0500.Now(time.UTC).Sub(maySecond2021At0200)}, - {description: "day before current and after current time, should run next month", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{2}, atTime: _getHours(8), lastRun: maySixth2021At0500.Now(time.UTC)}, wantTimeUntilNextRun: (31 * day) - maySixth2021At0500.Now(time.UTC).Sub(maySecond2021At0800)}, - {description: "current day and before current time, should run next month", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{6}, atTime: _getHours(2), lastRun: maySixth2021At0500.Now(time.UTC)}, wantTimeUntilNextRun: (31 * day) - maySixth2021At0500.Now(time.UTC).Sub(maySixth2021At0200)}, - {description: "current day and after current time, should run on current day", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{6}, atTime: _getHours(8), lastRun: maySixth2021At0500.Now(time.UTC)}, wantTimeUntilNextRun: maySixth2021At0800.Sub(maySixth2021At0500.Now(time.UTC))}, - {description: "day after current and before current time, should run on current month", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{10}, atTime: _getHours(2), lastRun: maySixth2021At0500.Now(time.UTC)}, wantTimeUntilNextRun: mayTenth2021At0200.Sub(maySixth2021At0500.Now(time.UTC))}, - {description: "day after current and after current time, should run on current month", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{10}, atTime: _getHours(8), lastRun: maySixth2021At0500.Now(time.UTC)}, wantTimeUntilNextRun: mayTenth2021At0800.Sub(maySixth2021At0500.Now(time.UTC))}, + {description: "day before current and before current time, should run next month", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{2}, atTimes: []time.Duration{_getHours(2)}, lastRun: maySixth2021At0500.Now(time.UTC)}, wantTimeUntilNextRun: (31 * day) - maySixth2021At0500.Now(time.UTC).Sub(maySecond2021At0200)}, + {description: "day before current and after current time, should run next month", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{2}, atTimes: []time.Duration{_getHours(8)}, lastRun: maySixth2021At0500.Now(time.UTC)}, wantTimeUntilNextRun: (31 * day) - maySixth2021At0500.Now(time.UTC).Sub(maySecond2021At0800)}, + {description: "current day and before current time, should run next month", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{6}, atTimes: []time.Duration{_getHours(2)}, lastRun: maySixth2021At0500.Now(time.UTC)}, wantTimeUntilNextRun: (31 * day) - maySixth2021At0500.Now(time.UTC).Sub(maySixth2021At0200)}, + {description: "current day and after current time, should run on current day", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{6}, atTimes: []time.Duration{_getHours(8)}, lastRun: maySixth2021At0500.Now(time.UTC)}, wantTimeUntilNextRun: maySixth2021At0800.Sub(maySixth2021At0500.Now(time.UTC))}, + {description: "day after current and before current time, should run on current month", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{10}, atTimes: []time.Duration{_getHours(2)}, lastRun: maySixth2021At0500.Now(time.UTC)}, wantTimeUntilNextRun: mayTenth2021At0200.Sub(maySixth2021At0500.Now(time.UTC))}, + {description: "day after current and after current time, should run on current month", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{10}, atTimes: []time.Duration{_getHours(8)}, lastRun: maySixth2021At0500.Now(time.UTC)}, wantTimeUntilNextRun: mayTenth2021At0800.Sub(maySixth2021At0500.Now(time.UTC))}, } for _, tc := range testCases { @@ -1960,10 +2007,10 @@ func TestScheduler_CheckCalculateDaysOfMonth(t *testing.T) { job *Job wantTimeUntilNextRun time.Duration }{ - {description: "should run current month 10", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{10, 6}, atTime: _getHours(0), lastRun: curTime.Now(time.UTC)}, wantTimeUntilNextRun: lastRunFirstCaseDate.Sub(curTime.Now(time.UTC))}, - {description: "should run current month 10", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{10, 6}, atTime: _getHours(5), lastRun: curTime.Now(time.UTC)}, wantTimeUntilNextRun: lastRunSecondCaseDate.Sub(curTime.Now(time.UTC))}, - {description: "should run next month 6", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{6, 7}, atTime: _getHours(0), lastRun: curTime.Now(time.UTC)}, wantTimeUntilNextRun: lastRunThirdCaseDate.Sub(curTime.Now(time.UTC))}, - {description: "should run next month 11", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{12, 11}, atTime: _getHours(0), lastRun: curTime.Now(time.UTC)}, wantTimeUntilNextRun: lastRunFourthCaseDate.Sub(curTime.Now(time.UTC))}, + {description: "should run current month 10", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{10, 6}, atTimes: []time.Duration{_getHours(0)}, lastRun: curTime.Now(time.UTC)}, wantTimeUntilNextRun: lastRunFirstCaseDate.Sub(curTime.Now(time.UTC))}, + {description: "should run current month 10", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{10, 6}, atTimes: []time.Duration{_getHours(5)}, lastRun: curTime.Now(time.UTC)}, wantTimeUntilNextRun: lastRunSecondCaseDate.Sub(curTime.Now(time.UTC))}, + {description: "should run next month 6", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{6, 7}, atTimes: []time.Duration{_getHours(0)}, lastRun: curTime.Now(time.UTC)}, wantTimeUntilNextRun: lastRunThirdCaseDate.Sub(curTime.Now(time.UTC))}, + {description: "should run next month 11", job: &Job{interval: 1, unit: months, daysOfTheMonth: []int{12, 11}, atTimes: []time.Duration{_getHours(0)}, lastRun: curTime.Now(time.UTC)}, wantTimeUntilNextRun: lastRunFourthCaseDate.Sub(curTime.Now(time.UTC))}, } for _, tc := range testCases { @@ -1988,7 +2035,7 @@ func TestScheduler_MonthLastDayAtTime(t *testing.T) { job *Job wantTimeUntilNextRun time.Duration }{ - {name: "month last day before run at time", job: &Job{interval: 1, unit: months, atTime: _getHours(20) + _getMinutes(0), daysOfTheMonth: []int{-1}, lastRun: time.Date(2022, 2, 28, 10, 0, 0, 0, time.UTC)}, wantTimeUntilNextRun: _getHours(10)}, + {name: "month last day before run at time", job: &Job{interval: 1, unit: months, atTimes: []time.Duration{_getHours(20) + _getMinutes(0)}, daysOfTheMonth: []int{-1}, lastRun: time.Date(2022, 2, 28, 10, 0, 0, 0, time.UTC)}, wantTimeUntilNextRun: _getHours(10)}, } for _, tc := range testCases { @@ -2011,3 +2058,51 @@ func TestScheduler_WeekdayIsCurrentDay(t *testing.T) { job, _ := s.Every(1).Week().Thursday().Friday().Saturday().At("23:00").Do(func() {}) assert.Equal(t, time.Date(2022, 2, 17, 23, 0, 0, 0, s.Location()), job.NextRun()) } + +func TestScheduler_MultipleAtTime(t *testing.T) { + getTime := func(hour, min, sec int) time.Time { + return time.Date(2022, 2, 16, hour, min, sec, 0, time.UTC) + } + + getMonthLastDayTime := func(hour, min, sec int) time.Time { + return time.Date(2022, 2, 28, hour, min, sec, 0, time.UTC) + } + + atTimes := []time.Duration{ + _getHours(3) + _getMinutes(20), + _getHours(5) + _getMinutes(30), + _getHours(7) + _getMinutes(0), + _getHours(14) + _getMinutes(10), + } + testCases := []struct { + description string + job *Job + wantTimeUntilNextRun time.Duration + }{ + {description: "day test1", job: &Job{interval: 1, unit: days, atTimes: atTimes, lastRun: getTime(1, 0, 0)}, wantTimeUntilNextRun: _getHours(2) + _getMinutes(20)}, + {description: "day test2", job: &Job{interval: 1, unit: days, atTimes: atTimes, lastRun: getTime(3, 30, 0)}, wantTimeUntilNextRun: _getHours(2)}, + {description: "day test3", job: &Job{interval: 1, unit: days, atTimes: atTimes, lastRun: getTime(5, 27, 10)}, wantTimeUntilNextRun: _getMinutes(2) + _getSeconds(50)}, + {description: "day test4", job: &Job{interval: 1, unit: days, atTimes: atTimes, lastRun: getTime(5, 30, 0)}, wantTimeUntilNextRun: _getHours(1) + _getMinutes(30)}, + {description: "day test5", job: &Job{interval: 1, unit: days, atTimes: atTimes, lastRun: getTime(15, 0, 0)}, wantTimeUntilNextRun: _getHours(12) + _getMinutes(20)}, + {description: "week test1", job: &Job{interval: 1, unit: weeks, atTimes: atTimes, lastRun: getTime(5, 30, 0)}, wantTimeUntilNextRun: _getDays(7) - _getHours(2) - _getMinutes(10)}, + {description: "week test2", job: &Job{interval: 1, unit: weeks, atTimes: atTimes, lastRun: getTime(15, 0, 0)}, wantTimeUntilNextRun: _getDays(7) - _getHours(15) + _getHours(3) + _getMinutes(20)}, + {description: "weekday before test1", job: &Job{interval: 1, unit: weeks, scheduledWeekdays: []time.Weekday{time.Tuesday}, atTimes: atTimes, lastRun: getTime(5, 30, 0)}, wantTimeUntilNextRun: _getDays(6) - _getHours(2) - _getMinutes(10)}, + {description: "weekday before test2", job: &Job{interval: 1, unit: weeks, scheduledWeekdays: []time.Weekday{time.Tuesday}, atTimes: atTimes, lastRun: getTime(15, 0, 0)}, wantTimeUntilNextRun: _getDays(6) - _getHours(15) + _getHours(3) + _getMinutes(20)}, + {description: "weekday equals test1", job: &Job{interval: 1, unit: weeks, scheduledWeekdays: []time.Weekday{time.Wednesday}, atTimes: atTimes, lastRun: getTime(5, 30, 0)}, wantTimeUntilNextRun: _getHours(1) + _getMinutes(30)}, + {description: "weekday equals test2", job: &Job{interval: 1, unit: weeks, scheduledWeekdays: []time.Weekday{time.Wednesday}, atTimes: atTimes, lastRun: getTime(15, 0, 0)}, wantTimeUntilNextRun: _getDays(6) + _getHours(9) + _getHours(3) + _getMinutes(20)}, + {description: "weekday after test1", job: &Job{interval: 1, unit: weeks, scheduledWeekdays: []time.Weekday{time.Thursday}, atTimes: atTimes, lastRun: getTime(5, 30, 0)}, wantTimeUntilNextRun: _getDays(1) - _getHours(2) - _getMinutes(10)}, + {description: "weekday after test2", job: &Job{interval: 1, unit: weeks, scheduledWeekdays: []time.Weekday{time.Thursday}, atTimes: atTimes, lastRun: getTime(15, 0, 0)}, wantTimeUntilNextRun: _getDays(1) - _getHours(15) + _getHours(3) + _getMinutes(20)}, + {description: "month test1", job: &Job{interval: 1, unit: months, atTimes: atTimes, lastRun: getTime(5, 30, 0), daysOfTheMonth: []int{1}}, wantTimeUntilNextRun: _getDays(13) - _getHours(2) - _getMinutes(10)}, + {description: "month test2", job: &Job{interval: 1, unit: months, atTimes: atTimes, lastRun: getTime(15, 0, 0), daysOfTheMonth: []int{1}}, wantTimeUntilNextRun: _getDays(12) + _getHours(9) + _getHours(3) + _getMinutes(20)}, + {description: "month last day test1", job: &Job{interval: 1, unit: months, atTimes: atTimes, lastRun: getMonthLastDayTime(5, 30, 0), daysOfTheMonth: []int{-1}}, wantTimeUntilNextRun: _getHours(1) + _getMinutes(30)}, + {description: "month last day test2", job: &Job{interval: 1, unit: months, atTimes: atTimes, lastRun: getMonthLastDayTime(15, 0, 0), daysOfTheMonth: []int{-1}}, wantTimeUntilNextRun: _getDays(30) + _getHours(9) + _getHours(3) + _getMinutes(20)}, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + s := NewScheduler(time.UTC) + got := s.durationToNextRun(tc.job.LastRun(), tc.job).duration + assert.Equalf(t, tc.wantTimeUntilNextRun, got, fmt.Sprintf("expected %s / got %s", tc.wantTimeUntilNextRun.String(), got.String())) + }) + } +}