Processing of todo.txt tasks
1 Tasks
1.1 Creating tasks
task
make-task
string->task
string->tasks
1.2 Converting tasks to strings
task->string
tasks->string
1.3 Sorting tasks
sort-spec
sort-tasks
2 Task groups
2.1 Creating task-groups
task-group
2.2 Converting a task-group to a string
task-group->string
2.3 Grouping tasks
task-group-spec
group-tasks
8.2

Processing of todo.txt tasks

Stefan Schwarzer

This collection provides APIs to process one or more tasks from todo.txt-formatted files.

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 : (hash/c string? string?)
A todo.txt task, where string-field/c is (or/c string? #f).

However, usually it will be easier to use the 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 : (hash/c string? string?) = (hash)
which takes zero or more keyword arguments.

Tasks can also be parsed from strings:

procedure

(string->task line)  task?

  line : string?
Converts a single-line string to a task. Trailing whitespace is removed from the description.

Example:
> (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")

 '#hash(("due" . "2021-04-03")))

Note that a single date is parsed as a creation date.

procedure

(string->tasks str)  (listof task?)

  str : string?
Converts a multi-line string to a list of tasks. This function ignores lines that are empty or contain only whitespace.

Example:
> (string->tasks "x A completed task @home\n \nAn incomplete task @work")

(list

 (task #t #f #f #f "A completed task @home" '() '("home") '#hash())

 (task #f #f #f #f "An incomplete task @work" '() '("work") '#hash()))

1.2 Converting tasks to strings

procedure

(task->string task)  string?

  task : task?
Converts a single task to a single-line string.

Example:
> (task->string (make-task #:completed? #t
                           #:creation-date "2021-04-02"
                           #:description "Uninteresting task"))

"x 2021-04-02 Uninteresting task"

procedure

(tasks->string tasks)  string?

  tasks : (listof task?)
Converts a list of tasks to a (possibly multi-line) string.

1.3 Sorting tasks

To sort a list of tasks, you must provide a list of sort-specs:

struct

(struct sort-spec (order field)
    #:transparent)
  order : (or/c 'asc 'desc)
  field : symbol?
order is one of the symbols 'asc or 'desc to sort in ascending or descending order, respectively.

field 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 sort-spec?)
Sort tasks according to sort-specs and return a new list containing the sorted tasks.

Tasks that don’t have 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 sort-specs, the tasks are sorted according to the first sort-spec first, then groups of tasks that sorted equally, are sorted by the second 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.

First let's define a few tasks to sort:
> (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" '() '() '#hash())

 (task #f #f #f "2021-04-03" "Late incomplete task" '() '() '#hash())

 (task #f #f #f "2021-04-02" "Early incomplete task" '() '() '#hash())

 (task #t #f #f "2021-04-03" "Late complete task" '() '() '#hash()))

Sort the tasks by a single sort spec:
> (displayln
    (tasks->string
      (sort-tasks my-tasks (list (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

Use descending order to sort completed tasks first:
> (displayln
    (tasks->string
      (sort-tasks my-tasks (list (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

Sort by 'completed? , then 'creation-date :
> (displayln
    (tasks->string
      (sort-tasks my-tasks (list (sort-spec 'asc 'completed?)
                                 (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

Sort by 'creation-date , then 'completed? :
> (displayln
    (tasks->string
      (sort-tasks my-tasks (list (sort-spec 'asc 'creation-date)
                                 (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" '() '() '#hash())

 (task #f #f #f #f "Task without creation date" '() '() '#hash())

 (task #f #f #f "2021-04-03" "Late task" '() '() '#hash()))

> (displayln
    (tasks->string
      (sort-tasks my-tasks (list (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 (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 grouped. Both operations are similar, but not the same:

2.1 Creating task-groups

struct

(struct task-group (title tasks)
    #:transparent)
  title : (or/c string? #f)
  tasks : (listof task?)
A group of tasks. You shouldn’t create task-group instances yourself; they’re created by group-tasks.

2.2 Converting a task-group to a string

procedure

(task-group->string group)  string?

  group : task-group?
Convert a task-group to a string. If the title of the group is a string, the first line of the result string is the title. If the title field is #f, no title string and a following newline are included. After a possible title, the string contains an indented list of task strings.

Examples:
> (define work-group
    (task-group
      "@work"
      (string->tasks "Task 1 @work\nTask 2 @work")))
> (displayln (task-group->string work-group))

@work

  Task 1 @work

  Task 2 @work

> (define group-with-empty-title
    (task-group
      #f
      (string->tasks "Task 1\nTask 2")))
> (displayln (task-group->string group-with-empty-title))

  Task 1

  Task 2

2.3 Grouping tasks

In order to group tasks, we need task-group-spec structs:

struct

(struct task-group-spec sort-spec (tag-key)
    #:transparent)
  tag-key : string?
A struct to specify how a list of tasks should be grouped.

A task-group-spec is similar to a sort-spec, but with the following differences:
  • The 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    
  task-group-specs    
  [sort-specs])  (listof task-group?)
  tasks : (listof task?)
  task-group-specs : (listof task-group-spec?)
  sort-specs : (listof sort-spec?) = '()
Group tasks according to task-group-specs. At the moment, only the first task-group-spec of the list is used, so nested grouping isn’t possible, at least not yet.

If sort-specs isn’t an empty list, the tasks inside each group 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 dummy task-group with an appropriate name as the last group, say, "No contexts".

Examples:

Let’s define some tasks and a helper function to display all groups:

> (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" '() '() '#hash())

 (task #f #f #f "2021-04-03" "Late incomplete task" '() '() '#hash())

 (task #f #f #f "2021-04-02" "Early incomplete task" '() '() '#hash())

 (task #t #f #f "2021-04-03" "Late complete task" '() '() '#hash()))

> (define (display-groups task-groups)
    (for ([group task-groups])
      (displayln (task-group->string group))))

Group by ascending and descending creation date, respectively:
> (display-groups
    (group-tasks
      my-tasks
      (list (task-group-spec 'asc 'creation-date #f))))

2021-04-02

  x 2021-04-02 Early complete task

  2021-04-02 Early incomplete task

2021-04-03

  2021-04-03 Late incomplete task

  x 2021-04-03 Late complete task

> (display-groups
    (group-tasks
      my-tasks
      (list (task-group-spec 'desc 'creation-date #f))))

2021-04-03

  2021-04-03 Late incomplete task

  x 2021-04-03 Late complete task

2021-04-02

  x 2021-04-02 Early complete task

  2021-04-02 Early incomplete task

Combine grouping and sorting:
> (display-groups
    (group-tasks
      my-tasks
      (list (task-group-spec 'asc 'creation-date #f))
      (list (sort-spec 'asc 'completed?))))

2021-04-02

  2021-04-02 Early incomplete task

  x 2021-04-02 Early complete task

2021-04-03

  2021-04-03 Late incomplete task

  x 2021-04-03 Late complete task

Create an extra group if a task doesn't have the grouping field:
> (define my-tasks
    (string->tasks
      (string-join
        '("x 2021-04-02 Early complete task"
          "2021-04-03 Late incomplete task"
          "Incomplete task without creation date"
          "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" '() '() '#hash())

 (task #f #f #f "2021-04-03" "Late incomplete task" '() '() '#hash())

 (task #f #f #f #f "Incomplete task without creation date" '() '() '#hash())

 (task #f #f #f "2021-04-02" "Early incomplete task" '() '() '#hash())

 (task #t #f #f "2021-04-03" "Late complete task" '() '() '#hash()))

> (display-groups
    (group-tasks
      my-tasks
      (list (task-group-spec 'asc 'creation-date #f))
      (list (sort-spec 'asc 'completed?))))

2021-04-02

  2021-04-02 Early incomplete task

  x 2021-04-02 Early complete task

2021-04-03

  2021-04-03 Late incomplete task

  x 2021-04-03 Late complete task

No creation date

  Incomplete task without creation date