0

I have an existing .org file with a TODO list. At the time I was working on these tasks, I did not know about closing items or about the setting

(setq org-log-done 'time)

Now I want to automatically add CLOSED timestamp lines to each of the DONE items in the TODO list. This should be possible by using the last CLOCK entry of each LOGBOOK.

How can I do it? (I am not a LISP programmer, btw.)

1 Answer 1

2

Short and sweet

Make sure to keep a backup copy of your file just in case. Then add the following section at the bottom of the file:

*  Code                                                            :noexport:

#+begin_src elisp :results drawer
  (defun my/org-close-done-headline()
    (let ((org-use-last-clock-out-time-as-effective-time t))
      (org-todo "")
      (org-todo "DONE")))

  (org-map-entries #'my/org-close-done-headline "TODO=\"DONE\"" 'file)
#+end_src

Then place the cursor somewhere on the source code block and press C-c C-c. That's it: magic in five lines of lisp code.

Long explanation

This kind of problem ("do something on every headline that meets some criteria") is tailor-made for the function org-map-entries (see its doc string with C-h f org-map-entries and the Mapping API section of the manual with C-h i g(emacs)Using the Mapping API). It basically does the looping over headlines for you, it can optionally filter headlines based on some matching criteria, and it can apply a function on each headline it selects. And it can affect headlines in one or many files depending on its scope argument.

In your case, we want to select headlines with a TODO state equal to DONE, in the scope of a single file and we want to do something on each one of these headlines. Let's imagine that we have a function do-something that will do whatever it is that we want to do to each headline. Then all we have to do is

(org-map-entries #'do-something "TODO=\"DONE\"" 'file)

You can test this on your file by writing a simple do-something function:


(defun do-something ()
   (org-entry-get nil "ITEM"))

When you do M-: (do-something) on a headline, it will give you the text of the headline. Now if you do M-: (org-map-entries #'do-something "TODO=\"DONE\"" 'file), it will give you a list of all the headlines that are marked DONE in the file. You should try this.

A convenient way to do that is to use Org Babel to add some code snippets to the file you are going to be processing. Add a new section at the end (I usually call it "Code") and add a code block. Here's an example that you can cut and paste:


* DONE foo

* bar

* DONE baz

*  Code                                                            :noexport:

#+begin_src elisp :results drawer
  (defun do-something()
    (org-entry-get nil "ITEM"))

  (org-map-entries #'do-something "TODO=\"DONE\"" 'file)
#+end_src

#+RESULTS:
:results:
(foo baz)
:end:

Putting your cursor on the code block and pressing C-c C-c evaluates the code block (i.e. it defines the do-something function and then evaluates the org-map-entries call to produce the results shown: the two headlines that are marked DONE.

Now all we have to do is make our do-something function do something more. In fact, let's give it a more descriptive name while we are at it. We'll call it my/org-close-done-headline. We'll do that in three steps:

  • we'll figure out how to add a CLOSE: but the time will be the current time.

  • then we'll figure out how to add a CLOSE: but with a time that we specify.

  • then we'll figure out how to get the appropriate time out of the LOGBOOK.

You can set the TODO state of a headline to e.g. DONE, by doing M-: (org-todo "DONE") while your cursor is on that headline (actually, from anywhere inside that section). The only trouble is that if it's already in DONE state, nothing happens. So we trick it with two calls of org-todo: the first call sets the TODO state to empty, and the second resets it to DONE but now since org-log-done is set to time, a CLOSE: entry will get added, albeit with the current time:


* DONE foo

* bar

* DONE baz

*  Code                                                            :noexport:

#+begin_src elisp :results drawer
  (defun my/org-close-done-headline()
    (org-todo "")
    (org-todo "DONE"))

  (org-map-entries #'my/org-close-done-headline "TODO=\"DONE\"" 'file)
#+end_src

Now if you C-c C-c on the code block, the my/org-close-done function will be applied on every DONE headline, so it will reset it to empty and back to DONE, adding the CLOSE: entry in the process. Try it and see.

The second and third steps are actually one step and a very simple one at that, because Org mode conveniently provides a feature that lets org-todo use the last clock-out time instead of the current time. It is controlled by the option org-use-last-clock-out-time-as-effective-time whose doc string says:

When non-nil, use the last clock out time for ‘org-todo’. Note that this option has precedence over the combined use of ‘org-use-effective-time’ and ‘org-extend-today-until’.

So all we have to do is modify the my/org-close-done-headline just a bit:

....

#+begin_src elisp
  (defun my/org-close-done-headline()
    (let ((org-use-last-clock-out-time-as-effective-time t))
      (org-todo "")
      (org-todo "DONE")))

  (org-map-entries #'my/org-close-done-headline "TODO=\"DONE\"" 'file)
#+end_src

We just let-bind the variable to t for the duration of the two org-todo calls and that's that: a fairly complicated transformation in five lines of lisp code.

One word of caution: just to be on the safe side, back up your file! Although you can always use undo if something goes wrong, having an extra copy of the file safely squirreled away will let you experiment without fear.

1
  • Awesome lesson! I just got through everything. Commented Mar 30, 2022 at 8:13

Your Answer

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

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