Processing of todo.txt tasks
For the command-line interface, todoreport, see the README.md file.
(require file/todo-txt) | package: todo-txt |
1 Tasks
1.1 Creating tasks
struct
(struct task ( completed? priority completion-date creation-date description projects contexts tags) #:transparent) completed? : boolean? priority : string-field/c completion-date : string-field/c creation-date : string-field/c description : string? projects : (listof string?) contexts : (listof string?) tags : (listof (cons/c string? string?))
However, usually it will be easier to use the following helper function make-task.
procedure
(make-task [ #:completed? completed? #:priority priority #:completion-date completion-date #:creation-date creation-date #:description description #:projects projects #:contexts contexts #:tags tags]) → task? completed? : boolean? = #f priority : string-field/c = #f completion-date : string-field/c = #f creation-date : string-field/c = #f description : string? = "" projects : (listof string?) = '() contexts : (listof string?) = '() tags : (listof (cons/c string? string?)) = '()
Note that make-task prevents the creation of a task with a completion date, but no creation date. This is because it’s not possible to express such a task in the todo.txt string form, since a single date is supposed to be creation date, not a completion date. If it turns out that there are applications where you need to create the task with a completion date only and add the creation date later, before converting the task to a string, the restriction in make-task may be removed.
Tasks can also be parsed from strings:
procedure
(string->task line) → task?
line : string?
> (string->task "x 2021-04-02 Task @home +todo.txt due:2021-04-03 ")
(task
#t
#f
#f
"2021-04-02"
"Task @home +todo.txt due:2021-04-03"
'("todo.txt")
'("home")
'(("due" . "2021-04-03")))
Note that a single date is parsed as a creation date.
procedure
(string->tasks str) → (listof task?)
str : string?
> (string->tasks "x A completed task @home\n \nAn incomplete task @work")
(list
(task #t #f #f #f "A completed task @home" '() '("home") '())
(task #f #f #f #f "An incomplete task @work" '() '("work") '()))
1.2 Converting tasks to strings
procedure
(task->string task [ #:omit-fields omit-fields]) → string? task : task? omit-fields : (listof symbol?) = '()
There’s a similar restriction as in make-task: If you include 'creation-date in the omit-fields list, you must also include 'completion-date, because you can’t get a valid string representation for the task if you have a single date which is a completion date. Note that this restriction is also applied if the concrete task doesn’t have a completion date. The rationale is that if you call task->string on different tasks with the same omit-fields argument, all of the conversions should succeed or all the conversions should fail.
> (define task (make-task #:completed? #t #:creation-date "2021-04-02" #:description "Uninteresting task")) > (task->string task) "x 2021-04-02 Uninteresting task"
> (task->string task #:omit-fields '(creation-date completion-date)) "x Uninteresting task"
procedure
(tasks->string tasks [ #:omit-fields omit-fields]) → string? tasks : (listof task?) omit-fields : (listof symbol?) = '()
1.3 Sorting tasks
To sort a list of tasks, you must provide a list of task-sort-specs:
struct
(struct task-sort-spec (order field) #:transparent) order : (or/c 'asc 'desc) field : symbol?
The field argument is one of the symbols 'completed?, 'priority etc., which correspond to the task fields. Since tasks can have more than one context, project or tag, respectively, it doesn’t make sense to sort by context, for example. Therefore 'contexts etc. aren’t valid field values.
For a 'completed? field, ascending order means sorting incomplete tasks before completed tasks.
procedure
(sort-tasks tasks sort-specs) → (listof task?)
tasks : (listof task?) sort-specs : (listof task-sort-spec?)
Tasks that don’t contain the field to sort by are moved to the end of the sorted list – regardless of ascending or descending order –, keeping their original relative order. Note that tasks always have a 'completed? field, which is #f for incomplete tasks.
If the sort-specs list contains more than one task-sort-spec, the tasks are sorted according to the first task-sort-spec first, then sets of tasks that sorted equally, are sorted by the second task-sort-spec, and so on.
The sort is stable, so if tasks are sorted by a given field and have the same field value, the tasks remain in their previous relative order.
If sort-specs is an empty list, the original list of tasks is returned unchanged.
Here are a few examples.
> (define my-tasks (string->tasks (string-join '("x 2021-04-02 Early complete task" "2021-04-03 Late incomplete task" "2021-04-02 Early incomplete task" "x 2021-04-03 Late complete task") "\n"))) > my-tasks
(list
(task #t #f #f "2021-04-02" "Early complete task" '() '() '())
(task #f #f #f "2021-04-03" "Late incomplete task" '() '() '())
(task #f #f #f "2021-04-02" "Early incomplete task" '() '() '())
(task #t #f #f "2021-04-03" "Late complete task" '() '() '()))
> (displayln (tasks->string (sort-tasks my-tasks (list (task-sort-spec 'asc 'completed?)))))
2021-04-03 Late incomplete task
2021-04-02 Early incomplete task
x 2021-04-02 Early complete task
x 2021-04-03 Late complete task
> (displayln (tasks->string (sort-tasks my-tasks (list (task-sort-spec 'desc 'completed?)))))
x 2021-04-02 Early complete task
x 2021-04-03 Late complete task
2021-04-03 Late incomplete task
2021-04-02 Early incomplete task
> (displayln (tasks->string (sort-tasks my-tasks (list (task-sort-spec 'asc 'completed?) (task-sort-spec 'asc 'creation-date)))))
2021-04-02 Early incomplete task
2021-04-03 Late incomplete task
x 2021-04-02 Early complete task
x 2021-04-03 Late complete task
> (displayln (tasks->string (sort-tasks my-tasks (list (task-sort-spec 'asc 'creation-date) (task-sort-spec 'asc 'completed?)))))
2021-04-02 Early incomplete task
x 2021-04-02 Early complete task
2021-04-03 Late incomplete task
x 2021-04-03 Late complete task
Tasks without the field are always sorted last, regardless of ascending or descending sort order:
> (define my-tasks (string->tasks (string-join '("2021-04-02 Early task" "Task without creation date" "2021-04-03 Late task") "\n"))) > my-tasks
(list
(task #f #f #f "2021-04-02" "Early task" '() '() '())
(task #f #f #f #f "Task without creation date" '() '() '())
(task #f #f #f "2021-04-03" "Late task" '() '() '()))
> (displayln (tasks->string (sort-tasks my-tasks (list (task-sort-spec 'asc 'creation-date)))))
2021-04-02 Early task
2021-04-03 Late task
Task without creation date
> (displayln (tasks->string (sort-tasks my-tasks (list (task-sort-spec 'desc 'creation-date)))))
2021-04-03 Late task
2021-04-02 Early task
Task without creation date
2 Task groups
Tasks can’t only be sorted, but also be grouped. Both operations are similar, but not the same:
Sorting returns a flat list of tasks. On the other hand, grouping returns a nested structure, a list of task-groups, where each group contains a title and a list of tasks or task-groups.
A task can be included in more than one group. This happens when grouping by a task field that can occur several times in a single task, namely the fields 'contexts, 'projects and 'tags. For example, the task "Example task @work @phone", when grouping by contexts, will be included in two groups, one with the title "@phone" and one with the title "@work". There’s also a way to group by a given tag key.
2.1 Creating task-groups
struct
(struct task-group (title contents) #:transparent) title : string? contents : (or/c (listof task-group?) (listof task?))
2.2 Grouping tasks
In order to group tasks, we need task-group-spec structs:
struct
(struct task-group-spec task-sort-spec (tag-key) #:transparent) tag-key : (or/c string? #f)
The task-sort-spec field field can also contain 'contexts, 'projects and 'tags, which don’t make sense for sorting, only for grouping.
To group by tag values, specify the tag-key field as a string for the tag. For example, to group tasks by due date, use (task-group-spec 'asc 'tags "due"). When not grouping by a tag, set tag-key to #f.
With task-group-spec explained, we can group tasks:
procedure
(group-tasks tasks [group-specs sort-specs])
→ (or/c (listof task-group?) (listof task?)) tasks : (listof task?) group-specs : (listof task-group-spec?) = '() sort-specs : (listof task-sort-spec?) = '()
If sort-specs isn’t an empty list, all task lists inside the groups are sorted according to sort-specs. This argument behaves the same as for sort-tasks.
If there are tasks that don’t have a "natural" group (example: you group by 'contexts and a task doesn’t have any contexts), group-tasks appends a task-group with an appropriate name as the last group, say, "No contexts". This special group always comes at the end, regardless of whether the group order is specified as 'asc or 'desc.
> (define my-tasks (string->tasks (string-join '("Task @home" "Task without context" "Task @work") "\n"))) > my-tasks
(list
(task #f #f #f #f "Task @home" '() '("home") '())
(task #f #f #f #f "Task without context" '() '() '())
(task #f #f #f #f "Task @work" '() '("work") '()))
> (group-tasks my-tasks (list (task-group-spec 'asc 'contexts #f)))
(list
(task-group
"@home"
(list (task #f #f #f #f "Task @home" '() '("home") '())))
(task-group
"@work"
(list (task #f #f #f #f "Task @work" '() '("work") '())))
(task-group
"No context"
(list (task #f #f #f #f "Task without context" '() '() '()))))
Note that the group with the title "@home" is sorted alphabetically before the group with the title "@work", whereas the title "No contexts" appears last.
> (group-tasks my-tasks (list (task-group-spec 'desc 'contexts #f)))
(list
(task-group
"@work"
(list (task #f #f #f #f "Task @work" '() '("work") '())))
(task-group
"@home"
(list (task #f #f #f #f "Task @home" '() '("home") '())))
(task-group
"No context"
(list (task #f #f #f #f "Task without context" '() '() '()))))
Note that the order of the groups titled "@home" and "@work" is swapped now, but the group titled "No contexts" still appears last.
Note that 'completed? in a task-group-spec seems to behave differently. You can control whether non-completed tasks come before completed tasks or the other way around by specifying 'asc or 'desc group order, respectively. This is because every task has a completed? field, which has the value #f for non-completed tasks. Therefore, the group title "Not completed" is used for tasks with a completed? value of #f instead of indicating a “missing” completion status.
> (define my-tasks (string->tasks (string-join '("x Complete task" "Incomplete task") "\n"))) > my-tasks
(list
(task #t #f #f #f "Complete task" '() '() '())
(task #f #f #f #f "Incomplete task" '() '() '()))
> (group-tasks my-tasks (list (task-group-spec 'asc 'completed? #f)))
(list
(task-group
"Not completed"
(list (task #f #f #f #f "Incomplete task" '() '() '())))
(task-group
"Completed"
(list (task #t #f #f #f "Complete task" '() '() '()))))
> (group-tasks my-tasks (list (task-group-spec 'desc 'completed? #f)))
(list
(task-group
"Completed"
(list (task #t #f #f #f "Complete task" '() '() '())))
(task-group
"Not completed"
(list (task #f #f #f #f "Incomplete task" '() '() '()))))
Note how the order of the groups changes.
Nested grouping is supported. Each additional task-group-spec in the group-specs argument causes an additional level of task-groups.
> (define my-tasks (string->tasks (string-join '("x (B) Complete task with priority B" "(A) Incomplete task with priority A" "(C) Incomplete task with priority C" "(C) Other incomplete task with priority C" "x (A) Complete task with priority A") "\n"))) > my-tasks
(list
(task #t "B" #f #f "Complete task with priority B" '() '() '())
(task #f "A" #f #f "Incomplete task with priority A" '() '() '())
(task #f "C" #f #f "Incomplete task with priority C" '() '() '())
(task #f "C" #f #f "Other incomplete task with priority C" '() '() '())
(task #t "A" #f #f "Complete task with priority A" '() '() '()))
> (group-tasks my-tasks (list (task-group-spec 'asc 'completed? #f) (task-group-spec 'asc 'priority #f)))
(list
(task-group
"Not completed"
(list
(task-group
"A"
(list (task #f "A" #f #f "Incomplete task with priority A" '() '() '())))
(task-group
"C"
(list
(task #f "C" #f #f "Incomplete task with priority C" '() '() '())
(task #f "C" #f #f "Other incomplete task with priority C" '() '() '())))))
(task-group
"Completed"
(list
(task-group
"A"
(list (task #t "A" #f #f "Complete task with priority A" '() '() '())))
(task-group
"B"
(list (task #t "B" #f #f "Complete task with priority B" '() '() '()))))))
Similarly, it’s possible to specify an empty list for the group-specs argument. In this case, the result doesn’t contain any task-groups, that is, the behavior is the same as for sort-tasks.
2.3 Converting a group-tasks result to a string
procedure
(group-tasks-result->string group-tasks-result [ #:omit-fields omit-fields]) → string? group-tasks-result : (or/c (listof task-group?) (listof task?)) omit-fields : (listof symbol?) = '()
> (define my-tasks (string->tasks (string-join '("x (B) Complete task with priority B" "(A) Incomplete task with priority A" "(C) Incomplete task with priority C" "(C) Other incomplete task with priority C" "x (A) Complete task with priority A") "\n"))) > my-tasks
(list
(task #t "B" #f #f "Complete task with priority B" '() '() '())
(task #f "A" #f #f "Incomplete task with priority A" '() '() '())
(task #f "C" #f #f "Incomplete task with priority C" '() '() '())
(task #f "C" #f #f "Other incomplete task with priority C" '() '() '())
(task #t "A" #f #f "Complete task with priority A" '() '() '()))
> (define my-grouping-result (group-tasks my-tasks (list (task-group-spec 'asc 'completed? #f) (task-group-spec 'asc 'priority #f)))) > (displayln (group-tasks-result->string my-grouping-result))
Not completed
A
(A) Incomplete task with priority A
C
(C) Incomplete task with priority C
(C) Other incomplete task with priority C
Completed
A
x (A) Complete task with priority A
B
x (B) Complete task with priority B
> (displayln (group-tasks-result->string my-grouping-result #:omit-fields '(completed? priority)))
Not completed
A
Incomplete task with priority A
C
Incomplete task with priority C
Other incomplete task with priority C
Completed
A
Complete task with priority A
B
Complete task with priority B
3 Questions and answers
How can I change the order of completed and non-completed tasks?
To sort non-completed tasks before completed tasks, use a task-sort-spec with order 'asc. To sort completed tasks before non-completed tasks, use the order 'desc. The same applies to the group-specs argument when calling group-tasks.
How can I group by tag values?
In the group-specs, use a task-group-spec with the third field specifying the name of the tag. For example, use (task-group-spec 'asc 'tags "due") to group by ascending due date.
How can I sort tasks inside groups?
By default, the relative order of tasks in each group is the same as in the original task list passed to group-tasks. To influence the order of tasks inside groups, pass a sort-specs argument as the third argument to group-tasks.
(group-tasks my-tasks (list (task-group-spec 'asc 'priority #f)) (list (task-sort-spec 'asc 'description)))
Is the result of group-tasks always a list of task-groups?
Usually yes, but not always. If the group-specs argument for group-tasks is an empty list, there’s nothing to group by and the result is a list of tasks, not a list of task-groups.
Is it an error to pass an empty task list to sort-tasks or group-tasks?
Although passing an empty task list may come from a bug in your code, from the point of view of sort-tasks and group-tasks, it’s not an error to pass an empty task list. For an empty list, the result of either function will be an empty list.
Background: Sorting a list of tasks gives a list with the same tasks, but sorted. So sorting an empty task list returns an empty task list. Similarly, when grouping a list of tasks, groups without tasks are omitted. Therefore, grouping an empty task list results in only empty groups, and since they’re omitted, the overall result is an empty list.
Is it an error to pass an empty task list to tasks->string or group-tasks-result->string?
Similarly to the previous question, it’s not an error to pass an empty tasks list. If you do, the result will be an empty string.
Background: Converting a list of tasks to a string gives a string with as many lines as tasks, with one line per task. So a list with zero tasks is converted to a string with zero lines, i.e. an empty string. The same logic applies to grouping, considering that empty groups are removed from the output (see the previous question).