Remember is no longer a separate package.
[planner-el.git] / planner-timeclock-summary.el
blob5deaaac4e43da77afc097642a2f65075f4f05477
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
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)
20 ;; any later version.
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.
32 ;;; Commentary:
34 ;; planner-timeclock-summary.el produces timeclock reports for planner
35 ;; files.
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
60 ;;; REQUIRE
61 ;; to make a nice text table, you need align.el from
62 ;; http://www.newartisans.com/johnw/Emacs/align.el
64 ;;; TODO
65 ;; - sort?
67 ;;; Contributors:
69 ;; Chris Parsons submitted a patch that allows date ranges when
70 ;; summarizing data.
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)
77 (require 'align)
78 (require 'time-date)
80 (eval-and-compile
81 ;; Workaround for Win2k time-date.el bug reported by David Lord
82 (unless (fboundp 'time-subtract)
83 (defalias 'time-subtract 'subtract-time)))
85 ;; User functions:
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
93 ;;; Code:
95 ;;;_+ User variables
97 (defgroup planner-timeclock-summary nil
98 "Timeclock reports for planner.el."
99 :prefix "planner-timeclock-summary"
100 :group 'planner)
102 (defcustom planner-timeclock-summary-section "Timeclock"
103 "Header for the timeclock summary section in a plan page."
104 :type 'string
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'."
109 :type 'string
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."
114 :type 'string
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."
121 :type 'character
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."
135 :type 'boolean
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
149 %r clocked/%s
150 %R clocked/%S"
151 :type 'string
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
177 returned non-nil.
179 Use the format specified in timeclock.el."
180 (let ((day-list (timeclock-day-alist))
181 entry-list
182 item
183 day)
184 (while day-list
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 "")))
193 (setq entry-list
194 (delq nil
195 (mapcar
196 (lambda (item)
197 (when (cond
198 ((stringp filter)
199 (and (nth 2 item) (string-match filter (nth 2 item))))
200 ((functionp filter)
201 (funcall filter item)))
202 item))
203 entry-list))))
204 (setq entry-list
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)))
208 entry-list))))
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)))
219 (cdr entry-list)))
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)))
225 (cddr entry-list)))
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)))
231 (cddr entry-list)))
233 (defun planner-timeclock-summary-extract-data (data-list)
234 "Return the timeclock data for dates included in DATA-LIST."
235 (with-planner
236 (let (target-data task-data entry)
237 (while data-list
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)))
247 ;; total time
248 (if target-data
249 (setcar target-data (+ (car target-data) entry-task-length))
250 (setq target-data (list entry-task-length)))
251 ;; updating project
252 (let ((projects (cdr target-data))
253 project-found
254 project)
255 (while projects
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))
263 task-found
264 task)
265 (while tasks
266 (setq task (car tasks))
267 (let ((task-name (car (cdr (cdr task)))))
268 (if (and task-name
269 (string-equal task-name
270 entry-task-name))
271 ;; the same task has been recorded, add
272 ;; time
273 (progn
274 (setcar task (+ (car task)
275 entry-task-length))
276 (setq tasks nil)
277 (setq task-found t))
278 (setq tasks (cdr tasks)))))
279 ;; make a new task record
280 (if (not task-found)
281 (setcar projects
282 (add-to-list 'project (list entry-task-length
284 entry-task-name) t)))
285 ;; update project time
286 (setcar (cdar project) (+ project-time
287 entry-task-length))
288 (setq projects nil)
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
294 entry-task-length
296 (list entry-task-length
298 entry-task-name)) t)))))
299 target-data)))
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:
304 (TotalTime
305 (((Project1Name Project1Time Project1Ratio) (p1t1time p1t1ratio p1t1name)
306 (p1t2time p1t2ratio p1t2name)
307 ...)
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
316 today, in seconds.
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
321 returned non-nil."
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))
330 (setq end last))
331 (setq span (timeclock-time-to-seconds (time-subtract last begin)))
332 (setq span2 (timeclock-time-to-seconds (time-subtract end begin)))
333 (mapcar
334 (lambda (replacement)
335 (setq target-string
336 (planner-replace-regexp-in-string
337 (car replacement)
338 (cdr replacement)
339 target-string
340 t t)))
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))))))
349 target-string))
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
354 today, in seconds."
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
363 returned non-nil."
364 (when (not end-date)
365 (setq end-date start-date))
366 (let (target-data)
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)))
372 (while projects
373 (let ((project (car projects))
374 (tasks (cdar projects)))
375 (setcar (cdr (cdar project)) (/ (car (cdar project)) total))
376 (while tasks
377 (let ((task (car tasks)))
378 (setcar (cdr task) (/ (car task) total))
379 (setq tasks (cdr tasks))))
380 (setq projects (cdr projects)))))
381 target-data))
383 ;;;_+ Presentation
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
392 returned non-nil.
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))
398 (let (source-list)
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"))
404 (if total
405 (with-temp-buffer
406 (erase-buffer)
407 (insert "Project | Time | Ratio | Task\n")
408 (while projects
409 (let ((project-data (caar projects))
410 (tasks (cdar projects))
411 (first-task t))
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)
421 (insert
422 (format "%s | %8s | %4s%% | %s\n"
423 desc-link
424 (timeclock-seconds-to-string (car task) t)
425 (format "%2.1f" (* 100 (cadr task)))
426 (car (cddr task))))))
427 tasks)
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))
435 (unless hide-summary
436 (insert (planner-timeclock-summary-make-summary-string-range
437 start-date end-date total filter)))
438 (buffer-string))
439 "No entries\n"))))
441 ;;;###autoload
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
446 (lambda ()
447 (add-hook
448 (cond
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))))
454 ;;;###autoload
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."
458 (interactive)
459 (save-excursion
460 (save-restriction
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
468 thepage t t))
469 (planner-timeclock-summary-make-text-table-day
470 nil nil
471 (concat "^" thepage
472 (unless planner-timeclock-summary-include-sub-plan-pages-flag ":"))
474 " \n"))))))
476 ;;;###autoload
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))
483 ;;;###autoload
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
490 returned non-nil.
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."
494 (interactive
495 (list
496 (read-string "Filter (regexp): " nil 'regexp-history)
497 (planner-read-date)))
498 (planner-timeclock-summary-show-range date date filter))
500 ;;;###autoload
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
507 returned non-nil.
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."
511 (interactive
512 (list
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
524 returned non-nil.
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))
529 (erase-buffer)
530 (let ((muse-current-project (muse-project planner-project)))
531 (insert "Timeclock summary report for "
532 (if (string-equal start-date end-date)
533 start-date
534 (concat start-date " - " end-date))
535 "\n\n"
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)
539 filter))
540 (planner-mode))
541 (goto-char (point-min)))
543 ;;;_+ experimental code
545 (defcustom planner-timeclock-summary-task-project-summary-string
546 "*Project Summary*"
547 "Task name for project summary."
548 :type 'string
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."
553 :type 'integer
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."
558 :type 'integer
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."
563 :type 'integer
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."
568 :type 'integer
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."
574 ;; (with-temp-buffer
575 ;; (erase-buffer)
576 (insert (planner-timeclock-summary-make-text-table-day date))
577 ;; (planner-mode)
578 (redraw-display)
579 (table-capture
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)
590 ;; (kill-line)
591 ;; (table-span-cell 'above))
592 ;; (buffer-string))
595 ;;;###autoload
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
600 given."
601 (interactive (planner-read-date))
602 (switch-to-buffer (get-buffer-create planner-timeclock-summary-buffer))
603 (erase-buffer)
604 (let ((muse-current-project (muse-project planner-project)))
605 (insert "Timeclock summary report for " date "\n\n")
606 (planner-mode)
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