The commando org+-collaps-clocks
from the following Elisp code collects the clock entries of the headline at point and collapses them into one clock entry with the minimal start time of the original entries and the total duration of all original entries.
The code is split into a set of small helper functions.
All of them are documented and
most of them have a trailing comment with a test that you can evaluate.
It was surprising how little the Org api was prepared for this task.
For an example there is no Org timestamp element generator that accepts
the start time and the end time in internal time format.
The code below contains org+-timestamp-from-time
as extension of
org-timestamp-from-time
that allows the generation of timestamps with end time.
The code is new and therefore not widely tested. So, use it with care!
(require 'cl-lib)
(require 'org-element)
(defun org+-element-parse-headline (&optional granularity visible-only)
"Parse current headline.
GRANULARITY and VISIBLE-ONLY are like the args of `org-element-parse-buffer'."
(let ((level (org-current-level)))
(org-element-map
(org-element-parse-buffer granularity visible-only)
'headline
(lambda (el)
(and
(eq (org-element-property :level el) level)
(<= (org-element-property :begin el) (point))
(<= (point) (org-element-property :end el))
el))
nil 'first-match 'no-recursion)))
(defun org+-element-clock (clock)
"If CLOCK is a string parse it to an Org clock element.
Otherwise return CLOCK."
(if (stringp clock)
(with-temp-buffer
(insert clock)
(goto-char (point-min))
(org-element-clock-parser (point-max)))
clock))
;; Test:
;; (org+-element-clock "CLOCK: [2022-11-11 Fri 11:13]--[2022-11-15 Tue 11:14]")
(defun org+-clock-to-time (clock &optional start-or-end decode)
"Transform Org CLOCK element to internal time.
If START-OR-END is nil or equal to 'start transform the start time
otherwise transform the end time.
If DECODE is non-nil return the decoded time."
(when (eq start-or-end 'start)
(setq start-or-end nil))
(let* ((timestamp (org-element-property :value (org+-element-clock clock)))
(time (org-timestamp-to-time timestamp start-or-end)))
(if decode
(decode-time time)
time)))
;; Test:
;; (org+-clock-to-time "CLOCK: [2022-11-12 Sat 13:14]--[2022-11-12 Sat 13:20]" 'start t)
(defun org+-encode-time (time)
"Encode TIME if it is the result of `decode-time'.
Otherwise return TIME untransformed."
(if (eq (length time) 9) ;; decoded time
(encode-time time)
time))
(cl-defun org+-timestamp-from-time (start &optional with-time inactive &key end minutes)
"Convert START time into a Org timestamp object.
START is an Emacs internal time representation, as returned, e.g.,
by ‘current-time’.
See `org-timestamp-from-time' for the meaning of WITH-TIME and INACTIVE.
Additional to `org-timestamp-from-time' this function also allows
to specify the end time by an internal time END or the number of MINUTES."
(setq start (org+-encode-time start))
(when (numberp minutes)
(setq end (time-add start (* 60 minutes))))
(let ((timestamp (org-timestamp-from-time start with-time inactive)))
(when end
(let ((end-timestamp (org-timestamp-from-time end with-time inactive)))
(cl-loop for prop on (cadr end-timestamp) by 'cddr
for name = (symbol-name (car prop))
when (string-suffix-p "-start" name)
do
(org-element-put-property timestamp (intern (concat (substring name nil (- (length "-start"))) "-end")) (cadr prop)))))
timestamp))
;; Tests:
;; (org-element-timestamp-interpreter (org+-timestamp-from-time (current-time) t t :minutes 60) nil)
;; (org-element-timestamp-interpreter (org+-timestamp-from-time (current-time) t t) nil)
(defun org+-element-delete (element &optional kill)
"Delete ELEMENT from :begin to :end.
Save into `kill-ring' if KILL is non-nil."
(let ((b (org-element-property :begin element))
(e (org-element-property :end element)))
(if kill
(kill-region b e)
(delete-region b e))))
(defsubst org+-clock-lessp (clock1 clock2 &optional start-or-end)
"Return non-nil if the start of CLOCK1 is less than that one of CLOCK2.
START-OR-END can be 'start or 'end.
START-OR-END defaults to 'start."
(time-less-p
(org+-clock-to-time clock1 start-or-end)
(org+-clock-to-time clock2 start-or-end)))
;; Test:
;; (org+-clock-lessp "CLOCK: [2022-11-11 Fri 11:11]--[2022-11-11 Fri 12:22]" "CLOCK: [2022-11-12 Fri 01:00]--[2022-11-12 Fri 2:00]")
(defun org+-clock-minutes (clock)
"Return duration of CLOCK line.
CLOCK may be string or a parsed clock line."
(let* ((tStart (org+-clock-to-time clock))
(tEnd (org+-clock-to-time clock 'end)))
(round (/ (time-to-seconds (time-subtract tEnd tStart)) 60.0))))
;; Test:
;; (org+-clock-minutes "CLOCK: [2022-11-11 Fri 11:11]--[2022-11-11 Fri 12:22]")
(defun org+-clock-time-limits (clocks)
"Return the limits of the smallest time interval containing CLOCKS.
CLOCKS is a list of Org elements of clocks either as strings
or as parsed elements.
The limits are returned as list (tMin tMax)."
(let* ((clock (car clocks))
(tMin (org+-clock-to-time clock 'start))
(tMax (org+-clock-to-time clock 'end)))
(dolist (clock (cdr clocks))
(let ((tStart (org+-clock-to-time clock 'start))
(tEnd (org+-clock-to-time clock 'end)))
(when (time-less-p tStart tMin)
(setq tMin tStart))
(when (time-less-p tMax tEnd)
(setq tMax tEnd))))
(list tMin tMax)))
;; Test:
;; (org+-clock-time-limits '("CLOCK: [2022-11-11 Fri 11:11]--[2022-11-11 Fri 12:22]" "CLOCK: [2022-11-11 Fri 01:00]--[2022-11-11 Fri 2:00]"))
(defun org+-clocks-same-day-p (clocks)
"Return non-nil if all CLOCKS have the same day."
(cl-multiple-value-bind (tMin tMax) (org+-clock-time-limits clocks)
(cl-every
(lambda (idx)
(eq (nth idx tMin) (nth idx tMax)))
'(
3 ;; day
4 ;; month
5 ;; year
))))
;; Tests:
;; (org+-clocks-same-day-p '("CLOCK: [2022-11-11 Fri 11:11]--[2022-11-11 Fri 12:22]" "CLOCK: [2022-11-11 Fri 01:00]--[2022-11-11 Fri 2:00]"))
;; (org+-clocks-same-day-p '("CLOCK: [2022-11-11 Fri 11:11]--[2022-11-11 Fri 12:22]" "CLOCK: [2022-11-12 Fri 01:00]--[2022-11-12 Fri 2:00]"))
(defun org+-collaps-clocks ()
"Collaps CLOCKS of the heading at point."
(interactive)
(let* ((clocks (org-element-map (org+-element-parse-headline) '(clock) #'identity))
(clock (car clocks))
(first-pos (org-element-property :begin clock))
(minutes (org+-clock-minutes clock))
(clockMin clock)
(clockMax (org+-clock-to-time clock 'end)))
(dolist (clock (cdr clocks))
(when (org+-clock-lessp clock clockMin)
(setq clockMin clock))
(when (org+-clock-lessp clockMax clock)
(setq clockMax clock))
(cl-incf minutes (org+-clock-minutes clock)))
(unless (org+-clocks-same-day-p (list clockMin clockMax))
(user-error "The clocks span several days"))
(dolist (clock (nreverse clocks))
(org+-element-delete clock))
(goto-char first-pos)
(let ((timestamp (org+-timestamp-from-time
(org-timestamp-to-time
(org-element-property :value clockMin))
t t
:minutes minutes)))
(org-element-put-property timestamp :type 'inactive-range)
(insert
(org-element-clock-interpreter
(org-element-create
'clock
(list
:status
'running
:value
timestamp
))
nil)
"\n"))
))
;; Tests:
;; (with-temp-buffer (insert "* heading\nCLOCK: [2022-11-11 Fri 08:00]--[2022-11-11 Fri 09:00]\nCLOCK: [2022-11-11 Fri 11:00]--[2022-11-11 Fri 12:00]\nSome text.") (org-mode) (goto-char 2) (org+-collaps-clocks) (buffer-substring-no-properties (point-min) (point-max)))
org-clock-sum
? Maybe, you do not need to compact the clock entries if you use that function.