Repository: go-co-op/gocron
Pull request #47 (2020-07-18 18:31:58)

Add support for executing jobs every Month/Months
### What does this do?
Add support for executing jobs every Month/Months

### Which issue(s) does this PR fix/relate to?
<!--- Put `Resolves #XXX` here to auto-close the issue that your PR fixes (if such) --->
#43 


### List any changes that modify/break current functionality
Doesn't break any current functionality

### Have you included tests for your changes?
Yes

### Did you document any new/modified functionality?

- [ ] Updated `example_test.go`
- [x] Updated `README.md`




Patch:
diff --git a/README.md b/README.md
index 9810569..90e04d3 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,8 @@ func main() {
     s2.Every(2).Days().Do(task)
     s2.Every(1).Week().Do(task)
     s2.Every(2).Weeks().Do(task)
+    s2.Every(1).Month(time.Now().Day()).Do(task)
+    s2.Every(2).Months(15).Do(task)
 
     // Do jobs with params
     s2.Every(1).Second().Do(taskWithParams, 1, "hello")
diff --git a/gocron.go b/gocron.go
index d5a303e..d7c0acb 100644
--- a/gocron.go
+++ b/gocron.go
@@ -38,6 +38,7 @@ const (
 	hours
 	days
 	weeks
+	months
 )
 
 func callJobFuncWithParams(jobFunc interface{}, params []interface{}) ([]reflect.Value, error) {
diff --git a/job.go b/job.go
index 682b077..792811a 100644
--- a/job.go
+++ b/job.go
@@ -17,6 +17,7 @@ type Job struct {
 	lastRun           time.Time                // datetime of last run
 	nextRun           time.Time                // datetime of next run
 	startDay          time.Weekday             // Specific day of the week to start on
+	dayOfTheMonth     int                      // Specific day of the month to run the job
 	funcs             map[string]interface{}   // Map for the function task store
 	fparams           map[string][]interface{} // Map for function and  params of function
 	lock              bool                     // lock the Job from running at same time form multiple instances
@@ -87,6 +88,8 @@ func (j *Job) setPeriodDuration() error {
 		j.periodDuration = interval * time.Hour * 24
 	case weeks:
 		j.periodDuration = interval * time.Hour * 24 * 7
+	case months:
+		// periodDuration doesn't apply here
 	default:
 		return ErrPeriodNotSpecified
 	}
diff --git a/scheduler.go b/scheduler.go
index d004c57..605eb7e 100644
--- a/scheduler.go
+++ b/scheduler.go
@@ -102,6 +102,11 @@ func (s *Scheduler) scheduleNextRun(j *Job) error {
 			j.nextRun = j.nextRun.Add(time.Duration(dayDiff) * 24 * time.Hour)
 		}
 		j.nextRun = j.nextRun.Add(j.atTime)
+	case months:
+		increment := j.lastRun.Month() + time.Month(j.interval)
+		nextMonth := increment % 12
+		year := j.lastRun.Year() + int(increment/12)
+		j.nextRun = time.Date(year, nextMonth, j.dayOfTheMonth, 0, 0, 0, 0, s.loc).Add(j.atTime)
 	}
 
 	// advance to next possible Schedule
@@ -421,6 +426,23 @@ func (s *Scheduler) Weeks() *Scheduler {
 	return s
 }
 
+// Month sets the unit with months
+func (s *Scheduler) Month(dayOfTheMonth int) *Scheduler {
+	return s.Months(dayOfTheMonth)
+}
+
+// Months sets the unit with months
+func (s *Scheduler) Months(dayOfTheMonth int) *Scheduler {
+	s.getCurrentJob().dayOfTheMonth = dayOfTheMonth
+	s.setUnit(months)
+	return s
+}
+
+// NOTE: If the dayOfTheMonth for the above two functions is
+// more than the number of days in that month, the extra day(s)
+// spill over to the next month. Similarly, if it's less than 0,
+// it will go back to the month before
+
 // Weekday sets the start with a specific weekday weekday
 func (s *Scheduler) Weekday(startDay time.Weekday) *Scheduler {
 	s.getCurrentJob().startDay = startDay


Test patch:
diff --git a/scheduler_test.go b/scheduler_test.go
index cf5ee7c..a8589ae 100644
--- a/scheduler_test.go
+++ b/scheduler_test.go
@@ -534,3 +534,15 @@ func TestScheduler_Stop(t *testing.T) {
 		assert.False(t, sched.running)
 	})
 }
+
+func TestMonths(t *testing.T) {
+	scheduler := NewScheduler(time.Local)
+	now := time.Now()
+	scheduler.Every(1).Month(now.Day()).Do(func() {})
+	_, nextRun := scheduler.NextRun()
+	assert.Equal(
+		t,
+		time.Date(now.Year(), (now.Month()+time.Month(1))%12, now.Day(), 0, 0, 0, 0, time.Local),
+		nextRun,
+	)
+}