emacs-wiki-discuss
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[emacs-wiki-discuss] planner-timeclock-summary.el: report timeclock info


From: Dryice Liu
Subject: [emacs-wiki-discuss] planner-timeclock-summary.el: report timeclock information
Date: Fri, 19 Nov 2004 09:22:06 +0800
User-agent: Gnus/5.1006 (Gnus v5.10.6) Emacs/21.3 (berkeley-unix)

I spend most of my day before the screen and I use
planner-timeclock.el to clock in and clock out my planner tasks. And I
scratched this planner-timeclock-summary.el to generate a report about
how my time was killed. I have a screen shot at
http://www.smth.edu.cn/bbscon.php?bid=573&id=17157&ap=706 
Basicly it will report how much of my time was spend on each project
on a given day.

It models planner-accomplishments.el and thank you, Sacha :)

This is my first lisp so welcome any comments and suggestions.

In fact I have a problem here: in the "experimental code" at the end
of the file, I tried to use table.el to make a nicer table but
failed. The table messed up if there's a long annotation in the task
description. The wired thing is, if I call
planner-timeclock-summary-show-2 with edebug, it works fine. I guess
this depends on if the target buffer is visible so has something to do
with the time planner buffer get rendered.

Anyway planner-timeclock-summary-show and
planner-timeclock-summary-update works fine. At least for me :)

Here's the code:
----------------------------------------------------------------------
;;; planner-timeclock-summary.el --- timeclock summary for the Emacs planner
;;

;; Keywords: emacs planner timeclock report summary
;; Author: Dryice Liu <dryice AT liu DOT com DOT cn>
;; Time-stamp: <2004-11-19 09:03:51 Dryice Liu>
;; Description: Summary timeclock of a day

;; This file is not part of GNU Emacs.

;; Copyright (C) 2004 Dryice Dong Liu . All rights reserved.

;; This is free software; you can redistribute it and/or modify it under
;; the terms of the GNU General Public License as published by the Free
;; Software Foundation; either version 2, or (at your option) any later
;; version.
;;
;; This is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
;; FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
;; for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING.  If not, write to the
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
;; MA 02111-1307, USA.

;;; Commentary:
;;
;; planner-timeclock-summary.el produces timeclock reports for planner
;; files. 
;;
;; There are two ways you can use it:
;;
;; 1. Display a temporary buffer
;;
;; Call `planner-timeclock-summary-show' and Emacs will ask you which
;; day's summary do you want. Choose the date as anywhere else of
;; Emacs planner, and a tempory buffer will be displayed with the
;; timeclock summary of that day.
;;
;; 2. Rewrit sections of your planner
;;
;; Choose this approach if you want timeclock summary to be in their
;; own section and you would like them to be readable in your plain
;; text files even outside Emacs. Caveat: The timeclock summary
;; section should already exist in your template and will be rewritten
;; when updated. Tip: Add `planner-timeclock-summary-section'
;; (default: "Timeclock") to your `planner-day-page-template'.
;;
;; To use, call `planner-timeclock-summary-update' in the planner day
;; page to update the section. If you want rewriting to be
;; automatically performed, call `planner-timeclock-summary-insinuate'
;; in your .emacs file

;;; REQUIRE
;; to make a nice text table, you need align.el from
;; http://www.newartisans.com/johnw/Emacs/align.el

;;; TODO
;; - report for a period of time
;; - support plan pages. Currently only day pages are supported.
;; - sort?

;;; CODE

(require 'planner-timeclock)
(require 'align)
(require 'cl)

;;; User variables

(defgroup planner-timeclock-summary nil
  "Timeclock reports for planner.el."
  :prefix "planner-timeclock-summary"
  :group 'planner)

(defcustom planner-timeclock-summary-section "Timeclock"
  "Header for the timeclock summary section in a plan page."
  :type 'string
  :group 'planner-timeclock-summary)

(defcustom planner-timeclock-summary-buffer "*Planner Timeclock Summary*"
  "Buffer name for timeclock reports from
`planner-timeclock-summary-show'."
  :type 'string
  :group 'planner-timeclock-summary)

(defcustom planner-timeclock-summary-not-planned-string "Not Planned"
  "Project name for the manually `timeclock-in' tasks that without a
project name."
  :type 'string
  :group 'planner-timeclock-summary)

(defcustom planner-timeclock-summary-summary-string 
           "\n\nDay begined: %B, Day ended: %E\nTime elapsed: %S, \
Time clocked: %C\nTime clocked ratio: %R\n"
  "The string below the report table.

%B the first time checked in the day
%L the last time checked in the day
%E the last time checked in the day, or the current time if it's today
%s span, the difference between %B and %L
%S span, the difference between %B and %E
%C the total time clocked
%r clocked/%s
%R clocked/%S"
  :type 'string
  :group 'planner-timeclock-summary)

;;;; User variables stop here

(defvar planner-timeclock-summary-empty-cell-string "====="
  "Internal use, don't touch")

(defvar planner-timeclock-summary-total-cell-string "======="
  "Internal use, don't touch")

;;;###autoload
(defun planner-timeclock-summary-insinuate ()
  "Automatically call `planner-timeclock-summary-update'. when the day
plan page is saved."
  (add-hook 'planner-mode-hook
            (lambda ()
              (add-hook
               (cond
                ((boundp 'write-file-functions) 'write-file-functions)
                ((boundp 'local-write-file-hooks) 'local-write-file-hooks)
                ((boundp 'write-file-hooks) 'write-file-hooks))
               'planner-timeclock-summary-update nil t))))

;;;###autoload
(defun planner-timeclock-summary-update ()
  "Update `planner-timeclock-summary-section'. in the current day plan
page. (If the section exists)"
  (interactive)
  (save-excursion
    (save-restriction
      (when (planner-narrow-to-section planner-timeclock-summary-section)
        (if (string-match planner-date-regexp (planner-page-name))
            (progn
              (delete-region (point-min) (point-max))
              (insert "* " planner-timeclock-summary-section "\n\n"
                      (planner-timeclock-summary-make-text-table-day
                       (replace-in-string (planner-page-name) "\\." "/" t))
                      " \n")
              nil)
          (message "Timeclock summary in plan pages are not supported yet, \
welcome your patches!")
          )))))

;;;###autoload
(defun planner-timeclock-summary-show (&optional date)
  "Display a buffer with the given day's timeclock summary.

date is a string in the form YYYY.MM.DD. It will be asked if not
given."
  (interactive)
  (if (not date)
      (setq date (planner-read-date)))
  (switch-to-buffer (get-buffer-create planner-timeclock-summary-buffer))
  (erase-buffer)
  (let ((emacs-wiki-project planner-project))
    (insert "Timeclock summary report for " date "\n\n"
            (planner-timeclock-summary-make-text-table-day
             (replace-in-string date "\\." "/" t)))
    (planner-mode))
  (goto-char (point-min))
  )


(defun planner-timeclock-summary-extract-data-day (date)
  "Make the alist that will fit into a summary for one day.
Read `timeclock-file' and give out an alist. The list will be:
  (TotalTime
   (((Project1Name Project1Time Project1Ratio) (p1t1time p1t1ratio p1t1name)
                                            (p1t2time p1t2ratio p1t2name)
                                            ...)
   ((p2name p2time p2ratio) ...)))"
  (let ((data-list (planner-timeclock-one-day-alist date))
        (target-data))
    (while data-list
      (setq entry (pop data-list))
      (setq task-data (planner-timeclock-summary-extract-task-data entry))
      (let ((entry-project-name (car task-data))
            (entry-task-name (cadr task-data))
            (entry-task-length (caddr task-data)))
        ;; total time
        (if target-data
            (setcar target-data (+ (car target-data) entry-task-length))
          (setq target-data (list entry-task-length)))
        ;; updating project
        (let ((projects (cdr target-data))
              (project-found nil))
          (while projects
            (setq project (car projects))
            (let ((project-name (caar project))
                  (project-time (cadar project)))
              (if (and project-name 
                       (string-equal project-name entry-project-name))
                  ;; the same project has been recorded, updating tasks
                  (let ((tasks (cdr project))
                        (task-found nil))
                    (while tasks
                      (setq task (car tasks))
                      (let ((task-name (caddr task)))
                        (if (and task-name
                                 (string-equal task-name
                                               entry-task-name))
                            ;; the same task has been recorded, add
                            ;; time
                            (progn
                              (setcar task (+ (car task)
                                              entry-task-length))
                              (setq tasks nil)
                              (setq task-found t))
                          (setq tasks (cdr tasks)))))
                    ;; make a new task record
                    (if (not task-found)
                        (setcar projects
                        (add-to-list 'project (list entry-task-length
                                                    0
                                                    entry-task-name) t)))
                    ;; update project time
                    (setcar (cdar project) (+ project-time
                                              entry-task-length))
                    (setq projects nil)
                    (setq project-found t))
                (setq projects (cdr projects)))))
          ;; make a new project record
          (if (not project-found)
              (add-to-list 'target-data (list (list entry-project-name
                                                    entry-task-length
                                                    0)
                                              (list entry-task-length
                                                    0
                                                    entry-task-name)) t))
        )))
    target-data))

(defun planner-timeclock-summary-calculate-ratio-day (date)
  "calculate ratio."
  (setq target-data (planner-timeclock-summary-extract-data-day date))
  (let ((total (car target-data))
        (projects (cdr target-data)))
    (while projects
      (let ((project (car projects))
            (tasks (cdar projects)))
        (setcar (cddar project) (/ (cadar project) total))
        (while tasks
          (let ((task (car tasks)))
            (setcar (cdr task) (/ (car task) total))
            (setq tasks (cdr tasks))))
        (setq projects (cdr projects)))))
  target-data)

(defun planner-timeclock-summary-make-text-table-day (date)
  "make summary table with plain text"
  (setq source-list (planner-timeclock-summary-calculate-ratio-day
                     date))
  (let ((projects (cdr source-list))
        (total (car source-list)))
    (if total
        (with-temp-buffer
          (erase-buffer)
          (insert (format "%s|%9s|%6s| %s\n" "Project" "Time" "Ratio" "Task"))
          (while projects
            (let ((project-data (caar projects))
                  (tasks (cdar projects)))
              (insert (format "%s|" (car project-data)))
              (setq task (car tasks))
              (insert (format "%9s|%5s%%| %s\n"
                              (timeclock-seconds-to-string (car task) t)
                              (format "%2.1f" (* 100 (cadr task)))
                              (caddr task)))
              (setq tasks (cdr tasks))
              (while tasks
                (let ((task (car tasks)))
                  (insert (format "%s|%9s|%5s%%| %s\n"
                                  planner-timeclock-summary-empty-cell-string
                                  (timeclock-seconds-to-string (car task) t)
                                  (format "%2.1f" (* 100 (cadr task)))
                                  (caddr task)))
                  (setq tasks (cdr tasks))))
              (insert (format "%s|%9s|%5s%%| %s\n"
                              planner-timeclock-summary-total-cell-string
                              (timeclock-seconds-to-string (cadr project-data) 
t)
                              (format "%2.1f" (* 100 (nth 2 project-data)))
                              planner-timeclock-summary-empty-cell-string))
              (setq projects (cdr projects))))
          (align-regexp (point-min) (point-max) "\\(\\s-*\\)|" 1 0 nil)
          (untabify (point-min) (point-max))
          (goto-char (point-min))
          (let ((total-regexp (format "%s\\s-*|"
                                      
planner-timeclock-summary-total-cell-string)))
            (while (search-forward-regexp total-regexp nil t)
              (let ((string-len (- (match-end 0) (match-beginning 0))))
                (setq new-string (format "%sTotal: |" 
                                         (make-string (- string-len 8)
                                                      (aref " " 0))))
                (replace-match new-string t))))
          (goto-char (point-min))
          (while (search-forward
                  planner-timeclock-summary-empty-cell-string nil t)
            (replace-match (make-string 
                            (length planner-timeclock-summary-empty-cell-string)
                            (aref " " 0))))
          (goto-char (point-max))
          (insert (planner-timeclock-summary-make-summary-string date total))
          (buffer-string))
      "")))

(defun planner-timeclock-summary-make-summary-string (date total)
  "make the summary according to
  `planner-timeclock-summary-summary-string'
date is in format YYYY/MM/DD
total is the total time clocked today in second"
  (let ((target-string planner-timeclock-summary-summary-string)
        (data (planner-timeclock-one-day-entry-no-date date))
        begin end last Span span)
    (setq begin (timeclock-day-begin data))
    (setq end (timeclock-day-end data))
    (if (string-equal date (format-time-string "%Y/%m/%d"))
        (setq last (current-time))
      (setq last end))
    (setq span (timeclock-time-to-seconds (time-subtract last begin)))
    (setq Span (timeclock-time-to-seconds (time-subtract end begin)))
    (setq target-string (replace-in-string target-string "%B" 
                                           (format-time-string "%H:%M:%S" 
begin) t))
    (setq target-string (replace-in-string target-string "%E"
                                           (format-time-string "%H:%M:%S" end) 
t))
    (setq target-string (replace-in-string target-string "%L"
                                           (format-time-string "%H:%M:%S" last) 
t))
    (setq target-string (replace-in-string target-string "%s"
                                           (timeclock-seconds-to-string span t) 
t))
    (setq target-string (replace-in-string target-string "%S"
                                           (timeclock-seconds-to-string Span t) 
t))
    (setq target-string (replace-in-string target-string "%C"
                                           (timeclock-seconds-to-string total 
t) t))
    (setq target-string (replace-in-string target-string "%r"
                                           (format "%2.1f%%" (* 100 (/ total 
span))) t))
    (setq target-string (replace-in-string target-string "%R"
                                           (format "%2.1f%%" (* 100 (/ total 
Span))) t))))
    
(defun planner-timeclock-summary-extract-task-data (entry)
  "Given a TIME-ENTRY (see `timeclock-log-data'), return a list of
  (project task length). 
note the PROJECT there is 'project: task' here"
  (let ((task-fullname (timeclock-entry-project entry))
        (task-length (timeclock-entry-length entry))
        project-name task-name)
    ;; in case some manually clocked in tasks
    ;; without ": "
    (if (string-match ": .*$" task-fullname)
        (progn
          (setq project-name (replace-match "" t t
                                            task-fullname))
          (if (string-match " " project-name)
              ;; there's " " in project-name, this is not a really wiki name
              (progn
                (setq project-name planner-timeclock-summary-not-planned-string)
                (setq task-name task-fullname))
            (setq task-name (replace-in-string
                             task-fullname "^.*?: " "" t))))
      ;; no ": " found, not planned task
      (progn 
        (setq project-name planner-timeclock-summary-not-planned-string)
        (setq task-name task-fullname))
      )
    (list project-name task-name task-length)
    ))

(defun planner-timeclock-one-day-entry (date)
  "the data of a given day. The data is an ENTRY as in
timeclock.el. Arg date in format YYYY/MM/DD."
  (let ((day-list (timeclock-day-alist))
        entry-list)
    (while day-list
      (let ((theday (pop day-list)))
        (if (string-match date (car theday))
            (progn
              (setq entry-list theday)
              (setq day-list nil))
          )))
    entry-list))

(defun planner-timeclock-one-day-entry-no-date (date)
  (let ((entry-list (planner-timeclock-one-day-entry date)))
    (cdr entry-list))
  )

(defun planner-timeclock-one-day-alist (date)
  "the data of a given day. Arg date in format YYYY/MM/DD."
  (let ((entry-list (planner-timeclock-one-day-entry date)))
    (cddr entry-list))
  )

;; XEmacs has `replace-in-string', Gnu Emacs has
;; `replace-regexp-in-string'
(unless (fboundp 'replace-in-string)
  (defsubst replace-in-string (string regexp newtext &optional literal)
    "Replace all matches in STRING for REGEXP with NEWTEXT."
    (replace-regexp-in-string regexp newtext string 'fixedcase literal)))

(provide 'planner-timeclock-summary)
;;; planner-timeclock-summary.el ends here

;;; experimental code

(defcustom planner-timeclock-summary-task-project-summary-string
  "*Project Summary*"
  "Task name for project summary."
  :type 'string
  :group 'planner-timeclock-summary)

(defcustom planner-timeclock-summary-project-column-min-width 22
  "Min width of the project column in the report table"
  :type 'integer
  :group 'planner-timeclock-summary)

(defcustom planner-timeclock-summary-time-column-min-width 8
  "Min width of the time column in the report table"
  :type 'integer
  :group 'planner-timeclock-summary)

(defcustom planner-timeclock-summary-ratio-column-min-width 5
  "Min width of the ratio column in the report table"
  :type 'integer
  :group 'planner-timeclock-summary)

(defcustom planner-timeclock-summary-task-column-min-width 40
  "Min width of the task column in the report table"
  :type 'integer
  :group 'planner-timeclock-summary)

;; (defun planner-timeclock-summary-make-table-list-day (date)
;;   "make the output of `planner-timeclock-summary-calculate-ratio-day'
;; usable by `planner-timeclock-summary-table-insert-list'. The CAR of
;; the return value is the row number of the table, and the CDR is what
;; `planner-timeclock-summary-table-insert-list' want."
;;   (setq source-list (planner-timeclock-summary-calculate-ratio-day
;;                   date))
;;   (let ((projects (cdr source-list))
;;      (dest-list (list "Project" "Time" "Ratio" "Task"))
;;      (row-count 0))
;;     (while projects
;;       (let ((project-data (caar projects))
;;          (tasks (cdar projects)))
;;      (add-to-list 'dest-list 
;;                   (format "%s \n Total: %s, %2.1f%%"
;;                           (car project-data)
;;                           (timeclock-seconds-to-string 
;;                            (cadr project-data) t)
;;                           (* 100 (caddr project-data))) t)
;;      (setq row-count (1+ row-count))
;;      (setq task (car tasks))
;;      (add-to-list 'dest-list 
;;                   (format "%s" (timeclock-seconds-to-string 
;;                                 (car task) t) )t)
;;      (add-to-list 'dest-list
;;                   (format "%2.1f%%" (* 100 (cadr task))) t)
;;      (add-to-list 'dest-list
;;                   (format "%s" (caddr task)) t)
;;      (setq row-count (1+ row-count))
;;      (setq tasks (cdr tasks))
;;      (while tasks
;;        (let ((task (car tasks)))
;;          (add-to-list 'dest-list
;; ;;                    'planner-timeclock-summary-table-span-cell-above
;;                       "0"
;;                       t)
;;          (add-to-list 'dest-list 
;;                       (format "%s" (timeclock-seconds-to-string 
;;                                     (car task) t) )t)
;;          (add-to-list 'dest-list
;;                       (format "%2.1f%%" (* 100 (cadr task))) t)
;;          (add-to-list 'dest-list
;;                       (format "%s" (caddr task)) t)
;;          (setq row-count (1+ row-count))
;;          (setq tasks (cdr tasks))
;;          ))
;;      (setq projects (cdr projects))))
;;     (cons row-count dest-list)))

;; (defun planner-timeclock-summary-generate-report-day (date)
;;   "make the report table"
;;   (with-temp-buffer
;;   (let ((data-list (planner-timeclock-summary-make-table-list-day
;;                  date)))
;;     (table-insert 4 (car data-list) 
;;                (list
;;                 planner-timeclock-summary-project-column-min-width
;;                 planner-timeclock-summary-time-column-min-width
;;                 planner-timeclock-summary-ratio-column-min-width
;;                 planner-timeclock-summary-task-column-min-width)
;;                1)
;;     (planner-mode)
;;     (planner-timeclock-summary-table-insert-list (cdr data-list)))
;;   (buffer-string)))

;; this is wired: it works fine in edebug, but when called from
;; -show-2, the table messed up if there is a long annotation in the
;; task string. This should have something to do with the planner page
;; render.
(defun planner-timeclock-summary-make-table-day (date, start-point)
  "manipulate the output of
`planner-timeclock-summary-make-text-table-day' to a real nice table"
;;   (with-temp-buffer
;;     (erase-buffer)
    (insert (planner-timeclock-summary-make-text-table-day date))
;;     (planner-mode)
    (redraw-display)
    (table-capture 42 (point-max) "|" "\n" 'left 
                   (list planner-timeclock-summary-project-column-min-width
                         planner-timeclock-summary-time-column-min-width
                         planner-timeclock-summary-ratio-column-min-width
                         planner-timeclock-summary-task-column-min-width)
                   )
    ;; make "=====" cell empty and span above
;;     (goto-char (point-min))
;;     (while (search-forward
;;          planner-timeclock-summary-empty-cell-string)
;;       (beginning-of-line)
;;       (kill-line)
;;       (table-span-cell 'above))
;;     (buffer-string))
)

(defun planner-timeclock-summary-show-2 (&optional date)
  "Display a buffer with the given day's timeclock summary.

date is a string in the form YYYY.MM.DD. It will be asked if not
given."
  (interactive)
  (if (not date)
      (setq date (planner-read-date)))
  (switch-to-buffer (get-buffer-create planner-timeclock-summary-buffer))
  (erase-buffer)
  (let ((emacs-wiki-project planner-project))
    (insert "Timeclock summary report for " date "\n\n")
    (planner-mode)
    (planner-timeclock-summary-make-table-day
             (replace-in-string date "\\." "/" t) (point))
    )
  (goto-char (point-min))
  )

;; (defun planner-timeclock-summary-table-insert-list (list)
;;   ""
;;   (save-excursion
;;     (while list
;;       (let ((info (pop list)))
;;      (if (functionp info)
;;          (funcall info)
;;        (table-with-cache-buffer
;; ;;       (goto-char (point-min))
;;          (erase-buffer)
;;          (emacs-wiki-mode)
;;          (insert info)
;;          (table--fill-region (point-min) (point) nil 'left))
;;        )
;;      (table-forward-cell)
;;      ))))

(defun planner-timeclock-summary-table-span-cell-left ()
  "merge the current cell with the left one"
  (table-span-cell 'left)
)

(defun planner-timeclock-summary-table-span-cell-above ()
  "merge the current cell with the left one"
  (table-span-cell 'above)
)


;;; obsolate

(defun planner-timeclock-summary-generate-report (date)
  "Generate the timeclock summary table of the given date. This is the
function do the real work."
  (with-temp-buffer
    (insert (format "%s | %s | %s\n" "Project" "time" "task"))
    (let ((day-list (timeclock-day-alist)))
      (while day-list
        (let ((theday (pop day-list)))
          (if (string-match date (car theday))
              (let ((projects-name-theday (timeclock-day-projects
                                           theday))
                    )
                (while projects-name-theday
                  (let ((project-name-theday (pop
                                              projects-name-theday))
                        (projects-data-theday (cddr theday))
                        (time 0))
                    (if project-name-theday
                        (progn
                          (while projects-data-theday
                            (let ((project-data-theday (pop
                                                        projects-data-theday)))
                              ;;                              (if (string-match 
project-name-theday (nth 2
                              (if (string-equal project-name-theday (nth 2
                                                                         
project-data-theday))
                                  (incf time  (-
                                               (timeclock-time-to-seconds (nth 
1 project-data-theday))
                                               (timeclock-time-to-seconds (nth
                                                                           0
                                                                           
project-data-theday)))))))
                          ;; in case some manually clocked in tasks
                          ;; without ": "
                          (if (string-match ": .*$" project-name-theday)
                              (setq project-name (replace-match "" t t
                                                                
project-name-theday))
                            (setq project-name "Not Planned"))
                          (setq task-name (replace-in-string
                                           project-name-theday "^.*: " "" t))
                          (insert (format "%s | %s | %s\n"
                                          project-name 
                                          (timeclock-seconds-to-string time t)
                                          task-name))

                          ))
                    )))))))
      (when (fboundp 'align-regexp)
        (align-regexp (point-min) (point-max) "\\(\\s-*\\)|" 1 0 t))
    (buffer-string)
    ))
----------------------------------------------------------------------


-- 
Cheers,
Dryice

http://dryice.3322.org





reply via email to

[Prev in Thread] Current Thread [Next in Thread]