| @@ -0,0 +1,133 @@ |
| |
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 | |
| |
8 | If you maintain a log of time clocks, you can create a weekly report that will |
| |
9 | break down time per heading, per day, per week. |
| |
10 | |
| |
11 | The 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: |
| |
24 | CLOCK: [2026-02-19 Thu 09:00]--[2026-02-19 Thu 12:00] => 3:00 |
| |
25 | :END: |
| |
26 | ,* Project 2 |
| |
27 | :LOGBOOK: |
| |
28 | CLOCK: [2026-02-20 Fri 10:00]--[2026-02-20 Fri 16:00] => 6:00 |
| |
29 | :END: |
| |
30 | #+end_src |
| |
31 | |
| |
32 | The 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 | |
| |
122 | The 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 |