Skip to content

Commit

Permalink
support monthly schedules counting backwards from end of month (#542)
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnRoesler committed Aug 20, 2023
1 parent a7577a9 commit 22cd6b7
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 14 deletions.
6 changes: 6 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,8 @@ func ExampleScheduler_Month() {
_, _ = s.Every(1).Months(1).Do(task)
_, _ = s.Every(1).Month(1, 2).Do(task)
_, _ = s.Month(1, 2).Every(1).Do(task)
_, _ = s.Every(1).Month(-1).Do(task)
_, _ = s.Every(1).Month(-2).Do(task)
}

func ExampleScheduler_MonthFirstWeekday() {
Expand All @@ -589,13 +591,17 @@ func ExampleScheduler_MonthLastDay() {

_, _ = s.Every(1).MonthLastDay().Do(task)
_, _ = s.Every(2).MonthLastDay().Do(task)
_, _ = s.Every(1).MonthLastDay(-1).Do(task)
_, _ = s.Every(1).MonthLastDay(-2).Do(task)
}

func ExampleScheduler_Months() {
s := gocron.NewScheduler(time.UTC)

_, _ = s.Every(1).Month(1).Do(task)
_, _ = s.Every(1).Months(1).Do(task)
_, _ = s.Every(1).Months(-1).Do(task)
_, _ = s.Every(1).Months(-2).Do(task)
}

func ExampleScheduler_NextRun() {
Expand Down
3 changes: 2 additions & 1 deletion gocron.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ var (

ErrAtTimeNotSupported = errors.New("gocron: the At() method is not supported for this time unit")
ErrWeekdayNotSupported = errors.New("gocron: weekday is not supported for time unit")
ErrInvalidDayOfMonthEntry = errors.New("gocron: only days 1 through 28 are allowed for monthly schedules")
ErrInvalidDayOfMonthEntry = errors.New("gocron: only days 1 through 28 and -1 through -28 are allowed for monthly schedules")
ErrInvalidMonthLastDayEntry = errors.New("gocron: only a single negative integer is permitted for MonthLastDay")
ErrTagsUnique = func(tag string) error { return fmt.Errorf("gocron: a non-unique tag was set on the job: %s", tag) }
ErrWrongParams = errors.New("gocron: wrong list of params")
ErrDoWithJobDetails = errors.New("gocron: DoWithJobDetails expects a function whose last parameter is a gocron.Job")
Expand Down
46 changes: 34 additions & 12 deletions scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,9 @@ func (s *Scheduler) durationToNextRun(lastRun time.Time, job *Job) nextRun {
}

func (s *Scheduler) calculateMonths(job *Job, lastRun time.Time) nextRun {
// Special case: the last day of the month
if len(job.daysOfTheMonth) == 1 && job.daysOfTheMonth[0] == -1 {
return calculateNextRunForLastDayOfMonth(s, job, lastRun)
// Special case: negative days from the end of the month
if len(job.daysOfTheMonth) == 1 && job.daysOfTheMonth[0] < 0 {
return calculateNextRunForLastDayOfMonth(s, job, lastRun, job.daysOfTheMonth[0])
}

if len(job.daysOfTheMonth) != 0 { // calculate days to job.daysOfTheMonth
Expand All @@ -299,7 +299,7 @@ func (s *Scheduler) calculateMonths(job *Job, lastRun time.Time) nextRun {
return nextRun{duration: until(lastRun, next), dateTime: next}
}

func calculateNextRunForLastDayOfMonth(s *Scheduler, job *Job, lastRun time.Time) nextRun {
func calculateNextRunForLastDayOfMonth(s *Scheduler, job *Job, lastRun time.Time, dayBeforeLastOfMonth int) nextRun {
// Calculate the last day of the next month, by adding job.interval+1 months (i.e. the
// first day of the month after the next month), and subtracting one day, unless the
// last run occurred before the end of the month.
Expand All @@ -315,7 +315,7 @@ func calculateNextRunForLastDayOfMonth(s *Scheduler, job *Job, lastRun time.Time
next := time.Date(lastRun.Year(), lastRun.Month(), 1, 0, 0, 0, 0, s.Location()).
Add(atTime).
AddDate(0, addMonth, 0).
AddDate(0, 0, -1)
AddDate(0, 0, dayBeforeLastOfMonth)
return nextRun{duration: until(lastRun, next), dateTime: next}
}

Expand Down Expand Up @@ -1123,8 +1123,7 @@ func (s *Scheduler) Hours() *Scheduler {

// Day sets the unit with days
func (s *Scheduler) Day() *Scheduler {
s.setUnit(days)
return s
return s.Days()
}

// Days set the unit with days
Expand All @@ -1146,27 +1145,50 @@ func (s *Scheduler) Weeks() *Scheduler {
}

// Month sets the unit with months
// Note: Only days 1 through 28 are allowed for monthly schedules
// Note: Multiple of the same day of month is not allowed
// Note: Negative numbers are special values and can only occur as single argument
// and count backwards from the end of the month -1 == last day of the month, -2 == penultimate day of the month
func (s *Scheduler) Month(daysOfMonth ...int) *Scheduler {
return s.Months(daysOfMonth...)
}

// MonthLastDay sets the unit with months at every last day of the month
func (s *Scheduler) MonthLastDay() *Scheduler {
return s.Months(-1)
// The optional parameter is a negative integer denoting days previous to the
// last day of the month. E.g. -1 == the penultimate day of the month,
// -2 == two days for the last day of the month
func (s *Scheduler) MonthLastDay(dayCountBeforeLastDayOfMonth ...int) *Scheduler {
job := s.getCurrentJob()

switch l := len(dayCountBeforeLastDayOfMonth); l {
case 0:
return s.Months(-1)
case 1:
count := dayCountBeforeLastDayOfMonth[0]
if count >= 0 {
job.error = wrapOrError(job.error, ErrInvalidMonthLastDayEntry)
return s
}
return s.Months(count - 1)
default:
job.error = wrapOrError(job.error, ErrInvalidMonthLastDayEntry)
return s
}
}

// Months sets the unit with months
// Note: Only days 1 through 28 are allowed for monthly schedules
// Note: Multiple add same days of month cannot be allowed
// Note: -1 is a special value and can only occur as single argument
// Note: Multiple of the same day of month is not allowed
// Note: Negative numbers are special values and can only occur as single argument
// and count backwards from the end of the month -1 == last day of the month, -2 == penultimate day of the month
func (s *Scheduler) Months(daysOfTheMonth ...int) *Scheduler {
job := s.getCurrentJob()

if len(daysOfTheMonth) == 0 {
job.error = wrapOrError(job.error, ErrInvalidDayOfMonthEntry)
} else if len(daysOfTheMonth) == 1 {
dayOfMonth := daysOfTheMonth[0]
if dayOfMonth != -1 && (dayOfMonth < 1 || dayOfMonth > 28) {
if dayOfMonth < -28 || dayOfMonth == 0 || dayOfMonth > 28 {
job.error = wrapOrError(job.error, ErrInvalidDayOfMonthEntry)
}
} else {
Expand Down
16 changes: 15 additions & 1 deletion scheduler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2395,7 +2395,21 @@ func TestScheduler_MonthLastDayAtTime(t *testing.T) {
job *Job
wantTimeUntilNextRun time.Duration
}{
{name: "month last day before run at time", job: calculateNextRunHelper(1, months, time.Date(2022, 2, 28, 10, 0, 0, 0, time.UTC), []time.Duration{_getHours(20) + _getMinutes(0)}, nil, []int{-1}), wantTimeUntilNextRun: _getHours(10)},
{
name: "month last day before run at time",
job: calculateNextRunHelper(1, months, time.Date(2022, 2, 28, 10, 0, 0, 0, time.UTC), []time.Duration{_getHours(20) + _getMinutes(0)}, nil, []int{-1}),
wantTimeUntilNextRun: _getHours(10),
},
{
name: "month last day penultimate day before run at time",
job: calculateNextRunHelper(1, months, time.Date(2022, 2, 27, 10, 0, 0, 0, time.UTC), []time.Duration{_getHours(20) + _getMinutes(0)}, nil, []int{-2}),
wantTimeUntilNextRun: _getHours(10),
},
{
name: "month last day 2 days before last day before run at time",
job: calculateNextRunHelper(1, months, time.Date(2022, 2, 26, 10, 0, 0, 0, time.UTC), []time.Duration{_getHours(20) + _getMinutes(0)}, nil, []int{-3}),
wantTimeUntilNextRun: _getHours(10),
},
}

for _, tc := range testCases {
Expand Down

0 comments on commit 22cd6b7

Please sign in to comment.