92384785 |
;;;; timesheet.lisp
(in-package #:timesheet)
|
2d700b03 |
(ubiquitous:restore 'timesheet)
|
92384785 |
;;; "timesheet" goes here. Hacks and glory await!
|
2d700b03 |
(defvar *default-time-sheet-file*)
(defvar *rate*)
|
51faefd0 |
(defmethod duration ((obj partial-entry))
(warn "incomplete entry detected for ~a" (client obj))
(local-time-duration:duration))
|
b953500d |
(defun make-complete-entry (date client memo duration)
(make-instance 'complete-entry
:date date
:client client
:memo memo
:duration duration))
(defun make-partial-entry (date client memo start-times)
(make-instance 'partial-entry
:date date
:client client
:memo memo
:start-times start-times))
|
2d700b03 |
(defun parse-file (&optional (file *default-time-sheet-file*))
|
d8b28a4b |
(with-open-file (s file :direction :input)
|
2d700b03 |
(let ((dest (make-string (file-length s))))
(read-sequence dest s)
|
b953500d |
(multiple-value-bind (parsed leftovers) (smug:parse (timesheet.parser::.date-records) dest)
(unless (string= leftovers "")
|
a91599bf |
(cerror "Continue?" 'parsing-error :leftovers leftovers))
|
b953500d |
parsed))))
|
2d700b03 |
(defun unroll-date (date-obj)
(with-slots (year month day) date-obj
(list day month year)))
|
d8b28a4b |
(defun combine-date-time (time-obj day month year)
(with-slots (second minute hour) time-obj
(local-time:encode-timestamp 0 second minute hour
day month year)))
|
2d700b03 |
(defun calculate-ranges (ranges year month day)
|
d8b28a4b |
(flet ((time-mod-unit-keyword (time-mod)
(make-keyword
(string-upcase
(if (string= (slot-value time-mod 'unit) "mins")
"minute"
"hour")))))
|
b953500d |
(loop with complete = nil
with partial = nil
for (start-obj end-obj mod) in ranges
|
d8b28a4b |
for start = (combine-date-time start-obj year month day)
|
b953500d |
for end = (when end-obj (combine-date-time end-obj year month day))
|
d8b28a4b |
for time-mod = (when mod
(let ((unit (time-mod-unit-keyword mod))
(amount (slot-value mod 'timesheet.parser:amount)))
(funcall #'local-time-duration:duration unit amount)))
|
b953500d |
if end do (push (local-time-duration:timestamp-difference end start) complete)
else do (push start partial)
when time-mod do (push time-mod complete)
finally (return (values complete partial)))))
|
2d700b03 |
(defun calculate-rounded-ranges (ranges)
(flet ((calc-duration-in-15mins (duration)
(let ((duration-in-minutes (local-time-duration:duration-as duration :minute)))
|
b953500d |
(coerce (/ (round duration-in-minutes 15) 4)
|
2d700b03 |
'float))))
(calc-duration-in-15mins
(reduce #'local-time-duration:duration+ ranges
:initial-value (local-time-duration:duration)))))
|
b953500d |
(defun get-entry-ranges (entry)
(let ((date (slot-value entry 'date)))
(with-slots (year month day) date
(loop for record in (slot-value entry 'records)
append (with-slots (client memo ranges) record
(multiple-value-bind (complete partial) (calculate-ranges ranges day month year)
(list*
(make-complete-entry date client memo (calculate-rounded-ranges complete))
(when partial
(list
(make-partial-entry date client memo partial))))))))))
|
2d700b03 |
(defun get-log (&optional (file *default-time-sheet-file*))
(block nil
|
b953500d |
(let* ((entries (parse-file file)))
(loop for entry in entries
for ranges = (get-entry-ranges entry)
append ranges))))
|
2d700b03 |
(defparameter +pprint-log-option-spec+
|
d8b28a4b |
'((("client" #\c) :type boolean :optional t :documentation "Sort by client")
(("reverse" #\r) :type boolean :optional t :documentation "Reverse sort")
(("status" #\s) :type boolean :optional t
|
0bd25181 |
:documentation "Print a summary of the hours worked and the prices")
|
d8b28a4b |
(("help" #\h) :type boolean :optional t :documentation "show help")))
|
2d700b03 |
(defparameter *version* "0:1")
(defun show-version ()
(format t "timesheet, common-lisp version ~a~%" *version*))
(defun show-help ()
(show-version)
(command-line-arguments:show-option-help +pprint-log-option-spec+ :sort-names t))
|
d8b28a4b |
(defun sort-by-date (results)
(stable-sort results #'local-time:timestamp<
:key (alambda (apply #'local-time:encode-timestamp
(append '(0 0 0 0)
|
b953500d |
(unroll-date (date it)))))))
(defun group-by-client (incompletes)
(let ((results (make-hash-table :test 'equalp)))
(loop for incomplete in incompletes
do (push incomplete (gethash (client incomplete) results)))
(hash-table-alist results)))
(defgeneric print-entries (entries)
|
a91599bf |
(:method ((entries list))
(mapcar #'print-entries entries))
(:method ((entry partial-entry))
(format t "~&~4<~>~a, ~a:~%~{~12<~>one starting at ~a~%~}"
(client entry)
(memo entry)
(mapcar
(alambda (local-time:format-timestring
nil it
:format '(:year #\/ (:month 2) #\/ (:day 2) #\Space
(:hour 2) #\: (:min 2) #\: (:sec 2))))
(start-times entry))))
(:method ((it complete-entry))
(format t "~&~4a ~10<~:(~a~)~> ~7,2F hrs ~a~%"
(date it) (client it) (duration it) (memo it))))
|
b953500d |
(defgeneric record-client (calc client hours)
(:method ((calc status-calculator) client hours)
(let ((client (make-keyword (string-upcase client))))
(incf (gethash client (clients calc) 0)
hours))))
|
a91599bf |
(defgeneric update (calculator entry)
(:method ((calculator status-calculator) entry)
(incf (total-hours calculator) (duration entry)))
(:method ((calculator status-line) entry)
(incf (duration calculator) (duration entry))))
(defun update-clients (clients-hash-table entry)
(with-accessors ((client client)) entry
(update (ensure-gethash client clients-hash-table
(make-instance 'status-line :client client))
entry)))
(defun calculate-results (results &optional (rate *rate*))
(let ((status-calculator
(make-instance 'status-calculator
:rate rate
:client-totals (make-hash-table :test 'equalp))))
(prog1 status-calculator
(loop for entry in results
do (update-clients (client-totals status-calculator) entry)
do (update status-calculator entry)))))
(defgeneric total-line (calc rate)
(:method ((calc status-calculator) rate)
(with-accessors ((total-hours total-hours)) calc
|
b953500d |
(format nil "~26<Total~>:~7,2F hours @ ~7,2F $/hr = $~7,2F"
|
a91599bf |
total-hours rate (* rate total-hours)))))
(defgeneric calculate-cost (calc time)
(:method ((calc status-calculator) (status-line status-line))
(* (rate calc) (duration status-line))))
|
b953500d |
(defun print-status (results)
|
a91599bf |
(let* ((status-calculator (calculate-results results)))
(flet ((print-status-line (status-line)
(format t "~&~:(~26<~a~>~):~7,2F hours @ ~7,2F $/hr = $~7,2F~%"
(client status-line)
(duration status-line)
(rate status-calculator)
(calculate-cost status-calculator status-line))))
(format t "~&~120,1,0,'-<~>~%")
(loop with client-totals = (client-totals status-calculator)
for client in (sort (hash-table-keys client-totals) #'string-lessp)
for status-line = (gethash client client-totals)
do (print-status-line status-line))
(format t (total-line status-calculator *rate*)))))
|
b953500d |
(defun pprint-results (results incompletes status)
|
a91599bf |
(print-entries results)
|
b953500d |
(when incompletes
|
a91599bf |
(format t "~&~120,1,0,'-<~>~%Partial Entries:~%")
|
b953500d |
(print-entries incompletes))
(when status
(print-status results)))
|
a91599bf |
(defun group-by-class (list &optional accum1 accum2)
(tagbody ; Let's do some TCO ...
start
(if (null list)
(return-from group-by-class (list accum1 accum2))
(destructuring-bind (head . tail) list
(etypecase head
(complete-entry (setf accum1 (cons head accum1))) ; Here we set the accumulators
(partial-entry (setf accum2 (cons head accum2)))) ; to the appropriate values.
(setf list tail) ; Here we step towards the terminating condition
(go start))))) ; Recurse
|
d8b28a4b |
(defun pprint-log (args &key client reverse status help)
(when help
(show-help)
(return-from pprint-log))
|
b953500d |
(flet ((sort-results (results &optional (client client))
|
d8b28a4b |
(setf results (sort-by-date results))
(when client
|
b953500d |
(setf results (stable-sort results #'string-lessp :key #'client)))
|
d8b28a4b |
(when reverse
(setf results (nreverse results)))
results))
|
b953500d |
(let ((*default-time-sheet-file* (or (car args) *default-time-sheet-file*))
(*print-pretty* t))
|
a91599bf |
(destructuring-bind (complete-ranges incomplete-ranges) (group-by-class (get-log *default-time-sheet-file*))
|
b953500d |
(let ((complete-results (sort-results complete-ranges))
(incomplete-results (sort-results incomplete-ranges t)))
(pprint-results complete-results incomplete-results status))))))
|
2d700b03 |
(defun pprint-log-main (argv)
|
d8b28a4b |
(setf *rate* (ubiquitous:defaulted-value 0 :rate)
*default-time-sheet-file* (ubiquitous:defaulted-value #p"~/time.md" :timesheet :file))
|
2d700b03 |
(command-line-arguments:handle-command-line
+pprint-log-option-spec+
'pprint-log
:command-line (cdr argv)
:name "timesheet"
:rest-arity t))
|
d8b28a4b |
|