3

I use Org-mode in Emacs to keep track of time. I sometimes clock in and out of the same task back-to-back, leading to two contiguous clock time intervals, such as:

CLOCK: [2019-02-27 Wed 10:21]--[2019-02-27 Wed 10:24] =>  0:03
CLOCK: [2019-02-27 Wed 08:40]--[2019-02-27 Wed 10:21] =>  1:41

To reduce clutter and disk space, I would like to combine them into a single interval such as:

CLOCK: [2019-02-27 Wed 08:40]--[2019-02-27 Wed 10:24] =>  1:44

Can Org-mode automatically collapse or combine clock times together?

Update: this question refers to clocks under the same heading, and so is different and simpler than Merging clock logs together in org-mode, which merges clocks from sub-headings to the parent heading.

  • What happens to clocks in subheadings? Maybe those could be joined below the common ancestor. But, that depends on the application. What happens to combinations of clocks directly below the heading and clocks in logbook drawers? – Tobias Feb 27 at 14:07
  • @Tobias: it's possible to add a time interval in a heading and a different interval in a subheading, so those would not be contiguous because they are on different headings. I have my clock timer outside of logbooks, so Org mode could check contiguous intervals only with the same setting (inside or outside logbooks). Are these answers what you meant? – miguelmorin Feb 27 at 15:33
  • Org-mode does not do this by default. To get this functionality, someone needs to write a function to this effect. Code that does the opposite, i.e. splits clocked times, can be found here: github.com/justintaft/org-clock-split – Heikki Feb 28 at 9:04
2

This function works if the two timestamps are the same and lets the user override a timestamp difference with a universal argument:

(defun org-clock-merge (arg)
  "Merge the org CLOCK line with the next CLOCK line.

Requires that the time ranges in two lines overlap, i.e. the
start time of the first line and the second time of the second
line are identical.

If the testing fails, move the cursor one line down.

Universal argument ARG overrides the test and merges 
the lines even if the ranges do not overlap."

  (interactive "P")
  (let* ((org-clock-regexp (concat "CLOCK: " org-ts-regexp3 "--" org-ts-regexp3))
         (first-line-start (line-beginning-position))
         (first-line (buffer-substring
                      (line-beginning-position) (line-end-position)))
         (first-line-t1 (if (string-match org-clock-regexp first-line)
                            (match-string 1 first-line)
                          (progn
                            (forward-line)
                            (user-error "The first line must have a valid CLOCK range"))))
         (first-line-t2 (match-string 9 first-line))
         (second-line (progn
                        (forward-line)
                        (buffer-substring
                         (line-beginning-position) (line-end-position))))
         (second-line-t1 (if (string-match org-clock-regexp second-line)
                             (match-string 1 second-line)
                           (user-error "The second line must have a valid CLOCK range")))
         (second-line-t2 (match-string 9 second-line)))

    ;; check if lines should be merged
    (unless (or arg (equal first-line-t1 second-line-t2))
      (user-error "Clock ranges not continuous. Override with universal argument"))

    ;; remove the two lines
    (delete-region first-line-start (line-end-position))
    ;; indent
    (org-cycle)
    ;; insert new time range
    (insert (concat "CLOCK: [" second-line-t1 "]--[" first-line-t2 "]"))
    ;; generate duration
    (org-ctrl-c-ctrl-c)))
1

Building on @Heikki's answer, I coded the functions below. The main benefit is that I call this function automatically after clocking out and it merges if the time discrepancy is zero, so I don't need to move point to the last clock-out line. If called interactively, it prompts the user to approve a merge where the discrepancy is not zero and shows the time difference in human-readable format. I also added a regression test with the Emacs Regression Testing (ERT) framework ((require 'ert)) to ensure that one function works as expected.

(require 'ert)

(defun org-get-clock-segment-timestamps (line)
  "Parses a clock segment line and returns the first and last timestamps in a list."
  (let* ((org-clock-regexp (concat "CLOCK: " org-ts-regexp3 "--" org-ts-regexp3))
     (t1 (if (string-match org-clock-regexp line)
         (match-string 1 line)
           (user-error "The argument must have a valid CLOCK range")))
     (t2 (match-string 9 line)))
    (list t1 t2)))

(ert-deftest org-timestamp-test ()
  (should (equal
           (org-get-clock-segment-timestamps "CLOCK: [2019-09-26 Thu 00:29]--[2019-09-26 Thu 01:11] => 0:42")
           '("2019-09-26 Thu 00:29" "2019-09-26 Thu 01:11"))))


(defun org-compute-timestamp-difference (later-timestamp earlier-timestamp)
  "Computes the difference in string timestamps as a float."
  (-
   (float-time (apply #'encode-time (org-parse-time-string later-timestamp)))
   (float-time (apply #'encode-time (org-parse-time-string earlier-timestamp)))))

(defun org-float-time-diff-to-hours-minutes (diff)
  "Returns a float time difference in hh:mm format."
  (let* ((hours (floor (/ diff 3600)))
     (diff_minus_hours (- diff (* 3600 hours)))
     (minutes (floor (/ diff_minus_hours 60))))
    (format "%2d:%02d" hours minutes)))


(defun org-clock-merge (&optional skip-merge-with-time-discrepancy)
  "Merge the org CLOCK line with the next CLOCK line. If the last
timestamp of the current line equals the first timestamp of the
next line, i.e. no discrepancy exists, then merge
automatically. If a discrepancy exists, prompt the user for
confirmation, unless skip-merge-with-time-discrepancy is
non-nil."

  (interactive "P")
  (let* ((first-line-start (line-beginning-position))
     (first-line (buffer-substring
              (line-beginning-position) (line-end-position)))
     (first-line-timestamps (org-get-timestamps first-line))
     (first-line-t1 (pop first-line-timestamps))
     (first-line-t2 (pop first-line-timestamps))
     (first-line-t2 (match-string 9 first-line))
     (second-line (progn
            (forward-line)
            (buffer-substring
             (line-beginning-position) (line-end-position))))
     (second-line-timestamps (org-get-timestamps second-line))
     (second-line-t1 (pop second-line-timestamps))
     (second-line-t2 (pop second-line-timestamps))
     (diff (org-compute-timestamp-difference first-line-t1 second-line-t2)))

    (when (> diff 0)
      (when skip-merge-with-time-discrepancy
    (error "Skipping clock-merge"))
      (unless (yes-or-no-p (concat (org-float-time-diff-to-hours-minutes diff)
                   " discrepancy in times to merge. Proceed anyway?"))
    (user-error "Cancelled org-clock-merge")))

    ;; remove the two lines
    (delete-region first-line-start (line-end-position))
    ;; indent
    (org-cycle)
    ;; insert new time range
    (insert (concat "CLOCK: [" second-line-t1 "]--[" first-line-t2 "]"))
    ;; generate duration
    (org-ctrl-c-ctrl-c)))

(defun org-try-merging-last-clock-out ()
  "Try to merge the latest clock-out, and catch the error if the discrepancy is not zero."
  (save-restriction
    (save-excursion
      (org-clock-goto)
      (search-forward org-last-inserted-timestamp)
      (condition-case nil
      (org-clock-merge t)
    (error)))))

(add-hook 'org-clock-out-hook #'org-try-merging-last-clock-out)

For details on testing with the Emacs Regression Testing (ERT), see this answer on StackOverflow.

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