diff --git a/internal/task/date.go b/internal/task/date.go index a0198f2..4dd2cc5 100644 --- a/internal/task/date.go +++ b/internal/task/date.go @@ -1,14 +1,31 @@ package task import ( + "strings" "time" ) +const ( + DateFormat = "2006-01-02 (Monday)" +) + +var Today Date + +func init() { + year, month, day := time.Now().Date() + Today = NewDate(year, int(month), day) +} + type Date struct { t time.Time } -func NewDate(year, month, day int) *Date { +func NewDate(year, month, day int) Date { + + if year == 0 && month == 0 && day == 0 { + return Date{} + } + var m time.Month switch month { case 1: @@ -37,21 +54,73 @@ func NewDate(year, month, day int) *Date { m = time.December } - t := time.Date(year, m, day, 10, 0, 0, 0, time.UTC) + t := time.Date(year, m, day, 0, 0, 0, 0, time.UTC) - if year == 0 && month == 0 && day == 0 { - t = time.Time{} - } - - return &Date{ + return Date{ t: t, } } +func NewDateFromString(date string) Date { + date = strings.ToLower(strings.TrimSpace(date)) + + if date == "no date" || date == "" { + return Date{} + } + + t, err := time.Parse(DateFormat, date) + if err == nil { + return Date{t: t} + } + t, err = time.Parse("2006-01-02", date) + if err == nil { + return Date{t: t} + } + + weekday := Today.Weekday() + var newWeekday time.Weekday + switch { + case date == "monday" || date == "maandag": + newWeekday = time.Monday + case date == "tuesday" || date == "dinsdag": + newWeekday = time.Tuesday + case date == "wednesday" || date == "woensdag": + newWeekday = time.Wednesday + case date == "thursday" || date == "donderdag": + newWeekday = time.Thursday + case date == "friday" || date == "vrijdag": + newWeekday = time.Friday + case date == "saturday" || date == "zaterdag": + newWeekday = time.Saturday + case date == "sunday" || date == "zondag": + newWeekday = time.Sunday + } + + daysToAdd := int(newWeekday) - weekday + if daysToAdd < 0 { + daysToAdd += 7 + } + + return Today.Add(daysToAdd) +} + func (d *Date) String() string { if d.t.IsZero() { return "no date" } - return d.t.Format("2006-01-02") + return strings.ToLower(d.t.Format(DateFormat)) +} + +func (d *Date) IsZero() bool { + return d.t.IsZero() +} + +func (d *Date) Weekday() int { + return int(d.t.Weekday()) +} + +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 494ed39..48f1df3 100644 --- a/internal/task/date_test.go +++ b/internal/task/date_test.go @@ -7,10 +7,58 @@ import ( "git.sr.ht/~ewintr/gte/internal/task" ) +func TestNewDateFromString(t *testing.T) { + task.Today = task.NewDate(2021, 1, 30) + for _, tc := range []struct { + name string + input string + exp task.Date + }{ + { + name: "empty", + exp: task.Date{}, + }, + { + name: "no date", + input: "no date", + exp: task.Date{}, + }, + { + name: "normal", + input: "2021-01-30 (saturday)", + exp: task.NewDate(2021, 1, 30), + }, + { + name: "short", + input: "2021-01-30", + exp: task.NewDate(2021, 1, 30), + }, + { + name: "english dayname lowercase", + input: "monday", + exp: task.NewDate(2021, 2, 1), + }, + { + name: "english dayname capitalized", + input: "Monday", + exp: task.NewDate(2021, 2, 1), + }, + { + name: "ducth dayname lowercase", + input: "maandag", + exp: task.NewDate(2021, 2, 1), + }, + } { + t.Run(tc.name, func(t *testing.T) { + test.Equals(t, tc.exp, task.NewDateFromString(tc.input)) + }) + } +} + func TestDateString(t *testing.T) { for _, tc := range []struct { name string - date *task.Date + date task.Date exp string }{ { @@ -21,12 +69,12 @@ func TestDateString(t *testing.T) { { name: "normal", date: task.NewDate(2021, 1, 30), - exp: "2021-01-30", + exp: "2021-01-30 (saturday)", }, { name: "normalize", date: task.NewDate(2021, 1, 32), - exp: "2021-02-01", + exp: "2021-02-01 (monday)", }, } { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/task/repo.go b/internal/task/repo.go index 894ebce..88f130b 100644 --- a/internal/task/repo.go +++ b/internal/task/repo.go @@ -3,6 +3,7 @@ package task import ( "errors" "fmt" + "strconv" "git.sr.ht/~ewintr/gte/pkg/mstore" ) @@ -67,43 +68,52 @@ func (tr *TaskRepo) Update(t *Task) error { // Cleanup removes older versions of tasks func (tr *TaskRepo) CleanUp() error { - // loop through folders, get all tasks - taskSet := make(map[string][]*Task) + // loop through folders, get all task version info + type msgInfo struct { + Version int + Message *mstore.Message + } + msgsSet := make(map[string][]msgInfo) for _, folder := range knownFolders { - tasks, err := tr.FindAll(folder) + msgs, err := tr.mstore.Messages(folder) if err != nil { - return err + return fmt.Errorf("%w: %v", ErrMStoreError, err) } - for _, t := range tasks { - if _, ok := taskSet[t.Id]; !ok { - taskSet[t.Id] = []*Task{} + for _, msg := range msgs { + id, _ := FieldFromBody(FIELD_ID, msg.Body) + versionStr, _ := FieldFromBody(FIELD_VERSION, msg.Body) + version, _ := strconv.Atoi(versionStr) + if _, ok := msgsSet[id]; !ok { + msgsSet[id] = []msgInfo{} } - taskSet[t.Id] = append(taskSet[t.Id], t) + msgsSet[id] = append(msgsSet[id], msgInfo{ + Version: version, + Message: msg, + }) } } // determine which ones need to be gone - var tobeRemoved []*Task - for _, tasks := range taskSet { + var tobeRemoved []*mstore.Message + for _, mInfos := range msgsSet { maxVersion := 0 - for _, t := range tasks { - if t.Version > maxVersion { - maxVersion = t.Version + for _, mInfo := range mInfos { + if mInfo.Version > maxVersion { + maxVersion = mInfo.Version } } - - for _, t := range tasks { - if t.Version < maxVersion { - tobeRemoved = append(tobeRemoved, t) + for _, mInfo := range mInfos { + if mInfo.Version < maxVersion { + tobeRemoved = append(tobeRemoved, mInfo.Message) } } } // remove them - for _, t := range tobeRemoved { - if err := tr.mstore.Remove(t.Message); err != nil { + for _, msg := range tobeRemoved { + if err := tr.mstore.Remove(msg); err != nil { return err } } diff --git a/internal/task/repo_test.go b/internal/task/repo_test.go index 4e385ed..e06caac 100644 --- a/internal/task/repo_test.go +++ b/internal/task/repo_test.go @@ -201,6 +201,5 @@ version: 3 }} actPlanned, err := mem.Messages(folderPlanned) test.OK(t, err) - fmt.Printf("act: %+v\n", actPlanned[0]) test.Equals(t, expPlanned, actPlanned) } diff --git a/internal/task/task.go b/internal/task/task.go index ad16a22..cfd6064 100644 --- a/internal/task/task.go +++ b/internal/task/task.go @@ -78,10 +78,12 @@ type Task struct { func New(msg *mstore.Message) *Task { // Id dirty := false + newId := false id, d := FieldFromBody(FIELD_ID, msg.Body) if id == "" { id = uuid.New().String() dirty = true + newId = true } if d { dirty = true @@ -106,10 +108,27 @@ func New(msg *mstore.Message) *Task { dirty = true } + // Due + dueStr, d := FieldFromBody(FIELD_DUE, msg.Body) + if dueStr == "" || d { + dirty = true + } + due := NewDateFromString(dueStr) + // Folder - folder := msg.Folder - if folder == FOLDER_INBOX { - folder = FOLDER_NEW + folderOld := msg.Folder + folderNew := folderOld + if folderOld == FOLDER_INBOX { + switch { + case newId: + folderNew = FOLDER_NEW + case !newId && due.IsZero(): + folderNew = FOLDER_UNPLANNED + case !newId && !due.IsZero(): + folderNew = FOLDER_PLANNED + } + } + if folderOld != folderNew { dirty = true } @@ -126,8 +145,9 @@ func New(msg *mstore.Message) *Task { return &Task{ Id: id, Version: version, - Folder: folder, + Folder: folderNew, Action: action, + Due: due, Project: project, Message: msg, Current: true, @@ -136,10 +156,16 @@ func New(msg *mstore.Message) *Task { } func (t *Task) FormatSubject() string { - order := []string{FIELD_PROJECT, FIELD_ACTION} + var order []string + if !t.Due.IsZero() { + order = append(order, FIELD_DUE) + } + order = append(order, FIELD_PROJECT, FIELD_ACTION) + fields := map[string]string{ FIELD_PROJECT: t.Project, FIELD_ACTION: t.Action, + FIELD_DUE: t.Due.String(), } parts := []string{} @@ -154,12 +180,13 @@ func (t *Task) FormatSubject() string { func (t *Task) FormatBody() string { body := fmt.Sprintf("\n") - order := []string{FIELD_ID, FIELD_VERSION, FIELD_PROJECT, FIELD_ACTION} + order := []string{FIELD_ACTION, FIELD_DUE, FIELD_PROJECT, FIELD_VERSION, FIELD_ID} fields := map[string]string{ FIELD_ID: t.Id, FIELD_VERSION: strconv.Itoa(t.Version), FIELD_PROJECT: t.Project, FIELD_ACTION: t.Action, + FIELD_DUE: t.Due.String(), } keyLen := 0 @@ -215,15 +242,11 @@ func FieldFromBody(field, body string) (string, bool) { } func FieldFromSubject(field, subject string) string { - if field == FIELD_ACTION { - return strings.ToLower(subject) + if field != FIELD_ACTION { + return "" } - return "" -} + terms := strings.Split(subject, SUBJECT_SEPARATOR) -func increment(s string) string { - i, _ := strconv.Atoi(s) - i++ - return strconv.Itoa(i) + return terms[len(terms)-1] } diff --git a/internal/task/task_test.go b/internal/task/task_test.go index 7163abb..8f3c83b 100644 --- a/internal/task/task_test.go +++ b/internal/task/task_test.go @@ -14,7 +14,7 @@ func TestNewFromMessage(t *testing.T) { version := 2 action := "some action" project := "project" - folder := task.FOLDER_NEW + date := task.NewDate(2021, 1, 20) for _, tc := range []struct { name string @@ -33,9 +33,10 @@ func TestNewFromMessage(t *testing.T) { { name: "id, action, project and folder", message: &mstore.Message{ - Folder: folder, + Folder: task.FOLDER_UNPLANNED, Body: fmt.Sprintf(` id: %s +due: no date version: %d action: %s project: %s @@ -46,21 +47,40 @@ project: %s exp: &task.Task{ Id: id, Version: version, - Folder: folder, + Folder: task.FOLDER_UNPLANNED, Action: action, Project: project, }, }, + { + name: "with date", + message: &mstore.Message{ + Folder: task.FOLDER_PLANNED, + Body: fmt.Sprintf(` +id: %s +due: %s +version: %d +action: %s +`, id, date.String(), version, action), + }, + hasId: true, + hasVersion: true, + exp: &task.Task{ + Id: id, + Folder: task.FOLDER_PLANNED, + Action: action, + Version: version, + Due: date, + }, + }, { name: "folder inbox get updated to new", message: &mstore.Message{ Folder: task.FOLDER_INBOX, Body: fmt.Sprintf(` -id: %s action: %s -`, id, action), +`, action), }, - hasId: true, exp: &task.Task{ Id: id, Folder: task.FOLDER_NEW, @@ -68,13 +88,51 @@ action: %s Dirty: true, }, }, + { + name: "folder inbox gets updated to planned", + message: &mstore.Message{ + Folder: task.FOLDER_INBOX, + Body: fmt.Sprintf(` +id: %s +due: %s +action: %s +`, id, date.String(), action), + }, + hasId: true, + exp: &task.Task{ + Id: id, + Folder: task.FOLDER_PLANNED, + Action: action, + Due: date, + Dirty: true, + }, + }, + { + name: "folder new gets updated to unplanned", + message: &mstore.Message{ + Folder: task.FOLDER_INBOX, + Body: fmt.Sprintf(` +id: %s +due: no date +action: %s +`, id, action), + }, + hasId: true, + exp: &task.Task{ + Id: id, + Folder: task.FOLDER_UNPLANNED, + Action: action, + Dirty: true, + }, + }, { name: "action in subject takes precedence", message: &mstore.Message{ - Folder: folder, + Folder: task.FOLDER_PLANNED, Subject: "some other action", Body: fmt.Sprintf(` id: %s +due: no date version: %d action: %s `, id, version, action), @@ -84,21 +142,21 @@ action: %s exp: &task.Task{ Id: id, Version: version, - Folder: folder, + Folder: task.FOLDER_PLANNED, Action: action, }, }, { name: "action from subject if not present in body", message: &mstore.Message{ - Folder: folder, + Folder: task.FOLDER_PLANNED, Subject: action, Body: fmt.Sprintf(`id: %s`, id), }, hasId: true, exp: &task.Task{ Id: id, - Folder: folder, + Folder: task.FOLDER_PLANNED, Action: action, Dirty: true, }, @@ -106,7 +164,7 @@ action: %s { name: "quoted fields", message: &mstore.Message{ - Folder: folder, + Folder: task.FOLDER_PLANNED, Body: fmt.Sprintf(` action: %s @@ -118,7 +176,7 @@ Forwarded message: hasId: true, exp: &task.Task{ Id: id, - Folder: folder, + Folder: task.FOLDER_PLANNED, Action: action, Dirty: true, }, @@ -143,6 +201,7 @@ Forwarded message: func TestFormatSubject(t *testing.T) { action := "an action" project := " a project" + due := task.NewDate(2021, 1, 30) for _, tc := range []struct { name string @@ -155,18 +214,34 @@ func TestFormatSubject(t *testing.T) { }, { name: "action", - task: &task.Task{Action: action}, - exp: action, + task: &task.Task{ + Action: action, + }, + exp: action, }, { name: "project", - task: &task.Task{Project: project}, - exp: project, + task: &task.Task{ + Project: project, + }, + exp: project, }, { name: "action and project", - task: &task.Task{Action: action, Project: project}, - exp: fmt.Sprintf("%s - %s", project, action), + task: &task.Task{ + Action: action, + Project: project, + }, + exp: fmt.Sprintf("%s - %s", project, action), + }, + { + name: "action, date and project", + task: &task.Task{ + Action: action, + Project: project, + Due: due, + }, + exp: fmt.Sprintf("%s - %s - %s", due.String(), project, action), }, } { t.Run(tc.name, func(t *testing.T) { @@ -190,10 +265,11 @@ func TestFormatBody(t *testing.T) { name: "empty", task: &task.Task{}, exp: ` -id: -version: 0 -project: action: +due: no date +project: +version: 0 +id: `, }, { @@ -203,15 +279,17 @@ action: Version: version, Action: action, Project: project, + Due: task.NewDate(2021, 1, 30), Message: &mstore.Message{ Body: "previous body", }, }, exp: ` -id: an id -version: 6 -project: project action: an action +due: 2021-01-30 (saturday) +project: project +version: 6 +id: an id Previous version: @@ -327,6 +405,7 @@ field: valuea } func TestFieldFromSubject(t *testing.T) { + action := "action" for _, tc := range []struct { name string field string @@ -335,7 +414,7 @@ func TestFieldFromSubject(t *testing.T) { }{ { name: "empty field", - subject: "subject", + subject: action, }, { name: "empty subject", @@ -344,13 +423,25 @@ func TestFieldFromSubject(t *testing.T) { { name: "unknown field", field: "unknown", - subject: "subject", + subject: action, }, { name: "known field", field: task.FIELD_ACTION, - subject: "subject", - exp: "subject", + subject: action, + exp: action, + }, + { + name: "with project", + field: task.FIELD_ACTION, + subject: fmt.Sprintf("project - %s", action), + exp: action, + }, + { + name: "with due and project", + field: task.FIELD_ACTION, + subject: fmt.Sprintf("due - project - %s", action), + exp: action, }, } { t.Run(tc.name, func(t *testing.T) {