1 ;;; planner-timeclock-summary.el --- timeclock summary for the Emacs planner
4 ;; Copyright (C) 2004, 2005 Dryice Dong Liu . All rights reserved.
5 ;; Parts copyright (C) 2004, 2005 Chris Parsons (chris.p AT rsons.org)
6 ;; Parts copyright (C) 2004, 2005 Free Software Foundation, Inc.
7 ;; Parts copyright (C) 2005 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 2, or (at your option)
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.
33 ;; planner-timeclock-summary.el produces timeclock reports for planner
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
60 ;; to make a nice text table, you need align.el from
61 ;; http://www.newartisans.com/johnw/Emacs/align.el
68 ;; Chris Parsons submitted a patch that allows date ranges when
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
)
80 ;; Workaround for Win2k time-date.el bug reported by David Lord
81 (unless (fboundp 'time-subtract
)
82 (defalias 'time-subtract
'subtract-time
)))
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
96 (defgroup planner-timeclock-summary nil
97 "Timeclock reports for planner.el."
98 :prefix
"planner-timeclock-summary"
101 (defcustom planner-timeclock-summary-section
"Timeclock"
102 "Header for the timeclock summary section in a plan page."
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'."
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."
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."
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."
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
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
178 Use the format specified in timeclock.el."
179 (let ((day-list (timeclock-day-alist))
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
"")))
198 (and (nth 2 item
) (string-match filter
(nth 2 item
))))
200 (funcall filter item
)))
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
)))
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
)))
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
)))
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
)))
232 (defun planner-timeclock-summary-extract-data (data-list)
233 "Return the timeclock data for dates included in DATA-LIST."
235 (let (target-data task-data entry
)
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
)))
248 (setcar target-data
(+ (car target-data
) entry-task-length
))
249 (setq target-data
(list entry-task-length
)))
251 (let ((projects (cdr target-data
))
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
))
265 (setq task
(car tasks
))
266 (let ((task-name (car (cdr (cdr task
)))))
268 (string-equal task-name
270 ;; the same task has been recorded, add
273 (setcar task
(+ (car task
)
277 (setq tasks
(cdr tasks
)))))
278 ;; make a new task record
281 (add-to-list 'project
(list entry-task-length
283 entry-task-name
) t
)))
284 ;; update project time
285 (setcar (cdar project
) (+ project-time
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
295 (list entry-task-length
297 entry-task-name
)) t
)))))
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:
304 (((Project1Name Project1Time Project1Ratio) (p1t1time p1t1ratio p1t1name)
305 (p1t2time p1t2ratio p1t2name)
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
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
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))
330 (setq span
(timeclock-time-to-seconds (time-subtract last begin
)))
331 (setq span2
(timeclock-time-to-seconds (time-subtract end begin
)))
333 (lambda (replacement)
335 (planner-replace-regexp-in-string
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
))))))
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
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
364 (setq end-date start-date
))
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
)))
372 (let ((project (car projects
))
373 (tasks (cdar projects
)))
374 (setcar (cdr (cdar project
)) (/ (car (cdar project
)) total
))
376 (let ((task (car tasks
)))
377 (setcar (cdr task
) (/ (car task
) total
))
378 (setq tasks
(cdr tasks
))))
379 (setq projects
(cdr projects
)))))
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
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
))
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"))
406 (insert "Project | Time | Ratio | Task\n")
408 (let ((project-data (caar projects
))
409 (tasks (cdar projects
))
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
)
421 (format "%s | %8s | %4s%% | %s\n"
423 (timeclock-seconds-to-string (car task
) t
)
424 (format "%2.1f" (* 100 (cadr task
)))
425 (car (cddr task
))))))
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))
435 (insert (planner-timeclock-summary-make-summary-string-range
436 start-date end-date total filter
)))
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
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
))))
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."
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 "\\." "/"
468 (planner-timeclock-summary-make-text-table-day
471 (unless planner-timeclock-summary-include-sub-plan-pages-flag
":"))
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
))
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
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."
495 (read-string "Filter (regexp): " nil
'regexp-history
)
496 (planner-read-date)))
497 (planner-timeclock-summary-show-range date date filter
))
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
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."
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
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
))
529 (let ((muse-current-project (muse-project planner-project
)))
530 (insert "Timeclock summary report for "
531 (if (string-equal start-date end-date
)
533 (concat start-date
" - " end-date
))
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
)
540 (goto-char (point-min)))
542 ;;;_+ experimental code
544 (defcustom planner-timeclock-summary-task-project-summary-string
546 "Task name for project summary."
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."
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."
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."
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."
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."
575 (insert (planner-timeclock-summary-make-text-table-day date
))
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)
590 ;; (table-span-cell 'above))
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
600 (interactive (planner-read-date))
601 (switch-to-buffer (get-buffer-create planner-timeclock-summary-buffer
))
603 (let ((muse-current-project (muse-project planner-project
)))
604 (insert "Timeclock summary report for " date
"\n\n")
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