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 '19 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 '19 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 '19 at 9:04

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)
                            (user-error "The first line must have a valid CLOCK range"))))
         (first-line-t2 (match-string 9 first-line))
         (second-line (progn
                         (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
    ;; insert new time range
    (insert (concat "CLOCK: [" second-line-t1 "]--[" first-line-t2 "]"))
    ;; generate duration
| improve this answer | |

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.

(defun my-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
           (my-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 my-org-compute-timestamp-difference (later-timestamp earlier-timestamp)
  "Computes the number of seconds 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 my-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))))
    (car (split-string (format "%2d:%02d" hours minutes)))))

(defun my-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 with a tolerance of up to 2 minutes, then merge
automatically. If a discrepancy exists, prompt the user for
confirmation, unless skip-merge-with-time-discrepancy is

  (interactive "P")
  (let* ((first-line-start (line-beginning-position))
     (first-line (buffer-substring
              (line-beginning-position) (line-end-position)))
     (first-line-timestamps (my-org-get-clock-segment-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
             (line-beginning-position) (line-end-position))))
     (second-line-timestamps (my-org-get-clock-segment-timestamps second-line))
     (second-line-t1 (pop second-line-timestamps))
     (second-line-t2 (pop second-line-timestamps))
     (diff (my-org-compute-timestamp-difference first-line-t1 second-line-t2)))

    ;; ignore discrepancies of 2 minutes or less
    (when (> diff 120)
      (when skip-merge-with-time-discrepancy
    (error "Skipping clock-merge"))
      (unless (yes-or-no-p (concat (my-org-float-time-diff-to-hours-minutes diff)
                   " discrepancy in times to merge. Proceed anyway?"))
    (user-error "Cancelled my-org-clock-merge")))

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

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

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

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

| improve this answer | |

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