Browse code
Use format string gen and allow multiple inputs
fiddlerwoaroof authored on 19/03/2016 17:22:50
Showing 9 changed files
Showing 9 changed files
- freshbooks.lisp
- macros.lisp
- main-classes.lisp
- package.lisp
- parser.lisp
- timesheet
- timesheet.asd
- timesheet.lisp
- utils/buildapp
... | ... |
@@ -153,9 +153,10 @@ |
153 | 153 |
(defun make-time-entry (project task date hours notes) |
154 | 154 |
(<:time_entry () |
155 | 155 |
(<:project_id () |
156 |
- (slot-value (get-project project) |
|
157 |
- 'project_id)) |
|
158 |
- (<:task_id () (identity task)) |
|
156 |
+ (parse-integer |
|
157 |
+ (slot-value (get-project project) |
|
158 |
+ 'project_id))) |
|
159 |
+ (<:task_id () (parse-integer task)) |
|
159 | 160 |
(<:date () (identity date)) |
160 | 161 |
(<:hours () (identity hours)) |
161 | 162 |
(<:notes () (identity notes)))) |
... | ... |
@@ -175,7 +176,7 @@ |
175 | 176 |
for project = (timesheet::client entry) |
176 | 177 |
for note = (timesheet::memo entry) |
177 | 178 |
for hours = (timesheet::duration entry) |
178 |
- for fmt-date = (format nil "~:@{~2,'0d-~2,'0d-~2,'0d~}" |
|
179 |
+ for fmt-date = (format nil "~:@{~2,'0d-~2,'0d-~2,'0d 00:00:00~}" |
|
179 | 180 |
(reverse (timesheet::unroll-date date))) |
180 | 181 |
collect (make-time-entry project task-id fmt-date hours note)))) |
181 | 182 |
|
... | ... |
@@ -4,7 +4,7 @@ |
4 | 4 |
`(defmethod == ((a ,class) (b ,class)) |
5 | 5 |
(declare (optimize (speed 3))) |
6 | 6 |
(and ,@(loop for (slot . test) in test-defs |
7 |
- with test-val = (or (car test) 'eql) |
|
7 |
+ for test-val = (or (car test) 'eql) |
|
8 | 8 |
collect `(,test-val (slot-value a ',slot) |
9 | 9 |
(slot-value b ',slot)))))) |
10 | 10 |
|
... | ... |
@@ -23,7 +23,10 @@ |
23 | 23 |
(defclass partial-entry (parsed-entry) |
24 | 24 |
((start-times :initarg :start-times :initform nil :accessor start-times))) |
25 | 25 |
|
26 |
-(define-condition incomplete-entry-warning (warning) ()) |
|
26 |
+(define-condition incomplete-entry-warning (warning) () |
|
27 |
+ (:report (lambda (condition stream) |
|
28 |
+ (declare (ignore condition)) |
|
29 |
+ (format stream "Incomplete Entry Found")))) |
|
27 | 30 |
|
28 | 31 |
(define-condition parsing-error () |
29 | 32 |
((leftovers :initarg :leftovers :accessor leftovers)) |
... | ... |
@@ -40,18 +43,25 @@ |
40 | 43 |
(:method ((calc status-calculator) (status-line status-line)) |
41 | 44 |
(* (rate calc) (duration status-line)))) |
42 | 45 |
|
46 |
+#| |
|
47 |
+(define-message print-partial-line (client memo duration starts) |
|
48 |
+ ) |
|
49 |
+|# |
|
50 |
+ |
|
43 | 51 |
(defgeneric print-entries (entries) |
44 | 52 |
(:method ((entries list)) |
45 | 53 |
(mapcar #'print-entries entries)) |
46 | 54 |
(:method ((entry partial-entry)) |
47 |
- (format t "~&~4<~>~a, ~a:~%~{~12<~>one starting at ~a~%~}" |
|
55 |
+ (format t "~&~4<~>~a, ~a (currently ~3,2f hours):~%~{~12<~>one starting at ~a~%~}" |
|
48 | 56 |
(client entry) |
49 | 57 |
(memo entry) |
58 |
+ (handler-bind ((incomplete-entry-warning (lambda (c) c (invoke-restart 'duration-to-now)))) |
|
59 |
+ (calculate-duration-in-15mins (duration entry))) |
|
50 | 60 |
(mapcar |
51 |
- (alambda (local-time:format-timestring |
|
52 |
- nil it |
|
53 |
- :format '(:year #\/ (:month 2) #\/ (:day 2) #\Space |
|
54 |
- (:hour 2) #\: (:min 2) #\: (:sec 2)))) |
|
61 |
+ (alambda (list (local-time:format-timestring |
|
62 |
+ nil it |
|
63 |
+ :format '(:year #\/ (:month 2) #\/ (:day 2) #\Space |
|
64 |
+ (:hour 2) #\: (:min 2) #\: (:sec 2))))) |
|
55 | 65 |
(start-times entry)))) |
56 | 66 |
(:method ((it complete-entry)) |
57 | 67 |
(format t "~&~4a ~10<~:(~a~)~> ~7,2F hrs ~a~%" |
... | ... |
@@ -71,8 +81,16 @@ |
71 | 81 |
|
72 | 82 |
|
73 | 83 |
(defmethod duration ((obj partial-entry)) |
74 |
- (warn "incomplete entry detected for ~a" (client obj)) |
|
75 |
- (local-time-duration:duration)) |
|
84 |
+ (restart-case |
|
85 |
+ (progn (warn 'incomplete-entry-warning) |
|
86 |
+ (local-time-duration:duration)) |
|
87 |
+ (zero-duration () (local-time-duration:duration)) |
|
88 |
+ (duration-to-now () |
|
89 |
+ (let ((now (local-time:now))) |
|
90 |
+ (local-time-duration:timestamp-difference |
|
91 |
+ now |
|
92 |
+ (extremum (start-times obj) #'local-time-duration:duration> |
|
93 |
+ :key (alambda (local-time-duration:timestamp-difference now it)))))))) |
|
76 | 94 |
|
77 | 95 |
(defun make-complete-entry (date client memo duration) |
78 | 96 |
(make-instance 'complete-entry |
... | ... |
@@ -58,7 +58,7 @@ |
58 | 58 |
(make-simple-equality time-record :test ==) |
59 | 59 |
(make-simple-equality time-obj :test eql) |
60 | 60 |
(make-equality date-obj |
61 |
- (day-of-week eql) |
|
61 |
+ (day-of-week ==) |
|
62 | 62 |
(year) (month) (day)) |
63 | 63 |
(make-simple-equality time-mod :test equal) |
64 | 64 |
|
... | ... |
@@ -378,8 +378,8 @@ |
378 | 378 |
|
379 | 379 |
(defun .date-record () |
380 | 380 |
(.let* ((date (.date-line)) |
381 |
- (records (.records))) |
|
382 |
- (.identity (make-day-entry date records)))) |
|
381 |
+ (records (.records))) |
|
382 |
+ (.identity (make-day-entry date records)))) |
|
383 | 383 |
|
384 | 384 |
(defun .date-records () |
385 | 385 |
(.first (.map 'list (.date-record)))) |
... | ... |
@@ -625,7 +625,9 @@ |
625 | 625 |
(st:should be == `(((,(make-time-obj 0 0 0) ,(make-time-obj 1 0 0) ,(make-time-mod -10 "mins")) . "")) |
626 | 626 |
(run (.time-range) "00:00--01:00-10mins"))) |
627 | 627 |
|
628 |
-(st:deftest == () |
|
628 |
+(st:deftest generic-eq () |
|
629 |
+ "Note: this really should be in the equality package with the name == |
|
630 |
+ should-test only checks tests for _internal_ symbols." |
|
629 | 631 |
(st:should be eql t (== #\1 #\1)) |
630 | 632 |
(st:should be eql t (== 1 1)) |
631 | 633 |
(st:should be eql t (== "1" "1")) |
... | ... |
@@ -633,6 +635,7 @@ |
633 | 635 |
(st:should be eql t (== #("1") #("1"))) |
634 | 636 |
(st:should be eql t (== '(1 . 2) '(1 . 2))) |
635 | 637 |
(st:should be eql t (== '((1 . 2)) '((1 . 2)))) |
638 |
+ (st:should be eql t (== #1=(make-date-obj "Monday" 2020 01 01) #1#)) |
|
636 | 639 |
|
637 | 640 |
(st:should be eql t |
638 | 641 |
(== (make-date-obj "Monday" 2012 01 01) |
... | ... |
@@ -6,21 +6,22 @@ |
6 | 6 |
:author "fiddlerwoaroof" |
7 | 7 |
:license "MIT" |
8 | 8 |
:depends-on (#:alexandria |
9 |
- #:serapeum |
|
10 | 9 |
#:anaphora |
10 |
+ #:cells |
|
11 |
+ #:command-line-arguments |
|
12 |
+ #:drakma |
|
13 |
+ #:format-string-builder |
|
14 |
+ #:fwoar.lisputils |
|
15 |
+ #:local-time-duration |
|
16 |
+ #:lquery |
|
17 |
+ #:manardb |
|
11 | 18 |
#:ningle |
12 |
- #:spinneret |
|
19 |
+ #:serapeum |
|
13 | 20 |
#:should-test |
14 |
- #:fwoar.lisputils |
|
15 | 21 |
#:smug |
16 |
- #:cells |
|
22 |
+ #:spinneret |
|
17 | 23 |
#:ubiquitous |
18 |
- #:command-line-arguments |
|
19 |
- #:manardb |
|
20 |
- #:local-time-duration |
|
21 | 24 |
#:xhtmlambda |
22 |
- #:drakma |
|
23 |
- #:lquery |
|
24 | 25 |
) |
25 | 26 |
:serial t |
26 | 27 |
:components ((:file "package") |
... | ... |
@@ -6,6 +6,16 @@ |
6 | 6 |
|
7 | 7 |
;;; "timesheet" goes here. Hacks and glory await! |
8 | 8 |
|
9 |
+(defmacro maybe-list (test &optional val) |
|
10 |
+ "If both arguments passed, when test is true, return a list containing val or, when test is false, return nil. |
|
11 |
+ If one argument passed, when test names something that is not a list, return a list containing it, otherwise |
|
12 |
+ return nil." |
|
13 |
+ (once-only (test) |
|
14 |
+ (let ((test (if val test `(not (listp ,test)))) |
|
15 |
+ (val (if val val test))) |
|
16 |
+ `(when ,test |
|
17 |
+ (list ,val))))) |
|
18 |
+ |
|
9 | 19 |
(defclass report () |
10 | 20 |
((status-calculator :initarg :status-calculator :accessor status-calculator) |
11 | 21 |
(status-lines :initform nil :accessor :status-lines) |
... | ... |
@@ -19,8 +29,11 @@ |
19 | 29 |
(let ((dest (make-string (file-length s)))) |
20 | 30 |
(read-sequence dest s) |
21 | 31 |
(multiple-value-bind (parsed leftovers) (smug:parse (timesheet.parser::.date-records) dest) |
22 |
- (unless (or (null leftovers) (string= leftovers "")) |
|
23 |
- (cerror "Continue?" 'parsing-error :leftovers leftovers)) |
|
32 |
+ (loop |
|
33 |
+ (restart-case |
|
34 |
+ (if (or (null leftovers) (string= leftovers "")) |
|
35 |
+ (return parsed) |
|
36 |
+ (cerror "Continue?" 'parsing-error :leftovers leftovers)))) |
|
24 | 37 |
parsed)))) |
25 | 38 |
|
26 | 39 |
(defun unroll-date (date-obj) |
... | ... |
@@ -58,18 +71,16 @@ |
58 | 71 |
when time-mod do (push time-mod complete) |
59 | 72 |
finally (return (values complete partial)))))) |
60 | 73 |
|
74 |
+(defun calculate-duration-in-15mins (duration) |
|
75 |
+ (let ((duration-in-minutes (local-time-duration:duration-as duration :minute))) |
|
76 |
+ (coerce (/ (round duration-in-minutes 15) 4) |
|
77 |
+ 'float))) |
|
78 |
+ |
|
61 | 79 |
(defun calculate-rounded-ranges (ranges) |
62 |
- (flet ((calc-duration-in-15mins (duration) |
|
63 |
- (let ((duration-in-minutes (local-time-duration:duration-as duration :minute))) |
|
64 |
- (coerce (/ (round duration-in-minutes 15) 4) |
|
65 |
- 'float)))) |
|
66 |
- (calc-duration-in-15mins |
|
67 |
- (reduce #'local-time-duration:duration+ ranges |
|
68 |
- :initial-value (local-time-duration:duration))))) |
|
69 |
- |
|
70 |
-(defmacro list-or-null (test val) |
|
71 |
- `(when ,test |
|
72 |
- (list ,val))) |
|
80 |
+ (let-each (:be *) |
|
81 |
+ (local-time-duration:duration) |
|
82 |
+ (reduce #'local-time-duration:duration+ ranges :initial-value *) |
|
83 |
+ (calculate-duration-in-15mins *))) |
|
73 | 84 |
|
74 | 85 |
(defclass log-entry () |
75 | 86 |
((complete :initarg :complete) |
... | ... |
@@ -82,8 +93,8 @@ |
82 | 93 |
(multiple-value-bind (complete partial) (calculate-ranges ranges date) |
83 | 94 |
(list* |
84 | 95 |
(make-complete-entry date client memo (calculate-rounded-ranges complete)) |
85 |
- (list-or-null partial |
|
86 |
- (make-partial-entry date client memo partial)))))))) |
|
96 |
+ (maybe-list partial |
|
97 |
+ (make-partial-entry date client memo partial)))))))) |
|
87 | 98 |
(let-each (:be *) |
88 | 99 |
(slot-value entry 'records) |
89 | 100 |
(mapcan #'make-entry *)))) |
... | ... |
@@ -95,13 +106,18 @@ |
95 | 106 |
(defparameter +pprint-log-option-spec+ |
96 | 107 |
'((("client" #\c) :type boolean :optional t :documentation "Sort by client") |
97 | 108 |
(("reverse" #\r) :type boolean :optional t :documentation "Reverse sort") |
109 |
+ (("version" #\v) :type boolean :optional t :documentation "Version") |
|
98 | 110 |
(("status" #\s) :type boolean :optional t |
99 | 111 |
:documentation "Print a summary of the hours worked and the prices") |
100 | 112 |
(("help" #\h) :type boolean :optional t :documentation "show help"))) |
101 | 113 |
|
102 |
-(defparameter *version* "0:1") |
|
114 |
+(defparameter *version* "0:3") |
|
115 |
+ |
|
116 |
+(define-message version-message (version) |
|
117 |
+ (:own-line () "timesheet file parser, version " :str)) |
|
118 |
+ |
|
103 | 119 |
(defun show-version () |
104 |
- (format t "timesheet, common-lisp version ~a~%" *version*)) |
|
120 |
+ (version-message t *version*)) |
|
105 | 121 |
|
106 | 122 |
(defun show-help () |
107 | 123 |
(show-version) |
... | ... |
@@ -122,7 +138,7 @@ |
122 | 138 |
|
123 | 139 |
(defun update-clients (status-calculator entry) |
124 | 140 |
(flet ((ensure-client (client) |
125 |
- (ensure-gethash client |
|
141 |
+ (ensure-gethash client |
|
126 | 142 |
(client-totals status-calculator) |
127 | 143 |
(make-instance 'status-line :client client)))) |
128 | 144 |
(with-accessors ((client client)) entry |
... | ... |
@@ -135,19 +151,18 @@ |
135 | 151 |
(update-clients status-calculator result) |
136 | 152 |
(update status-calculator result)))) |
137 | 153 |
|
138 |
-;; Uses the first arg as a list. Adds 26 blanks to left |
|
139 |
-(defparameter +status-line-format-string+ "~&~:@{~:(~26<~a~>~):~7,2F hours @ ~7,2F $/hr = $~7,2F~}~%") |
|
154 |
+(define-message status-line-format (client duration rate cost) |
|
155 |
+ (:own-line () |
|
156 |
+ (:titlecase () (:rjust (26) :str)) |
|
157 |
+ ": " (:float 7 2) " hours @ " (:float 7 2) " $/hr = $" (:float 7 2))) |
|
158 |
+ |
|
140 | 159 |
(defun print-status (results) |
141 | 160 |
(let* ((status-calculator (calculate-results results))) |
142 |
- (labels ((status-line-format (&rest args) |
|
143 |
- (format t +status-line-format-string+ args)) |
|
144 |
- (print-status-line (status-line) |
|
161 |
+ (labels ((print-status-line (status-line) |
|
145 | 162 |
(with-slots (client duration) status-line |
146 |
- (status-line-format |
|
147 |
- client |
|
148 |
- duration |
|
149 |
- (rate status-calculator) |
|
150 |
- (calculate-cost status-calculator status-line)))) |
|
163 |
+ (status-line-format t client duration |
|
164 |
+ (rate status-calculator) |
|
165 |
+ (calculate-cost status-calculator status-line)))) |
|
151 | 166 |
(print-separator () |
152 | 167 |
(format t "~&~120,1,0,'-<~>~%"))) |
153 | 168 |
(let ((client-totals (client-totals status-calculator))) |
... | ... |
@@ -181,11 +196,15 @@ |
181 | 196 |
(setf list tail) ; Here we step towards the terminating condition |
182 | 197 |
(go start))))) ; Recurse |
183 | 198 |
|
184 |
-(defun pprint-log (args &key client reverse status help) |
|
199 |
+(defun pprint-log (args &key client reverse status help version) |
|
185 | 200 |
(when help |
186 | 201 |
(show-help) |
187 | 202 |
(return-from pprint-log)) |
188 | 203 |
|
204 |
+ (when version |
|
205 |
+ (show-version) |
|
206 |
+ (return-from pprint-log)) |
|
207 |
+ |
|
189 | 208 |
(flet ((sort-results (results &optional (client client)) |
190 | 209 |
(setf results (sort-by-date results)) |
191 | 210 |
(when client |
... | ... |
@@ -194,10 +213,11 @@ |
194 | 213 |
(setf results (nreverse results))) |
195 | 214 |
results)) |
196 | 215 |
|
197 |
- (let ((*default-time-sheet-file* (or (car args) *default-time-sheet-file*)) |
|
216 |
+ (let ((*default-time-sheet-file* (or args *default-time-sheet-file*)) |
|
198 | 217 |
(*print-pretty* t)) |
199 | 218 |
(let-each (:be *) |
200 |
- (get-log *default-time-sheet-file*) |
|
219 |
+ (loop for file in (ensure-list *default-time-sheet-file*) |
|
220 |
+ append (get-log file)) |
|
201 | 221 |
(group-by-class *) |
202 | 222 |
(destructuring-bind (complete-ranges incomplete-ranges) * |
203 | 223 |
(let ((complete-results (sort-results complete-ranges client)) |