Search code examples
emacsorg-mode

Is there a way to calculate org-mode heading properties based on sub heading todo-keywords?


This is how I use org-capture to journal my day

*** 2020-05-14 Thursday
:PROPERTIES:
:DoneA: 1
:DoneB: 1
:DoneC: 0
:Done: 0
:Kill: 1
:Gone: 1
:Rate: 5
:END:
:LOGBOOK:
CLOCK: [2020-05-14 Thu 04:55]--[2020-05-14 Thu 05:05] =>  0:10
:END:
**** DONE [#A] task a
**** GONE [#A] another task a
**** DONE [#B] task b
**** KILL other task

I manually count and type to each properties and use simple rate to manually fill Rate PROPERTIES

for DONE [#A] I give score 5, #B 3, KILL 2, GONE -5 so the Rate PROPERTY is = 5

Is there a function or emacs built in to count sub heading based on org-todo-keywords and put it in the upper-level heading PROPERTIES?

my elisp skill is not good enough to make custom function


Solution

  • There is no such function (it would depend on the details of what you want to find and how you want to record it) but there are certainly building blocks to build your own, so I would encourage you to improve your elisp skills, although I'll try to give a motivated example of how you would go about it below.

    The first thing to know is that Org mode provides a powerful function, org-map-entries, which can iterate over a set of selected headlines and apply an arbitrary function as it is visiting each headline. E.g. here's a simple example of its use (straight from the manual - see the link above), to count the number of entries matching some expression match in a subtree:

    (defun count-tasks (match)
      (length (org-map-entries t match 'tree)))
    

    If you define this function, put your cursor on the headline of the date of interest (*** 2020-05-14 Thursday in your example) and then invoke it like this:

     ESC-ESC-: (count-tasks "/+DONE) RET
    

    it will return the number of DONE entries in the subtree - in your example, that number should be 2.

    The second thing to know is that there is a Property API that provides functions to get (org-entry-get) and set (org-entry-put) properties on a headline. So for example, you can set a "Done" property to the number of DONE tasks in the subtree by doing this (I'm assuming that your cursor is still at the headline as before):

      ESC-ESC-: (org-entry-put (point) "Done" (format "%d" (count-tasks "/+DONE")) RET
    

    We count the tasks, turn the number into a string with format and add (or modify) a property named Done to give it that stringified number as value.

    Those are the pieces. You now have to put them together to do what you want. Some matches are a bit more complicated, e.g. to count all the DONE headlines with priority A, you'll need to say (count-tasks "PRIORITY=\"A\"/+DONE").

    So you just have to do this for each of the items that you want to count, and set the appropriate property but also remember each count, so that you can calculate your rate. That is just a weighted sum of these counts, and if you know a bit about vectors, you might realize that it is the dot (scalar) product of two vectors: the vector of weights and the vector of counts, which you get by multiplying corresponding elements of the two vectors together and adding all the results (btw, your weights are not specified completely: you are missing a weight for the DoneC category - I chose to add a weight of 1 for that category below). There are many ways to do that in lisp, depending on how you represent those vectors. To take a simple example, I can represent them as lists of elements and then implement the rate as a map-reduce operation on the two lists:

    (defun rate (counts)
      (let ((weights '(5 3 1 2 -5)))
       (seq-reduce #'+ (mapcar* #'* weights counts) 0)))
    

    You should read the doc strings of mapcar* and seq-reduce: they are very useful (APL was a language that made a lot of use of these operations), but iff you find that a bit obscure, you can also implement it using a loop:

    (defun rate-iter (counts)
      (let ((weights '(5 3 1 2 -5))
            (rate 0))
        (while counts
          (setq rate (+ rate (* (car counts) (car weights)))
                counts (cdr counts)
                weights (cdr weights)))
        rate))
    

    You can then enter it as a property in exactly the same way as above:

    ESC-ESC-: (org-entry-put (point) "Rate" (format "%d" (rate counts)))
    

    Those are the pieces but you still have to write some code to put it all together. Hope this helps.