8 Commits
1.0.0 ... 1.3.0

Author SHA1 Message Date
JamesClonk
2e04f1d233 GetTask bugfix
correctly return pointer to Task inside TaskList.
2014-01-16 12:18:21 +01:00
JamesClonk
e29d17784e Ready for new release, v1.2 2014-01-13 20:32:21 +01:00
JamesClonk
a0e8e1801d Added new methods on TaskList. 2014-01-13 20:27:58 +01:00
JamesClonk
c1f16b56a5 Work in progress
Started working on new functionality, like internal task id and new
useful methods on tasklists.
2014-01-13 15:37:21 +01:00
jamesclonk@jamesclonk.ch
96cc4ce27c make sure Projects and Contexts are sorted 2014-01-04 16:32:40 +01:00
jamesclonk@jamesclonk.ch
e8603bbc85 forgot header 2014-01-04 16:11:31 +01:00
jamesclonk@jamesclonk.ch
4af9e2abe8 added codebot and bitdeli badge 2014-01-04 14:26:42 +01:00
jamesclonk@jamesclonk.ch
48dacccdf1 added Complete() and Reopen() to Task. 2014-01-04 14:13:37 +01:00
13 changed files with 1058 additions and 168 deletions

View File

@@ -3,7 +3,7 @@ go-todotxt
A Go todo.txt library. A Go todo.txt library.
[![GoDoc](https://godoc.org/github.com/JamesClonk/go-todotxt?status.png)](https://godoc.org/github.com/JamesClonk/go-todotxt) [![Build Status](https://travis-ci.org/JamesClonk/go-todotxt.png?branch=master)](https://travis-ci.org/JamesClonk/go-todotxt) [![GoDoc](https://godoc.org/github.com/JamesClonk/go-todotxt?status.png)](https://godoc.org/github.com/JamesClonk/go-todotxt) [![Build Status](https://travis-ci.org/JamesClonk/go-todotxt.png?branch=master)](https://travis-ci.org/JamesClonk/go-todotxt) [![Codebot](https://codebot.io/badge/github.com/JamesClonk/go-todotxt.png)](http://codebot.io/doc/pkg/github.com/JamesClonk/go-todotxt "Codebot") [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/JamesClonk/go-todotxt/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
The *todotxt* package is a Go client library for Gina Trapani's [todo.txt](https://github.com/ginatrapani/todo.txt-cli/) files. The *todotxt* package is a Go client library for Gina Trapani's [todo.txt](https://github.com/ginatrapani/todo.txt-cli/) files.
It allows for parsing and manipulating of task lists and tasks in the todo.txt format. It allows for parsing and manipulating of task lists and tasks in the todo.txt format.
@@ -39,6 +39,12 @@ go-todotxt requires Go1.1 or higher.
fmt.Printf("Task 3: %v\n", tasklist[2]) fmt.Printf("Task 3: %v\n", tasklist[2])
fmt.Printf("Task 4, has priority: %v\n\n", tasklist[3].HasPriority()) fmt.Printf("Task 4, has priority: %v\n\n", tasklist[3].HasPriority())
fmt.Print(tasklist) fmt.Print(tasklist)
// Filter list to get only completed tasks
completedList := testTasklist.Filter(func(t Task) bool {
return t.Completed
})
fmt.Print(completedList)
} }
``` ```

48
example_test.go Normal file
View File

@@ -0,0 +1,48 @@
/* 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"
"log"
)
func ExampleLoadFromFilename() {
if tasklist, err := LoadFromFilename("todo.txt"); err != nil {
log.Fatal(err)
} else {
fmt.Print(tasklist) // String representation of TaskList works as expected.
}
// Output:
// (A) Call Mom @Phone +Family
// (A) Schedule annual checkup +Health
// (B) Outline chapter 5 @Computer +Novel
// (C) Add cover sheets @Office +TPSReports
// Plan backyard herb garden @Home
// Pick up milk @GroceryStore
// Research self-publishing services @Computer +Novel
// x Download Todo.txt mobile app @Phone
}
func ExampleTaskList_LoadFromFilename() {
var tasklist TaskList
// This will overwrite whatever was in the tasklist before.
// Irrelevant here since the list is still empty.
if err := tasklist.LoadFromFilename("todo.txt"); err != nil {
log.Fatal(err)
}
fmt.Println(tasklist[0].Todo) // Text part of first task (Call Mom)
fmt.Println(tasklist[2].Contexts) // Slice of contexts from third task ([Computer])
fmt.Println(tasklist[3].Priority) // Priority of fourth task (C)
fmt.Println(tasklist[7].Completed) // Completed flag of eigth task (true)
// Output:
// Call Mom
// [Computer]
// C
// true
}

32
sort.go
View File

@@ -35,7 +35,7 @@ func (tasklist *TaskList) Sort(sortFlag int) error {
case SORT_DUE_DATE_ASC, SORT_DUE_DATE_DESC: case SORT_DUE_DATE_ASC, SORT_DUE_DATE_DESC:
tasklist.sortByDueDate(sortFlag) tasklist.sortByDueDate(sortFlag)
default: default:
return errors.New("Unrecognized sort option") return errors.New("unrecognized sort option")
} }
return nil return nil
} }
@@ -68,19 +68,17 @@ func (tasklist *TaskList) sortBy(by func(t1, t2 *Task) bool) *TaskList {
func (tasklist *TaskList) sortByPriority(order int) *TaskList { func (tasklist *TaskList) sortByPriority(order int) *TaskList {
tasklist.sortBy(func(t1, t2 *Task) bool { tasklist.sortBy(func(t1, t2 *Task) bool {
if order == SORT_PRIORITY_DESC { // DESC if order == SORT_PRIORITY_ASC { // ASC
if t1.HasPriority() && t2.HasPriority() {
return t1.Priority > t2.Priority
} else {
return !t1.HasPriority()
}
} else { // ASC
if t1.HasPriority() && t2.HasPriority() { if t1.HasPriority() && t2.HasPriority() {
return t1.Priority < t2.Priority return t1.Priority < t2.Priority
} else {
return t1.HasPriority()
} }
return t1.HasPriority()
} }
// DESC
if t1.HasPriority() && t2.HasPriority() {
return t1.Priority > t2.Priority
}
return !t1.HasPriority()
}) })
return tasklist return tasklist
} }
@@ -89,16 +87,14 @@ func sortByDate(asc bool, hasDate1, hasDate2 bool, date1, date2 time.Time) bool
if asc { // ASC if asc { // ASC
if hasDate1 && hasDate2 { if hasDate1 && hasDate2 {
return date1.Before(date2) return date1.Before(date2)
} else {
return hasDate2
}
} else { // DESC
if hasDate1 && hasDate2 {
return date1.After(date2)
} else {
return !hasDate2
} }
return hasDate2
} }
// DESC
if hasDate1 && hasDate2 {
return date1.After(date2)
}
return !hasDate2
} }
func (tasklist *TaskList) sortByCreatedDate(order int) *TaskList { func (tasklist *TaskList) sortByCreatedDate(order int) *TaskList {

View File

@@ -1,3 +1,7 @@
/* 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 package todotxt
import ( import (
@@ -14,7 +18,9 @@ func TestTaskSortByPriority(t *testing.T) {
testTasklist = testTasklist[taskId : taskId+6] testTasklist = testTasklist[taskId : taskId+6]
testTasklist.Sort(SORT_PRIORITY_ASC) if err := testTasklist.Sort(SORT_PRIORITY_ASC); err != nil {
t.Fatal(err)
}
testExpected = "(A) 2012-01-30 Call Mom @Call @Phone +Family" testExpected = "(A) 2012-01-30 Call Mom @Call @Phone +Family"
testGot = testTasklist[0].Task() testGot = testTasklist[0].Task()
@@ -52,7 +58,9 @@ func TestTaskSortByPriority(t *testing.T) {
t.Errorf("Expected Task[6] after Sort() to be [%s], but got [%s]", testExpected, testGot) t.Errorf("Expected Task[6] after Sort() to be [%s], but got [%s]", testExpected, testGot)
} }
testTasklist.Sort(SORT_PRIORITY_DESC) if err := testTasklist.Sort(SORT_PRIORITY_DESC); err != nil {
t.Fatal(err)
}
testExpected = "x 2014-01-03 Create golang library @Go +go-todotxt due:2014-01-05" testExpected = "x 2014-01-03 Create golang library @Go +go-todotxt due:2014-01-05"
testGot = testTasklist[0].Task() testGot = testTasklist[0].Task()
@@ -97,7 +105,9 @@ func TestTaskSortByCreatedDate(t *testing.T) {
testTasklist = testTasklist[taskId : taskId+5] testTasklist = testTasklist[taskId : taskId+5]
testTasklist.Sort(SORT_CREATED_DATE_ASC) if err := testTasklist.Sort(SORT_CREATED_DATE_ASC); err != nil {
t.Fatal(err)
}
testExpected = "x 2014-01-03 Create golang library @Go +go-todotxt due:2014-01-05" testExpected = "x 2014-01-03 Create golang library @Go +go-todotxt due:2014-01-05"
testGot = testTasklist[0].Task() testGot = testTasklist[0].Task()
@@ -129,7 +139,9 @@ func TestTaskSortByCreatedDate(t *testing.T) {
t.Errorf("Expected Task[5] after Sort() to be [%s], but got [%s]", testExpected, testGot) t.Errorf("Expected Task[5] after Sort() to be [%s], but got [%s]", testExpected, testGot)
} }
testTasklist.Sort(SORT_CREATED_DATE_DESC) if err := testTasklist.Sort(SORT_CREATED_DATE_DESC); err != nil {
t.Fatal(err)
}
testExpected = "x (C) 2014-01-01 Create golang library documentation @Go +go-todotxt due:2014-01-12" testExpected = "x (C) 2014-01-01 Create golang library documentation @Go +go-todotxt due:2014-01-12"
testGot = testTasklist[0].Task() testGot = testTasklist[0].Task()
@@ -168,7 +180,9 @@ func TestTaskSortByCompletedDate(t *testing.T) {
testTasklist = testTasklist[taskId : taskId+6] testTasklist = testTasklist[taskId : taskId+6]
testTasklist.Sort(SORT_COMPLETED_DATE_ASC) if err := testTasklist.Sort(SORT_COMPLETED_DATE_ASC); err != nil {
t.Fatal(err)
}
testExpected = "x Download Todo.txt mobile app @Phone" testExpected = "x Download Todo.txt mobile app @Phone"
testGot = testTasklist[0].Task() testGot = testTasklist[0].Task()
@@ -206,7 +220,9 @@ func TestTaskSortByCompletedDate(t *testing.T) {
t.Errorf("Expected Task[6] after Sort() to be [%s], but got [%s]", testExpected, testGot) t.Errorf("Expected Task[6] after Sort() to be [%s], but got [%s]", testExpected, testGot)
} }
testTasklist.Sort(SORT_COMPLETED_DATE_DESC) if err := testTasklist.Sort(SORT_COMPLETED_DATE_DESC); err != nil {
t.Fatal(err)
}
testExpected = "x 2014-01-04 2014-01-01 Create some more golang library test cases @Go +go-todotxt" testExpected = "x 2014-01-04 2014-01-01 Create some more golang library test cases @Go +go-todotxt"
testGot = testTasklist[0].Task() testGot = testTasklist[0].Task()
@@ -251,7 +267,9 @@ func TestTaskSortByDueDate(t *testing.T) {
testTasklist = testTasklist[taskId : taskId+4] testTasklist = testTasklist[taskId : taskId+4]
testTasklist.Sort(SORT_DUE_DATE_ASC) if err := testTasklist.Sort(SORT_DUE_DATE_ASC); err != nil {
t.Fatal(err)
}
testExpected = "x 2014-01-02 (B) 2013-12-30 Create golang library test cases @Go +go-todotxt" testExpected = "x 2014-01-02 (B) 2013-12-30 Create golang library test cases @Go +go-todotxt"
testGot = testTasklist[0].Task() testGot = testTasklist[0].Task()
@@ -277,7 +295,9 @@ func TestTaskSortByDueDate(t *testing.T) {
t.Errorf("Expected Task[4] after Sort() to be [%s], but got [%s]", testExpected, testGot) t.Errorf("Expected Task[4] after Sort() to be [%s], but got [%s]", testExpected, testGot)
} }
testTasklist.Sort(SORT_DUE_DATE_DESC) if err := testTasklist.Sort(SORT_DUE_DATE_DESC); err != nil {
t.Fatal(err)
}
testExpected = "(B) 2013-12-01 Outline chapter 5 @Computer +Novel Level:5 private:false due:2014-02-17" testExpected = "(B) 2013-12-01 Outline chapter 5 @Computer +Novel Level:5 private:false due:2014-02-17"
testGot = testTasklist[0].Task() testGot = testTasklist[0].Task()
@@ -303,3 +323,13 @@ func TestTaskSortByDueDate(t *testing.T) {
t.Errorf("Expected Task[4] after Sort() to be [%s], but got [%s]", testExpected, testGot) t.Errorf("Expected Task[4] after Sort() to be [%s], but got [%s]", testExpected, testGot)
} }
} }
func TestTaskSortError(t *testing.T) {
testTasklist.LoadFromFilename(testInputSort)
if err := testTasklist.Sort(123); err == nil {
t.Errorf("Expected Sort() to fail because of unrecognized sort option, but it didn't!")
} else if err.Error() != "unrecognized sort option" {
t.Error(err)
}
}

149
task.go
View File

@@ -6,12 +6,29 @@ package todotxt
import ( import (
"fmt" "fmt"
"regexp"
"sort" "sort"
"strings"
"time" "time"
) )
var (
// DateLayout is used for formatting time.Time into todo.txt date format and vice-versa.
DateLayout = "2006-01-02"
priorityRx = regexp.MustCompile(`^(x|x \d{4}-\d{2}-\d{2}|)\s*\(([A-Z])\)\s+`) // Match priority: '(A) ...' or 'x (A) ...' or 'x 2012-12-12 (A) ...'
// Match created date: '(A) 2012-12-12 ...' or 'x 2012-12-12 (A) 2012-12-12 ...' or 'x (A) 2012-12-12 ...'or 'x 2012-12-12 2012-12-12 ...' or '2012-12-12 ...'
createdDateRx = regexp.MustCompile(`^(\([A-Z]\)|x \d{4}-\d{2}-\d{2} \([A-Z]\)|x \([A-Z]\)|x \d{4}-\d{2}-\d{2}|)\s*(\d{4}-\d{2}-\d{2})\s+`)
completedRx = regexp.MustCompile(`^x\s+`) // Match completed: 'x ...'
completedDateRx = regexp.MustCompile(`^x\s*(\d{4}-\d{2}-\d{2})\s+`) // Match completed date: 'x 2012-12-12 ...'
addonTagRx = regexp.MustCompile(`(^|\s+)([\w-]+):(\S+)`) // Match additional tags date: '... due:2012-12-12 ...'
contextRx = regexp.MustCompile(`(^|\s+)@(\S+)`) // Match contexts: '@Context ...' or '... @Context ...'
projectRx = regexp.MustCompile(`(^|\s+)\+(\S+)`) // Match projects: '+Project...' or '... +Project ...')
)
// Task represents a todo.txt task entry. // Task represents a todo.txt task entry.
type Task struct { type Task struct {
Id int // Internal task id.
Original string // Original raw task text. Original string // Original raw task text.
Todo string // Todo part of task text. Todo string // Todo part of task text.
Priority string Priority string
@@ -53,12 +70,14 @@ func (task Task) String() string {
text += task.Todo text += task.Todo
if len(task.Contexts) > 0 { if len(task.Contexts) > 0 {
sort.Strings(task.Contexts)
for _, context := range task.Contexts { for _, context := range task.Contexts {
text += fmt.Sprintf(" @%s", context) text += fmt.Sprintf(" @%s", context)
} }
} }
if len(task.Projects) > 0 { if len(task.Projects) > 0 {
sort.Strings(task.Projects)
for _, project := range task.Projects { for _, project := range task.Projects {
text += fmt.Sprintf(" +%s", project) text += fmt.Sprintf(" +%s", project)
} }
@@ -67,7 +86,7 @@ func (task Task) String() string {
if len(task.AdditionalTags) > 0 { if len(task.AdditionalTags) > 0 {
// Sort map alphabetically by keys // Sort map alphabetically by keys
keys := make([]string, 0, len(task.AdditionalTags)) keys := make([]string, 0, len(task.AdditionalTags))
for key, _ := range task.AdditionalTags { for key := range task.AdditionalTags {
keys = append(keys, key) keys = append(keys, key)
} }
sort.Strings(keys) sort.Strings(keys)
@@ -83,6 +102,108 @@ func (task Task) String() string {
return text return text
} }
// NewTask creates a new empty Task with default values. (CreatedDate is set to Now())
func NewTask() Task {
task := Task{}
task.CreatedDate = time.Now()
return task
}
// ParseTask parses the input text string into a Task struct.
func ParseTask(text string) (*Task, error) {
var err error
task := Task{}
task.Original = strings.Trim(text, "\t\n\r ")
task.Todo = task.Original
// Check for completed
if completedRx.MatchString(task.Original) {
task.Completed = true
// Check for completed date
if completedDateRx.MatchString(task.Original) {
if date, err := time.Parse(DateLayout, completedDateRx.FindStringSubmatch(task.Original)[1]); err == nil {
task.CompletedDate = date
} else {
return nil, err
}
}
// Remove from Todo text
task.Todo = completedDateRx.ReplaceAllString(task.Todo, "") // Strip CompletedDate first, otherwise it wouldn't match anymore (^x date...)
task.Todo = completedRx.ReplaceAllString(task.Todo, "") // Strip 'x '
}
// Check for priority
if priorityRx.MatchString(task.Original) {
task.Priority = priorityRx.FindStringSubmatch(task.Original)[2]
task.Todo = priorityRx.ReplaceAllString(task.Todo, "") // Remove from Todo text
}
// Check for created date
if createdDateRx.MatchString(task.Original) {
if date, err := time.Parse(DateLayout, createdDateRx.FindStringSubmatch(task.Original)[2]); err == nil {
task.CreatedDate = date
task.Todo = createdDateRx.ReplaceAllString(task.Todo, "") // Remove from Todo text
} else {
return nil, err
}
}
// function for collecting projects/contexts as slices from text
getSlice := func(rx *regexp.Regexp) []string {
matches := rx.FindAllStringSubmatch(task.Original, -1)
slice := make([]string, 0, len(matches))
seen := make(map[string]bool, len(matches))
for _, match := range matches {
word := strings.Trim(match[2], "\t\n\r ")
if _, found := seen[word]; !found {
slice = append(slice, word)
seen[word] = true
}
}
sort.Strings(slice)
return slice
}
// Check for contexts
if contextRx.MatchString(task.Original) {
task.Contexts = getSlice(contextRx)
task.Todo = contextRx.ReplaceAllString(task.Todo, "") // Remove from Todo text
}
// Check for projects
if projectRx.MatchString(task.Original) {
task.Projects = getSlice(projectRx)
task.Todo = projectRx.ReplaceAllString(task.Todo, "") // Remove from Todo text
}
// Check for additional tags
if addonTagRx.MatchString(task.Original) {
matches := addonTagRx.FindAllStringSubmatch(task.Original, -1)
tags := make(map[string]string, len(matches))
for _, match := range matches {
key, value := match[2], match[3]
if key == "due" { // due date is a known addon tag, it has its own struct field
if date, err := time.Parse(DateLayout, value); err == nil {
task.DueDate = date
} else {
return nil, err
}
} else if key != "" && value != "" {
tags[key] = value
}
}
task.AdditionalTags = tags
task.Todo = addonTagRx.ReplaceAllString(task.Todo, "") // Remove from Todo text
}
// Trim any remaining whitespaces from Todo text
task.Todo = strings.Trim(task.Todo, "\t\n\r\f ")
return &task, err
}
// Task returns a complete task string in todo.txt format. // Task returns a complete task string in todo.txt format.
// See *Task.String() for further information. // See *Task.String() for further information.
func (task *Task) Task() string { func (task *Task) Task() string {
@@ -106,7 +227,25 @@ func (task *Task) HasDueDate() bool {
// HasCompletedDate returns true if the task has a completed date. // HasCompletedDate returns true if the task has a completed date.
func (task *Task) HasCompletedDate() bool { func (task *Task) HasCompletedDate() bool {
return !task.CompletedDate.IsZero() return !task.CompletedDate.IsZero() && task.Completed
}
// Complete sets Task.Completed to 'true' if the task was not already completed.
// Also sets Task.CompletedDate to time.Now()
func (task *Task) Complete() {
if !task.Completed {
task.Completed = true
task.CompletedDate = time.Now()
}
}
// Reopen sets Task.Completed to 'false' if the task was completed.
// Also resets Task.CompletedDate.
func (task *Task) Reopen() {
if task.Completed {
task.Completed = false
task.CompletedDate = time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC) // time.IsZero() value
}
} }
// IsOverdue returns true if due date is in the past. // IsOverdue returns true if due date is in the past.
@@ -116,9 +255,8 @@ func (task *Task) HasCompletedDate() bool {
func (task *Task) IsOverdue() bool { func (task *Task) IsOverdue() bool {
if task.HasDueDate() { if task.HasDueDate() {
return task.DueDate.Before(time.Now()) return task.DueDate.Before(time.Now())
} else {
return false
} }
return false
} }
// Due returns the duration passed since due date, or until due date from now. // Due returns the duration passed since due date, or until due date from now.
@@ -129,7 +267,6 @@ func (task *Task) IsOverdue() bool {
func (task *Task) Due() time.Duration { func (task *Task) Due() time.Duration {
if task.IsOverdue() { if task.IsOverdue() {
return time.Now().Sub(task.DueDate) return time.Now().Sub(task.DueDate)
} else {
return task.DueDate.Sub(time.Now())
} }
return task.DueDate.Sub(time.Now())
} }

View File

@@ -1,3 +1,7 @@
/* 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 package todotxt
import ( import (
@@ -9,6 +13,184 @@ var (
testInputTask = "testdata/task_todo.txt" testInputTask = "testdata/task_todo.txt"
) )
func TestNewTask(t *testing.T) {
task := NewTask()
testExpected = 0
testGot = task.Id
if testGot != testExpected {
t.Errorf("Expected new Task to have default Id [%d], but got [%d]", testExpected, testGot)
}
testExpected = ""
testGot = task.Original
if testGot != testExpected {
t.Errorf("Expected new Task to be empty, but got [%s]", testGot)
}
testExpected = ""
testGot = task.Todo
if testGot != testExpected {
t.Errorf("Expected new Task to be empty, but got [%s]", testGot)
}
testExpected = false
testGot = task.HasPriority()
if testGot != testExpected {
t.Errorf("Expected new Task to have no priority, but got [%v]", testGot)
}
testExpected = 0
testGot = len(task.Projects)
if testGot != testExpected {
t.Errorf("Expected new Task to have %d projects, but got [%d]", testExpected, testGot)
}
testExpected = 0
testGot = len(task.Contexts)
if testGot != testExpected {
t.Errorf("Expected new Task to have %d contexts, but got [%d]", testExpected, testGot)
}
testExpected = 0
testGot = len(task.AdditionalTags)
if testGot != testExpected {
t.Errorf("Expected new Task to have %d additional tags, but got [%d]", testExpected, testGot)
}
testExpected = true
testGot = task.HasCreatedDate()
if testGot != testExpected {
t.Errorf("Expected new Task to have a created date, but got [%v]", testGot)
}
testExpected = false
testGot = task.HasCompletedDate()
if testGot != testExpected {
t.Errorf("Expected new Task to not have a completed date, but got [%v]", testGot)
}
testExpected = false
testGot = task.HasDueDate()
if testGot != testExpected {
t.Errorf("Expected new Task to not have a due date, but got [%v]", testGot)
}
testExpected = false
testGot = task.Completed
if testGot != testExpected {
t.Errorf("Expected new Task to not be completed, but got [%v]", testGot)
}
}
func TestParseTask(t *testing.T) {
task, err := ParseTask("x (C) 2014-01-01 @Go due:2014-01-12 Create golang library documentation +go-todotxt ")
if err != nil {
t.Error(err)
}
testExpected = "x (C) 2014-01-01 Create golang library documentation @Go +go-todotxt due:2014-01-12"
testGot = task.Task()
if testGot != testExpected {
t.Errorf("Expected Task to be [%s], but got [%s]", testExpected, testGot)
}
testExpected = 0
testGot = task.Id
if testGot != testExpected {
t.Errorf("Expected Task to have default Id [%d], but got [%d]", testExpected, testGot)
}
testExpected = "x (C) 2014-01-01 @Go due:2014-01-12 Create golang library documentation +go-todotxt"
testGot = task.Original
if testGot != testExpected {
t.Errorf("Expected Task to be [%s], but got [%s]", testExpected, testGot)
}
testExpected = "Create golang library documentation"
testGot = task.Todo
if testGot != testExpected {
t.Errorf("Expected Task to be [%s], but got [%s]", testExpected, testGot)
}
testExpected = true
testGot = task.HasPriority()
if testGot != testExpected {
t.Errorf("Expected Task to have no priority, but got [%v]", testGot)
}
testExpected = "C"
testGot = task.Priority
if testGot != testExpected {
t.Errorf("Expected Task to have priority [%v], but got [%v]", testExpected, testGot)
}
testExpected = 1
testGot = len(task.Projects)
if testGot != testExpected {
t.Errorf("Expected Task to have %d projects, but got [%d]", testExpected, testGot)
}
testExpected = 1
testGot = len(task.Contexts)
if testGot != testExpected {
t.Errorf("Expected Task to have %d contexts, but got [%d]", testExpected, testGot)
}
testExpected = 0
testGot = len(task.AdditionalTags)
if testGot != testExpected {
t.Errorf("Expected Task to have %d additional tags, but got [%d]", testExpected, testGot)
}
testExpected = true
testGot = task.HasCreatedDate()
if testGot != testExpected {
t.Errorf("Expected Task to have a created date, but got [%v]", testGot)
}
testExpected = false
testGot = task.HasCompletedDate()
if testGot != testExpected {
t.Errorf("Expected Task to not have a completed date, but got [%v]", testGot)
}
testExpected = true
testGot = task.HasDueDate()
if testGot != testExpected {
t.Errorf("Expected Task to have a due date, but got [%v]", testGot)
}
testExpected = true
testGot = task.Completed
if testGot != testExpected {
t.Errorf("Expected Task to be completed, but got [%v]", testGot)
}
}
func TestTaskId(t *testing.T) {
testTasklist.LoadFromFilename(testInputTask)
taskId := 1
testGot = testTasklist[taskId-1].Id
if testGot != taskId {
t.Errorf("Expected Task[%d] to have Id [%d], but got [%d]", taskId, taskId, testGot)
}
taskId = 5
testGot = testTasklist[taskId-1].Id
if testGot != taskId {
t.Errorf("Expected Task[%d] to have Id [%d], but got [%d]", taskId, taskId, testGot)
}
taskId = 27
testGot = testTasklist[taskId-1].Id
if testGot != taskId {
t.Errorf("Expected Task[%d] to have Id [%d], but got [%d]", taskId, taskId, testGot)
}
taskId++
}
func TestTaskString(t *testing.T) { func TestTaskString(t *testing.T) {
testTasklist.LoadFromFilename(testInputTask) testTasklist.LoadFromFilename(testInputTask)
taskId := 1 taskId := 1
@@ -263,21 +445,21 @@ func TestTaskCompleted(t *testing.T) {
testExpected = true testExpected = true
testGot = testTasklist[taskId-1].Completed testGot = testTasklist[taskId-1].Completed
if testGot != testExpected { if testGot != testExpected {
t.Errorf("Expected Task[%d] not to be completed, but got '%v'", taskId, testGot) t.Errorf("Expected Task[%d] to be completed, but got '%v'", taskId, testGot)
} }
taskId++ taskId++
testExpected = true testExpected = true
testGot = testTasklist[taskId-1].Completed testGot = testTasklist[taskId-1].Completed
if testGot != testExpected { if testGot != testExpected {
t.Errorf("Expected Task[%d] not to be completed, but got '%v'", taskId, testGot) t.Errorf("Expected Task[%d] to be completed, but got '%v'", taskId, testGot)
} }
taskId++ taskId++
testExpected = true testExpected = true
testGot = testTasklist[taskId-1].Completed testGot = testTasklist[taskId-1].Completed
if testGot != testExpected { if testGot != testExpected {
t.Errorf("Expected Task[%d] not to be completed, but got '%v'", taskId, testGot) t.Errorf("Expected Task[%d] to be completed, but got '%v'", taskId, testGot)
} }
taskId++ taskId++
@@ -379,6 +561,145 @@ func TestTaskIsOverdue(t *testing.T) {
t.Errorf("Expected Task[%d] to be due since 72 hours, but got '%v'", taskId, testGot) t.Errorf("Expected Task[%d] to be due since 72 hours, but got '%v'", taskId, testGot)
} }
taskId++ taskId++
testGot = testTasklist[taskId-1].IsOverdue()
if testGot.(bool) {
t.Errorf("Expected Task[%d] not to be overdue, but got '%v'", taskId, testGot)
}
taskId++
}
func TestTaskComplete(t *testing.T) {
testTasklist.LoadFromFilename(testInputTask)
taskId := 44
// first 4 tasks should all match the same tests
for i := 0; i < 4; i++ {
testExpected = false
testGot = testTasklist[taskId-1].Completed
if testGot != testExpected {
t.Errorf("Expected Task[%d] not to be completed, but got '%v'", taskId, testGot)
}
testGot = testTasklist[taskId-1].HasCompletedDate()
if testGot != testExpected {
t.Errorf("Expected Task[%d] not to have a completed date, but got '%v'", taskId, testGot)
}
testTasklist[taskId-1].Complete()
testExpected = true
testGot = testTasklist[taskId-1].Completed
if testGot != testExpected {
t.Errorf("Expected Task[%d] to be completed, but got '%v'", taskId, testGot)
}
testGot = testTasklist[taskId-1].HasCompletedDate()
if testGot != testExpected {
t.Errorf("Expected Task[%d] to have a completed date, but got '%v'", taskId, testGot)
}
testExpected = time.Now().Format(DateLayout)
testGot = testTasklist[taskId-1].CompletedDate.Format(DateLayout)
if testGot != testExpected {
t.Errorf("Expected Task[%d] to have a completed date of '%v', but got '%v'", taskId, testExpected, testGot)
}
taskId++
}
testExpected = true
testGot = testTasklist[taskId-1].Completed
if testGot != testExpected {
t.Errorf("Expected Task[%d] to be completed, but got '%v'", taskId, testGot)
}
testGot = testTasklist[taskId-1].HasCompletedDate()
if testGot != testExpected {
t.Errorf("Expected Task[%d] to have a completed date, but got '%v'", taskId, testGot)
}
testTasklist[taskId-1].Complete()
testGot = testTasklist[taskId-1].Completed // should be unchanged
if testGot != testExpected {
t.Errorf("Expected Task[%d] to be completed, but got '%v'", taskId, testGot)
}
testGot = testTasklist[taskId-1].HasCompletedDate() // should be unchanged
if testGot != testExpected {
t.Errorf("Expected Task[%d] to have a completed date, but got '%v'", taskId, testGot)
}
testExpected = "2012-01-01" // should be unchanged
testGot = testTasklist[taskId-1].CompletedDate.Format(DateLayout)
if testGot != testExpected {
t.Errorf("Expected Task[%d] to have a completed date of '%v', but got '%v'", taskId, testExpected, testGot)
}
taskId++
}
func TestTaskReopen(t *testing.T) {
testTasklist.LoadFromFilename(testInputTask)
taskId := 49
// the first 2 tasks should match the same tests
for i := 0; i < 2; i++ {
testExpected = true
testGot = testTasklist[taskId-1].Completed
if testGot != testExpected {
t.Errorf("Expected Task[%d] to be completed, but got '%v'", taskId, testGot)
}
testExpected = false
testGot = testTasklist[taskId-1].HasCompletedDate()
if testGot != testExpected {
t.Errorf("Expected Task[%d] to have a completed date, but got '%v'", taskId, testGot)
}
testTasklist[taskId-1].Reopen()
testExpected = false
testGot = testTasklist[taskId-1].Completed
if testGot != testExpected {
t.Errorf("Expected Task[%d] to not be completed, but got '%v'", taskId, testGot)
}
testGot = testTasklist[taskId-1].HasCompletedDate()
if testGot != testExpected {
t.Errorf("Expected Task[%d] to not have a completed date, but got '%v'", taskId, testGot)
}
taskId++
}
// the next 3 tasks should all match the same tests
for i := 0; i < 3; i++ {
testExpected = true
testGot = testTasklist[taskId-1].Completed
if testGot != testExpected {
t.Errorf("Expected Task[%d] to be completed, but got '%v'", taskId, testGot)
}
testGot = testTasklist[taskId-1].HasCompletedDate()
if testGot != testExpected {
t.Errorf("Expected Task[%d] to have a completed date, but got '%v'", taskId, testGot)
}
testTasklist[taskId-1].Reopen()
testExpected = false
testGot = testTasklist[taskId-1].Completed
if testGot != testExpected {
t.Errorf("Expected Task[%d] to not be completed, but got '%v'", taskId, testGot)
}
testGot = testTasklist[taskId-1].HasCompletedDate()
if testGot != testExpected {
t.Errorf("Expected Task[%d] to not have a completed date, but got '%v'", taskId, testGot)
}
taskId++
}
testExpected = false
testGot = testTasklist[taskId-1].Completed
if testGot != testExpected {
t.Errorf("Expected Task[%d] to be completed, but got '%v'", taskId, testGot)
}
testGot = testTasklist[taskId-1].HasCompletedDate()
if testGot != testExpected {
t.Errorf("Expected Task[%d] to have a completed date, but got '%v'", taskId, testGot)
}
testTasklist[taskId-1].Reopen()
testGot = testTasklist[taskId-1].Completed // should be unchanged
if testGot != testExpected {
t.Errorf("Expected Task[%d] to be completed, but got '%v'", taskId, testGot)
}
testGot = testTasklist[taskId-1].HasCompletedDate() // should be unchanged
if testGot != testExpected {
t.Errorf("Expected Task[%d] to have a completed date, but got '%v'", taskId, testGot)
}
taskId++
} }
func compareSlices(list1 []string, list2 []string) bool { func compareSlices(list1 []string, list2 []string) bool {

View File

@@ -59,3 +59,20 @@ x 2014-01-03 2014-01-01 Create some more golang library test cases @Go +go-todot
x 2014-01-04 (B) 2013-12-30 Create golang library @Go +go-todotxt due:2014-01-02 x 2014-01-04 (B) 2013-12-30 Create golang library @Go +go-todotxt due:2014-01-02
(B) 2013-12-01 private:false Outline chapter 5 +Novel @Computer due:2017-07-17 Level:5 (B) 2013-12-01 private:false Outline chapter 5 +Novel @Computer due:2017-07-17 Level:5
Research self-publishing services +Novel +Novel +Novel due:2014-01-01 Research self-publishing services +Novel +Novel +Novel due:2014-01-01
x 2014-01-03 2014-01-01 Create some more golang library test cases @Go +go-todotxt
# Complete test cases
Create golang library @Go +go-todotxt due:2014-01-05
(A) @Phone Call Mom @Call +Family
2014-01-03 Create golang library @Go +go-todotxt due:2014-01-05
(B) 2013-12-30 Create golang library test cases @Go +go-todotxt
x 2012-01-01 More Go!
# Reopen test cases
x Download Todo.txt mobile app @Phone
x (C) 2014-01-01 Create golang library documentation @Go +go-todotxt due:2014-01-12
x 2014-01-03 Create golang library @Go +go-todotxt due:2014-01-05
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
(B) 2013-12-30 Create golang library test cases @Go +go-todotxt

View File

@@ -0,0 +1,6 @@
x (C) 2014-01-01 Create golang library documentation @Go +go-todotxt due:2014-01-12
x Download Todo.txt mobile app @Phone
x 2014-01-03 Create golang library @Go +go-todotxt due:2014-01-05
x (C) 2014-01-01 Create golang library documentation @Go +go-todotxt due:2014-01-12
x 2014-01-02 (B) 2013-12-30 Create golang library test cases @Go +go-todotxt
x 2014-25-04 2014-01-01 Create some more golang library test cases @Go +go-todotxt

View File

@@ -0,0 +1,5 @@
2013-02-22 Pick up milk @GroceryStore
x Download Todo.txt mobile app @Phone
(B) 2013-13-01 private:false Outline chapter 5 +Novel @Computer Level:5 due:2014-02-17
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

5
testdata/tasklist_dueDate_error.txt vendored Normal file
View File

@@ -0,0 +1,5 @@
B) 2013-12-01 private:false Outline chapter 5 +Novel @Computer Level:5 due:2014-02-17
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
(B) 2013-12-01 private:false Outline chapter 5 +Novel @Computer Level:5 due:2014-02-32
x (C) 2014-01-01 Create golang library documentation @Go +go-todotxt due:2014-01-12

2
testdata/tasklist_scanner_error.txt vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -10,39 +10,32 @@ package todotxt
import ( import (
"bufio" "bufio"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"regexp"
"sort"
"strings" "strings"
"time"
) )
// TaskList represents a list of todo.txt task entries. // TaskList represents a list of todo.txt task entries.
// It is usually loaded from a whole todo.txt file. // It is usually loaded from a whole todo.txt file.
type TaskList []Task type TaskList []Task
// IgnoreComments can be set to 'false', in order to revert to more standard todo.txt behaviour. // IgnoreComments can be set to 'false', in order to revert to a more standard todo.txt behaviour.
// The todo.txt format does not define comments. // The todo.txt format does not define comments.
var ( var (
// Used for formatting time.Time into todo.txt date format and vice-versa. // IgnoreComments is used to switch ignoring of comments (lines starting with "#").
DateLayout = "2006-01-02" // If this is set to 'false', then lines starting with "#" will be parsed as tasks.
// Ignores comments (Lines/Text starting with "#").
IgnoreComments = true IgnoreComments = true
// unexported vars
priorityRx = regexp.MustCompile(`^(x|x \d{4}-\d{2}-\d{2}|)\s*\(([A-Z])\)\s+`) // Match priority: '(A) ...' or 'x (A) ...' or 'x 2012-12-12 (A) ...'
// Match created date: '(A) 2012-12-12 ...' or 'x 2012-12-12 (A) 2012-12-12 ...' or 'x (A) 2012-12-12 ...'or 'x 2012-12-12 2012-12-12 ...' or '2012-12-12 ...'
createdDateRx = regexp.MustCompile(`^(\([A-Z]\)|x \d{4}-\d{2}-\d{2} \([A-Z]\)|x \([A-Z]\)|x \d{4}-\d{2}-\d{2}|)\s*(\d{4}-\d{2}-\d{2})\s+`)
completedRx = regexp.MustCompile(`^x\s+`) // Match completed: 'x ...'
completedDateRx = regexp.MustCompile(`^x\s*(\d{4}-\d{2}-\d{2})\s+`) // Match completed date: 'x 2012-12-12 ...'
addonTagRx = regexp.MustCompile(`(^|\s+)([\w-]+):(\S+)`) // Match additional tags date: '... due:2012-12-12 ...'
contextRx = regexp.MustCompile(`(^|\s+)@(\S+)`) // Match contexts: '@Context ...' or '... @Context ...'
projectRx = regexp.MustCompile(`(^|\s+)\+(\S+)`) // Match projects: '+Project...' or '... +Project ...'
) )
// String returns a complete tasklist string in todo.txt format. // NewTaskList creates a new empty TaskList.
func NewTaskList() TaskList {
tasklist := TaskList{}
return tasklist
}
// String returns a complete list of tasks in todo.txt format.
func (tasklist TaskList) String() (text string) { func (tasklist TaskList) String() (text string) {
for _, task := range tasklist { for _, task := range tasklist {
text += fmt.Sprintf("%s\n", task.String()) text += fmt.Sprintf("%s\n", task.String())
@@ -50,6 +43,84 @@ func (tasklist TaskList) String() (text string) {
return text return text
} }
// AddTask appends a Task to the current TaskList and takes care to set the Task.Id correctly, modifying the Task by the given pointer!
func (tasklist *TaskList) AddTask(task *Task) {
task.Id = 0
for _, t := range *tasklist {
if t.Id > task.Id {
task.Id = t.Id
}
}
task.Id += 1
*tasklist = append(*tasklist, *task)
}
// GetTask returns a Task by given task 'id' from the TaskList. The returned Task pointer can be used to update the Task inside the TaskList.
// Returns an error if Task could not be found.
func (tasklist *TaskList) GetTask(id int) (*Task, error) {
for i := range *tasklist {
if ([]Task(*tasklist))[i].Id == id {
return &([]Task(*tasklist))[i], nil
}
}
return nil, errors.New("task not found")
}
// RemoveTaskById removes any Task with given Task 'id' from the TaskList.
// Returns an error if no Task was removed.
func (tasklist *TaskList) RemoveTaskById(id int) error {
var newList TaskList
found := false
for _, t := range *tasklist {
if t.Id != id {
newList = append(newList, t)
} else {
found = true
}
}
if !found {
return errors.New("task not found")
}
*tasklist = newList
return nil
}
// RemoveTask removes any Task from the TaskList with the same String representation as the given Task.
// Returns an error if no Task was removed.
func (tasklist *TaskList) RemoveTask(task Task) error {
var newList TaskList
found := false
for _, t := range *tasklist {
if t.String() != task.String() {
newList = append(newList, t)
} else {
found = true
}
}
if !found {
return errors.New("task not found")
}
*tasklist = newList
return nil
}
// Filter filters the current TaskList for the given predicate (a function that takes a task as input and returns a bool),
// and returns a new TaskList. The original TaskList is not modified.
func (tasklist *TaskList) Filter(predicate func(Task) bool) *TaskList {
var newList TaskList
for _, t := range *tasklist {
if predicate(t) {
newList = append(newList, t)
}
}
return &newList
}
// LoadFromFile loads a TaskList from *os.File. // LoadFromFile loads a TaskList from *os.File.
// //
// Using *os.File instead of a filename allows to also use os.Stdin. // Using *os.File instead of a filename allows to also use os.Stdin.
@@ -58,102 +129,24 @@ func (tasklist TaskList) String() (text string) {
func (tasklist *TaskList) LoadFromFile(file *os.File) error { func (tasklist *TaskList) LoadFromFile(file *os.File) error {
*tasklist = []Task{} // Empty tasklist *tasklist = []Task{} // Empty tasklist
taskId := 1
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
task := Task{} text := strings.Trim(scanner.Text(), "\t\n\r ") // Read line
task.Original = strings.Trim(scanner.Text(), "\t\n\r ") // Read line
task.Todo = task.Original
// Ignore blank or comment lines // Ignore blank or comment lines
if task.Todo == "" || (IgnoreComments && strings.HasPrefix(task.Todo, "#")) { if text == "" || (IgnoreComments && strings.HasPrefix(text, "#")) {
continue continue
} }
// Check for completed task, err := ParseTask(text)
if completedRx.MatchString(task.Original) { if err != nil {
task.Completed = true return err
// Check for completed date
if completedDateRx.MatchString(task.Original) {
if date, err := time.Parse(DateLayout, completedDateRx.FindStringSubmatch(task.Original)[1]); err != nil {
return err
} else {
task.CompletedDate = date
}
}
// Remove from Todo text
task.Todo = completedDateRx.ReplaceAllString(task.Todo, "") // Strip CompletedDate first, otherwise it wouldn't match anymore (^x date...)
task.Todo = completedRx.ReplaceAllString(task.Todo, "") // Strip 'x '
} }
task.Id = taskId
// Check for priority *tasklist = append(*tasklist, *task)
if priorityRx.MatchString(task.Original) { taskId++
task.Priority = priorityRx.FindStringSubmatch(task.Original)[2]
task.Todo = priorityRx.ReplaceAllString(task.Todo, "") // Remove from Todo text
}
// Check for created date
if createdDateRx.MatchString(task.Original) {
if date, err := time.Parse(DateLayout, createdDateRx.FindStringSubmatch(task.Original)[2]); err != nil {
return err
} else {
task.CreatedDate = date
task.Todo = createdDateRx.ReplaceAllString(task.Todo, "") // Remove from Todo text
}
}
// function for collecting projects/contexts as slices from text
getSlice := func(rx *regexp.Regexp) []string {
matches := rx.FindAllStringSubmatch(task.Original, -1)
slice := make([]string, 0, len(matches))
seen := make(map[string]bool, len(matches))
for _, match := range matches {
word := strings.Trim(match[2], "\t\n\r ")
if _, found := seen[word]; !found {
slice = append(slice, word)
seen[word] = true
}
}
sort.Strings(slice)
return slice
}
// Check for contexts
if contextRx.MatchString(task.Original) {
task.Contexts = getSlice(contextRx)
task.Todo = contextRx.ReplaceAllString(task.Todo, "") // Remove from Todo text
}
// Check for projects
if projectRx.MatchString(task.Original) {
task.Projects = getSlice(projectRx)
task.Todo = projectRx.ReplaceAllString(task.Todo, "") // Remove from Todo text
}
// Check for additional tags
if addonTagRx.MatchString(task.Original) {
matches := addonTagRx.FindAllStringSubmatch(task.Original, -1)
tags := make(map[string]string, len(matches))
for _, match := range matches {
key, value := match[2], match[3]
if key == "due" { // due date is a known addon tag, it has its own struct field
if date, err := time.Parse(DateLayout, value); err != nil {
return err
} else {
task.DueDate = date
}
} else if key != "" && value != "" {
tags[key] = value
}
}
task.AdditionalTags = tags
task.Todo = addonTagRx.ReplaceAllString(task.Todo, "") // Remove from Todo text
}
// Trim any remaining whitespaces from Todo text
task.Todo = strings.Trim(task.Todo, "\t\n\r\f ")
*tasklist = append(*tasklist, task)
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
return err return err
@@ -187,7 +180,7 @@ func (tasklist *TaskList) LoadFromFilename(filename string) error {
// WriteToFilename writes a TaskList to the specified file (most likely called "todo.txt"). // WriteToFilename writes a TaskList to the specified file (most likely called "todo.txt").
func (tasklist *TaskList) WriteToFilename(filename string) error { func (tasklist *TaskList) WriteToFilename(filename string) error {
return ioutil.WriteFile(filename, []byte(tasklist.String()), 0644) return ioutil.WriteFile(filename, []byte(tasklist.String()), 0640)
} }
// LoadFromFile loads and returns a TaskList from *os.File. // LoadFromFile loads and returns a TaskList from *os.File.
@@ -195,8 +188,10 @@ func (tasklist *TaskList) WriteToFilename(filename string) error {
// Using *os.File instead of a filename allows to also use os.Stdin. // Using *os.File instead of a filename allows to also use os.Stdin.
func LoadFromFile(file *os.File) (TaskList, error) { func LoadFromFile(file *os.File) (TaskList, error) {
tasklist := TaskList{} tasklist := TaskList{}
err := tasklist.LoadFromFile(file) if err := tasklist.LoadFromFile(file); err != nil {
return tasklist, err return nil, err
}
return tasklist, nil
} }
// WriteToFile writes a TaskList to *os.File. // WriteToFile writes a TaskList to *os.File.
@@ -209,8 +204,10 @@ func WriteToFile(tasklist *TaskList, file *os.File) error {
// LoadFromFilename loads and returns a TaskList from a file (most likely called "todo.txt"). // LoadFromFilename loads and returns a TaskList from a file (most likely called "todo.txt").
func LoadFromFilename(filename string) (TaskList, error) { func LoadFromFilename(filename string) (TaskList, error) {
tasklist := TaskList{} tasklist := TaskList{}
err := tasklist.LoadFromFilename(filename) if err := tasklist.LoadFromFilename(filename); err != nil {
return tasklist, err return nil, err
}
return tasklist, nil
} }
// WriteToFilename writes a TaskList to the specified file (most likely called "todo.txt"). // WriteToFilename writes a TaskList to the specified file (most likely called "todo.txt").

View File

@@ -8,15 +8,20 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"testing" "testing"
"time"
) )
var ( var (
testInputTasklist = "testdata/tasklist_todo.txt" testInputTasklist = "testdata/tasklist_todo.txt"
testOutput = "testdata/ouput_todo.txt" testInputTasklistCreatedDateError = "testdata/tasklist_createdDate_error.txt"
testExpectedOutput = "testdata/expected_todo.txt" testInputTasklistDueDateError = "testdata/tasklist_dueDate_error.txt"
testTasklist TaskList testInputTasklistCompletedDateError = "testdata/tasklist_completedDate_error.txt"
testExpected interface{} testInputTasklistScannerError = "testdata/tasklist_scanner_error.txt"
testGot interface{} testOutput = "testdata/ouput_todo.txt"
testExpectedOutput = "testdata/expected_todo.txt"
testTasklist TaskList
testExpected interface{}
testGot interface{}
) )
func TestLoadFromFile(t *testing.T) { func TestLoadFromFile(t *testing.T) {
@@ -33,12 +38,16 @@ func TestLoadFromFile(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
testExpected := string(data) testExpected = string(data)
testGot := testTasklist.String() testGot = testTasklist.String()
if testGot != testExpected { if testGot != testExpected {
t.Errorf("Expected TaskList to be [%s], but got [%s]", testExpected, testGot) t.Errorf("Expected TaskList to be [%s], but got [%s]", testExpected, testGot)
} }
} }
if testTasklist, err := LoadFromFile(nil); testTasklist != nil || err == nil {
t.Errorf("Expected LoadFromFile to fail, but got TaskList back: [%s]", testTasklist)
}
} }
func TestLoadFromFilename(t *testing.T) { func TestLoadFromFilename(t *testing.T) {
@@ -49,12 +58,16 @@ func TestLoadFromFilename(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
testExpected := string(data) testExpected = string(data)
testGot := testTasklist.String() testGot = testTasklist.String()
if testGot != testExpected { if testGot != testExpected {
t.Errorf("Expected TaskList to be [%s], but got [%s]", testExpected, testGot) t.Errorf("Expected TaskList to be [%s], but got [%s]", testExpected, testGot)
} }
} }
if testTasklist, err := LoadFromFilename("some_file_that_does_not_exists.txt"); testTasklist != nil || err == nil {
t.Errorf("Expected LoadFromFilename to fail, but got TaskList back: [%s]", testTasklist)
}
} }
func TestWriteFile(t *testing.T) { func TestWriteFile(t *testing.T) {
@@ -92,8 +105,8 @@ func TestWriteFile(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
testExpected := string(data) testExpected = string(data)
testGot := testTasklist.String() testGot = testTasklist.String()
if testGot != testExpected { if testGot != testExpected {
t.Errorf("Expected TaskList to be [%s], but got [%s]", testExpected, testGot) t.Errorf("Expected TaskList to be [%s], but got [%s]", testExpected, testGot)
} }
@@ -134,8 +147,8 @@ func TestTaskListWriteFile(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
testExpected := string(data) testExpected = string(data)
testGot := testTasklist.String() testGot = testTasklist.String()
if testGot != testExpected { if testGot != testExpected {
t.Errorf("Expected TaskList to be [%s], but got [%s]", testExpected, testGot) t.Errorf("Expected TaskList to be [%s], but got [%s]", testExpected, testGot)
} }
@@ -159,8 +172,8 @@ func TestWriteFilename(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
testExpected := string(data) testExpected = string(data)
testGot := testTasklist.String() testGot = testTasklist.String()
if testGot != testExpected { if testGot != testExpected {
t.Errorf("Expected TaskList to be [%s], but got [%s]", testExpected, testGot) t.Errorf("Expected TaskList to be [%s], but got [%s]", testExpected, testGot)
} }
@@ -184,19 +197,326 @@ func TestTaskListWriteFilename(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
testExpected := string(data) testExpected = string(data)
testGot := testTasklist.String() testGot = testTasklist.String()
if testGot != testExpected { if testGot != testExpected {
t.Errorf("Expected TaskList to be [%s], but got [%s]", testExpected, testGot) t.Errorf("Expected TaskList to be [%s], but got [%s]", testExpected, testGot)
} }
} }
func TestTaskListCount(t *testing.T) { func TestNewTaskList(t *testing.T) {
testTasklist.LoadFromFilename(testInputTasklist) testTasklist := NewTaskList()
testExpected := 63 testExpected = 0
testGot := len(testTasklist) testGot = len(testTasklist)
if testGot != testExpected { if testGot != testExpected {
t.Errorf("Expected TaskList to contain %d tasks, but got %d", testExpected, testGot) t.Errorf("Expected TaskList to contain %d tasks, but got %d", testExpected, testGot)
} }
} }
func TestTaskListCount(t *testing.T) {
if err := testTasklist.LoadFromFilename(testInputTasklist); err != nil {
t.Fatal(err)
}
testExpected = 63
testGot = len(testTasklist)
if testGot != testExpected {
t.Errorf("Expected TaskList to contain %d tasks, but got %d", testExpected, testGot)
}
}
func TestTaskListAddTask(t *testing.T) {
if err := testTasklist.LoadFromFilename(testInputTasklist); err != nil {
t.Fatal(err)
}
// add new empty task
task := NewTask()
testTasklist.AddTask(&task)
testExpected = 64
testGot = len(testTasklist)
if testGot != testExpected {
t.Errorf("Expected TaskList to contain %d tasks, but got %d", testExpected, testGot)
}
taskId := 64
testExpected = time.Now().Format(DateLayout) + " " // tasks created by NewTask() have their created date set
testGot = testTasklist[taskId-1].String()
if testGot != testExpected {
t.Errorf("Expected Task[%d] to be [%s], but got [%s]", taskId, testExpected, testGot)
}
testExpected = 64
testGot = testTasklist[taskId-1].Id
if testGot != testExpected {
t.Errorf("Expected Task[%d] to be [%d], but got [%d]", taskId, testExpected, testGot)
}
taskId++
// add parsed task
parsed, err := ParseTask("x (C) 2014-01-01 Create golang library documentation @Go +go-todotxt due:2014-01-12")
if err != nil {
t.Error(err)
}
testTasklist.AddTask(parsed)
testExpected = "x (C) 2014-01-01 Create golang library documentation @Go +go-todotxt due:2014-01-12"
testGot = testTasklist[taskId-1].String()
if testGot != testExpected {
t.Errorf("Expected Task[%d] to be [%s], but got [%s]", taskId, testExpected, testGot)
}
testExpected = 65
testGot = testTasklist[taskId-1].Id
if testGot != testExpected {
t.Errorf("Expected Task[%d] to be [%d], but got [%d]", taskId, testExpected, testGot)
}
taskId++
// add selfmade task
createdDate := time.Now()
testTasklist.AddTask(&Task{
CreatedDate: createdDate,
Todo: "Go shopping..",
Contexts: []string{"GroceryStore"},
})
testExpected = createdDate.Format(DateLayout) + " Go shopping.. @GroceryStore"
testGot = testTasklist[taskId-1].String()
if testGot != testExpected {
t.Errorf("Expected Task[%d] to be [%s], but got [%s]", taskId, testExpected, testGot)
}
testExpected = 66
testGot = testTasklist[taskId-1].Id
if testGot != testExpected {
t.Errorf("Expected Task[%d] to be [%d], but got [%d]", taskId, testExpected, testGot)
}
taskId++
// add task with explicit Id, AddTask() should ignore this!
testTasklist.AddTask(&Task{
Id: 101,
})
testExpected = 67
testGot = testTasklist[taskId-1].Id
if testGot != testExpected {
t.Errorf("Expected Task[%d] to be [%d], but got [%d]", taskId, testExpected, testGot)
}
taskId++
}
func TestTaskListGetTask(t *testing.T) {
if err := testTasklist.LoadFromFilename(testInputTasklist); err != nil {
t.Fatal(err)
}
taskId := 3
task, err := testTasklist.GetTask(taskId)
if err != nil {
t.Error(err)
}
testExpected = "(B) 2013-12-01 Outline chapter 5 @Computer +Novel Level:5 private:false due:2014-02-17"
testGot = task.String()
if testGot != testExpected {
t.Errorf("Expected Task[%d] to be [%s], but got [%s]", taskId, testExpected, testGot)
}
testExpected = 3
testGot = testTasklist[taskId-1].Id
if testGot != testExpected {
t.Errorf("Expected Task[%d] to be [%d], but got [%d]", taskId, testExpected, testGot)
}
taskId++
}
func TestTaskListUpdateTask(t *testing.T) {
if err := testTasklist.LoadFromFilename(testInputTasklist); err != nil {
t.Fatal(err)
}
taskId := 3
task, err := testTasklist.GetTask(taskId)
if err != nil {
t.Error(err)
}
testExpected = "(B) 2013-12-01 Outline chapter 5 @Computer +Novel Level:5 private:false due:2014-02-17"
testGot = task.String()
if testGot != testExpected {
t.Errorf("Expected Task[%d] to be [%s], but got [%s]", taskId, testExpected, testGot)
}
testExpected = 3
testGot = testTasklist[taskId-1].Id
if testGot != testExpected {
t.Errorf("Expected Task[%d] to be [%d], but got [%d]", taskId, testExpected, testGot)
}
task.Priority = "C"
task.Todo = "Go home!"
date, err := time.Parse(DateLayout, "2011-11-11")
if err != nil {
t.Error(err)
}
task.DueDate = date
testGot := task
os.Remove(testOutput)
if err := testTasklist.WriteToFilename(testOutput); err != nil {
t.Fatal(err)
}
if err := testTasklist.LoadFromFilename(testOutput); err != nil {
t.Fatal(err)
}
testExpected, err := testTasklist.GetTask(taskId)
if err != nil {
t.Error(err)
}
if testGot.Task() != testExpected.Task() {
t.Errorf("Expected Task to be [%v]\n, but got [%v]", testExpected, testGot)
}
}
func TestTaskListRemoveTaskById(t *testing.T) {
if err := testTasklist.LoadFromFilename(testInputTasklist); err != nil {
t.Fatal(err)
}
taskId := 10
if err := testTasklist.RemoveTaskById(taskId); err != nil {
t.Error(err)
}
testExpected = 62
testGot = len(testTasklist)
if testGot != testExpected {
t.Errorf("Expected TaskList to contain %d tasks, but got %d", testExpected, testGot)
}
task, err := testTasklist.GetTask(taskId)
if err == nil || task != nil {
t.Errorf("Expected no Task to be found anymore, but got %v", task)
}
taskId = 27
if err := testTasklist.RemoveTaskById(taskId); err != nil {
t.Error(err)
}
testExpected = 61
testGot = len(testTasklist)
if testGot != testExpected {
t.Errorf("Expected TaskList to contain %d tasks, but got %d", testExpected, testGot)
}
task, err = testTasklist.GetTask(taskId)
if err == nil || task != nil {
t.Errorf("Expected no Task to be found anymore, but got %v", task)
}
taskId = 99
if err := testTasklist.RemoveTaskById(taskId); err == nil {
t.Errorf("Expected no Task to be found for removal")
}
}
func TestTaskListRemoveTask(t *testing.T) {
if err := testTasklist.LoadFromFilename(testInputTasklist); err != nil {
t.Fatal(err)
}
taskId := 52 // Is "unique" in tasklist
task, err := testTasklist.GetTask(taskId)
if err != nil {
t.Error(err)
}
if err := testTasklist.RemoveTask(*task); err != nil {
t.Error(err)
}
testExpected = 62
testGot = len(testTasklist)
if testGot != testExpected {
t.Errorf("Expected TaskList to contain %d tasks, but got %d", testExpected, testGot)
}
task, err = testTasklist.GetTask(taskId)
if err == nil || task != nil {
t.Errorf("Expected no Task to be found anymore, but got %v", task)
}
taskId = 2 // Exists 3 times in tasklist
task, err = testTasklist.GetTask(taskId)
if err != nil {
t.Error(err)
}
if err := testTasklist.RemoveTask(*task); err != nil {
t.Error(err)
}
testExpected = 59
testGot = len(testTasklist)
if testGot != testExpected {
t.Errorf("Expected TaskList to contain %d tasks, but got %d", testExpected, testGot)
}
task, err = testTasklist.GetTask(taskId)
if err == nil || task != nil {
t.Errorf("Expected no Task to be found anymore, but got %v", task)
}
if err := testTasklist.RemoveTask(NewTask()); err == nil {
t.Errorf("Expected no Task to be found for removal")
}
}
func TestTaskListFilter(t *testing.T) {
if err := testTasklist.LoadFromFilename(testInputTasklist); err != nil {
t.Fatal(err)
}
// Filter list to get only completed tasks
completedList := testTasklist.Filter(func(t Task) bool { return t.Completed })
testExpected = 33
testGot = len(*completedList)
if testGot != testExpected {
t.Errorf("Expected TaskList to contain %d tasks, but got %d", testExpected, testGot)
}
// Filter list to get only tasks with a due date
dueDateList := testTasklist.Filter(func(t Task) bool { return t.HasDueDate() })
testExpected = 26
testGot = len(*dueDateList)
if testGot != testExpected {
t.Errorf("Expected TaskList to contain %d tasks, but got %d", testExpected, testGot)
}
// Filter list to get only tasks with "B" priority
prioBList := testTasklist.Filter(func(t Task) bool {
return t.HasPriority() && t.Priority == "B"
})
testExpected = 17
testGot = len(*prioBList)
if testGot != testExpected {
t.Errorf("Expected TaskList to contain %d tasks, but got %d", testExpected, testGot)
}
}
func TestTaskListReadErrors(t *testing.T) {
if testTasklist, err := LoadFromFilename(testInputTasklistCreatedDateError); testTasklist != nil || err == nil {
t.Errorf("Expected LoadFromFilename to fail because of invalid created date, but got TaskList back: [%s]", testTasklist)
} else if err.Error() != `parsing time "2013-13-01": month out of range` {
t.Error(err)
}
if testTasklist, err := LoadFromFilename(testInputTasklistDueDateError); testTasklist != nil || err == nil {
t.Errorf("Expected LoadFromFilename to fail because of invalid due date, but got TaskList back: [%s]", testTasklist)
} else if err.Error() != `parsing time "2014-02-32": day out of range` {
t.Error(err)
}
if testTasklist, err := LoadFromFilename(testInputTasklistCompletedDateError); testTasklist != nil || err == nil {
t.Errorf("Expected LoadFromFilename to fail because of invalid completed date, but got TaskList back: [%s]", testTasklist)
} else if err.Error() != `parsing time "2014-25-04": month out of range` {
t.Error(err)
}
// really silly test
if testTasklist, err := LoadFromFilename(testInputTasklistScannerError); testTasklist != nil || err == nil {
t.Errorf("Expected LoadFromFilename to fail because of invalid file, but got TaskList back: [%s]", testTasklist)
} else if err.Error() != `bufio.Scanner: token too long` {
t.Error(err)
}
}