recur daily, weekly, biweekly

This commit is contained in:
Erik Winter 2021-01-31 12:11:02 +01:00
parent 18d3074633
commit b210c57a3c
6 changed files with 329 additions and 70 deletions

View File

@ -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)
}

View File

@ -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))
}

View File

@ -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))
})
}
}

View File

@ -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,
}
}
// yyyy-mm-dd, weekly, wednesday
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())
}
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()))
}
*/

View File

@ -8,26 +8,132 @@ import (
"git.sr.ht/~ewintr/gte/internal/task"
)
func TestNewRecurrer(t *testing.T) {
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
input string
exp task.Recurrer
date task.Date
exp bool
}{
{
name: "empty",
name: "before",
date: task.NewDate(2021, 1, 30),
},
{
name: "weekly",
input: "2021-01-31, weekly, wednesday",
exp: task.Weekly{
Start: task.NewDate(2021, 1, 31),
Weekday: time.Wednesday,
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, task.NewRecurrer(tc.input))
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))
})
}
})
}

View File

@ -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: