git.fiddlerwoaroof.com
Raw Blame History
;; 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)
             (slot-value time-mod 'unit))
           (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)))