diff --git a/ultralist/create_todo.go b/ultralist/create_todo.go index af837604..9df927c1 100644 --- a/ultralist/create_todo.go +++ b/ultralist/create_todo.go @@ -4,17 +4,6 @@ import "time" // CreateTodo will create a TodoItem from a Filter func CreateTodo(filter *Filter) (*Todo, error) { - dueDateString := "" - - if filter.LastDue() != "" { - dateParser := &DateParser{} - dueDate, err := dateParser.ParseDate(filter.LastDue(), time.Now()) - if err != nil { - return nil, err - } - dueDateString = dueDate.Format("2006-01-02") - } - todoItem := &Todo{ Subject: filter.Subject, Archived: filter.Archived, @@ -22,7 +11,7 @@ func CreateTodo(filter *Filter) (*Todo, error) { Completed: filter.Completed, Projects: filter.Projects, Contexts: filter.Contexts, - Due: dueDateString, + Due: filter.Due, Status: filter.LastStatus(), } if todoItem.Completed { diff --git a/ultralist/date_parser.go b/ultralist/date_parser.go index 797040ca..40bb7934 100644 --- a/ultralist/date_parser.go +++ b/ultralist/date_parser.go @@ -15,6 +15,8 @@ func (dp *DateParser) ParseDate(dateString string, pivotDay time.Time) (date tim switch dateString { case "none": return time.Time{}, nil + case "yesterday", "yes": + return bod(pivotDay).AddDate(0, 0, -1), nil case "today", "tod": return bod(pivotDay), nil case "tomorrow", "tom": diff --git a/ultralist/edit_todo.go b/ultralist/edit_todo.go index eb3b3139..a31d9e36 100644 --- a/ultralist/edit_todo.go +++ b/ultralist/edit_todo.go @@ -1,21 +1,9 @@ package ultralist -import "time" - // EditTodo edits a todo based upon a filter func EditTodo(todo *Todo, filter *Filter) error { if filter.HasDue { - dateParser := &DateParser{} - dueDate, err := dateParser.ParseDate(filter.LastDue(), time.Now()) - if err != nil { - return err - } - - if dueDate.IsZero() { - todo.Due = "" - } else { - todo.Due = dueDate.Format("2006-01-02") - } + todo.Due = filter.Due } if filter.HasCompleted { diff --git a/ultralist/filter.go b/ultralist/filter.go index 8f1934d7..31525a5e 100644 --- a/ultralist/filter.go +++ b/ultralist/filter.go @@ -7,26 +7,32 @@ type Filter struct { IsPriority bool Completed bool - HasCompleted bool - HasCompletedAt bool - HasArchived bool - HasIsPriority bool - HasDue bool - HasStatus bool - HasProjectFilter bool - HasContextFilter bool + Due string + DueBefore string + DueAfter string Contexts []string - Due []string Projects []string Status []string ExcludeContexts []string - ExcludeDue []string ExcludeProjects []string ExcludeStatus []string CompletedAt []string + + HasCompleted bool + HasCompletedAt bool + HasArchived bool + HasIsPriority bool + + HasDueBefore bool + HasDue bool + HasDueAfter bool + + HasStatus bool + HasProjectFilter bool + HasContextFilter bool } // LastStatus returns the last status from the filter @@ -36,11 +42,3 @@ func (f *Filter) LastStatus() string { } return f.Status[len(f.Status)-1] } - -// LastDue returns the last due from the filter -func (f *Filter) LastDue() string { - if len(f.Due) == 0 { - return "" - } - return f.Due[len(f.Due)-1] -} diff --git a/ultralist/input_parser.go b/ultralist/input_parser.go index a94c55ef..bb27fcbf 100644 --- a/ultralist/input_parser.go +++ b/ultralist/input_parser.go @@ -3,6 +3,7 @@ package ultralist import ( "regexp" "strings" + "time" ) // InputParser parses text to extract a Filter struct @@ -37,7 +38,6 @@ project:one,-two // Parse parses raw input and returns a Filter object func (p *InputParser) Parse(input string) (*Filter, error) { - filter := &Filter{ HasStatus: false, HasCompleted: false, @@ -45,8 +45,13 @@ func (p *InputParser) Parse(input string) (*Filter, error) { HasIsPriority: false, HasProjectFilter: false, HasContextFilter: false, + HasDueBefore: false, HasDue: false, + HasDueAfter: false, } + + dateParser := &DateParser{} + var subjectMatches []string cr, _ := regexp.Compile(`\@[\p{L}\d_-]+`) @@ -78,10 +83,51 @@ func (p *InputParser) Parse(input string) (*Filter, error) { match = true } - r4, _ := regexp.Compile(`due:.*$`) - if r4.MatchString(word) { + rDueBefore, _ := regexp.Compile(`duebefore:.*$`) + if rDueBefore.MatchString(word) { + filter.HasDueBefore = true + dueDate, err := dateParser.ParseDate(rDueBefore.FindString(word)[10:], time.Now()) + if err != nil { + return filter, err + } + + if dueDate.IsZero() { + filter.DueBefore = "" + } else { + filter.DueBefore = dueDate.Format("2006-01-02") + } + match = true + } + + rDue, _ := regexp.Compile(`due:.*$`) + if rDue.MatchString(word) { filter.HasDue = true - filter.Due, filter.ExcludeDue = p.parseString(r4.FindString(word)[4:]) + dueDate, err := dateParser.ParseDate(rDue.FindString(word)[4:], time.Now()) + if err != nil { + return filter, err + } + + if dueDate.IsZero() { + filter.Due = "" + } else { + filter.Due = dueDate.Format("2006-01-02") + } + match = true + } + + rDueAfter, _ := regexp.Compile(`dueafter:.*$`) + if rDueAfter.MatchString(word) { + filter.HasDueAfter = true + dueDate, err := dateParser.ParseDate(rDueAfter.FindString(word)[9:], time.Now()) + if err != nil { + return filter, err + } + + if dueDate.IsZero() { + filter.DueAfter = "" + } else { + filter.DueAfter = dueDate.Format("2006-01-02") + } match = true } @@ -122,16 +168,16 @@ func (p *InputParser) Parse(input string) (*Filter, error) { } func (p *InputParser) parseString(input string) ([]string, []string) { - var positive []string - var negative []string + var include []string + var exclude []string for _, str := range strings.Split(input, ",") { if strings.HasPrefix(str, "-") { - negative = append(negative, str[1:]) + exclude = append(exclude, str[1:]) } else { - positive = append(positive, str) + include = append(include, str) } } - return positive, negative + return include, exclude } func (p *InputParser) parseBool(input string) bool { diff --git a/ultralist/input_parser_test.go b/ultralist/input_parser_test.go index 8365d083..5d92faaf 100644 --- a/ultralist/input_parser_test.go +++ b/ultralist/input_parser_test.go @@ -3,6 +3,7 @@ package ultralist import ( "fmt" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -18,8 +19,8 @@ func TestInputParser(t *testing.T) { assert.Equal("now", filter.Status[0]) assert.Equal("next", filter.Status[1]) - assert.Equal(1, len(filter.Due)) - assert.Equal("tom", filter.Due[0]) + tomorrow := time.Now().AddDate(0, 0, 1).Format("2006-01-02") + assert.Equal(tomorrow, filter.Due) assert.Equal("do this thing", filter.Subject) } @@ -30,7 +31,8 @@ func TestSubject(t *testing.T) { filter, _ := parser.Parse("due:tom here is the subject") assert.Equal("here is the subject", filter.Subject) - assert.Equal("tom", filter.Due[0]) + tomorrow := time.Now().AddDate(0, 0, 1).Format("2006-01-02") + assert.Equal(tomorrow, filter.Due) } func TestProjectsInSubject(t *testing.T) { diff --git a/ultralist/todo_filter.go b/ultralist/todo_filter.go index c9bf42d7..2f3c5a4f 100644 --- a/ultralist/todo_filter.go +++ b/ultralist/todo_filter.go @@ -1,5 +1,9 @@ package ultralist +import ( + "time" +) + // TodoFilter filters todos based on patterns. type TodoFilter struct { Filter *Filter @@ -54,6 +58,42 @@ func (f *TodoFilter) ApplyFilter() []*Todo { continue } + // has exact due date + if f.Filter.HasDue { + if todo.Due == f.Filter.Due { + filtered = append(filtered, todo) + } + continue + } + + if f.Filter.HasDueBefore { + if todo.Due == "" { + continue + } + + todoTime, _ := time.Parse("2006-01-02", todo.Due) + dueBeforeTime, _ := time.Parse("2006-01-02", f.Filter.DueBefore) + + if todoTime.Before(dueBeforeTime) { + filtered = append(filtered, todo) + } + continue + } + + if f.Filter.HasDueAfter { + if todo.Due == "" { + continue + } + + todoTime, _ := time.Parse("2006-01-02", todo.Due) + dueAfterTime, _ := time.Parse("2006-01-02", f.Filter.DueAfter) + + if todoTime.After(dueAfterTime) { + filtered = append(filtered, todo) + } + continue + } + filtered = append(filtered, todo) } diff --git a/ultralist/todo_filter_test.go b/ultralist/todo_filter_test.go index 2847a062..cd0bea1c 100644 --- a/ultralist/todo_filter_test.go +++ b/ultralist/todo_filter_test.go @@ -2,6 +2,7 @@ package ultralist import ( "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -9,14 +10,15 @@ import ( func TestNoFilterCriteriaExcludesArchived(t *testing.T) { assert := assert.New(t) + todos := SetupTodoList() todoFilter := &TodoFilter{ Filter: &Filter{}, - Todos: SetupTodoList(), + Todos: todos, } filtered := todoFilter.ApplyFilter() - assert.Equal(5, len(filtered)) + assert.Equal(len(todos)-1, len(filtered)) for _, todo := range filtered { assert.Equal(false, todo.Archived) } @@ -25,9 +27,10 @@ func TestNoFilterCriteriaExcludesArchived(t *testing.T) { func TestFilterPriority(t *testing.T) { assert := assert.New(t) + todos := SetupTodoList() todoFilter := &TodoFilter{ Filter: &Filter{HasIsPriority: true, IsPriority: true}, - Todos: SetupTodoList(), + Todos: todos, } filtered := todoFilter.ApplyFilter() @@ -42,7 +45,7 @@ func TestFilterPriority(t *testing.T) { filtered = todoFilter.ApplyFilter() - assert.Equal(4, len(filtered)) + assert.Equal(len(todos)-2, len(filtered)) assert.Equal("not priority", filtered[0].Subject) } @@ -67,17 +70,18 @@ func TestFilterInclusive(t *testing.T) { func TestFilterExclusive(t *testing.T) { assert := assert.New(t) + todos := SetupTodoList() todoFilter := &TodoFilter{ Filter: &Filter{ HasProjectFilter: true, ExcludeProjects: []string{"p2"}, }, - Todos: SetupTodoList(), + Todos: todos, } filtered := todoFilter.ApplyFilter() - assert.Equal(4, len(filtered)) + assert.Equal(len(todos)-2, len(filtered)) assert.Equal("has priority", filtered[0].Subject) assert.Equal("not priority", filtered[1].Subject) assert.Equal("+p1 +p3", filtered[2].Subject) @@ -103,6 +107,57 @@ func TestFilterInclusveAndExclusive(t *testing.T) { assert.Equal("+p1", filtered[1].Subject) } +func TestFilterDue(t *testing.T) { + assert := assert.New(t) + + todoFilter := &TodoFilter{ + Filter: &Filter{ + HasDue: true, + Due: time.Now().Format("2006-01-02"), + }, + Todos: SetupTodoList(), + } + + filtered := todoFilter.ApplyFilter() + + assert.Equal(1, len(filtered)) + assert.Equal("due today", filtered[0].Subject) +} + +func TestFilterDueBefore(t *testing.T) { + assert := assert.New(t) + + todoFilter := &TodoFilter{ + Filter: &Filter{ + HasDueBefore: true, + DueBefore: time.Now().Format("2006-01-02"), + }, + Todos: SetupTodoList(), + } + + filtered := todoFilter.ApplyFilter() + + assert.Equal(1, len(filtered)) + assert.Equal("due yesterday", filtered[0].Subject) +} + +func TestFilterDueAfter(t *testing.T) { + assert := assert.New(t) + + todoFilter := &TodoFilter{ + Filter: &Filter{ + HasDueAfter: true, + DueAfter: time.Now().Format("2006-01-02"), + }, + Todos: SetupTodoList(), + } + + filtered := todoFilter.ApplyFilter() + + assert.Equal(1, len(filtered)) + assert.Equal("due tomorrow", filtered[0].Subject) +} + func SetupTodoList() []*Todo { var todos []*Todo @@ -132,6 +187,17 @@ func SetupTodoList() []*Todo { todo, _ = CreateTodo(filter) todos = append(todos, todo) - return todos + filter, _ = parser.Parse("due tomorrow due:tom") + todo, _ = CreateTodo(filter) + todos = append(todos, todo) + filter, _ = parser.Parse("due today due:tod") + todo, _ = CreateTodo(filter) + todos = append(todos, todo) + + filter, _ = parser.Parse("due yesterday due:yes") + todo, _ = CreateTodo(filter) + todos = append(todos, todo) + + return todos }