diff --git a/internal/task/date.go b/internal/task/date.go index 62464e0..53d4629 100644 --- a/internal/task/date.go +++ b/internal/task/date.go @@ -201,6 +201,10 @@ func (d Date) Weekday() time.Weekday { return d.t.Weekday() } +func (d Date) Day() int { + return d.t.Day() +} + func (d Date) Add(days int) Date { year, month, day := d.t.Date() return NewDate(year, int(month), day+days) diff --git a/internal/task/date_test.go b/internal/task/date_test.go index 480db58..22fd89f 100644 --- a/internal/task/date_test.go +++ b/internal/task/date_test.go @@ -195,13 +195,13 @@ func TestDateString(t *testing.T) { }, { name: "normal", - date: task.NewDate(2021, 1, 30), - exp: "2021-01-30 (saturday)", + date: task.NewDate(2021, 5, 30), + exp: "2021-05-30 (sunday)", }, { name: "normalize", - date: task.NewDate(2021, 1, 32), - exp: "2021-02-01 (monday)", + date: task.NewDate(2021, 5, 32), + exp: "2021-06-01 (tuesday)", }, } { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/task/recur.go b/internal/task/recur.go index c0e4ff4..c40d069 100644 --- a/internal/task/recur.go +++ b/internal/task/recur.go @@ -37,6 +37,9 @@ func NewRecurrer(recurStr string) Recurrer { if recur, ok := ParseEveryNWeeks(start, terms); ok { return recur } + if recur, ok := ParseEveryNMonths(start, terms); ok { + return recur + } return nil } @@ -191,7 +194,7 @@ type EveryNWeeks struct { // yyyy-mm-dd, every 3 weeks func ParseEveryNWeeks(start Date, terms []string) (Recurrer, bool) { - if len(terms) < 1 || len(terms) > 1 { + if len(terms) != 1 { return nil, false } @@ -225,3 +228,41 @@ func (enw EveryNWeeks) RecursOn(date Date) bool { func (enw EveryNWeeks) String() string { return fmt.Sprintf("%s, every %d weeks", enw.Start.String(), enw.N) } + +type EveryNMonths struct { + Start Date + N int +} + +// yyyy-mm-dd, every 3 months +func ParseEveryNMonths(start Date, terms []string) (Recurrer, bool) { + if len(terms) != 1 { + return nil, false + } + + terms = strings.Split(terms[0], " ") + if len(terms) != 3 || terms[0] != "every" || terms[2] != "months" { + return nil, false + } + n, err := strconv.Atoi(terms[1]) + if err != nil { + return nil, false + } + + return EveryNMonths{ + Start: start, + N: n, + }, true +} + +func (enm EveryNMonths) RecursOn(date Date) bool { + if enm.Start.After(date) { + return false + } + + return enm.Start.Day() == date.Day() +} + +func (enm EveryNMonths) String() string { + return fmt.Sprintf("%s, every %d months", enm.Start.String(), enm.N) +} diff --git a/internal/task/recur_test.go b/internal/task/recur_test.go index bb95f91..8beae11 100644 --- a/internal/task/recur_test.go +++ b/internal/task/recur_test.go @@ -300,3 +300,50 @@ func TestEveryNWeeks(t *testing.T) { } }) } + +func TestEveryNMonths(t *testing.T) { + everyNMonths := task.EveryNMonths{ + Start: task.NewDate(2021, 2, 3), + N: 3, + } + everyNMonthsStr := "2021-02-03 (wednesday), every 3 months" + + t.Run("parse", func(t *testing.T) { + test.Equals(t, everyNMonths, task.NewRecurrer(everyNMonthsStr)) + }) + + t.Run("string", func(t *testing.T) { + test.Equals(t, everyNMonthsStr, everyNMonths.String()) + }) + + t.Run("recurs on", func(t *testing.T) { + for _, tc := range []struct { + name string + date task.Date + exp bool + }{ + { + name: "before start", + date: task.NewDate(2021, 1, 27), + }, + { + name: "on start", + date: task.NewDate(2021, 2, 3), + exp: true, + }, + { + name: "8 weeks after", + date: task.NewDate(2021, 3, 31), + }, + { + name: "one month", + date: task.NewDate(2021, 3, 3), + exp: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + test.Equals(t, tc.exp, everyNMonths.RecursOn(tc.date)) + }) + } + }) +}