2

I have an org document with the following structure

* Tasks
** First category
- [X] Task

** Second Category
- [ ] Task

And I'd like to get statistics for both the overall completion and each subheading. If my tasks were TODO headings, I could get what I wanted by giving each heading a statistics cookie and setting the recursive property on the cookie, (or by setting org-hierarchical-todo-statistics to nil). That works as follows, and gives the expected result after updating all cookies:

* Headings and TODOs [1/2]
  :PROPERTIES:
  :COOKIE_DATA: todo recursive
  :END:
** First category [1/1] 
*** DONE Task 1
** Second Category [0/1]
*** TODO Task

If I try to do the same with the checkboxes, either by setting the recursive property on the cookie, or by looking at the variable org-checkbox-hierarchical-statistics, I get the following result

* Checkboxes [0/0]
  :PROPERTIES:
  :COOKIE_DATA: checkbox recursive
  :END:

** First category [1/2]
- [X] Task

** Second Category [0/1]
- [ ] Task

Where the first line says * Checkboxes [0/0] instead of the expected * Checkboxes [1/2]

I therefore have two questions:

  • Is this the intended behaviour, so that I'm misunderstanding how checkboxes and subheadings are supposed to interact? Does the "hierarchy" of hierarchical checkboxes only work in a single nested list and not across subheadings?

  • How do I get the behaviour I do want? As a last resort I know I can just convert my document entirely to subheadings, but I'd rather not have to do that

2
  • 1
    See if this answer helps.
    – NickD
    Sep 17, 2020 at 15:06
  • @NickD I guess it does, in that it indicates there's no built-in way of getting the behaviour I want. I'll see if I can put something together then.
    – Sten
    Sep 17, 2020 at 18:41

1 Answer 1

2

It seems I was right in my fear that there was no built-in way of getting the behaviour I wanted. I ended up designing the following solution:

  • Add a non-inherited keyword "aggregate" to the COOKIE_DATA property for the headings where I want special behaviour
  • Whenever a checkbox is altered, and org recalculates the statistics cookie, see if the property is set on the current headline
  • If it is, calculate the sum of all statistics cookies of the form [n/m] in the first level of children
  • Replace the statistics cookie of the current headline if it's present
  • Recurse up the tree and do the same thing.

I know this is a fairly brittle solution, and I can think of many ways things could go wrong. Fortunately, the only files I want to do this in are already highly structured, so hopefully it'll work as it should there. It's the first bit of elisp I've ever written that was more than just a 5 line toy function, and it ended up being a fun challenge.

(defun aggregate-org-cookies ()
  (save-excursion
    (org-back-to-heading t)
    (let* ((prop (string-match "\\<aggregate\\>"
                               (or (org-entry-get nil "COOKIE_DATA") "")))
           )
      (if prop
          (let* ((counts (aggregate-one-level))
                 (numerator (car counts))
                 (denominator (cadr counts))
                 (cookie-regex "\\[\\([0-9]*\\)/\\([0-9]*\\)\\]")
                 (new (format "[%d/%d]" numerator denominator)))
            (re-search-forward cookie-regex (line-end-position) t)
            (if (match-beginning 0)
                (progn
                  (setq beg (match-beginning 0)
                        ndel (- (match-end 0) beg))
                  (goto-char beg)
                  (insert new)
                  (delete-region (point) (+ (point) ndel))
                  ))))
      (if (org-up-heading-safe)
          (aggregate-org-cookies))
      )))

(defun aggregate-one-level ()
  (save-excursion
    (let* ((current (point))
           (next (save-excursion (outline-next-heading) (point)))
           (numerator 0)
           (denominator 0)
           (cookie-regex "\\[\\([0-9]*\\)/\\([0-9]*\\)\\]")
           )
      (defun count-one ()
        (re-search-forward cookie-regex (line-end-position) t)
        (if (> (match-end 1) (match-beginning 1))
            `(,(string-to-number (match-string 1))
              ,(string-to-number (match-string 2)))
          (0 0 )))
        (while (> next current)
          (goto-char next)
          (setq current next
                next (save-excursion (org-forward-heading-same-level 1) (point))
                current_total (count-one)
                denominator (+ denominator (cadr current_total))
                numerator (+ numerator (car current_total))))
          
        `(,numerator ,denominator)
        )))

(add-hook 'org-checkbox-statistics-hook (function aggregate-org-cookies))

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.