Compare commits

...

2 commits

Author SHA1 Message Date
6a40c31c78 org-clock-agg: add export to CSV
Some checks failed
melpazoid / build (push) Has been cancelled
2025-11-20 19:06:23 +03:00
e8cfd456bc fix: update `org-clock-agg--parse-clocks' for org 9.7.11 2025-11-20 16:02:11 +03:00

View file

@ -216,30 +216,31 @@ Return a list of alists with the following keys:
- `:start' - start time in seconds since the epoch - `:start' - start time in seconds since the epoch
- `:end' - end time in seconds since the epoch - `:end' - end time in seconds since the epoch
- `:duration' - duration in seconds." - `:duration' - duration in seconds."
(let ((contents (buffer-substring-no-properties (save-restriction
;; contents-begin starts after the headline ;; I used to insert a substring into a separate buffer to run
(org-element-property :contents-begin headline) ;; `org-element-parse-buffer', but somehow this broke on the most
(org-element-property :contents-end headline)))) ;; recent `org-mode'.
(with-temp-buffer (narrow-to-region
(insert contents) (org-element-property :contents-begin headline)
(let (res) (org-element-property :contents-end headline))
(org-element-map (org-element-parse-buffer) 'clock (let (res)
(lambda (clock) (org-element-map (org-element-parse-buffer) 'clock
(let ((start (time-convert (lambda (clock)
(org-timestamp-to-time (org-element-property :value clock)) (let ((start (time-convert
'integer)) (org-timestamp-to-time (org-element-property :value clock))
(end (time-convert 'integer))
(org-timestamp-to-time (org-element-property :value clock) t) (end (time-convert
'integer))) (org-timestamp-to-time (org-element-property :value clock) t)
(push 'integer)))
`((:start . ,start) (push
(:end . ,end) `((:start . ,start)
(:duration . ,(- end start))) (:end . ,end)
res))) (:duration . ,(- end start)))
;; The last argument stops parsing after the first headline. res)))
;; So only clocks in the first headline are parsed. ;; The last argument stops parsing after the first headline.
nil nil 'headline) ;; So only clocks in the first headline are parsed.
res)))) nil nil 'headline)
res)))
(defun org-clock-agg--properties-at-point () (defun org-clock-agg--properties-at-point ()
"Return a list of selected properties at point. "Return a list of selected properties at point.
@ -304,6 +305,7 @@ list of properties to select
(:properties . ,properties) (:properties . ,properties)
(:category . ,category))))) (:category . ,category)))))
;; TODO use `org-read-date'
(defun org-clock-agg--normalize-time-predicate (val kind) (defun org-clock-agg--normalize-time-predicate (val kind)
"Normalize VAL to a time predicate. "Normalize VAL to a time predicate.
@ -553,7 +555,9 @@ BODY can also contain the following keyword arguments:
:readable-name "TODO keyword" :readable-name "TODO keyword"
:default-sort total :default-sort total
(list (substring-no-properties (list (substring-no-properties
(org-element-property :todo-keyword (alist-get :headline elem))))) (or
(org-element-property :todo-keyword (alist-get :headline elem))
"No TODO"))))
(org-clock-agg-defgroupby is-done (org-clock-agg-defgroupby is-done
:readable-name "Is done" :readable-name "Is done"
@ -1356,6 +1360,52 @@ attributes."
(with-temp-file file-name (with-temp-file file-name
(insert csv-string)))) (insert csv-string))))
(defun org-clock-agg--flatten-tree (tree &optional attrs)
"Flatten an `org-clock-agg' TREE.
TREE is as defined by `org-clock-agg--groupby'. ATTRS is a recursive
parameter."
(let (res)
(cl-loop for (name . node) in tree
for groupby-name = (intern
(string-replace
" " "-"
(downcase
(alist-get :readable-name
(alist-get :groupby node)))))
if (seq-empty-p (alist-get :children node))
do (push
`((name . ,name)
(total . ,(alist-get :total node))
(parent-share . ,(alist-get :parent-share node))
(total-share . ,(alist-get :total-share node))
(elems-count . ,(seq-length (alist-get :elems node)))
(,groupby-name . ,name)
,@attrs)
res)
else
do (setq res
(append
(org-clock-agg--flatten-tree
(alist-get :children node)
`(,@attrs
(,groupby-name . ,name)))
res nil)))
res))
(defun org-clock-agg-tree-csv ()
"Export the current `org-clock-agg' tree into CSV."
(interactive)
(unless org-clock-agg--tree
(user-error "Tree not found"))
(let* ((data (org-clock-agg--flatten-tree
org-clock-agg--tree))
(csv-string (org-clock-agg--csv-alist-to-string data))
(file-name (read-file-name "Save CSV: " nil "report.csv")))
(with-temp-file file-name
(insert csv-string))))
(defun org-clock-agg (from to files groupby sort sort-order extra-params) (defun org-clock-agg (from to files groupby sort sort-order extra-params)
"Aggregate org-clock data. "Aggregate org-clock data.