git.fiddlerwoaroof.com
Browse code

Use format string gen and allow multiple inputs

fiddlerwoaroof authored on 19/03/2016 17:22:50
Showing 9 changed files
... ...
@@ -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
... ...
@@ -27,4 +27,5 @@
27 27
 
28 28
 (defpackage #:timesheet
29 29
   (:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils
30
-        #:timesheet.parser))
30
+        #:timesheet.parser)
31
+  (:import-from #:format-string-builder #:define-message))
... ...
@@ -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)
639 642
deleted file mode 100755
640 643
Binary files a/timesheet and /dev/null differ
... ...
@@ -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))
204 224
deleted file mode 100755
205 225
Binary files a/utils/buildapp and /dev/null differ