Fix Bug #11932.
[planner-el.git] / planner-timeclock-summary.el
blobd19835b9bb7d402573350d1596598b45469a5d0b
1 ;;; planner-timeclock-summary.el --- timeclock summary for the Emacs planner
2 ;;
4 ;; Copyright (C) 2004, 2005, 2008 Dryice Dong Liu . All rights reserved.
5 ;; Parts copyright (C) 2004, 2005, 2008 Chris Parsons (chris.p AT rsons.org)
6 ;; Parts copyright (C) 2004, 2005, 2008 Free Software Foundation, Inc.
7 ;; Parts copyright (C) 2005, 2008 Peter K. Lee
9 ;; Keywords: emacs planner timeclock report summary
10 ;; Author: Dryice Liu <dryice AT liu DOT com DOT cn>
11 ;; Time-stamp: <16/07/2005 21:43:50 Yann Hodique>
12 ;; Description: Summary timeclock of a day
14 ;; This file is part of Planner. It is not part of GNU Emacs.
16 ;; Planner is free software; you can redistribute it and/or modify it
17 ;; under the terms of the GNU General Public License as published by
18 ;; the Free Software Foundation; either version 3, or (at your option)
19 ;; any later version.
21 ;; Planner is distributed in the hope that it will be useful, but
22 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
23 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
24 ;; General Public License for more details.
26 ;; You should have received a copy of the GNU General Public License
27 ;; along with Planner; see the file COPYING. If not, write to the
28 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
29 ;; Boston, MA 02110-1301, USA.
31 ;;; Commentary:
33 ;; planner-timeclock-summary.el produces timeclock reports for planner
34 ;; files.
36 ;; There are two ways you can use it:
38 ;; 1. Display a temporary buffer
40 ;; Call `planner-timeclock-summary-show' and Emacs will ask you which
41 ;; day's summary do you want. Choose the date as anywhere else of
42 ;; Emacs planner, and a tempory buffer will be displayed with the
43 ;; timeclock summary of that day.
45 ;; 2. Rewrite sections of your planner
47 ;; Choose this approach if you want timeclock summary to be in their
48 ;; own section and you would like them to be readable in your plain
49 ;; text files even outside Emacs. Caveat: The timeclock summary
50 ;; section should already exist in your template and will be rewritten
51 ;; when updated. Tip: Add `planner-timeclock-summary-section'
52 ;; (default: "Timeclock") to your `planner-day-page-template'.
54 ;; To use, call `planner-timeclock-summary-update' in the planner day
55 ;; page to update the section. If you want rewriting to be
56 ;; automatically performed, call `planner-timeclock-summary-insinuate'
57 ;; in your .emacs file
59 ;;; REQUIRE
60 ;; to make a nice text table, you need align.el from
61 ;; http://www.newartisans.com/johnw/Emacs/align.el
63 ;;; TODO
64 ;; - sort?
66 ;;; Contributors:
68 ;; Chris Parsons submitted a patch that allows date ranges when
69 ;; summarizing data.
71 ;; Peter K. Lee made it so that even if there is an error with timelog
72 ;; file (which happens frequently), you don't lose the * Timeclock
73 ;; section, among other fixes.
75 (require 'planner-timeclock)
76 (require 'align)
77 (require 'time-date)
79 (eval-and-compile
80 ;; Workaround for Win2k time-date.el bug reported by David Lord
81 (unless (fboundp 'time-subtract)
82 (defalias 'time-subtract 'subtract-time)))
84 ;; User functions:
85 ;; planner-timeclock-summary-insinuate
86 ;; planner-timeclock-summary-update
87 ;; planner-timeclock-summary-show
88 ;; planner-timeclock-summary-show-range
89 ;; planner-timeclock-summary-show-filter
90 ;; planner-timeclock-summary-show-range-filter
92 ;;; Code:
94 ;;;_+ User variables
96 (defgroup planner-timeclock-summary nil
97 "Timeclock reports for planner.el."
98 :prefix "planner-timeclock-summary"
99 :group 'planner)
101 (defcustom planner-timeclock-summary-section "Timeclock"
102 "Header for the timeclock summary section in a plan page."
103 :type 'string
104 :group 'planner-timeclock-summary)
106 (defcustom planner-timeclock-summary-buffer "*Planner Timeclock Summary*"
107 "Buffer name for timeclock reports from `planner-timeclock-summary-show'."
108 :type 'string
109 :group 'planner-timeclock-summary)
111 (defcustom planner-timeclock-summary-not-planned-string "Not Planned"
112 "Project name for `timeclock-in' tasks without a project name."
113 :type 'string
114 :group 'planner-timeclock-summary)
116 (defcustom planner-timeclock-summary-placeholder-char "."
117 "Placeholder for blank cells in the report table.
118 If there are have blank cells in a simple table, the generated HTML
119 table will mess up. This character will be used as a placeholder."
120 :type 'character
121 :group 'planner-timeclock-summary)
123 (defcustom planner-timeclock-summary-include-sub-plan-pages-flag nil
124 "Non-nil means include 'sub plan pages' when doing plan page reports.
126 If non-nil, when updating timeclock reports on plan pages we will
127 also include plan pages which have this page's name as a prefix. If
128 nil, only exact matches will be included.
130 For example: if nil, on a plan page called 'Personal' we would only
131 include timeclock data marked as 'Personal' (this is the default
132 behaviour). If non-nil, we would additionally include
133 'PersonalHomework', 'PersonalYodeling' etc."
134 :type 'boolean
135 :group 'planner-timeclock-summary)
137 (defcustom planner-timeclock-summary-summary-string
138 "\n\nDay began: %B, Day ended: %E\nTime elapsed: %S, \
139 Time clocked: %C\nTime clocked ratio: %R\n"
140 "The string below the report table.
142 %B the first time checked in the day
143 %L the last time checked in the day
144 %E the last time checked in the day, or the current time if it's today
145 %s span, the difference between %B and %L
146 %S span, the difference between %B and %E
147 %C the total time clocked
148 %r clocked/%s
149 %R clocked/%S"
150 :type 'string
151 :group 'planner-timeclock-summary)
153 ;;;_+ Internal variables and utility functions
155 (defvar planner-timeclock-summary-empty-cell-string "====="
156 "Internal use, don't touch.")
158 (defvar planner-timeclock-summary-total-cell-string "======="
159 "Internal use, don't touch.")
161 (defun planner-timeclock-within-date-range (start-date end-date test-date)
162 "Return non-nil if START-DATE and END-DATE contain TEST-DATE.
163 Dates should be of the form YYYY/MM/DD or YYYY.MM.DD."
164 (not (or (string< test-date start-date)
165 (string< end-date test-date))))
167 ;;;_+ Data extraction
169 (defun planner-timeclock-summary-day-range-entry (start-date end-date &optional filter)
170 "Return the data between START-DATE and END-DATE (inclusive)
171 START-DATE and END-DATE should be strings of the form YYYY/MM/DD.
173 If FILTER is a regexp, only plan pages matching that regexp will
174 be included. If FILTER is a function, it will be called with the
175 current timeclock item, and the line considered if the function
176 returned non-nil.
178 Use the format specified in timeclock.el."
179 (let ((day-list (timeclock-day-alist))
180 entry-list
181 item
182 day)
183 (while day-list
184 (setq day (car day-list))
185 (setq day-list (cdr day-list))
186 (when (and (or (not start-date)
187 (planner-timeclock-within-date-range start-date end-date (car day))))
188 (setq entry-list (append (cddr day) entry-list))))
189 (unless (or (null filter)
190 (and (not (functionp filter))
191 (string= filter "")))
192 (setq entry-list
193 (delq nil
194 (mapcar
195 (lambda (item)
196 (when (cond
197 ((stringp filter)
198 (and (nth 2 item) (string-match filter (nth 2 item))))
199 ((functionp filter)
200 (funcall filter item)))
201 item))
202 entry-list))))
203 (setq entry-list
204 (cons (cond ((not start-date) (prin1-to-string filter))
205 ((string= start-date end-date) start-date)
206 (t (concat start-date " - " end-date)))
207 entry-list))))
209 (defun planner-timeclock-summary-one-day-entry (date)
210 "Return the data associated with DATE.
211 DATE should be a string of the form YYYY/MM/DD."
212 (planner-timeclock-summary-day-range-entry date date))
214 (defun planner-timeclock-summary-one-day-entry-no-date (date)
215 "Return the entries for DATE.
216 DATE should be a string of the form YYYY/MM/DD."
217 (let ((entry-list (planner-timeclock-summary-day-range-entry date date)))
218 (cdr entry-list)))
220 (defun planner-timeclock-summary-one-day-alist (date)
221 "Return the entries for DATE as an alist.
222 DATE should be a string of the form YYYY/MM/DD."
223 (let ((entry-list (planner-timeclock-summary-day-range-entry date date)))
224 (cddr entry-list)))
226 (defun planner-timeclock-summary-day-range-alist (start-date end-date)
227 "Return the entries between START-DATE and END-DATE (inclusive) as an alist.
228 START-DATE and END-DATE should be strings of the form YYYY/MM/DD."
229 (let ((entry-list (planner-timeclock-summary-day-range-entry start-date end-date)))
230 (cddr entry-list)))
232 (defun planner-timeclock-summary-extract-data (data-list)
233 "Return the timeclock data for dates included in DATA-LIST."
234 (with-planner
235 (let (target-data task-data entry)
236 (while data-list
237 (setq entry (car data-list))
238 (setq data-list (cdr data-list))
239 (setq task-data (planner-timeclock-task-info entry))
240 (let* ((plan (planner-timeclock-task-plan task-data))
241 (entry-project-name (if plan
242 (planner-link-base plan)
243 planner-timeclock-summary-not-planned-string))
244 (entry-task-name (planner-timeclock-task-description task-data))
245 (entry-task-length (planner-timeclock-task-length task-data)))
246 ;; total time
247 (if target-data
248 (setcar target-data (+ (car target-data) entry-task-length))
249 (setq target-data (list entry-task-length)))
250 ;; updating project
251 (let ((projects (cdr target-data))
252 project-found
253 project)
254 (while projects
255 (setq project (car projects))
256 (let ((project-name (caar project))
257 (project-time (car (cdar project))))
258 (if (and project-name
259 (string-equal project-name entry-project-name))
260 ;; the same project has been recorded, updating tasks
261 (let ((tasks (cdr project))
262 task-found
263 task)
264 (while tasks
265 (setq task (car tasks))
266 (let ((task-name (car (cdr (cdr task)))))
267 (if (and task-name
268 (string-equal task-name
269 entry-task-name))
270 ;; the same task has been recorded, add
271 ;; time
272 (progn
273 (setcar task (+ (car task)
274 entry-task-length))
275 (setq tasks nil)
276 (setq task-found t))
277 (setq tasks (cdr tasks)))))
278 ;; make a new task record
279 (if (not task-found)
280 (setcar projects
281 (add-to-list 'project (list entry-task-length
283 entry-task-name) t)))
284 ;; update project time
285 (setcar (cdar project) (+ project-time
286 entry-task-length))
287 (setq projects nil)
288 (setq project-found t))
289 (setq projects (cdr projects)))))
290 ;; make a new project record
291 (if (not project-found)
292 (add-to-list 'target-data (list (list entry-project-name
293 entry-task-length
295 (list entry-task-length
297 entry-task-name)) t)))))
298 target-data)))
300 (defun planner-timeclock-summary-extract-data-day (date)
301 "Prepare the data for the summary for DATE.
302 Read `timeclock-file' and return an alist. The list will be of the form:
303 (TotalTime
304 (((Project1Name Project1Time Project1Ratio) (p1t1time p1t1ratio p1t1name)
305 (p1t2time p1t2ratio p1t2name)
306 ...)
307 ((p2name p2time p2ratio) ...)))"
308 (planner-timeclock-summary-extract-data
309 (planner-timeclock-summary-one-day-alist date)))
311 (defun planner-timeclock-summary-make-summary-string-range
312 (start-date end-date total &optional filter)
313 "Use `planner-timeclock-summary-summary-string' from START-DATE to END-DATE.
314 Dates are in format YYYY/MM/DD. TOTAL is the total time clocked
315 today, in seconds.
317 If FILTER is a regexp, only plan pages matching that regexp will
318 be included. If FILTER is a function, it will be called with the
319 current timeclock item, and the line considered if the function
320 returned non-nil."
321 (let* ((target-string planner-timeclock-summary-summary-string)
322 (data (planner-timeclock-summary-day-range-entry start-date end-date filter))
323 begin end last span2 span
324 (case-fold-search nil))
325 (setq begin (timeclock-day-begin data))
326 (setq last (timeclock-day-end data))
327 (if (string-equal end-date (format-time-string "%Y/%m/%d"))
328 (setq end (current-time))
329 (setq end last))
330 (setq span (timeclock-time-to-seconds (time-subtract last begin)))
331 (setq span2 (timeclock-time-to-seconds (time-subtract end begin)))
332 (mapcar
333 (lambda (replacement)
334 (setq target-string
335 (planner-replace-regexp-in-string
336 (car replacement)
337 (cdr replacement)
338 target-string
339 t t)))
340 (list (cons "%B" (format-time-string "%H:%M:%S" begin))
341 (cons "%E" (format-time-string "%H:%M:%S" end))
342 (cons "%L" (format-time-string "%H:%M:%S" last))
343 (cons "%s" (timeclock-seconds-to-string span t))
344 (cons "%S" (timeclock-seconds-to-string span2 t))
345 (cons "%C" (timeclock-seconds-to-string total t))
346 (cons "%r" (format "%2.1f%%" (* 100 (/ total span))))
347 (cons "%R" (format "%2.1f%%" (* 100 (/ total span2))))))
348 target-string))
350 (defun planner-timeclock-summary-make-summary-string (date total)
351 "Convenience function for getting the summary string for DATE.
352 DATE is in the form YYYY/MM/DD. TOTAL is the total time clocked
353 today, in seconds."
354 (planner-timeclock-summary-make-summary-string-range date date total))
356 (defun planner-timeclock-summary-calculate-ratio-day (start-date &optional end-date filter)
357 "Calculate time ratio for START-DATE to END-DATE.
359 If FILTER is a regexp, only plan pages matching that regexp will
360 be included. If FILTER is a function, it will be called with the
361 current timeclock item, and the line considered if the function
362 returned non-nil."
363 (when (not end-date)
364 (setq end-date start-date))
365 (let (target-data)
366 (setq target-data (planner-timeclock-summary-extract-data
367 (cdr (planner-timeclock-summary-day-range-entry
368 start-date end-date filter))))
369 (let ((total (car target-data))
370 (projects (cdr target-data)))
371 (while projects
372 (let ((project (car projects))
373 (tasks (cdar projects)))
374 (setcar (cdr (cdar project)) (/ (car (cdar project)) total))
375 (while tasks
376 (let ((task (car tasks)))
377 (setcar (cdr task) (/ (car task) total))
378 (setq tasks (cdr tasks))))
379 (setq projects (cdr projects)))))
380 target-data))
382 ;;;_+ Presentation
384 (defun planner-timeclock-summary-make-text-table-day
385 (start-date &optional end-date filter hide-summary)
386 "Make the summary table for START-DATE to END-DATE using plain text.
388 If FILTER is a regexp, only plan pages matching that regexp will
389 be included. If FILTER is a function, it will be called with the
390 current item, and the line considered if the function
391 returned non-nil.
393 If START-DATE is nil, then it will ignore the date information
394 and return data for everything. If HIDE-SUMMARY is non-nil, do
395 not include the summary."
396 (unless end-date (setq end-date start-date))
397 (let (source-list)
398 (setq source-list (planner-timeclock-summary-calculate-ratio-day
399 start-date end-date filter))
400 (let ((projects (cdr source-list))
401 (total (car source-list))
402 (project-name-format "20.20s"))
403 (if total
404 (with-temp-buffer
405 (erase-buffer)
406 (insert "Project | Time | Ratio | Task\n")
407 (while projects
408 (let ((project-data (caar projects))
409 (tasks (cdar projects))
410 (first-task t))
411 (mapcar (lambda (task)
412 (let* ((project-link (car project-data))
413 (desc-link (if first-task
414 (if (or (null project-link)
415 (string= project-link planner-timeclock-summary-not-planned-string))
416 planner-timeclock-summary-not-planned-string
417 (planner-make-link project-link))
418 planner-timeclock-summary-placeholder-char)))
419 (setq first-task nil)
420 (insert
421 (format "%s | %8s | %4s%% | %s\n"
422 desc-link
423 (timeclock-seconds-to-string (car task) t)
424 (format "%2.1f" (* 100 (cadr task)))
425 (car (cddr task))))))
426 tasks)
427 (insert (format "Total: | %8s | %4s%% | %s\n"
428 (timeclock-seconds-to-string (cadr project-data) t)
429 (format "%2.1f" (* 100 (nth 2 project-data)))
430 planner-timeclock-summary-placeholder-char)))
431 (setq projects (cdr projects)))
432 (planner-align-table)
433 (goto-char (point-max))
434 (unless hide-summary
435 (insert (planner-timeclock-summary-make-summary-string-range
436 start-date end-date total filter)))
437 (buffer-string))
438 "No entries\n"))))
440 ;;;###autoload
441 (defun planner-timeclock-summary-insinuate ()
442 "Automatically call `planner-timeclock-summary-update'.
443 This function is called when the day page is saved."
444 (add-hook 'planner-mode-hook
445 (lambda ()
446 (add-hook
447 (cond
448 ((boundp 'write-file-functions) 'write-file-functions)
449 ((boundp 'local-write-file-hooks) 'local-write-file-hooks)
450 ((boundp 'write-file-hooks) 'write-file-hooks))
451 'planner-timeclock-summary-update nil t))))
453 ;;;###autoload
454 (defun planner-timeclock-summary-update ()
455 "Update `planner-timeclock-summary-section'. in the current day page.
456 The section is updated only if it exists."
457 (interactive)
458 (save-excursion
459 (save-restriction
460 (when (planner-narrow-to-section planner-timeclock-summary-section)
461 (delete-region (point-min) (point-max))
462 (let ((thepage (planner-page-name)))
463 (insert "* " planner-timeclock-summary-section "\n\n")
464 (insert (if (and thepage (string-match planner-date-regexp thepage))
465 (planner-timeclock-summary-make-text-table-day
466 (planner-replace-regexp-in-string "\\." "/"
467 thepage t t))
468 (planner-timeclock-summary-make-text-table-day
469 nil nil
470 (concat "^" thepage
471 (unless planner-timeclock-summary-include-sub-plan-pages-flag ":"))
473 " \n"))))))
475 ;;;###autoload
476 (defun planner-timeclock-summary-show (&optional date)
477 "Display a buffer with the timeclock summary for DATE.
478 Date is a string in the form YYYY.MM.DD."
479 (interactive (list (planner-read-date)))
480 (planner-timeclock-summary-show-range date date))
482 ;;;###autoload
483 (defun planner-timeclock-summary-show-filter (filter date)
484 "Show a timeclock report filtered by FILTER for DATE.
486 If FILTER is a regexp, only plan pages matching that regexp will
487 be included. If FILTER is a function, it will be called with the
488 current timeclock item, and the line considered if the function
489 returned non-nil.
491 If called interactively, prompt for FILTER (a regexp) and DATE.
492 DATE is a string in the form YYYY.MM.DD and can be nil."
493 (interactive
494 (list
495 (read-string "Filter (regexp): " nil 'regexp-history)
496 (planner-read-date)))
497 (planner-timeclock-summary-show-range date date filter))
499 ;;;###autoload
500 (defun planner-timeclock-summary-show-range-filter (filter start-date end-date)
501 "Show a timeclock report filtered by FILTER for START-DATE to END-DATE.
503 If FILTER is a regexp, only plan pages matching that regexp will
504 be included. If FILTER is a function, it will be called with the
505 current timeclock item, and the line considered if the function
506 returned non-nil.
508 If called interactively, prompt for FILTER (a regexp), START-DATE and END-DATE.
509 Dates are strings in the form YYYY.MM.DD and can be nil."
510 (interactive
511 (list
512 (read-string "Filter (regexp): " nil 'regexp-history)
513 (planner-read-date "Start")
514 (planner-read-date "End")))
515 (planner-timeclock-summary-show-range start-date end-date filter))
517 (defun planner-timeclock-summary-show-range (start-date end-date &optional filter)
518 "Show a timeclock report for the date range START-DATE to END-DATE.
520 If FILTER is a regexp, only plan pages matching that regexp will
521 be included. If FILTER is a function, it will be called with the
522 current timeclock item, and the line considered if the function
523 returned non-nil.
525 Dates are strings in the form YYYY.MM.DD and can be nil."
526 (interactive (list (planner-read-date "Start") (planner-read-date "End")))
527 (switch-to-buffer (get-buffer-create planner-timeclock-summary-buffer))
528 (erase-buffer)
529 (let ((muse-current-project (muse-project planner-project)))
530 (insert "Timeclock summary report for "
531 (if (string-equal start-date end-date)
532 start-date
533 (concat start-date " - " end-date))
534 "\n\n"
535 (planner-timeclock-summary-make-text-table-day
536 (planner-replace-regexp-in-string "\\." "/" start-date t t)
537 (planner-replace-regexp-in-string "\\." "/" end-date t t)
538 filter))
539 (planner-mode))
540 (goto-char (point-min)))
542 ;;;_+ experimental code
544 (defcustom planner-timeclock-summary-task-project-summary-string
545 "*Project Summary*"
546 "Task name for project summary."
547 :type 'string
548 :group 'planner-timeclock-summary)
550 (defcustom planner-timeclock-summary-project-column-min-width 22
551 "Minimum width of the project column in the report table."
552 :type 'integer
553 :group 'planner-timeclock-summary)
555 (defcustom planner-timeclock-summary-time-column-min-width 8
556 "Minimum width of the time column in the report table."
557 :type 'integer
558 :group 'planner-timeclock-summary)
560 (defcustom planner-timeclock-summary-ratio-column-min-width 5
561 "Minimum width of the ratio column in the report table."
562 :type 'integer
563 :group 'planner-timeclock-summary)
565 (defcustom planner-timeclock-summary-task-column-min-width 40
566 "Minimum width of the task column in the report table."
567 :type 'integer
568 :group 'planner-timeclock-summary)
570 (defun planner-timeclock-summary-make-table-day (date start-point)
571 "Format `planner-timeclock-summary-make-text-table-day' neatly.
572 The report is prepared for DATE. START-POINT is not used."
573 ;; (with-temp-buffer
574 ;; (erase-buffer)
575 (insert (planner-timeclock-summary-make-text-table-day date))
576 ;; (planner-mode)
577 (redraw-display)
578 (table-capture
579 42 (point-max) "|" "\n" 'left
580 (list planner-timeclock-summary-project-column-min-width
581 planner-timeclock-summary-time-column-min-width
582 planner-timeclock-summary-ratio-column-min-width
583 planner-timeclock-summary-task-column-min-width))
584 ;; make "=====" cell empty and span above
585 ;; (goto-char (point-min))
586 ;; (while (search-forward
587 ;; planner-timeclock-summary-empty-cell-string)
588 ;; (beginning-of-line)
589 ;; (kill-line)
590 ;; (table-span-cell 'above))
591 ;; (buffer-string))
594 ;;;###autoload
595 (defun planner-timeclock-summary-show-2 (&optional date)
596 "Display a buffer with the timeclock summary for DATE.
598 Date is a string in the form YYYY.MM.DD. It will be asked if not
599 given."
600 (interactive (planner-read-date))
601 (switch-to-buffer (get-buffer-create planner-timeclock-summary-buffer))
602 (erase-buffer)
603 (let ((muse-current-project (muse-project planner-project)))
604 (insert "Timeclock summary report for " date "\n\n")
605 (planner-mode)
606 (planner-timeclock-summary-make-table-day
607 (planner-replace-regexp-in-string "\\." "/" date t t) (point)))
608 (goto-char (point-min)))
610 (defun planner-timeclock-summary-table-span-cell-left ()
611 "Merge the current cell with the one to the left."
612 (table-span-cell 'left))
614 (defun planner-timeclock-summary-table-span-cell-above ()
615 "Merge the current cell with the one above it."
616 (table-span-cell 'above))
618 (provide 'planner-timeclock-summary)
619 ;;; planner-timeclock-summary.el ends here