diff --git a/README.md b/README.md index 661ec3f..c33ed9e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,26 @@ go-todotxt ========== -A Go todo.txt library (http://todotxt.com) +A Go todo.txt library. + +The Package todotxt 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. + +[![GoDoc](https://godoc.org/github.com/JamesClonk/go-todotxt?status.png)](https://godoc.org/github.com/JamesClonk/go-todotxt) + +## Installation + + $ go get github.com/JamesClonk/go-todotxt + +## Requirements + +go-todotxt requires Go1.1 or higher. + +## Documentation + +See [GoDoc - Documentation](https://godoc.org/github.com/JamesClonk/go-todotxt) for further documentation. + +## License + +The source files are distributed under the [Mozilla Public License, version 2.0](http://mozilla.org/MPL/2.0/), unless otherwise noted. +Please read the [FAQ](http://www.mozilla.org/MPL/2.0/FAQ.html) if you have further questions regarding the license. diff --git a/todo.txt b/todo.txt index 5648c8a..3411f96 100644 --- a/todo.txt +++ b/todo.txt @@ -1,8 +1,8 @@ (A) 2012-01-30 Call Mom @Phone +Family (A) Schedule annual checkup +Health -(B) Outline chapter 5 +Novel @Computer +(B) 2013-12-01 Outline chapter 5 +Novel @Computer due:2014-01-01 (C) Add cover sheets @Office +TPSReports Plan backyard herb garden @Home 2013-02-22 Pick up milk @GroceryStore -Research self-publishing services +Novel @Computer +Research self-publishing services +Novel @Computer due:2014-01-01 x Download Todo.txt mobile app @Phone diff --git a/todotxt.go b/todotxt.go index 095c89c..7a46692 100644 --- a/todotxt.go +++ b/todotxt.go @@ -2,18 +2,24 @@ * 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 is a Go client library for Gina Trapani's todo.txt files. +// It allows for parsing and manipulating of task lists and tasks in the todo.txt format. +// +// Source code and project home: https://github.com/JamesClonk/go-todotxt package todotxt import ( "bufio" + "fmt" "os" "regexp" "time" ) +// Task represents a todo.txt task entry. type Task struct { - Task string // Raw text - Todo string // Only actual todo part of text + Original string // Original raw task text + Todo string // Todo part of task text Priority string Projects []string Contexts []string @@ -23,52 +29,86 @@ type Task struct { Completed bool } +// TaskList represents a list of todo.txt task entries. +// It is usually loaded from a whole todo.txt file. type TaskList []Task var ( + // Used for formatting time.Time into todo.txt date format. + DateLayout = "2006-01-02" + + // unexported vars priorityRx = regexp.MustCompile(`^\(([A-Z])\)\s+`) // Match priority value: '(A) ...' createdDateRx = regexp.MustCompile(`^(\([A-Z]\)|)\s*([\d]{4}-[\d]{2}-[\d]{2})\s+`) // Match date value: '(A) 2012-12-12 ...' or '2012-12-12 ...' ) -// Return raw task text for String() +// String returns a complete task string in todo.txt format. +// +// For example: +// "(A) 2013-07-23 Call Dad @Phone +Family due:2013-07-31" func (task *Task) String() string { - return task.Task + var text string + if task.HasPriority() { + text += fmt.Sprintf("(%s) ", task.Priority) + } + if task.HasCreatedDate() { + text += fmt.Sprintf("%s ", task.CreatedDate.Format(DateLayout)) + } + text += task.Todo + if task.HasDueDate() { + text += fmt.Sprintf(" %s", task.DueDate.Format(DateLayout)) + } + return text } +// Task returns a complete task string in todo.txt format. +// The same as *Task.String(). +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() } -// Loading from *os.File allows to also use os.Stdin instead of just actual files +// LoadFromFile loads a TaskList from *os.File. +// +// Using *os.File instead of a filename allows to also use os.Stdin. +// +// Note: This will clear the current TaskList and overwrite it's contents with whatever is in *os.File. func (tasklist *TaskList) LoadFromFile(file *os.File) error { - *tasklist = []Task{} // Reset tasklist + *tasklist = []Task{} // Empty tasklist scanner := bufio.NewScanner(file) for scanner.Scan() { task := Task{} - task.Task = scanner.Text() + task.Original = scanner.Text() // Check for priority - if priorityRx.MatchString(task.Task) { - task.Priority = priorityRx.FindStringSubmatch(task.Task)[1] // First match is priority value + if priorityRx.MatchString(task.Original) { + task.Priority = priorityRx.FindStringSubmatch(task.Original)[1] // First match is priority value } // Check for created date - if createdDateRx.MatchString(task.Task) { + if createdDateRx.MatchString(task.Original) { // Second match is created date value - if date, err := time.Parse("2006-01-02", createdDateRx.FindStringSubmatch(task.Task)[2]); err != nil { + if date, err := time.Parse(DateLayout, createdDateRx.FindStringSubmatch(task.Original)[2]); err != nil { return err } else { task.CreatedDate = date @@ -84,7 +124,9 @@ func (tasklist *TaskList) LoadFromFile(file *os.File) error { return nil } -// Convenience method, since most of the time tasks will be loaded from an actual file, called "todo.txt" most likely ;) +// LoadFromFilename loads a TaskList from a file (most likely called "todo.txt"). +// +// Note: This will clear the current TaskList and overwrite it's contents with whatever is in the file. func (tasklist *TaskList) LoadFromFilename(filename string) error { file, err := os.Open(filename) if err != nil { @@ -94,3 +136,19 @@ func (tasklist *TaskList) LoadFromFilename(filename string) error { return tasklist.LoadFromFile(file) } + +// LoadFromFile loads and returns a TaskList from *os.File. +// +// Using *os.File instead of a filename allows to also use os.Stdin. +func LoadFromFile(file *os.File) (*TaskList, error) { + tasklist := &TaskList{} + err := tasklist.LoadFromFile(file) + return tasklist, err +} + +// LoadFromFilename loads and returns a TaskList from a file (most likely called "todo.txt"). +func LoadFromFilename(filename string) (*TaskList, error) { + tasklist := &TaskList{} + err := tasklist.LoadFromFilename(filename) + return tasklist, err +} diff --git a/todotxt_test.go b/todotxt_test.go index 500ffaa..3cf187d 100644 --- a/todotxt_test.go +++ b/todotxt_test.go @@ -17,31 +17,49 @@ func TestLoadFromFile(t *testing.T) { } defer file.Close() - var tasklist TaskList - if err := tasklist.LoadFromFile(file); err != nil { + if tasklist, err := LoadFromFile(file); err != nil { t.Fatal(err) + } else { + loadTest(t, *tasklist) } - loadTest(t, tasklist) } func TestLoadFromFilename(t *testing.T) { - var tasklist TaskList - if err := tasklist.LoadFromFilename("todo.txt"); err != nil { + if tasklist, err := LoadFromFilename("todo.txt"); err != nil { t.Fatal(err) + } else { + loadTest(t, *tasklist) } - loadTest(t, tasklist) } func loadTest(t *testing.T, tasklist TaskList) { var expected, got interface{} var err error + // ------------------------------------------------------------------------------------- + // count tasks expected = 8 got = len(tasklist) if got != expected { t.Errorf("Expected TaskList to contain %d tasks, but got %d", expected, got) } + // ------------------------------------------------------------------------------------- + // complete task strings + expected = "x Download Todo.txt mobile app @Phone" + got = tasklist[7].String() + if got != expected { + t.Errorf("Expected eight Task to be [%s], but got [%s]", expected, got) + } + + expected = "(B) 2013-12-01 Outline chapter 5 +Novel @Computer due:2014-01-01" + got = tasklist[2].Task() + if got != expected { + t.Errorf("Expected third Task to be [%s], but got [%s]", expected, got) + } + + // ------------------------------------------------------------------------------------- + // task priority expected = "B" got = tasklist[2].Priority if got != expected { @@ -52,7 +70,9 @@ func loadTest(t *testing.T, tasklist TaskList) { t.Errorf("Expected fifth task to have no priority, but got '%s'", tasklist[4].Priority) } - expected, err = time.Parse("2006-01-02", "2012-01-30") + // ------------------------------------------------------------------------------------- + // task created date + expected, err = time.Parse(DateLayout, "2012-01-30") if err != nil { t.Fatal(err) } @@ -61,7 +81,7 @@ func loadTest(t *testing.T, tasklist TaskList) { t.Errorf("Expected first task to have created date '%s', but got '%v'", expected, got) } - expected, err = time.Parse("2006-01-02", "2013-02-22") + expected, err = time.Parse(DateLayout, "2013-02-22") if err != nil { t.Fatal(err) } @@ -73,4 +93,19 @@ func loadTest(t *testing.T, tasklist TaskList) { if tasklist[4].HasCreatedDate() { t.Errorf("Expected fifth task to have no created date, but got '%v'", tasklist[4].CreatedDate) } + + // ------------------------------------------------------------------------------------- + // task due date + expected, err = time.Parse(DateLayout, "2014-01-01") + if err != nil { + t.Fatal(err) + } + got = tasklist[2].DueDate + if got != expected { + t.Errorf("Expected third task to have due date '%s', but got '%v'", expected, got) + } + + if tasklist[0].HasDueDate() { + t.Errorf("Expected first task to have no due date, but got '%v'", tasklist[0].DueDate) + } }