1 ;;; planner-timeclock-summary.el --- timeclock summary for the Emacs planner
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
8 ;; Parts copyright (C) 2006, 2007 Software Freedom Law Center
10 ;; Keywords: emacs planner timeclock report summary
11 ;; Author: Dryice Liu <dryice AT liu DOT com DOT cn>
12 ;; Time-stamp: <16/07/2005 21:43:50 Yann Hodique>
13 ;; Description: Summary timeclock of a day
15 ;; This file is part of Planner. It is not part of GNU Emacs.
17 ;; Planner is free software; you can redistribute it and/or modify it
18 ;; under the terms of the GNU General Public License as published by
19 ;; the Free Software Foundation; either version 3, or (at your option)
22 ;; Planner is distributed in the hope that it will be useful, but
23 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
24 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
25 ;; General Public License for more details.
27 ;; You should have received a copy of the GNU General Public License
28 ;; along with Planner; see the file COPYING. If not, write to the
29 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
30 ;; Boston, MA 02110-1301, USA.
34 ;; planner-timeclock-summary.el produces timeclock reports for planner
37 ;; There are two ways you can use it:
39 ;; 1. Display a temporary buffer
41 ;; Call `planner-timeclock-summary-show' and Emacs will ask you which
42 ;; day's summary do you want. Choose the date as anywhere else of
43 ;; Emacs planner, and a tempory buffer will be displayed with the
44 ;; timeclock summary of that day.
46 ;; 2. Rewrite sections of your planner
48 ;; Choose this approach if you want timeclock summary to be in their
49 ;; own section and you would like them to be readable in your plain
50 ;; text files even outside Emacs. Caveat: The timeclock summary
51 ;; section should already exist in your template and will be rewritten
52 ;; when updated. Tip: Add `planner-timeclock-summary-section'
53 ;; (default: "Timeclock") to your `planner-day-page-template'.
55 ;; To use, call `planner-timeclock-summary-update' in the planner day
56 ;; page to update the section. If you want rewriting to be
57 ;; automatically performed, call `planner-timeclock-summary-insinuate'
58 ;; in your .emacs file
61 ;; to make a nice text table, you need align.el from
62 ;; http://www.newartisans.com/johnw/Emacs/align.el
69 ;; Chris Parsons submitted a patch that allows date ranges when
72 ;; Peter K. Lee made it so that even if there is an error with timelog
73 ;; file (which happens frequently), you don't lose the * Timeclock
74 ;; section, among other fixes.
76 (require 'planner-timeclock
)
81 ;; Workaround for Win2k time-date.el bug reported by David Lord
82 (unless (fboundp 'time-subtract
)
83 (defalias 'time-subtract
'subtract-time
)))
86 ;; planner-timeclock-summary-insinuate
87 ;; planner-timeclock-summary-update
88 ;; planner-timeclock-summary-show
89 ;; planner-timeclock-summary-show-range
90 ;; planner-timeclock-summary-show-filter
91 ;; planner-timeclock-summary-show-range-filter
97 (defgroup planner-timeclock-summary nil
98 "Timeclock reports for planner.el."
99 :prefix
"planner-timeclock-summary"
102 (defcustom planner-timeclock-summary-section
"Timeclock"
103 "Header for the timeclock summary section in a plan page."
105 :group
'planner-timeclock-summary
)
107 (defcustom planner-timeclock-summary-buffer
"*Planner Timeclock Summary*"
108 "Buffer name for timeclock reports from `planner-timeclock-summary-show'."
110 :group
'planner-timeclock-summary
)
112 (defcustom planner-timeclock-summary-not-planned-string
"Not Planned"
113 "Project name for `timeclock-in' tasks without a project name."
115 :group
'planner-timeclock-summary
)
117 (defcustom planner-timeclock-summary-placeholder-char
"."
118 "Placeholder for blank cells in the report table.
119 If there are have blank cells in a simple table, the generated HTML
120 table will mess up. This character will be used as a placeholder."
122 :group
'planner-timeclock-summary
)
124 (defcustom planner-timeclock-summary-include-sub-plan-pages-flag nil
125 "Non-nil means include 'sub plan pages' when doing plan page reports.
127 If non-nil, when updating timeclock reports on plan pages we will
128 also include plan pages which have this page's name as a prefix. If
129 nil, only exact matches will be included.
131 For example: if nil, on a plan page called 'Personal' we would only
132 include timeclock data marked as 'Personal' (this is the default
133 behaviour). If non-nil, we would additionally include
134 'PersonalHomework', 'PersonalYodeling' etc."
136 :group
'planner-timeclock-summary
)
138 (defcustom planner-timeclock-summary-summary-string
139 "\n\nDay began: %B, Day ended: %E\nTime elapsed: %S, \
140 Time clocked: %C\nTime clocked ratio: %R\n"
141 "The string below the report table.
143 %B the first time checked in the day
144 %L the last time checked in the day
145 %E the last time checked in the day, or the current time if it's today
146 %s span, the difference between %B and %L
147 %S span, the difference between %B and %E
148 %C the total time clocked
152 :group
'planner-timeclock-summary
)
154 ;;;_+ Internal variables and utility functions
156 (defvar planner-timeclock-summary-empty-cell-string
"====="
157 "Internal use, don't touch.")
159 (defvar planner-timeclock-summary-total-cell-string
"======="
160 "Internal use, don't touch.")
162 (defun planner-timeclock-within-date-range (start-date end-date test-date
)
163 "Return non-nil if START-DATE and END-DATE contain TEST-DATE.
164 Dates should be of the form YYYY/MM/DD or YYYY.MM.DD."
165 (not (or (string< test-date start-date
)
166 (string< end-date test-date
))))
168 ;;;_+ Data extraction
170 (defun planner-timeclock-summary-day-range-entry (start-date end-date
&optional filter
)
171 "Return the data between START-DATE and END-DATE (inclusive)
172 START-DATE and END-DATE should be strings of the form YYYY/MM/DD.
174 If FILTER is a regexp, only plan pages matching that regexp will
175 be included. If FILTER is a function, it will be called with the
176 current timeclock item, and the line considered if the function
179 Use the format specified in timeclock.el."
180 (let ((day-list (timeclock-day-alist))
185 (setq day
(car day-list
))
186 (setq day-list
(cdr day-list
))
187 (when (and (or (not start-date
)
188 (planner-timeclock-within-date-range start-date end-date
(car day
))))
189 (setq entry-list
(append (cddr day
) entry-list
))))
190 (unless (or (null filter
)
191 (and (not (functionp filter
))
192 (string= filter
"")))
199 (and (nth 2 item
) (string-match filter
(nth 2 item
))))
201 (funcall filter item
)))
205 (cons (cond ((not start-date
) (prin1-to-string filter
))
206 ((string= start-date end-date
) start-date
)
207 (t (concat start-date
" - " end-date
)))
210 (defun planner-timeclock-summary-one-day-entry (date)
211 "Return the data associated with DATE.
212 DATE should be a string of the form YYYY/MM/DD."
213 (planner-timeclock-summary-day-range-entry date date
))
215 (defun planner-timeclock-summary-one-day-entry-no-date (date)
216 "Return the entries for DATE.
217 DATE should be a string of the form YYYY/MM/DD."
218 (let ((entry-list (planner-timeclock-summary-day-range-entry date date
)))
221 (defun planner-timeclock-summary-one-day-alist (date)
222 "Return the entries for DATE as an alist.
223 DATE should be a string of the form YYYY/MM/DD."
224 (let ((entry-list (planner-timeclock-summary-day-range-entry date date
)))
227 (defun planner-timeclock-summary-day-range-alist (start-date end-date
)
228 "Return the entries between START-DATE and END-DATE (inclusive) as an alist.
229 START-DATE and END-DATE should be strings of the form YYYY/MM/DD."
230 (let ((entry-list (planner-timeclock-summary-day-range-entry start-date end-date
)))
233 (defun planner-timeclock-summary-extract-data (data-list)
234 "Return the timeclock data for dates included in DATA-LIST."
236 (let (target-data task-data entry
)
238 (setq entry
(car data-list
))
239 (setq data-list
(cdr data-list
))
240 (setq task-data
(planner-timeclock-task-info entry
))
241 (let* ((plan (planner-timeclock-task-plan task-data
))
242 (entry-project-name (if plan
243 (planner-link-base plan
)
244 planner-timeclock-summary-not-planned-string
))
245 (entry-task-name (planner-timeclock-task-description task-data
))
246 (entry-task-length (planner-timeclock-task-length task-data
)))
249 (setcar target-data
(+ (car target-data
) entry-task-length
))
250 (setq target-data
(list entry-task-length
)))
252 (let ((projects (cdr target-data
))
256 (setq project
(car projects
))
257 (let ((project-name (caar project
))
258 (project-time (car (cdar project
))))
259 (if (and project-name
260 (string-equal project-name entry-project-name
))
261 ;; the same project has been recorded, updating tasks
262 (let ((tasks (cdr project
))
266 (setq task
(car tasks
))
267 (let ((task-name (car (cdr (cdr task
)))))
269 (string-equal task-name
271 ;; the same task has been recorded, add
274 (setcar task
(+ (car task
)
278 (setq tasks
(cdr tasks
)))))
279 ;; make a new task record
282 (add-to-list 'project
(list entry-task-length
284 entry-task-name
) t
)))
285 ;; update project time
286 (setcar (cdar project
) (+ project-time
289 (setq project-found t
))
290 (setq projects
(cdr projects
)))))
291 ;; make a new project record
292 (if (not project-found
)
293 (add-to-list 'target-data
(list (list entry-project-name
296 (list entry-task-length
298 entry-task-name
)) t
)))))
301 (defun planner-timeclock-summary-extract-data-day (date)
302 "Prepare the data for the summary for DATE.
303 Read `timeclock-file' and return an alist. The list will be of the form:
305 (((Project1Name Project1Time Project1Ratio) (p1t1time p1t1ratio p1t1name)
306 (p1t2time p1t2ratio p1t2name)
308 ((p2name p2time p2ratio) ...)))"
309 (planner-timeclock-summary-extract-data
310 (planner-timeclock-summary-one-day-alist date
)))
312 (defun planner-timeclock-summary-make-summary-string-range
313 (start-date end-date total
&optional filter
)
314 "Use `planner-timeclock-summary-summary-string' from START-DATE to END-DATE.
315 Dates are in format YYYY/MM/DD. TOTAL is the total time clocked
318 If FILTER is a regexp, only plan pages matching that regexp will
319 be included. If FILTER is a function, it will be called with the
320 current timeclock item, and the line considered if the function
322 (let* ((target-string planner-timeclock-summary-summary-string
)
323 (data (planner-timeclock-summary-day-range-entry start-date end-date filter
))
324 begin end last span2 span
325 (case-fold-search nil
))
326 (setq begin
(timeclock-day-begin data
))
327 (setq last
(timeclock-day-end data
))
328 (if (string-equal end-date
(format-time-string "%Y/%m/%d"))
329 (setq end
(current-time))
331 (setq span
(timeclock-time-to-seconds (time-subtract last begin
)))
332 (setq span2
(timeclock-time-to-seconds (time-subtract end begin
)))
334 (lambda (replacement)
336 (planner-replace-regexp-in-string
341 (list (cons "%B" (format-time-string "%H:%M:%S" begin
))
342 (cons "%E" (format-time-string "%H:%M:%S" end
))
343 (cons "%L" (format-time-string "%H:%M:%S" last
))
344 (cons "%s" (timeclock-seconds-to-string span t
))
345 (cons "%S" (timeclock-seconds-to-string span2 t
))
346 (cons "%C" (timeclock-seconds-to-string total t
))
347 (cons "%r" (format "%2.1f%%" (* 100 (/ total span
))))
348 (cons "%R" (format "%2.1f%%" (* 100 (/ total span2
))))))
351 (defun planner-timeclock-summary-make-summary-string (date total
)
352 "Convenience function for getting the summary string for DATE.
353 DATE is in the form YYYY/MM/DD. TOTAL is the total time clocked
355 (planner-timeclock-summary-make-summary-string-range date date total
))
357 (defun planner-timeclock-summary-calculate-ratio-day (start-date &optional end-date filter
)
358 "Calculate time ratio for START-DATE to END-DATE.
360 If FILTER is a regexp, only plan pages matching that regexp will
361 be included. If FILTER is a function, it will be called with the
362 current timeclock item, and the line considered if the function
365 (setq end-date start-date
))
367 (setq target-data
(planner-timeclock-summary-extract-data
368 (cdr (planner-timeclock-summary-day-range-entry
369 start-date end-date filter
))))
370 (let ((total (car target-data
))
371 (projects (cdr target-data
)))
373 (let ((project (car projects
))
374 (tasks (cdar projects
)))
375 (setcar (cdr (cdar project
)) (/ (car (cdar project
)) total
))
377 (let ((task (car tasks
)))
378 (setcar (cdr task
) (/ (car task
) total
))
379 (setq tasks
(cdr tasks
))))
380 (setq projects
(cdr projects
)))))
385 (defun planner-timeclock-summary-make-text-table-day
386 (start-date &optional end-date filter hide-summary
)
387 "Make the summary table for START-DATE to END-DATE using plain text.
389 If FILTER is a regexp, only plan pages matching that regexp will
390 be included. If FILTER is a function, it will be called with the
391 current item, and the line considered if the function
394 If START-DATE is nil, then it will ignore the date information
395 and return data for everything. If HIDE-SUMMARY is non-nil, do
396 not include the summary."
397 (unless end-date
(setq end-date start-date
))
399 (setq source-list
(planner-timeclock-summary-calculate-ratio-day
400 start-date end-date filter
))
401 (let ((projects (cdr source-list
))
402 (total (car source-list
))
403 (project-name-format "20.20s"))
407 (insert "Project | Time | Ratio | Task\n")
409 (let ((project-data (caar projects
))
410 (tasks (cdar projects
))
412 (mapcar (lambda (task)
413 (let* ((project-link (car project-data
))
414 (desc-link (if first-task
415 (if (or (null project-link
)
416 (string= project-link planner-timeclock-summary-not-planned-string
))
417 planner-timeclock-summary-not-planned-string
418 (planner-make-link project-link
))
419 planner-timeclock-summary-placeholder-char
)))
420 (setq first-task nil
)
422 (format "%s | %8s | %4s%% | %s\n"
424 (timeclock-seconds-to-string (car task
) t
)
425 (format "%2.1f" (* 100 (cadr task
)))
426 (car (cddr task
))))))
428 (insert (format "Total: | %8s | %4s%% | %s\n"
429 (timeclock-seconds-to-string (cadr project-data
) t
)
430 (format "%2.1f" (* 100 (nth 2 project-data
)))
431 planner-timeclock-summary-placeholder-char
)))
432 (setq projects
(cdr projects
)))
433 (planner-align-table)
434 (goto-char (point-max))
436 (insert (planner-timeclock-summary-make-summary-string-range
437 start-date end-date total filter
)))
442 (defun planner-timeclock-summary-insinuate ()
443 "Automatically call `planner-timeclock-summary-update'.
444 This function is called when the day page is saved."
445 (add-hook 'planner-mode-hook
449 ((boundp 'write-file-functions
) 'write-file-functions
)
450 ((boundp 'local-write-file-hooks
) 'local-write-file-hooks
)
451 ((boundp 'write-file-hooks
) 'write-file-hooks
))
452 'planner-timeclock-summary-update nil t
))))
455 (defun planner-timeclock-summary-update ()
456 "Update `planner-timeclock-summary-section'. in the current day page.
457 The section is updated only if it exists."
461 (when (planner-narrow-to-section planner-timeclock-summary-section
)
462 (delete-region (point-min) (point-max))
463 (let ((thepage (planner-page-name)))
464 (insert "* " planner-timeclock-summary-section
"\n\n")
465 (insert (if (and thepage
(string-match planner-date-regexp thepage
))
466 (planner-timeclock-summary-make-text-table-day
467 (planner-replace-in-string thepage
"[\\.\\-]" "/" t
469 (planner-timeclock-summary-make-text-table-day
472 (unless planner-timeclock-summary-include-sub-plan-pages-flag
":"))
477 (defun planner-timeclock-summary-show (&optional date
)
478 "Display a buffer with the timeclock summary for DATE.
479 Date is a string in the form YYYY.MM.DD."
480 (interactive (list (planner-read-date)))
481 (planner-timeclock-summary-show-range date date
))
484 (defun planner-timeclock-summary-show-filter (filter date
)
485 "Show a timeclock report filtered by FILTER for DATE.
487 If FILTER is a regexp, only plan pages matching that regexp will
488 be included. If FILTER is a function, it will be called with the
489 current timeclock item, and the line considered if the function
492 If called interactively, prompt for FILTER (a regexp) and DATE.
493 DATE is a string in the form YYYY.MM.DD and can be nil."
496 (read-string "Filter (regexp): " nil
'regexp-history
)
497 (planner-read-date)))
498 (planner-timeclock-summary-show-range date date filter
))
501 (defun planner-timeclock-summary-show-range-filter (filter start-date end-date
)
502 "Show a timeclock report filtered by FILTER for START-DATE to END-DATE.
504 If FILTER is a regexp, only plan pages matching that regexp will
505 be included. If FILTER is a function, it will be called with the
506 current timeclock item, and the line considered if the function
509 If called interactively, prompt for FILTER (a regexp), START-DATE and END-DATE.
510 Dates are strings in the form YYYY.MM.DD and can be nil."
513 (read-string "Filter (regexp): " nil
'regexp-history
)
514 (planner-read-date "Start")
515 (planner-read-date "End")))
516 (planner-timeclock-summary-show-range start-date end-date filter
))
518 (defun planner-timeclock-summary-show-range (start-date end-date
&optional filter
)
519 "Show a timeclock report for the date range START-DATE to END-DATE.
521 If FILTER is a regexp, only plan pages matching that regexp will
522 be included. If FILTER is a function, it will be called with the
523 current timeclock item, and the line considered if the function
526 Dates are strings in the form YYYY.MM.DD and can be nil."
527 (interactive (list (planner-read-date "Start") (planner-read-date "End")))
528 (switch-to-buffer (get-buffer-create planner-timeclock-summary-buffer
))
530 (let ((muse-current-project (muse-project planner-project
)))
531 (insert "Timeclock summary report for "
532 (if (string-equal start-date end-date
)
534 (concat start-date
" - " end-date
))
536 (planner-timeclock-summary-make-text-table-day
537 (planner-replace-regexp-in-string "[\\.\\-]" "/" start-date t t
)
538 (planner-replace-regexp-in-string "[\\.\\-]" "/" end-date t t
)
541 (goto-char (point-min)))
543 ;;;_+ experimental code
545 (defcustom planner-timeclock-summary-task-project-summary-string
547 "Task name for project summary."
549 :group
'planner-timeclock-summary
)
551 (defcustom planner-timeclock-summary-project-column-min-width
22
552 "Minimum width of the project column in the report table."
554 :group
'planner-timeclock-summary
)
556 (defcustom planner-timeclock-summary-time-column-min-width
8
557 "Minimum width of the time column in the report table."
559 :group
'planner-timeclock-summary
)
561 (defcustom planner-timeclock-summary-ratio-column-min-width
5
562 "Minimum width of the ratio column in the report table."
564 :group
'planner-timeclock-summary
)
566 (defcustom planner-timeclock-summary-task-column-min-width
40
567 "Minimum width of the task column in the report table."
569 :group
'planner-timeclock-summary
)
571 (defun planner-timeclock-summary-make-table-day (date start-point
)
572 "Format `planner-timeclock-summary-make-text-table-day' neatly.
573 The report is prepared for DATE. START-POINT is not used."
576 (insert (planner-timeclock-summary-make-text-table-day date
))
580 42 (point-max) "|" "\n" 'left
581 (list planner-timeclock-summary-project-column-min-width
582 planner-timeclock-summary-time-column-min-width
583 planner-timeclock-summary-ratio-column-min-width
584 planner-timeclock-summary-task-column-min-width
))
585 ;; make "=====" cell empty and span above
586 ;; (goto-char (point-min))
587 ;; (while (search-forward
588 ;; planner-timeclock-summary-empty-cell-string)
589 ;; (beginning-of-line)
591 ;; (table-span-cell 'above))
596 (defun planner-timeclock-summary-show-2 (&optional date
)
597 "Display a buffer with the timeclock summary for DATE.
599 Date is a string in the form YYYY.MM.DD. It will be asked if not
601 (interactive (planner-read-date))
602 (switch-to-buffer (get-buffer-create planner-timeclock-summary-buffer
))
604 (let ((muse-current-project (muse-project planner-project
)))
605 (insert "Timeclock summary report for " date
"\n\n")
607 (planner-timeclock-summary-make-table-day
608 (planner-replace-regexp-in-string "[\\.\\-]" "/" date t t
) (point)))
609 (goto-char (point-min)))
611 (defun planner-timeclock-summary-table-span-cell-left ()
612 "Merge the current cell with the one to the left."
613 (table-span-cell 'left
))
615 (defun planner-timeclock-summary-table-span-cell-above ()
616 "Merge the current cell with the one above it."
617 (table-span-cell 'above
))
619 (provide 'planner-timeclock-summary
)
620 ;;; planner-timeclock-summary.el ends here