recur daily, weekly, biweekly
This commit is contained in:
parent
18d3074633
commit
b210c57a3c
|
@ -31,7 +31,7 @@ func main() {
|
||||||
}
|
}
|
||||||
for _, t := range tasks {
|
for _, t := range tasks {
|
||||||
if t.RecursToday() {
|
if t.RecursToday() {
|
||||||
subject, body, err := t.CreateNextMessage(task.Today)
|
subject, body, err := t.CreateDueMessage(task.Today)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package task
|
package task
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -68,11 +69,7 @@ func NewDateFromString(date string) Date {
|
||||||
return Date{}
|
return Date{}
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := time.Parse(DateFormat, date)
|
t, err := time.Parse("2006-01-02", fmt.Sprintf("%.10s", date))
|
||||||
if err == nil {
|
|
||||||
return Date{t: t}
|
|
||||||
}
|
|
||||||
t, err = time.Parse("2006-01-02", date)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return Date{t: t}
|
return Date{t: t}
|
||||||
}
|
}
|
||||||
|
@ -129,6 +126,56 @@ func (d *Date) Add(days int) Date {
|
||||||
return NewDate(year, int(month), day+days)
|
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 {
|
func (d *Date) After(ud Date) bool {
|
||||||
return d.t.After(ud.Time())
|
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))
|
||||||
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,76 +1,162 @@
|
||||||
package task
|
package task
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Period int
|
|
||||||
type Recurrer interface {
|
type Recurrer interface {
|
||||||
RecursOn(date Date) bool
|
RecursOn(date Date) bool
|
||||||
FirstAfter(date Date) Date
|
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRecurrer(recurStr string) Recurrer {
|
func NewRecurrer(recurStr string) Recurrer {
|
||||||
terms := strings.Split(recurStr, ", ")
|
terms := strings.Split(recurStr, ", ")
|
||||||
if len(terms) < 3 {
|
if len(terms) < 2 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
startDate, err := time.Parse("2006-01-02", terms[0])
|
start := NewDateFromString(terms[0])
|
||||||
if err != nil {
|
if start.IsZero() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if terms[1] != "weekly" {
|
terms = terms[1:]
|
||||||
return nil
|
|
||||||
|
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
|
||||||
return nil
|
}
|
||||||
}
|
|
||||||
|
type Daily struct {
|
||||||
year, month, date := startDate.Date()
|
Start Date
|
||||||
return Weekly{
|
}
|
||||||
Start: NewDate(year, int(month), date),
|
|
||||||
Weekday: time.Wednesday,
|
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 {
|
type Weekly struct {
|
||||||
Start Date
|
Start Date
|
||||||
Weekday time.Weekday
|
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 {
|
func (w Weekly) RecursOn(date Date) bool {
|
||||||
if !w.Start.After(date) {
|
if w.Start.After(date) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return w.Weekday == date.Weekday()
|
return w.Weekday == date.Weekday()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w Weekly) FirstAfter(date Date) Date {
|
|
||||||
//sd := w.Start.Weekday()
|
|
||||||
|
|
||||||
return date
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w Weekly) String() string {
|
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
|
Start Date
|
||||||
Weekday Weekday
|
Weekday time.Weekday
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecurringTask struct {
|
// yyyy-mm-dd, biweekly, wednesday
|
||||||
Action string
|
func ParseBiweekly(start Date, terms []string) (Recurrer, bool) {
|
||||||
Start Date
|
if len(terms) < 2 {
|
||||||
Recurrer Recurrer
|
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()))
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
|
@ -8,26 +8,132 @@ import (
|
||||||
"git.sr.ht/~ewintr/gte/internal/task"
|
"git.sr.ht/~ewintr/gte/internal/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewRecurrer(t *testing.T) {
|
func TestDaily(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
daily := task.Daily{
|
||||||
name string
|
Start: task.NewDate(2021, 1, 31), // a sunday
|
||||||
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))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -195,13 +195,6 @@ func (t *Task) FormatSubject() string {
|
||||||
FIELD_DUE: t.Due.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{}
|
parts := []string{}
|
||||||
for _, f := range order {
|
for _, f := range order {
|
||||||
if fields[f] != "" {
|
if fields[f] != "" {
|
||||||
|
@ -260,12 +253,11 @@ func (t *Task) RecursToday() bool {
|
||||||
if !t.IsRecurrer() {
|
if !t.IsRecurrer() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
|
|
||||||
return t.Recur.RecursOn(Today)
|
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() {
|
if !t.IsRecurrer() {
|
||||||
return "", "", ErrTaskIsNotRecurring
|
return "", "", ErrTaskIsNotRecurring
|
||||||
}
|
}
|
||||||
|
@ -275,7 +267,7 @@ func (t *Task) CreateNextMessage(date Date) (string, string, error) {
|
||||||
Version: 1,
|
Version: 1,
|
||||||
Action: t.Action,
|
Action: t.Action,
|
||||||
Project: t.Project,
|
Project: t.Project,
|
||||||
Due: t.Recur.FirstAfter(date),
|
Due: date,
|
||||||
}
|
}
|
||||||
|
|
||||||
return tempTask.FormatSubject(), tempTask.FormatBody(), nil
|
return tempTask.FormatSubject(), tempTask.FormatBody(), nil
|
||||||
|
@ -313,6 +305,7 @@ func FieldFromBody(field, body string) (string, bool) {
|
||||||
|
|
||||||
func FieldFromSubject(field, subject string) string {
|
func FieldFromSubject(field, subject string) string {
|
||||||
|
|
||||||
|
// TODO there are also subjects with date and without project
|
||||||
terms := strings.Split(subject, SUBJECT_SEPARATOR)
|
terms := strings.Split(subject, SUBJECT_SEPARATOR)
|
||||||
switch field {
|
switch field {
|
||||||
case FIELD_ACTION:
|
case FIELD_ACTION:
|
||||||
|
|
Loading…
Reference in New Issue