From b210c57a3cea33b87ade653b38f8d533f132a668 Mon Sep 17 00:00:00 2001 From: Erik Winter Date: Sun, 31 Jan 2021 12:11:02 +0100 Subject: [PATCH] recur daily, weekly, biweekly --- cmd/gte-generate-recurring/main.go | 2 +- internal/task/date.go | 57 ++++++++++- internal/task/date_test.go | 27 +++++ internal/task/recur.go | 152 ++++++++++++++++++++++------- internal/task/recur_test.go | 148 ++++++++++++++++++++++++---- internal/task/task.go | 13 +-- 6 files changed, 329 insertions(+), 70 deletions(-) diff --git a/cmd/gte-generate-recurring/main.go b/cmd/gte-generate-recurring/main.go index da776f8..75fdf00 100644 --- a/cmd/gte-generate-recurring/main.go +++ b/cmd/gte-generate-recurring/main.go @@ -31,7 +31,7 @@ func main() { } for _, t := range tasks { if t.RecursToday() { - subject, body, err := t.CreateNextMessage(task.Today) + subject, body, err := t.CreateDueMessage(task.Today) if err != nil { log.Fatal(err) } diff --git a/internal/task/date.go b/internal/task/date.go index 0773bb6..d54a8b0 100644 --- a/internal/task/date.go +++ b/internal/task/date.go @@ -1,6 +1,7 @@ package task import ( + "fmt" "strings" "time" ) @@ -68,11 +69,7 @@ func NewDateFromString(date string) Date { return Date{} } - t, err := time.Parse(DateFormat, date) - if err == nil { - return Date{t: t} - } - t, err = time.Parse("2006-01-02", date) + t, err := time.Parse("2006-01-02", fmt.Sprintf("%.10s", date)) if err == nil { return Date{t: t} } @@ -129,6 +126,56 @@ func (d *Date) Add(days int) Date { return NewDate(year, int(month), day+days) } +func (d *Date) Equal(ud Date) bool { + return d.t.Equal(ud.Time()) +} + +// After reports whether d is after ud func (d *Date) After(ud Date) bool { return d.t.After(ud.Time()) } + +func (d *Date) AddDays(amount int) Date { + year, month, date := d.t.Date() + + return NewDate(year, int(month), date+amount) +} + +func ParseWeekday(wd string) (time.Weekday, bool) { + switch lowerAndTrim(wd) { + case "monday": + return time.Monday, true + case "tuesday": + return time.Tuesday, true + case "wednesday": + return time.Wednesday, true + case "thursday": + return time.Thursday, true + case "friday": + return time.Friday, true + case "saturday": + return time.Saturday, true + case "sunday": + return time.Sunday, true + case "maandag": + return time.Monday, true + case "dinsdag": + return time.Tuesday, true + case "woensdag": + return time.Wednesday, true + case "donderdag": + return time.Thursday, true + case "vrijdag": + return time.Friday, true + case "zaterdag": + return time.Saturday, true + case "zondag": + return time.Sunday, true + } + + return time.Monday, false +} + +func lowerAndTrim(str string) string { + return strings.TrimSpace(strings.ToLower(str)) +} diff --git a/internal/task/date_test.go b/internal/task/date_test.go index 48f1df3..8ee8804 100644 --- a/internal/task/date_test.go +++ b/internal/task/date_test.go @@ -82,3 +82,30 @@ func TestDateString(t *testing.T) { }) } } + +func TestDateAfter(t *testing.T) { + day := task.NewDate(2021, 1, 31) + for _, tc := range []struct { + name string + tDay task.Date + exp bool + }{ + { + name: "after", + tDay: task.NewDate(2021, 1, 30), + exp: true, + }, + { + name: "on", + tDay: day, + }, + { + name: "before", + tDay: task.NewDate(2021, 2, 1), + }, + } { + t.Run(tc.name, func(t *testing.T) { + test.Equals(t, tc.exp, day.After(tc.tDay)) + }) + } +} diff --git a/internal/task/recur.go b/internal/task/recur.go index 45cacd8..b319029 100644 --- a/internal/task/recur.go +++ b/internal/task/recur.go @@ -1,76 +1,162 @@ package task import ( + "fmt" "strings" "time" ) -type Period int type Recurrer interface { RecursOn(date Date) bool - FirstAfter(date Date) Date String() string } func NewRecurrer(recurStr string) Recurrer { terms := strings.Split(recurStr, ", ") - if len(terms) < 3 { + if len(terms) < 2 { return nil } - startDate, err := time.Parse("2006-01-02", terms[0]) - if err != nil { + start := NewDateFromString(terms[0]) + if start.IsZero() { return nil } - if terms[1] != "weekly" { - return nil + terms = terms[1:] + + if recur, ok := ParseDaily(start, terms); ok { + return recur + } + if recur, ok := ParseWeekly(start, terms); ok { + return recur + } + if recur, ok := ParseBiweekly(start, terms); ok { + return recur } - if terms[2] != "wednesday" { - return nil - } - - year, month, date := startDate.Date() - return Weekly{ - Start: NewDate(year, int(month), date), - Weekday: time.Wednesday, - } + return nil +} + +type Daily struct { + Start Date +} + +func ParseDaily(start Date, terms []string) (Recurrer, bool) { + if len(terms) < 1 { + return nil, false + } + + if terms[0] != "daily" { + return nil, false + } + + return Daily{ + Start: start, + }, true +} + +func (d Daily) RecursOn(date Date) bool { + return date.Equal(d.Start) || date.After(d.Start) +} + +func (d Daily) String() string { + return fmt.Sprintf("%s, daily", d.Start.String()) } -// yyyy-mm-dd, weekly, wednesday type Weekly struct { Start Date Weekday time.Weekday } +// yyyy-mm-dd, weekly, wednesday +func ParseWeekly(start Date, terms []string) (Recurrer, bool) { + if len(terms) < 2 { + return nil, false + } + + if terms[0] != "weekly" { + return nil, false + } + + wd, ok := ParseWeekday(terms[1]) + if !ok { + return nil, false + } + + return Weekly{ + Start: start, + Weekday: wd, + }, true +} + func (w Weekly) RecursOn(date Date) bool { - if !w.Start.After(date) { + if w.Start.After(date) { return false } return w.Weekday == date.Weekday() } -func (w Weekly) FirstAfter(date Date) Date { - //sd := w.Start.Weekday() - - return date -} - func (w Weekly) String() string { - return "2021-01-31, weekly, wednesday" + return fmt.Sprintf("%s, weekly, %s", w.Start.String(), strings.ToLower(w.Weekday.String())) } -/* -type BiWeekly struct { +type Biweekly struct { Start Date - Weekday Weekday + Weekday time.Weekday } -type RecurringTask struct { - Action string - Start Date - Recurrer Recurrer +// yyyy-mm-dd, biweekly, wednesday +func ParseBiweekly(start Date, terms []string) (Recurrer, bool) { + if len(terms) < 2 { + return nil, false + } + + if terms[0] != "biweekly" { + return nil, false + } + + wd, ok := ParseWeekday(terms[1]) + if !ok { + return nil, false + } + + return Biweekly{ + Start: start, + Weekday: wd, + }, true +} + +func (b Biweekly) RecursOn(date Date) bool { + if b.Start.After(date) { + return false + } + + if b.Weekday != date.Weekday() { + return false + } + + // find first + tDate := b.Start + for { + if tDate.Weekday() == b.Weekday { + break + } + tDate = tDate.AddDays(1) + } + + // add weeks + for { + switch { + case tDate.Equal(date): + return true + case tDate.After(date): + return false + } + tDate = tDate.AddDays(14) + } +} + +func (b Biweekly) String() string { + return fmt.Sprintf("%s, biweekly, %s", b.Start.String(), strings.ToLower(b.Weekday.String())) } -*/ diff --git a/internal/task/recur_test.go b/internal/task/recur_test.go index a6b9e76..0e7e12c 100644 --- a/internal/task/recur_test.go +++ b/internal/task/recur_test.go @@ -8,26 +8,132 @@ import ( "git.sr.ht/~ewintr/gte/internal/task" ) -func TestNewRecurrer(t *testing.T) { - for _, tc := range []struct { - name string - input string - exp task.Recurrer - }{ - { - name: "empty", - }, - { - name: "weekly", - input: "2021-01-31, weekly, wednesday", - exp: task.Weekly{ - Start: task.NewDate(2021, 1, 31), - Weekday: time.Wednesday, - }, - }, - } { - t.Run(tc.name, func(t *testing.T) { - test.Equals(t, tc.exp, task.NewRecurrer(tc.input)) - }) +func TestDaily(t *testing.T) { + daily := task.Daily{ + Start: task.NewDate(2021, 1, 31), // a sunday } + dailyStr := "2021-01-31 (sunday), daily" + + t.Run("parse", func(t *testing.T) { + test.Equals(t, daily, task.NewRecurrer(dailyStr)) + }) + + t.Run("string", func(t *testing.T) { + test.Equals(t, dailyStr, daily.String()) + }) + + t.Run("recurs_on", func(t *testing.T) { + for _, tc := range []struct { + name string + date task.Date + exp bool + }{ + { + name: "before", + date: task.NewDate(2021, 1, 30), + }, + { + name: "on", + date: daily.Start, + exp: true, + }, + { + name: "after", + date: task.NewDate(2021, 2, 1), + exp: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + test.Equals(t, tc.exp, daily.RecursOn(tc.date)) + }) + } + }) +} + +func TestWeekly(t *testing.T) { + weekly := task.Weekly{ + Start: task.NewDate(2021, 1, 31), // a sunday + Weekday: time.Wednesday, + } + weeklyStr := "2021-01-31 (sunday), weekly, wednesday" + + t.Run("parse", func(t *testing.T) { + test.Equals(t, weekly, task.NewRecurrer(weeklyStr)) + }) + + t.Run("string", func(t *testing.T) { + test.Equals(t, weeklyStr, weekly.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), // a wednesday + }, + { + name: "wrong weekday", + date: task.NewDate(2021, 2, 1), // a monday + }, + { + name: "right day", + date: task.NewDate(2021, 2, 3), // a wednesday + exp: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + test.Equals(t, tc.exp, weekly.RecursOn(tc.date)) + }) + } + }) +} + +func TestBiweekly(t *testing.T) { + biweekly := task.Biweekly{ + Start: task.NewDate(2021, 1, 31), // a sunday + Weekday: time.Wednesday, + } + biweeklyStr := "2021-01-31 (sunday), biweekly, wednesday" + + t.Run("parse", func(t *testing.T) { + test.Equals(t, biweekly, task.NewRecurrer(biweeklyStr)) + }) + + t.Run("string", func(t *testing.T) { + test.Equals(t, biweeklyStr, biweekly.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), // a wednesday + }, + { + name: "wrong weekday", + date: task.NewDate(2021, 2, 1), // a monday + }, + { + name: "odd week count", + date: task.NewDate(2021, 2, 10), // a wednesday + }, + { + name: "right", + date: task.NewDate(2021, 2, 17), // a wednesday + exp: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + test.Equals(t, tc.exp, biweekly.RecursOn(tc.date)) + }) + } + }) } diff --git a/internal/task/task.go b/internal/task/task.go index 569ccda..f3ce461 100644 --- a/internal/task/task.go +++ b/internal/task/task.go @@ -195,13 +195,6 @@ func (t *Task) FormatSubject() string { FIELD_DUE: t.Due.String(), } - if fields[FIELD_DUE] != "" && fields[FIELD_PROJECT] == "" { - fields[FIELD_PROJECT] = " " - } - if fields[FIELD_PROJECT] != "" && fields[FIELD_ACTION] == "" { - fields[FIELD_ACTION] = " " - } - parts := []string{} for _, f := range order { if fields[f] != "" { @@ -260,12 +253,11 @@ func (t *Task) RecursToday() bool { if !t.IsRecurrer() { return false } - return true return t.Recur.RecursOn(Today) } -func (t *Task) CreateNextMessage(date Date) (string, string, error) { +func (t *Task) CreateDueMessage(date Date) (string, string, error) { if !t.IsRecurrer() { return "", "", ErrTaskIsNotRecurring } @@ -275,7 +267,7 @@ func (t *Task) CreateNextMessage(date Date) (string, string, error) { Version: 1, Action: t.Action, Project: t.Project, - Due: t.Recur.FirstAfter(date), + Due: date, } return tempTask.FormatSubject(), tempTask.FormatBody(), nil @@ -313,6 +305,7 @@ func FieldFromBody(field, body string) (string, bool) { func FieldFromSubject(field, subject string) string { + // TODO there are also subjects with date and without project terms := strings.Split(subject, SUBJECT_SEPARATOR) switch field { case FIELD_ACTION: