diff --git a/sort.go b/sort.go new file mode 100644 index 0000000..7f84323 --- /dev/null +++ b/sort.go @@ -0,0 +1,143 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package todotxt + +import ( + "errors" + "sort" +) + +// Flags for defining sort element and order. +const ( + SORT_PRIORITY_ASC = iota + SORT_PRIORITY_DESC + SORT_CREATED_DATE_ASC + SORT_CREATED_DATE_DESC + SORT_COMPLETED_DATE_ASC + SORT_COMPLETED_DATE_DESC + SORT_DUE_DATE_ASC + SORT_DUE_DATE_DESC +) + +// Sort allows a TaskList to be sorted by certain predefined fields. +// See constants SORT_* for fields and sort order. +func (tasklist *TaskList) Sort(sortFlag int) error { + switch sortFlag { + case SORT_PRIORITY_ASC, SORT_PRIORITY_DESC: + tasklist.sortByPriority(sortFlag) + case SORT_CREATED_DATE_ASC, SORT_CREATED_DATE_DESC: + tasklist.sortByCreatedDate(sortFlag) + case SORT_COMPLETED_DATE_ASC, SORT_COMPLETED_DATE_DESC: + tasklist.sortByCompletedDate(sortFlag) + case SORT_DUE_DATE_ASC, SORT_DUE_DATE_DESC: + tasklist.sortByDueDate(sortFlag) + default: + return errors.New("Unrecognized sort option") + } + + return nil +} + +type tasklistSort struct { + tasklists TaskList + by func(t1, t2 *Task) bool +} + +func (ts *tasklistSort) Len() int { + return len(ts.tasklists) +} + +func (ts *tasklistSort) Swap(l, r int) { + ts.tasklists[l], ts.tasklists[r] = ts.tasklists[r], ts.tasklists[l] +} + +func (ts *tasklistSort) Less(l, r int) bool { + return ts.by(&ts.tasklists[l], &ts.tasklists[r]) +} + +func (tasklist *TaskList) sortBy(by func(t1, t2 *Task) bool) *TaskList { + ts := &tasklistSort{ + tasklists: *tasklist, + by: by, + } + sort.Sort(ts) + return tasklist +} + +func (tasklist *TaskList) sortByPriority(order int) *TaskList { + tasklist.sortBy(func(t1, t2 *Task) bool { + if order == SORT_PRIORITY_DESC { // DESC + if t1.HasPriority() && t2.HasPriority() { + return t1.Priority > t2.Priority + } else { + return !t1.HasPriority() + } + } else { // ASC + if t1.HasPriority() && t2.HasPriority() { + return t1.Priority < t2.Priority + } else { + return t1.HasPriority() + } + } + }) + return tasklist +} + +func (tasklist *TaskList) sortByCreatedDate(order int) *TaskList { + tasklist.sortBy(func(t1, t2 *Task) bool { + if order == SORT_CREATED_DATE_DESC { // DESC + if t1.HasCreatedDate() && t2.HasCreatedDate() { + return t1.CreatedDate.After(t2.CreatedDate) + } else { + return !t1.HasCreatedDate() + } + } else { // ASC + if t1.HasCreatedDate() && t2.HasCreatedDate() { + return t1.CreatedDate.Before(t2.CreatedDate) + } else { + return t1.HasCreatedDate() + } + } + }) + return tasklist +} + +func (tasklist *TaskList) sortByCompletedDate(order int) *TaskList { + tasklist.sortBy(func(t1, t2 *Task) bool { + if order == SORT_COMPLETED_DATE_DESC { // DESC + if t1.HasCompletedDate() && t2.HasCompletedDate() { + return t1.CompletedDate.After(t2.CompletedDate) + } else { + return !t1.HasCompletedDate() + } + } else { // ASC + if t1.HasCompletedDate() && t2.HasCompletedDate() { + return t1.CompletedDate.Before(t2.CompletedDate) + } else { + return t1.HasCompletedDate() + } + } + }) + return tasklist +} + +func (tasklist *TaskList) sortByDueDate(order int) *TaskList { + tasklist.sortBy(func(t1, t2 *Task) bool { + if order == SORT_DUE_DATE_DESC { // DESC + if t1.HasDueDate() && t2.HasDueDate() { + return t1.DueDate.After(t2.DueDate) + } else { + return !t1.HasDueDate() + } + } else { // ASC + if t1.HasDueDate() && t2.HasDueDate() { + return t1.DueDate.Before(t2.DueDate) + } else { + return t1.HasDueDate() + } + } + }) + return tasklist +} diff --git a/task.go b/task.go new file mode 100644 index 0000000..7b4ac16 --- /dev/null +++ b/task.go @@ -0,0 +1,110 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package todotxt + +import ( + "fmt" + "sort" + "time" +) + +// Task represents a todo.txt task entry. +type Task struct { + Original string // Original raw task text. + Todo string // Todo part of task text. + Priority string + Projects []string + Contexts []string + AdditionalTags map[string]string // Addon tags will be available here. + CreatedDate time.Time + DueDate time.Time + CompletedDate time.Time + Completed bool +} + +// String returns a complete task string in todo.txt format. +// +// Contexts, Projects and additional tags are alphabetically sorted, +// and appendend at the end in the following order: +// Contexts, Projects, Tags +// +// For example: +// "(A) 2013-07-23 Call Dad @Home @Phone +Family due:2013-07-31 customTag1:Important!" +func (task Task) String() string { + var text string + + if task.Completed { + text += "x " + if task.HasCompletedDate() { + text += fmt.Sprintf("%s ", task.CompletedDate.Format(DateLayout)) + } + } + + if task.HasPriority() { + text += fmt.Sprintf("(%s) ", task.Priority) + } + + if task.HasCreatedDate() { + text += fmt.Sprintf("%s ", task.CreatedDate.Format(DateLayout)) + } + + text += task.Todo + + if len(task.Contexts) > 0 { + for _, context := range task.Contexts { + text += fmt.Sprintf(" @%s", context) + } + } + + if len(task.Projects) > 0 { + for _, project := range task.Projects { + text += fmt.Sprintf(" +%s", project) + } + } + + if len(task.AdditionalTags) > 0 { + // Sort map alphabetically by keys + keys := make([]string, 0, len(task.AdditionalTags)) + for key, _ := range task.AdditionalTags { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + text += fmt.Sprintf(" %s:%s", key, task.AdditionalTags[key]) + } + } + + if task.HasDueDate() { + text += fmt.Sprintf(" due:%s", task.DueDate.Format(DateLayout)) + } + + return text +} + +// Task returns a complete task string in todo.txt format. +// See *Task.String() for further information. +func (task *Task) Task() string { + return task.String() +} + +// HasPriority returns true if the task has a priority. +func (task *Task) HasPriority() bool { + return task.Priority != "" +} + +// HasCreatedDate returns true if the task has a created date. +func (task *Task) HasCreatedDate() bool { + return !task.CreatedDate.IsZero() +} + +// HasDueDate returns true if the task has a due date. +func (task *Task) HasDueDate() bool { + return !task.DueDate.IsZero() +} + +// HasCompletedDate returns true if the task has a completed date. +func (task *Task) HasCompletedDate() bool { + return !task.CompletedDate.IsZero() +} diff --git a/testdata/expected_todo.txt b/testdata/expected_todo.txt index ac54598..e85dd5c 100644 --- a/testdata/expected_todo.txt +++ b/testdata/expected_todo.txt @@ -36,3 +36,9 @@ x (C) 2014-01-01 Create golang library documentation @Go +go-todotxt due:2014-01 x 2014-01-02 (B) 2013-12-30 Create golang library test cases @Go +go-todotxt x 2014-01-03 2014-01-01 Create some more golang library test cases @Go +go-todotxt 2013-02-22 Pick up milk @GroceryStore +2013-02-22 Pick up milk @GroceryStore +x 2014-01-02 (B) 2013-12-30 Create golang library test cases @Go +go-todotxt +x (C) 2014-01-01 Create golang library documentation @Go +go-todotxt due:2014-01-12 +(D) 2013-12-01 Outline chapter 5 @Computer +Novel Level:5 private:false due:2014-02-17 +x 2014-01-03 Create golang library @Go +go-todotxt due:2014-01-05 +(A) 2012-01-30 Call Mom @Call @Phone +Family diff --git a/testdata/input_todo.txt b/testdata/input_todo.txt index 1f269fb..f627162 100644 --- a/testdata/input_todo.txt +++ b/testdata/input_todo.txt @@ -53,3 +53,11 @@ x (C) 2014-01-01 Create golang library documentation @Go +go-todotxt due:2014-01 x 2014-01-02 (B) 2013-12-30 Create golang library test cases @Go +go-todotxt x 2014-01-03 2014-01-01 Create some more golang library test cases @Go +go-todotxt 2013-02-22 Pick up milk @GroceryStore + +# Sort Priority test case +2013-02-22 Pick up milk @GroceryStore +x 2014-01-02 (B) 2013-12-30 Create golang library test cases @Go +go-todotxt +x (C) 2014-01-01 Create golang library documentation @Go +go-todotxt due:2014-01-12 +(D) 2013-12-01 private:false Outline chapter 5 +Novel @Computer Level:5 due:2014-02-17 +x 2014-01-03 Create golang library @Go +go-todotxt due:2014-01-05 +(A) 2012-01-30 @Phone Call Mom @Call +Family diff --git a/testdata/ouput_todo.txt b/testdata/ouput_todo.txt index ac54598..e85dd5c 100644 --- a/testdata/ouput_todo.txt +++ b/testdata/ouput_todo.txt @@ -36,3 +36,9 @@ x (C) 2014-01-01 Create golang library documentation @Go +go-todotxt due:2014-01 x 2014-01-02 (B) 2013-12-30 Create golang library test cases @Go +go-todotxt x 2014-01-03 2014-01-01 Create some more golang library test cases @Go +go-todotxt 2013-02-22 Pick up milk @GroceryStore +2013-02-22 Pick up milk @GroceryStore +x 2014-01-02 (B) 2013-12-30 Create golang library test cases @Go +go-todotxt +x (C) 2014-01-01 Create golang library documentation @Go +go-todotxt due:2014-01-12 +(D) 2013-12-01 Outline chapter 5 @Computer +Novel Level:5 private:false due:2014-02-17 +x 2014-01-03 Create golang library @Go +go-todotxt due:2014-01-05 +(A) 2012-01-30 Call Mom @Call @Phone +Family diff --git a/todotxt.go b/todotxt.go index 98968cb..d12f8ae 100644 --- a/todotxt.go +++ b/todotxt.go @@ -19,20 +19,6 @@ import ( "time" ) -// Task represents a todo.txt task entry. -type Task struct { - Original string // Original raw task text. - Todo string // Todo part of task text. - Priority string - Projects []string - Contexts []string - AdditionalTags map[string]string // Addon tags will be available here. - CreatedDate time.Time - DueDate time.Time - CompletedDate time.Time - Completed bool -} - // TaskList represents a list of todo.txt task entries. // It is usually loaded from a whole todo.txt file. type TaskList []Task @@ -64,91 +50,6 @@ func (tasklist TaskList) String() (text string) { return text } -// String returns a complete task string in todo.txt format. -// -// Contexts, Projects and additional tags are alphabetically sorted, -// and appendend at the end in the following order: -// Contexts, Projects, Tags -// -// For example: -// "(A) 2013-07-23 Call Dad @Home @Phone +Family due:2013-07-31 customTag1:Important!" -func (task Task) String() string { - var text string - - if task.Completed { - text += "x " - if task.HasCompletedDate() { - text += fmt.Sprintf("%s ", task.CompletedDate.Format(DateLayout)) - } - } - - if task.HasPriority() { - text += fmt.Sprintf("(%s) ", task.Priority) - } - - if task.HasCreatedDate() { - text += fmt.Sprintf("%s ", task.CreatedDate.Format(DateLayout)) - } - - text += task.Todo - - if len(task.Contexts) > 0 { - for _, context := range task.Contexts { - text += fmt.Sprintf(" @%s", context) - } - } - - if len(task.Projects) > 0 { - for _, project := range task.Projects { - text += fmt.Sprintf(" +%s", project) - } - } - - if len(task.AdditionalTags) > 0 { - // Sort map alphabetically by keys - keys := make([]string, 0, len(task.AdditionalTags)) - for key, _ := range task.AdditionalTags { - keys = append(keys, key) - } - sort.Strings(keys) - for _, key := range keys { - text += fmt.Sprintf(" %s:%s", key, task.AdditionalTags[key]) - } - } - - if task.HasDueDate() { - text += fmt.Sprintf(" due:%s", task.DueDate.Format(DateLayout)) - } - - return text -} - -// Task returns a complete task string in todo.txt format. -// See *Task.String() for further information. -func (task *Task) Task() string { - return task.String() -} - -// HasPriority returns true if the task has a priority. -func (task *Task) HasPriority() bool { - return task.Priority != "" -} - -// HasCreatedDate returns true if the task has a created date. -func (task *Task) HasCreatedDate() bool { - return !task.CreatedDate.IsZero() -} - -// HasDueDate returns true if the task has a due date. -func (task *Task) HasDueDate() bool { - return !task.DueDate.IsZero() -} - -// HasCompletedDate returns true if the task has a completed date. -func (task *Task) HasCompletedDate() bool { - return !task.CompletedDate.IsZero() -} - // LoadFromFile loads a TaskList from *os.File. // // Using *os.File instead of a filename allows to also use os.Stdin. diff --git a/todotxt_test.go b/todotxt_test.go index baf2b7f..2328fcb 100644 --- a/todotxt_test.go +++ b/todotxt_test.go @@ -195,7 +195,7 @@ func TestTaskListWriteFilename(t *testing.T) { func TestTaskListCount(t *testing.T) { testTasklist.LoadFromFilename(testInput) - testExpected := 38 + testExpected := 44 testGot := len(testTasklist) if testGot != testExpected { t.Errorf("Expected TaskList to contain %d tasks, but got %d", testExpected, testGot) @@ -529,6 +529,89 @@ func TestTaskCompletedDate(t *testing.T) { taskId++ } +func TestTaskSortByPriority(t *testing.T) { + testTasklist.LoadFromFilename(testInput) + taskId := 38 + + testTasklist = testTasklist[taskId:] + + testTasklist.Sort(SORT_PRIORITY_ASC) + + testExpected = "(A) 2012-01-30 Call Mom @Call @Phone +Family" + testGot = testTasklist[0].Task() + if testGot != testExpected { + t.Errorf("Expected Task[1] after Sort() to be [%s], but got [%s]", testExpected, testGot) + } + + testExpected = "x 2014-01-02 (B) 2013-12-30 Create golang library test cases @Go +go-todotxt" + testGot = testTasklist[1].Task() + if testGot != testExpected { + t.Errorf("Expected Task[2] after Sort() to be [%s], but got [%s]", testExpected, testGot) + } + + testExpected = "x (C) 2014-01-01 Create golang library documentation @Go +go-todotxt due:2014-01-12" + testGot = testTasklist[2].Task() + if testGot != testExpected { + t.Errorf("Expected Task[3] after Sort() to be [%s], but got [%s]", testExpected, testGot) + } + + testExpected = "(D) 2013-12-01 Outline chapter 5 @Computer +Novel Level:5 private:false due:2014-02-17" + testGot = testTasklist[3].Task() + if testGot != testExpected { + t.Errorf("Expected Task[4] after Sort() to be [%s], but got [%s]", testExpected, testGot) + } + + testExpected = "2013-02-22 Pick up milk @GroceryStore" + testGot = testTasklist[4].Task() + if testGot != testExpected { + t.Errorf("Expected Task[5] after Sort() to be [%s], but got [%s]", testExpected, testGot) + } + + testExpected = "x 2014-01-03 Create golang library @Go +go-todotxt due:2014-01-05" + testGot = testTasklist[5].Task() + if testGot != testExpected { + t.Errorf("Expected Task[6] after Sort() to be [%s], but got [%s]", testExpected, testGot) + } + + testTasklist.Sort(SORT_PRIORITY_DESC) + + testExpected = "x 2014-01-03 Create golang library @Go +go-todotxt due:2014-01-05" + testGot = testTasklist[0].Task() + if testGot != testExpected { + t.Errorf("Expected Task[1] after Sort() to be [%s], but got [%s]", testExpected, testGot) + } + + testExpected = "2013-02-22 Pick up milk @GroceryStore" + testGot = testTasklist[1].Task() + if testGot != testExpected { + t.Errorf("Expected Task[2] after Sort() to be [%s], but got [%s]", testExpected, testGot) + } + + testExpected = "(D) 2013-12-01 Outline chapter 5 @Computer +Novel Level:5 private:false due:2014-02-17" + testGot = testTasklist[2].Task() + if testGot != testExpected { + t.Errorf("Expected Task[3] after Sort() to be [%s], but got [%s]", testExpected, testGot) + } + + testExpected = "x (C) 2014-01-01 Create golang library documentation @Go +go-todotxt due:2014-01-12" + testGot = testTasklist[3].Task() + if testGot != testExpected { + t.Errorf("Expected Task[4] after Sort() to be [%s], but got [%s]", testExpected, testGot) + } + + testExpected = "x 2014-01-02 (B) 2013-12-30 Create golang library test cases @Go +go-todotxt" + testGot = testTasklist[4].Task() + if testGot != testExpected { + t.Errorf("Expected Task[5] after Sort() to be [%s], but got [%s]", testExpected, testGot) + } + + testExpected = "(A) 2012-01-30 Call Mom @Call @Phone +Family" + testGot = testTasklist[5].Task() + if testGot != testExpected { + t.Errorf("Expected Task[6] after Sort() to be [%s], but got [%s]", testExpected, testGot) + } +} + func compareSlices(list1 []string, list2 []string) bool { if len(list1) != len(list2) { return false