cmc/cleberg.net
My personal web garden & blog.
clone: git clone https://gitbay.org/cmc/cleberg.net.git
main: content/garden/org-mode.org · raw
1#+title: 🌱 org-mode
2#+slug: org-mode
3#+description: Tidbits about the best plaintext language: org-mode.
4#+date: [2026-02-21 Sat 12:39:27]
5
6* Time Reporting
7
8If you maintain a log of time clocks, you can create a weekly report that will
9break down time per heading, per day, per week.
10
11The time tracker file:
12
13#+begin_src org
14,#+title: Time Tracker
15,#+startup: showall
16
17# To generate a time report:
18# 1. Open Emacs with this file
19# 2. SPC z t (or M-x tr/time-report-interactive)
20# 3. Enter this file as input, a path for output, and optional date range (YYYY-MM-DD)
21
22,* Project 1
23:LOGBOOK:
24CLOCK: [2026-02-19 Thu 09:00]--[2026-02-19 Thu 12:00] => 3:00
25:END:
26,* Project 2
27:LOGBOOK:
28CLOCK: [2026-02-20 Fri 10:00]--[2026-02-20 Fri 16:00] => 6:00
29:END:
30#+end_src
31
32The elisp function to generate the report:
33
34#+begin_src elisp
35;; Time Report
36(defun tr/parse-org-clock-entries (file)
37 "Return list of (date heading minutes) from FILE's CLOCK entries."
38 (let (entries)
39 (with-current-buffer (find-file-noselect file)
40 (org-with-wide-buffer
41 (goto-char (point-min))
42 (while (re-search-forward
43 "CLOCK: \\[\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\) [A-Za-z]\\{3\\} [0-9:]\\{5\\}\\]--\\[\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\) [A-Za-z]\\{3\\} \\([0-9:]\\{5\\}\\)\\] => *\\([0-9]+\\):\\([0-9]+\\)"
44 nil t)
45 (let* ((date (match-string 1))
46 (h (string-to-number (match-string 4)))
47 (m (string-to-number (match-string 5)))
48 (mins (+ (* h 60) m))
49 (heading (save-excursion
50 (org-back-to-heading t)
51 (org-get-heading t t t t))))
52 (push (list date heading mins) entries)))))
53 (nreverse entries)))
54
55(defun tr/format-minutes (mins)
56 (format "%d:%02d" (/ mins 60) (mod mins 60)))
57
58(defun tr/generate-report (input-file output-file &optional tstart tend)
59 "Parse INPUT-FILE and write time report to OUTPUT-FILE."
60 (let* ((entries (tr/parse-org-clock-entries input-file))
61 (entries (if (or tstart tend)
62 (cl-remove-if-not
63 (lambda (e)
64 (let ((d (car e)))
65 (and (or (null tstart) (not (string< d tstart)))
66 (or (null tend) (not (string> d tend))))))
67 entries)
68 entries))
69 (totals (make-hash-table :test #'equal)))
70 (dolist (e entries)
71 (let ((key (cons (nth 0 e) (nth 1 e))))
72 (puthash key (+ (gethash key totals 0) (nth 2 e)) totals)))
73 (let* ((keys (hash-table-keys totals))
74 (keys (sort keys (lambda (a b)
75 (or (string< (car a) (car b))
76 (and (string= (car a) (car b))
77 (string< (cdr a) (cdr b)))))))
78 (current-week nil))
79 (with-temp-file output-file
80 (insert "#+title: Time Report\n\n")
81 (insert (format "| %-12s | %-45s | %5s |\n" "Date" "Code" "Hours"))
82 (insert (concat "|" (make-string 14 ?-) "+" (make-string 47 ?-) "+" (make-string 7 ?-) "|\n"))
83 (let (week-mins week-start)
84 (dolist (k keys)
85 (let* ((date (car k))
86 (heading (cdr k))
87 (mins (gethash k totals))
88 (time (date-to-time (concat date " 00:00:00")))
89 (week (format-time-string "%Y-W%V" time)))
90 (when (and week-start (not (string= week current-week)))
91 (insert (concat "|" (make-string 14 ?-) "+" (make-string 47 ?-) "+" (make-string 7 ?-) "|\n"))
92 (insert (format "| %-12s | %-45s | %5s |\n" current-week "WEEK TOTAL" (tr/format-minutes week-mins)))
93 (insert (concat "|" (make-string 14 ?-) "+" (make-string 47 ?-) "+" (make-string 7 ?-) "|\n"))
94 (setq week-mins 0))
95 (unless (string= week current-week)
96 (setq current-week week week-start date week-mins 0))
97 (setq week-mins (+ week-mins mins))
98 (insert (format "| %-12s | %-45s | %5s |\n" date heading (tr/format-minutes mins)))))
99 (when week-mins
100 (insert (concat "|" (make-string 14 ?-) "+" (make-string 47 ?-) "+" (make-string 7 ?-) "|\n"))
101 (insert (format "| %-12s | %-45s | %5s |\n" current-week "WEEK TOTAL" (tr/format-minutes week-mins)))))
102 (insert "\n")))))
103
104(defun tr/time-report-interactive ()
105 "Interactively generate a time report from an org file."
106 (interactive)
107 (let* ((input (read-file-name "Time tracking file: " nil nil t))
108 (output (read-file-name "Output report file: "))
109 (tstart (read-string "Start date (YYYY-MM-DD, blank for none): "))
110 (tend (read-string "End date (YYYY-MM-DD, blank for none): "))
111 (tstart (if (string-empty-p tstart) nil tstart))
112 (tend (if (string-empty-p tend) nil tend)))
113 (tr/generate-report input output tstart tend)
114 (find-file output)
115 (message "Report written to %s" output)))
116
117;; Map time report to keybindings
118(map! :leader
119 :desc "Generate time report" "z t" #'tr/time-report-interactive)
120#+end_src
121
122The output file:
123
124#+begin_src org
125,#+title: Time Report
126
127| Date | Code | Hours |
128|------------+------------+-------|
129| 2026-02-19 | Project 1 | 3:00 |
130| 2026-02-20 | Project 2 | 6:00 |
131|------------+------------+-------|
132| 2026-W08 | WEEK TOTAL | 9:00 |
133#+end_src