org-clock-agg: add csv export

This commit is contained in:
Pavel Korytov 2024-07-16 21:33:49 +03:00
parent 74ae4be588
commit e8642c02b0

View file

@ -522,7 +522,8 @@ BODY can also contain the following keyword arguments:
"Group org-clock entries by headline." "Group org-clock entries by headline."
:readable-name "Headline" :readable-name "Headline"
:default-sort total :default-sort total
(list (org-element-property :raw-value (alist-get :headline elem)))) (list (substring-no-properties
(org-element-property :raw-value (alist-get :headline elem)))))
(org-clock-agg-defgroupby day (org-clock-agg-defgroupby day
:readable-name "Day" :readable-name "Day"
@ -1018,6 +1019,16 @@ WIDGET is the instance of the widget that was changed."
(org-clock-agg-refresh)) (org-clock-agg-refresh))
"Refresh") "Refresh")
(insert " ") (insert " ")
(widget-create 'push-button
:notify (lambda (&rest _)
(org-clock-agg-view-elems))
"View records")
(insert " ")
(widget-create 'push-button
:notify (lambda (&rest _)
(org-clock-agg-csv))
"Export records to CSV")
(insert " ")
(unless (org-clock-agg--drill-down-p) (unless (org-clock-agg--drill-down-p)
(widget-create 'push-button (widget-create 'push-button
:notify (lambda (&rest _) :notify (lambda (&rest _)
@ -1159,24 +1170,33 @@ elements as well. LEVEL is the level of the node."
(org-clock-agg--render-tree-node child show-elems (1+ level))) (org-clock-agg--render-tree-node child show-elems (1+ level)))
(alist-get :children (cdr node)))) (alist-get :children (cdr node))))
(defun org-clock-agg-view-elems-at-point () (defun org-clock-agg--view-elems (tree)
"View elements of the `org-clock-agg' node at point." "View elements of an `org-clock-agg' TREE with `org-ql'."
(interactive)
;; `org-ql' doesn't requrire this by default, I assume for ;; `org-ql' doesn't requrire this by default, I assume for
;; optimization purposes. I won't interfere. ;; optimization purposes. I won't interfere.
(require 'org-ql-view) (require 'org-ql-view)
(let* ((elems (org-clock-agg--ungroup tree))
(strings (mapcar (lambda (elem)
(org-ql-view--format-element
(alist-get :headline elem)))
elems)))
(org-ql-view--display
:buffer "*org-clock-agg-elems*"
:header (format "Elements: %s" (if (length= tree 1) (caar tree) "tree"))
:strings strings)))
(defun org-clock-agg-view-elems-at-point ()
"View elements of an `org-clock-agg' node at point."
(interactive)
(let ((node-at-point (get-text-property (point) 'node))) (let ((node-at-point (get-text-property (point) 'node)))
(unless node-at-point (unless node-at-point
(user-error "No node at point!")) (user-error "No node at point!"))
(let* ((elems (org-clock-agg--ungroup (list node-at-point))) (org-clock-agg--view-elems (list node-at-point))))
(strings (mapcar (lambda (elem)
(org-ql-view--format-element (defun org-clock-agg-view-elems ()
(alist-get :headline elem))) "View the found elements in the current `org-clock-agg' buffer."
elems))) (interactive)
(org-ql-view--display (org-clock-agg--view-elems org-clock-agg--tree))
:buffer "*org-clock-agg-elems*"
:header (format "Elements: %s" (car node-at-point))
:strings strings))))
(defun org-clock-agg-drill-down-at-point () (defun org-clock-agg-drill-down-at-point ()
"Open the report buffer solely for the element at point." "Open the report buffer solely for the element at point."
@ -1265,6 +1285,73 @@ return value description."
'(,from ,to ,files ,groupby ,sort ,sort-order ,extra-params))))))) '(,from ,to ,files ,groupby ,sort ,sort-order ,extra-params)))))))
(switch-to-buffer buffer))) (switch-to-buffer buffer)))
(defun org-clock-agg--csv-elems-to-alist (elems)
"Convert ELEMS to an alist.
ELEMS is a list as described in `org-clock-agg--parse-headline'."
(cl-loop for elem in elems
collect
`((start . ,(thread-last elem
(alist-get :start)
(seconds-to-time)
(format-time-string (cdr org-time-stamp-formats))))
(end . ,(thread-last elem
(alist-get :end)
(seconds-to-time)
(format-time-string (cdr org-time-stamp-formats))))
(duration . ,(alist-get :duration elem))
(headline . ,(car (org-clock-agg--groupby-headline elem nil)))
(todo-keyword . ,(car (org-clock-agg--groupby-todo elem nil)))
(is-done . ,(car (org-clock-agg--groupby-is-done elem nil)))
(day-of-week . ,(car (org-clock-agg--groupby-day-of-week elem nil)))
(category . ,(car (org-clock-agg--groupby-category elem nil)))
(org-file . ,(car (org-clock-agg--groupby-org-file elem nil)))
(outline-path . ,(string-join
(org-clock-agg--groupby-outline-path elem nil) "/"))
(tags . ,(string-join
(org-clock-agg--groupby-tags elem nil) "/")))))
(defun org-clock-agg--csv-alist-to-string (data)
"Convert DATA to csv string.
DATA has to be an alist, where each item has the same set of
attributes."
(concat
(mapconcat
(lambda (datum)
(symbol-name (car datum)))
(car data)
",")
"\n"
(cl-loop with keys = (mapcar #'car (car data))
for datum in data
concat (mapconcat
(lambda (key)
(let ((item (alist-get key datum)))
(cond ((numberp item) (format "%s" item))
((and (stringp item)
(string-match-p (rx (| " " "," "\"")) item))
(format "\"%s\"" (string-replace
"\""
"\"\""
item)))
((stringp item) (format "%s" item))
(t ""))))
keys ",")
concat "\n")))
(defun org-clock-agg-csv ()
"Export the found elements in the `org-clock-agg' buffer as CSV."
(interactive)
(unless org-clock-agg--elems
(user-error "Nothing found in the current buffer!"))
(let* ((data (org-clock-agg--csv-elems-to-alist
org-clock-agg--elems))
(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.