Browse code
Cleanup
- Rewrote parts of the timesheet printer to use CLOS more effectively
- Renamed a condition to fix package locks
- Updated the binary to use SBCL's --compress-core option
Showing 3 changed files
... | ... |
@@ -80,9 +80,9 @@ |
80 | 80 |
(setf amount amnt unit unt) |
81 | 81 |
it))) |
82 | 82 |
|
83 |
-(define-condition parse-error () ()) |
|
83 |
+(define-condition parsing-error (parse-error) ()) |
|
84 | 84 |
|
85 |
-(define-condition invalid-day-of-week (parse-error) |
|
85 |
+(define-condition invalid-day-of-week (parsing-error) |
|
86 | 86 |
((day-of-week :initarg :day-of-week :reader day-of-week)) |
87 | 87 |
(:report (lambda (condition stream) |
88 | 88 |
(format stream "~s is not a valid day of the week" |
... | ... |
@@ -9,6 +9,15 @@ |
9 | 9 |
(defvar *default-time-sheet-file*) |
10 | 10 |
(defvar *rate*) |
11 | 11 |
|
12 |
+(defclass status-calculator () |
|
13 |
+ ((rate :initarg :rate :accessor rate) |
|
14 |
+ (total-hours :initform 0 :initarg :total-hour :accessor total-hours) |
|
15 |
+ (client-totals :initarg :client-totals :accessor client-totals))) |
|
16 |
+ |
|
17 |
+(defclass status-line () |
|
18 |
+ ((client :initarg :client :accessor client) |
|
19 |
+ (duration :initarg :duration :accessor duration :initform 0))) |
|
20 |
+ |
|
12 | 21 |
(defclass parsed-entry () |
13 | 22 |
((date :initarg :date :accessor date) |
14 | 23 |
(client :initarg :client :accessor client) |
... | ... |
@@ -34,20 +43,7 @@ |
34 | 43 |
:memo memo |
35 | 44 |
:start-times start-times)) |
36 | 45 |
|
37 |
-(defmacro /. (&rest body) |
|
38 |
- (let ((args '()) |
|
39 |
- forms) |
|
40 |
- (loop for (head . tail) on body |
|
41 |
- until (eql head '->) |
|
42 |
- do (push head args) |
|
43 |
- finally (setf args (nreverse args)) |
|
44 |
- finally (setf forms tail)) |
|
45 |
- `(macrolet |
|
46 |
- ((>< (&rest form) |
|
47 |
- (list* (cadr form) (car form) (cddr form)))) |
|
48 |
- (lambda ,args ,@forms)))) |
|
49 |
- |
|
50 |
-(define-condition parse-error () |
|
46 |
+(define-condition parsing-error () |
|
51 | 47 |
((leftovers :initarg :leftovers :accessor leftovers)) |
52 | 48 |
(:report (lambda (condition stream) |
53 | 49 |
(format stream "Parse error: ~20s leftover" (leftovers condition))))) |
... | ... |
@@ -58,7 +54,7 @@ |
58 | 54 |
(read-sequence dest s) |
59 | 55 |
(multiple-value-bind (parsed leftovers) (smug:parse (timesheet.parser::.date-records) dest) |
60 | 56 |
(unless (string= leftovers "") |
61 |
- (cerror "Continue?" 'parse-error :leftovers leftovers)) |
|
57 |
+ (cerror "Continue?" 'parsing-error :leftovers leftovers)) |
|
62 | 58 |
parsed)))) |
63 | 59 |
|
64 | 60 |
(defun unroll-date (date-obj) |
... | ... |
@@ -147,23 +143,21 @@ |
147 | 143 |
(hash-table-alist results))) |
148 | 144 |
|
149 | 145 |
(defgeneric print-entries (entries) |
150 |
- (:method ((incompletes list)) |
|
151 |
- (format t "~&~120,1,0,'-<~>~%Partial Entries:~%") |
|
152 |
- (loop for (client . entries) in (group-by-client incompletes) |
|
153 |
- do (loop for entry in entries |
|
154 |
- do (format t "~&~4<~>~a, ~a:~%~{~12<~>one starting at ~a~%~}" |
|
155 |
- (client entry) |
|
156 |
- (memo entry) |
|
157 |
- (mapcar |
|
158 |
- (alambda (local-time:format-timestring |
|
159 |
- nil it |
|
160 |
- :format '(:year #\/ (:month 2) #\/ (:day 2) #\Space |
|
161 |
- (:hour 2) #\: (:min 2) #\: (:sec 2)))) |
|
162 |
- (start-times entry))))))) |
|
163 |
- |
|
164 |
-(defclass status-calculator () |
|
165 |
- ((clients :initform (make-hash-table) :accessor clients) |
|
166 |
- (total-cost :initform 0 :accessor total-cost))) |
|
146 |
+ (:method ((entries list)) |
|
147 |
+ (mapcar #'print-entries entries)) |
|
148 |
+ (:method ((entry partial-entry)) |
|
149 |
+ (format t "~&~4<~>~a, ~a:~%~{~12<~>one starting at ~a~%~}" |
|
150 |
+ (client entry) |
|
151 |
+ (memo entry) |
|
152 |
+ (mapcar |
|
153 |
+ (alambda (local-time:format-timestring |
|
154 |
+ nil it |
|
155 |
+ :format '(:year #\/ (:month 2) #\/ (:day 2) #\Space |
|
156 |
+ (:hour 2) #\: (:min 2) #\: (:sec 2)))) |
|
157 |
+ (start-times entry)))) |
|
158 |
+ (:method ((it complete-entry)) |
|
159 |
+ (format t "~&~4a ~10<~:(~a~)~> ~7,2F hrs ~a~%" |
|
160 |
+ (date it) (client it) (duration it) (memo it)))) |
|
167 | 161 |
|
168 | 162 |
(defgeneric record-client (calc client hours) |
169 | 163 |
(:method ((calc status-calculator) client hours) |
... | ... |
@@ -171,53 +165,74 @@ |
171 | 165 |
(incf (gethash client (clients calc) 0) |
172 | 166 |
hours)))) |
173 | 167 |
|
174 |
-(defgeneric total-line (calc results) |
|
175 |
- (:method ((calc status-calculator) results) |
|
176 |
- (with-accessors ((total-cost total-cost)) calc |
|
168 |
+(defgeneric update (calculator entry) |
|
169 |
+ (:method ((calculator status-calculator) entry) |
|
170 |
+ (incf (total-hours calculator) (duration entry))) |
|
171 |
+ (:method ((calculator status-line) entry) |
|
172 |
+ (incf (duration calculator) (duration entry)))) |
|
173 |
+ |
|
174 |
+(defun update-clients (clients-hash-table entry) |
|
175 |
+ (with-accessors ((client client)) entry |
|
176 |
+ (update (ensure-gethash client clients-hash-table |
|
177 |
+ (make-instance 'status-line :client client)) |
|
178 |
+ entry))) |
|
179 |
+ |
|
180 |
+(defun calculate-results (results &optional (rate *rate*)) |
|
181 |
+ (let ((status-calculator |
|
182 |
+ (make-instance 'status-calculator |
|
183 |
+ :rate rate |
|
184 |
+ :client-totals (make-hash-table :test 'equalp)))) |
|
185 |
+ (prog1 status-calculator |
|
186 |
+ (loop for entry in results |
|
187 |
+ do (update-clients (client-totals status-calculator) entry) |
|
188 |
+ do (update status-calculator entry))))) |
|
189 |
+ |
|
190 |
+(defgeneric total-line (calc rate) |
|
191 |
+ (:method ((calc status-calculator) rate) |
|
192 |
+ (with-accessors ((total-hours total-hours)) calc |
|
177 | 193 |
(format nil "~26<Total~>:~7,2F hours @ ~7,2F $/hr = $~7,2F" |
178 |
- (loop for result in results |
|
179 |
- do (record-client calc (client result) (duration result)) |
|
180 |
- do (incf total-cost (* (duration result) *rate*)) |
|
181 |
- sum (duration result)) |
|
182 |
- *rate* |
|
183 |
- total-cost)))) |
|
194 |
+ total-hours rate (* rate total-hours))))) |
|
195 |
+ |
|
196 |
+(defgeneric calculate-cost (calc time) |
|
197 |
+ (:method ((calc status-calculator) (status-line status-line)) |
|
198 |
+ (* (rate calc) (duration status-line)))) |
|
184 | 199 |
|
185 | 200 |
(defun print-status (results) |
186 |
- (let ((status-calculator (make-instance 'status-calculator))) |
|
187 |
- (flet ((fix-assoc (alist) |
|
188 |
- (mapcar (destructuring-lambda ((client . time)) |
|
189 |
- (list client |
|
190 |
- time |
|
191 |
- *rate* |
|
192 |
- (* time *rate*))) |
|
193 |
- alist))) |
|
194 |
- (let ((total (total-line status-calculator results))) |
|
195 |
- (format t "~&~120,1,0,'-<~>~%~:{~:(~26<~a~>~):~7,2F hours @ ~7,2F $/hr = $~7,2F~%~}" |
|
196 |
- (stable-sort (fix-assoc (hash-table-alist (clients status-calculator))) |
|
197 |
- #'string< |
|
198 |
- :key (alambda (car it)))) |
|
199 |
- (format t total))))) |
|
201 |
+ (let* ((status-calculator (calculate-results results))) |
|
202 |
+ (flet ((print-status-line (status-line) |
|
203 |
+ (format t "~&~:(~26<~a~>~):~7,2F hours @ ~7,2F $/hr = $~7,2F~%" |
|
204 |
+ (client status-line) |
|
205 |
+ (duration status-line) |
|
206 |
+ (rate status-calculator) |
|
207 |
+ (calculate-cost status-calculator status-line)))) |
|
208 |
+ (format t "~&~120,1,0,'-<~>~%") |
|
209 |
+ (loop with client-totals = (client-totals status-calculator) |
|
210 |
+ for client in (sort (hash-table-keys client-totals) #'string-lessp) |
|
211 |
+ for status-line = (gethash client client-totals) |
|
212 |
+ do (print-status-line status-line)) |
|
213 |
+ (format t (total-line status-calculator *rate*))))) |
|
200 | 214 |
|
201 | 215 |
(defun pprint-results (results incompletes status) |
202 |
- (format t "~&~:{~4a ~10<~:(~a~)~> ~7,2F hrs ~a~%~}" |
|
203 |
- (mapcar (alambda (list (date it) (client it) (duration it) (memo it))) |
|
204 |
- results)) |
|
216 |
+ (print-entries results) |
|
205 | 217 |
|
206 | 218 |
(when incompletes |
219 |
+ (format t "~&~120,1,0,'-<~>~%Partial Entries:~%") |
|
207 | 220 |
(print-entries incompletes)) |
208 | 221 |
|
209 | 222 |
(when status |
210 | 223 |
(print-status results))) |
211 | 224 |
|
212 |
-(defun group-by-class (list) |
|
213 |
- (loop with completes = '() |
|
214 |
- with partials = '() |
|
215 |
- with complete-class = (find-class 'complete-entry) |
|
216 |
- with partial-class = (find-class 'partial-entry) |
|
217 |
- for el in list |
|
218 |
- when (eq (class-of el) complete-class) do (push el completes) |
|
219 |
- when (eq (class-of el) partial-class) do (push el partials) |
|
220 |
- finally (return (values completes partials)))) |
|
225 |
+(defun group-by-class (list &optional accum1 accum2) |
|
226 |
+ (tagbody ; Let's do some TCO ... |
|
227 |
+ start |
|
228 |
+ (if (null list) |
|
229 |
+ (return-from group-by-class (list accum1 accum2)) |
|
230 |
+ (destructuring-bind (head . tail) list |
|
231 |
+ (etypecase head |
|
232 |
+ (complete-entry (setf accum1 (cons head accum1))) ; Here we set the accumulators |
|
233 |
+ (partial-entry (setf accum2 (cons head accum2)))) ; to the appropriate values. |
|
234 |
+ (setf list tail) ; Here we step towards the terminating condition |
|
235 |
+ (go start))))) ; Recurse |
|
221 | 236 |
|
222 | 237 |
(defun pprint-log (args &key client reverse status help) |
223 | 238 |
(when help |
... | ... |
@@ -234,7 +249,7 @@ |
234 | 249 |
|
235 | 250 |
(let ((*default-time-sheet-file* (or (car args) *default-time-sheet-file*)) |
236 | 251 |
(*print-pretty* t)) |
237 |
- (multiple-value-bind (complete-ranges incomplete-ranges) (group-by-class (get-log *default-time-sheet-file*)) |
|
252 |
+ (destructuring-bind (complete-ranges incomplete-ranges) (group-by-class (get-log *default-time-sheet-file*)) |
|
238 | 253 |
(let ((complete-results (sort-results complete-ranges)) |
239 | 254 |
(incomplete-results (sort-results incomplete-ranges t))) |
240 | 255 |
(pprint-results complete-results incomplete-results status)))))) |