git.fiddlerwoaroof.com
tempores.lisp
3bd147ec
 ;; tempores.lisp
 
 (in-package #:tempores)
 
 ;;; "tempores" goes here. Hacks and glory await!
 
 (defmacro maybe-list (test &optional val)
   "If both arguments passed, when test is true, return a list containing val
    or, when test is false, return nil.  If one argument passed, when test names
    something that is not a list, return a list containing it, otherwise
    return nil."
   (once-only (test)
     (let ((test (if val test `(not (listp ,test))))
           (val (if val val test)))
       `(when ,test
          (list ,val)))))
 
 (defun combine-date-time (time-obj day month year)
   (declare (optimize (debug 3)))
   (with-slots (second minute hour) time-obj
     (local-time:encode-timestamp 0 second minute hour
                                  day month year)))
 
 (defun calculate-ranges (ranges date)
   (declare (optimize (debug 3)))
   (labels ((time-mod-unit-keyword (time-mod)
016c00a0
              (slot-value time-mod 'unit))
3bd147ec
            (make-mod (mod)
              (when mod
                (let ((unit (time-mod-unit-keyword mod))
                      (amount (slot-value mod 'tempores.parser:amount)))
                  (funcall #'local-time-duration:duration unit amount)))))
     (with-slots (year month day) date
       (loop with complete = nil
         with partial = nil
         for (start-obj end-obj mod) in ranges
         for start = (combine-date-time start-obj day month year)
         for end = (when end-obj (combine-date-time end-obj day month year))
         for time-mod = (when mod (make-mod mod))
         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))))))
 
 (defun calculate-duration-in-15mins (duration)
   (let ((duration-in-minutes (local-time-duration:duration-as duration :minute)))
     (coerce (/ (round duration-in-minutes 15) 4)
             'float)))
 
 (defun calculate-rounded-ranges (ranges)
   (let-each (:be *)
     (local-time-duration:duration)
     (reduce #'local-time-duration:duration+ ranges :initial-value *)
     (calculate-duration-in-15mins *)))
 
 (defclass log-entry ()
   ((complete :initarg :complete)
    (incomplete :initarg :incomplete)))
 
 (defun get-entry-ranges (entry)
   (flet ((make-entry (record)
            (let ((date (slot-value entry 'date)))
              (with-slots (client memo ranges) record
                (multiple-value-bind (complete partial) (calculate-ranges ranges date)
                  (list*
                    (make-complete-entry date client memo (calculate-rounded-ranges complete))
                    (maybe-list partial
                                (make-partial-entry date client memo partial))))))))
     (let-each (:be *)
       (slot-value entry 'records)
       (mapcan #'make-entry *))))
 
 (defun get-log (&optional (file *default-time-sheet-file*) ignore-whitespace)
   (let* ((entries (tempores.cli::parse-file file ignore-whitespace)))
     (mapcan #'get-entry-ranges entries)))
 
 (defun group-by-client (incompletes)
   (let ((results (make-hash-table :test 'equalp)))
     (loop for incomplete in incompletes
           for client = (client incomplete)
           do (push incomplete (gethash client results)))
     (hash-table-alist results)))
 
 (defun update-clients (status-calculator entry)
   (flet ((ensure-client (client)
            (ensure-gethash client
                            (client-totals status-calculator)
                            (make-instance 'status-line :client client))))
     (with-accessors ((client client)) entry
       (let ((client-hash-table (ensure-client client)))
         (update client-hash-table entry)))))
 
 (defun calculate-results (results &optional (rate *rate*))
   (let-first (:be status-calculator) (make-status-calculator rate)
     (dolist (result results)
       (update-clients status-calculator result)
       (update status-calculator result))))
 
 (define-message status-line-format (client duration rate cost)
   (:own-line ()
    (:titlecase () (:rjust (26) :str))
    ": " (:float 7 2) " hours @ " (:float 7 2) " $/hr = $" (:float 7 2)))
 
 (defun print-status (results)
   (let* ((status-calculator (calculate-results results))
          (client-totals (client-totals status-calculator)))
     (labels ((print-status-line (status-line)
                (with-slots (client duration) status-line
                  (status-line-format t client duration
                                      (rate status-calculator)
                                      (calculate-cost status-calculator status-line))))
              (print-separator ()
                (format t "~&~120,1,0,'-<~>~%")))
       (let-each (:be *)
         (print-separator)
         (hash-table-keys client-totals)
         (sort * #'string-lessp)
         (dolist (client *)
           (print-status-line (gethash client client-totals)))
         (format t (total-line status-calculator *rate*))))))
 
 
 (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
 
 (defmacro with-tempores-configuration (() &body body)
   `(progn
      (ubiquitous:restore 'tempores)
      (let ((*rate* (ubiquitous:defaulted-value 0 :rate))
            (*default-time-sheet-file*
              (ubiquitous:defaulted-value #p"~/bucket/time.md" :tempores :file)))
        ,@body)))