2014-01-03 20:27:11 +00:00
package todotxt
import (
"fmt"
2014-01-13 14:37:21 +00:00
"regexp"
2014-01-03 20:27:11 +00:00
"sort"
2014-01-13 14:37:21 +00:00
"strings"
2014-01-03 20:27:11 +00:00
"time"
)
2014-01-13 14:37:21 +00:00
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 ...')
)
2023-08-23 14:17:16 +00:00
// Todo represents a todo.txt task entry.
type Todo struct {
2023-08-23 16:10:03 +00:00
Id int ` json:"id" ` // Internal todo id.
Original string ` json:"original" ` // Original raw todo text.
Todo string ` json:"todo" ` // Todo part of todo text.
Priority string ` json:"priority" `
Projects [ ] string ` json:"projects" `
Contexts [ ] string ` json:"contexts" `
AdditionalTags map [ string ] string ` json:"additionalTags" ` // Addon tags will be available here.
CreatedDate time . Time ` json:"createdDate" `
DueDate time . Time ` json:"dueDate" `
CompletedDate time . Time ` json:"completedDate" `
Completed bool ` json:"completed" `
2014-01-03 20:27:11 +00:00
}
2023-08-23 14:17:16 +00:00
// String returns a complete todo string in todo.txt format.
2014-01-03 20:27:11 +00:00
//
// Contexts, Projects and additional tags are alphabetically sorted,
// and appendend at the end in the following order:
// Contexts, Projects, Tags
//
// For example:
2023-08-03 21:38:37 +00:00
//
// "(A) 2013-07-23 Call Dad @Home @Phone +Family due:2013-07-31 customTag1:Important!"
2023-08-23 14:17:16 +00:00
func ( todo Todo ) String ( ) string {
2014-01-03 20:27:11 +00:00
var text string
2023-08-23 14:17:16 +00:00
if todo . Completed {
2014-01-03 20:27:11 +00:00
text += "x "
2023-08-23 14:17:16 +00:00
if todo . HasCompletedDate ( ) {
text += fmt . Sprintf ( "%s " , todo . CompletedDate . Format ( DateLayout ) )
2014-01-03 20:27:11 +00:00
}
}
2023-08-23 14:17:16 +00:00
if todo . HasPriority ( ) {
text += fmt . Sprintf ( "(%s) " , todo . Priority )
2014-01-03 20:27:11 +00:00
}
2023-08-23 14:17:16 +00:00
if todo . HasCreatedDate ( ) {
text += fmt . Sprintf ( "%s " , todo . CreatedDate . Format ( DateLayout ) )
2014-01-03 20:27:11 +00:00
}
2023-08-23 14:17:16 +00:00
text += todo . Todo
2014-01-03 20:27:11 +00:00
2023-08-23 14:17:16 +00:00
if len ( todo . Contexts ) > 0 {
sort . Strings ( todo . Contexts )
for _ , context := range todo . Contexts {
2014-01-03 20:27:11 +00:00
text += fmt . Sprintf ( " @%s" , context )
}
}
2023-08-23 14:17:16 +00:00
if len ( todo . Projects ) > 0 {
sort . Strings ( todo . Projects )
for _ , project := range todo . Projects {
2014-01-03 20:27:11 +00:00
text += fmt . Sprintf ( " +%s" , project )
}
}
2023-08-23 14:17:16 +00:00
if len ( todo . AdditionalTags ) > 0 {
2014-01-03 20:27:11 +00:00
// Sort map alphabetically by keys
2023-08-23 14:17:16 +00:00
keys := make ( [ ] string , 0 , len ( todo . AdditionalTags ) )
for key := range todo . AdditionalTags {
2014-01-03 20:27:11 +00:00
keys = append ( keys , key )
}
sort . Strings ( keys )
for _ , key := range keys {
2023-08-23 14:17:16 +00:00
text += fmt . Sprintf ( " %s:%s" , key , todo . AdditionalTags [ key ] )
2014-01-03 20:27:11 +00:00
}
}
2023-08-23 14:17:16 +00:00
if todo . HasDueDate ( ) {
text += fmt . Sprintf ( " due:%s" , todo . DueDate . Format ( DateLayout ) )
2014-01-03 20:27:11 +00:00
}
return text
}
2023-08-23 14:17:16 +00:00
// NewTodo creates a new empty Todo with default values. (CreatedDate is set to Now())
func NewTodo ( ) Todo {
todo := Todo { }
todo . CreatedDate = time . Now ( )
return todo
2014-01-13 14:37:21 +00:00
}
2023-08-23 14:17:16 +00:00
// ParseTodo parses the input text string into a Todo struct.
func ParseTodo ( text string ) ( * Todo , error ) {
2014-01-13 14:37:21 +00:00
var err error
2023-08-23 14:17:16 +00:00
todo := Todo { }
todo . Original = strings . Trim ( text , "\t\n\r " )
todo . Todo = todo . Original
2014-01-13 14:37:21 +00:00
// Check for completed
2023-08-23 14:17:16 +00:00
if completedRx . MatchString ( todo . Original ) {
todo . Completed = true
2014-01-13 14:37:21 +00:00
// Check for completed date
2023-08-23 14:17:16 +00:00
if completedDateRx . MatchString ( todo . Original ) {
if date , err := time . Parse ( DateLayout , completedDateRx . FindStringSubmatch ( todo . Original ) [ 1 ] ) ; err == nil {
todo . CompletedDate = date
2014-01-13 14:37:21 +00:00
} else {
return nil , err
}
}
// Remove from Todo text
2023-08-23 14:17:16 +00:00
todo . Todo = completedDateRx . ReplaceAllString ( todo . Todo , "" ) // Strip CompletedDate first, otherwise it wouldn't match anymore (^x date...)
todo . Todo = completedRx . ReplaceAllString ( todo . Todo , "" ) // Strip 'x '
2014-01-13 14:37:21 +00:00
}
// Check for priority
2023-08-23 14:17:16 +00:00
if priorityRx . MatchString ( todo . Original ) {
todo . Priority = priorityRx . FindStringSubmatch ( todo . Original ) [ 2 ]
todo . Todo = priorityRx . ReplaceAllString ( todo . Todo , "" ) // Remove from Todo text
2014-01-13 14:37:21 +00:00
}
// Check for created date
2023-08-23 14:17:16 +00:00
if createdDateRx . MatchString ( todo . Original ) {
if date , err := time . Parse ( DateLayout , createdDateRx . FindStringSubmatch ( todo . Original ) [ 2 ] ) ; err == nil {
todo . CreatedDate = date
todo . Todo = createdDateRx . ReplaceAllString ( todo . Todo , "" ) // Remove from Todo text
2014-01-13 14:37:21 +00:00
} else {
return nil , err
}
}
// function for collecting projects/contexts as slices from text
getSlice := func ( rx * regexp . Regexp ) [ ] string {
2023-08-23 14:17:16 +00:00
matches := rx . FindAllStringSubmatch ( todo . Original , - 1 )
2014-01-13 14:37:21 +00:00
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
2023-08-23 14:17:16 +00:00
if contextRx . MatchString ( todo . Original ) {
todo . Contexts = getSlice ( contextRx )
todo . Todo = contextRx . ReplaceAllString ( todo . Todo , "" ) // Remove from Todo text
2014-01-13 14:37:21 +00:00
}
// Check for projects
2023-08-23 14:17:16 +00:00
if projectRx . MatchString ( todo . Original ) {
todo . Projects = getSlice ( projectRx )
todo . Todo = projectRx . ReplaceAllString ( todo . Todo , "" ) // Remove from Todo text
2014-01-13 14:37:21 +00:00
}
// Check for additional tags
2023-08-23 14:17:16 +00:00
if addonTagRx . MatchString ( todo . Original ) {
matches := addonTagRx . FindAllStringSubmatch ( todo . Original , - 1 )
2014-01-13 14:37:21 +00:00
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 {
2023-08-23 14:17:16 +00:00
todo . DueDate = date
2014-01-13 14:37:21 +00:00
} else {
return nil , err
}
} else if key != "" && value != "" {
tags [ key ] = value
}
}
2023-08-23 14:17:16 +00:00
todo . AdditionalTags = tags
todo . Todo = addonTagRx . ReplaceAllString ( todo . Todo , "" ) // Remove from Todo text
2014-01-13 14:37:21 +00:00
}
// Trim any remaining whitespaces from Todo text
2023-08-23 14:17:16 +00:00
todo . Todo = strings . Trim ( todo . Todo , "\t\n\r\f " )
2014-01-13 14:37:21 +00:00
2023-08-23 14:17:16 +00:00
return & todo , err
2014-01-03 20:27:11 +00:00
}
// HasPriority returns true if the task has a priority.
2023-08-23 14:17:16 +00:00
func ( todo * Todo ) HasPriority ( ) bool {
return todo . Priority != ""
2014-01-03 20:27:11 +00:00
}
// HasCreatedDate returns true if the task has a created date.
2023-08-23 14:17:16 +00:00
func ( todo * Todo ) HasCreatedDate ( ) bool {
return ! todo . CreatedDate . IsZero ( )
2014-01-03 20:27:11 +00:00
}
// HasDueDate returns true if the task has a due date.
2023-08-23 14:17:16 +00:00
func ( todo * Todo ) HasDueDate ( ) bool {
return ! todo . DueDate . IsZero ( )
2014-01-03 20:27:11 +00:00
}
// HasCompletedDate returns true if the task has a completed date.
2023-08-23 14:17:16 +00:00
func ( todo * Todo ) HasCompletedDate ( ) bool {
return ! todo . CompletedDate . IsZero ( ) && todo . Completed
2014-01-04 13:13:37 +00:00
}
// Complete sets Task.Completed to 'true' if the task was not already completed.
// Also sets Task.CompletedDate to time.Now()
2023-08-23 14:17:16 +00:00
func ( todo * Todo ) Complete ( ) {
if ! todo . Completed {
todo . Completed = true
todo . CompletedDate = time . Now ( )
2014-01-04 13:13:37 +00:00
}
}
// Reopen sets Task.Completed to 'false' if the task was completed.
// Also resets Task.CompletedDate.
2023-08-23 14:17:16 +00:00
func ( todo * Todo ) Reopen ( ) {
if todo . Completed {
todo . Completed = false
todo . CompletedDate = time . Date ( 1 , 1 , 1 , 0 , 0 , 0 , 0 , time . UTC ) // time.IsZero() value
2014-01-04 13:13:37 +00:00
}
2014-01-03 20:27:11 +00:00
}
2014-01-04 00:43:27 +00:00
// IsOverdue returns true if due date is in the past.
//
// This function does not take the Completed flag into consideration.
// You should check Task.Completed first if needed.
2023-08-23 14:17:16 +00:00
func ( todo * Todo ) IsOverdue ( ) bool {
if todo . HasDueDate ( ) {
return todo . DueDate . Before ( time . Now ( ) )
2014-01-04 00:43:27 +00:00
}
2014-01-04 13:13:37 +00:00
return false
2014-01-04 00:43:27 +00:00
}
// Due returns the duration passed since due date, or until due date from now.
// Check with IsOverdue() if the task is overdue or not.
//
// Just as with IsOverdue(), this function does also not take the Completed flag into consideration.
// You should check Task.Completed first if needed.
2023-08-23 14:17:16 +00:00
func ( todo * Todo ) Due ( ) time . Duration {
if todo . IsOverdue ( ) {
return time . Now ( ) . Sub ( todo . DueDate )
2014-01-04 00:43:27 +00:00
}
2023-08-23 14:17:16 +00:00
return todo . DueDate . Sub ( time . Now ( ) )
2014-01-04 00:43:27 +00:00
}
2023-08-03 21:38:37 +00:00
2023-08-23 14:17:16 +00:00
func ( todo * Todo ) HasContext ( ctx string ) bool {
for _ , c := range todo . Contexts {
2023-08-03 21:38:37 +00:00
if c == ctx {
return true
}
}
return false
}
2023-08-23 14:17:16 +00:00
func ( todo * Todo ) HasProject ( proj string ) bool {
for _ , p := range todo . Projects {
2023-08-03 21:38:37 +00:00
if p == proj {
return true
}
}
return false
}