git.fiddlerwoaroof.com
Browse code

Aditional changes of various kinds.

fiddlerwoaroof authored on 24/04/2016 05:37:26
Showing 3 changed files
... ...
@@ -28,10 +28,17 @@
28 28
 (defpackage #:timesheet
29 29
   (:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils
30 30
         #:timesheet.parser)
31
-  (:import-from #:format-string-builder #:define-message))
31
+  (:import-from #:format-string-builder #:define-message)
32
+  (:export #:with-timesheet-configuration #:pprint-log #:get-log #:timesheet
33
+           #:*default-time-sheet-file* #:*rate* #:group-by-class #:print-status
34
+           #:print-entries #:autocorrect-warning))
32 35
 
33 36
 (defpackage #:timesheet.cli
34 37
   (:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils
35 38
         #:timesheet.parser #:timesheet #:net.didierverna.clon)
36 39
   (:import-from #:format-string-builder #:define-message))
37 40
 
41
+(in-package #:timesheet)
42
+
43
+(defvar *default-time-sheet-file*)
44
+(defvar *rate*)
... ...
@@ -1,41 +1,191 @@
1
-
2 1
 (in-package #:timesheet.cli)
3 2
 
3
+(defparameter *interactive* nil)
4
+(defparameter *version* "0:5")
5
+
4 6
 (defsynopsis (:postfix "TIMESHEETS ...")
5 7
   (text :contents "A program for managing logs of hours worked")
6
-  (group (:header "Main actions")
7
-         (flag :short-name "c"
8
-               :long-name "client"
9
-               :description "Sort records by client")
10
-         (flag :short-name "r"
11
-               :long-name "reverse"
12
-               :description "Reverse the sort direction")
8
+  (group (:header "Display options")
9
+         (flag :short-name "s" :long-name "status"
10
+               :description "Print a short summary of work status")
13 11
          (flag :short-name "W"
14 12
                :long-name "ignore-whitespace"
15 13
                :description "Ignore whitespace errors in input")
16 14
          (flag :short-name "i" :long-name "interactive"
17
-               :description "Run interactively") 
18
-         (flag :short-name "s" :long-name "status"
19
-               :description "Print a short summary of work status"))
20
-  (group (:header "Other options")
15
+               :description "Run interactively"))
16
+  (group (:header "Sort options")
17
+         (flag :short-name "r"
18
+               :long-name "reverse"
19
+               :description "Reverse the sort direction")
20
+         (flag :short-name "c"
21
+               :long-name "client"
22
+               :description "Sort records by client"))
23
+  (group (:header "Generic options")
21 24
          (flag :short-name "v" :long-name "version"
22
-               :description "Show the program version")  
25
+               :description "Show the program version")
23 26
          (flag :short-name "h" :long-name "help"
24 27
                :description "Show this help")))
25 28
 
29
+(define-message version-message (version)
30
+  (:own-line () "timesheet file parser, version " :str))
31
+
32
+(defun unroll-date (date-obj)
33
+  (with-slots (year month day) date-obj
34
+    (list day month year)))
35
+
36
+(defun show-version ()
37
+  (version-message t *version*))
38
+
39
+(defun split-time (time)
40
+  (let ((time-parts (split-sequence #\: time)))
41
+    (destructuring-bind (hours minutes . optional-seconds) time-parts
42
+      (let ((hours (parse-integer hours))
43
+            (minutes (parse-integer minutes))
44
+            (seconds (parse-integer (or (car optional-seconds) "0")))
45
+            (extra (cdr optional-seconds)))
46
+        (values hours minutes seconds extra)))))
47
+
48
+(defun try-fix-time (failed-time)
49
+  (handler-case
50
+    (multiple-value-bind (hours minutes seconds extra) (split-time failed-time)
51
+      (if (and (< hours 24) (< minutes 60) (< seconds 60) (null extra))
52
+        (values (format nil "~2,'0d:~2,'0d:~2,'0d" hours minutes seconds) t)
53
+        (values nil nil)))
54
+    (parse-error (c) c (values nil nil))))
55
+
56
+(defun call-with-prompt (stream prompt args cb)
57
+  (apply #'format stream prompt args)
58
+  (finish-output *query-io*)
59
+  (funcall cb (read-line *query-io*)))
60
+
61
+(defmacro with-prompt ((result-sym stream prompt &rest args) &body body)
62
+  `(call-with-prompt ,stream ,prompt (list ,@args)
63
+                     (lambda (,result-sym)
64
+                       ,@body)))
65
+
66
+(defun abort-with-message (stream message &rest args)
67
+  (apply #'format stream message args)
68
+  (abort))
69
+
70
+(define-condition parse-time-error (parse-error)
71
+  ((time-string :initarg :time-string :accessor time-string))
72
+  (:report (lambda (condition stream)
73
+             (format stream "Time input did not parse correctly: ~s" (time-string condition)))))
74
+
75
+(defun call-with-prompt-for-time (stream prompt args cb)
76
+  (call-with-prompt
77
+    stream prompt args
78
+    (lambda (time-string)
79
+      (multiple-value-bind (hours minutes seconds extra) (split-time time-string)
80
+        (funcall cb hours minutes seconds extra)))))
81
+
82
+(defmacro with-prompt-for-time ((result-syms stream prompt &rest args) &body body)
83
+  `(call-with-prompt-for-time ,stream ,prompt (list ,@args)
84
+                              (lambda (,@result-syms)
85
+                                ,@body)))
86
+
87
+(define-message format-time (hours minutes &optional (seconds 0))
88
+  (:decimal 2 '0) #\: (:decimal 2 '0) #\: (:decimal 2 '0))
89
+
90
+(defun handle-invalid-time (c) c
91
+  (let ((time (timesheet.parser::failed-chunk c)))
92
+    (multiple-value-bind (new-value success) (try-fix-time time)
93
+      (when success
94
+        (progn (warn 'timesheet::autocorrect-warning
95
+                     :old-value time
96
+                     :new-value new-value)
97
+               (smug:replace-invalid time new-value))))
98
+    (if *interactive*
99
+      (loop
100
+        (handler-case
101
+          (with-prompt-for-time ((hours minutes seconds &rest rest)
102
+                                 *query-io* "Invalid time ~a, replacement? " time)
103
+            (declare (ignore rest))
104
+            (let ((replacement (format-time nil hours minutes seconds)))
105
+              (format *query-io* "~&Replacing ~s with ~s.~%---~%" time replacement)
106
+              (smug:replace-invalid time replacement)))
107
+          (parse-error (c) c (format t "~&Invalid entry.~%"))))
108
+      (abort-with-message t "~&Time ~a is invalid.~%" time))))
109
+
110
+(defun handle-invalid-whitespace (ignore-whitespace-errors)
111
+  (lambda (c) c
112
+    (let ((extra-whitespace (timesheet.parser::failed-chunk c)))
113
+      (if (or ignore-whitespace-errors
114
+              (when *interactive*
115
+                (y-or-n-p "Invalid extra whitespace ~s, truncate?" extra-whitespace)))
116
+        (smug:replace-invalid extra-whitespace "")
117
+        (abort-with-message t "~&Whitespace errors~%")))))
118
+
119
+(defun parse-file (&optional (file *default-time-sheet-file*) ignore-whitespace-errors)
120
+  (flet ((parse-string (string)
121
+           (handler-bind ((timesheet.parser::invalid-whitespace
122
+                            (handle-invalid-whitespace ignore-whitespace-errors))
123
+                          (parse-error #'handle-invalid-time) 
124
+                          (timesheet.parser::invalid-time #'handle-invalid-time) )
125
+             (smug:parse (timesheet.parser::.date-records) string))))
126
+    (multiple-value-bind (parsed leftovers) (parse-string (read-file-into-string file))
127
+      (if (or (null leftovers) (string= leftovers ""))
128
+        parsed
129
+        (cerror "Continue?" 'parsing-error :leftovers leftovers)))))
130
+
131
+(defun pprint-results (results incompletes status)
132
+  (print-entries results)
133
+
134
+  (when incompletes
135
+    (format t "~&~120,1,0,'-<~>~%Partial Entries:~%")
136
+    (print-entries incompletes))
137
+
138
+  (when status
139
+    (print-status results)))
140
+
141
+(defun sort-by-date (results)
142
+  (stable-sort results #'local-time:timestamp<
143
+               :key (alambda (apply #'local-time:encode-timestamp
144
+                                    (list* 0 0 0 0 (unroll-date (date it)))))))
145
+
146
+(defun pprint-log (args &key client reverse status ignore-whitespace interactive)
147
+  (flet ((sort-results (results &optional (client client))
148
+           (setf results (sort-by-date results))
149
+           (when client
150
+             (setf results (stable-sort results #'string-lessp :key #'client)))
151
+           (when reverse
152
+             (setf results (nreverse results)))
153
+           results)
154
+         (get-logs (files)
155
+           (loop for file in (ensure-list files)
156
+                 append (timesheet:get-log file ignore-whitespace)) ))
157
+
158
+    (let ((*default-time-sheet-file* (or args *default-time-sheet-file*))
159
+          (*interactive* interactive)
160
+          (*print-pretty* t))
161
+      (let-each (:be *)
162
+        (get-logs *default-time-sheet-file*)
163
+        (group-by-class *)
164
+        (destructuring-bind (complete-ranges incomplete-ranges) *
165
+          (let ((complete-results (sort-results complete-ranges client))
166
+                (incomplete-results (sort-results incomplete-ranges t)))
167
+            (pprint-results complete-results incomplete-results status)))))))
168
+
26 169
 (defun pprint-log-main ()
27 170
   (make-context)
28
-  (cond 
29
-    ((getopt :long-name "help") (help))
30
-    (t (timesheet::with-timesheet-configuration ()
31
-         (timesheet::pprint-log
32
-           (remainder)
33
-           :client (getopt :long-name "client")
34
-           :interactive (getopt :long-name "interactive")  
35
-           :ignore-whitespace (getopt :long-name "ignore-whitespace")  
36
-           :version (getopt :long-name "version")  
37
-           :status (getopt :long-name "status")  
38
-           :reverse (getopt :long-name "reverse"))))))
171
+  (tagbody
172
+    start
173
+    (restart-case
174
+      (cond
175
+        ((getopt :long-name "help") (help))
176
+        ((getopt :long-name "version") (show-version))
177
+        (t (with-timesheet-configuration ()
178
+             (pprint-log
179
+               (remainder)
180
+               :client (getopt :long-name "client")
181
+               :interactive (getopt :long-name "interactive")
182
+               :ignore-whitespace (getopt :long-name "ignore-whitespace")
183
+               :status (getopt :long-name "status")
184
+               :reverse (getopt :long-name "reverse")))))
185
+      (retry () (go start))
186
+      (abort ()))))
39 187
 
40 188
 (defun make-executable ()
41
-  (dump "timesheet" pprint-log-main))
189
+  (dump "timesheet" pprint-log-main
190
+        :compression 8
191
+        :purify t))
... ...
@@ -20,62 +20,6 @@
20 20
    (status-lines :initform nil :accessor :status-lines)
21 21
    (entries :initform nil :accessor :entries)))
22 22
 
23
-(defvar *default-time-sheet-file*)
24
-(defvar *rate*)
25
-(defparameter *interactive* nil)
26
-
27
-(defun try-fix-time (failed-time)
28
-  (let ((time-parts (split-sequence #\: failed-time)))
29
-    (destructuring-bind (hours minutes . optional-seconds) time-parts
30
-      (let ((hours (parse-integer hours))
31
-            (minutes (parse-integer minutes))
32
-            (seconds (parse-integer (or (car optional-seconds) "0"))))
33
-        (if (and (< hours 24) (< minutes 60) (< seconds 60))
34
-          (values (format nil "~2,'0d:~2,'0d:~2,'0d" hours minutes seconds) t)
35
-          (values nil nil))))))
36
-
37
-(defun parse-file (&optional (file *default-time-sheet-file*) ignore-whitespace-errors)
38
-  (flet ((parse-string (string)
39
-           (handler-bind ((timesheet.parser::invalid-whitespace
40
-                            (lambda (c) c
41
-                              (let ((extra-whitespace (timesheet.parser::failed-chunk c)))
42
-                                (if (or ignore-whitespace-errors
43
-                                        (when *interactive*
44
-                                          (y-or-n-p "Invalid extra whitespace ~s, truncate?" extra-whitespace)))
45
-                                  (smug:replace-invalid extra-whitespace "")
46
-                                  (progn (format t "~&Whitespace errors~%")
47
-                                         (abort))))))
48
-                            (timesheet.parser::invalid-time
49
-                              (lambda (c) c
50
-                                (let ((time (timesheet.parser::failed-chunk c)))
51
-                                  (multiple-value-bind (new-value success) (try-fix-time time)
52
-                                    (when success
53
-                                      (progn (warn 'autocorrect-warning
54
-                                                   :old-value time
55
-                                                   :new-value new-value)
56
-                                             (smug:replace-invalid time new-value)))
57
-                                    (if *interactive*
58
-                                      (progn
59
-                                        (format *query-io* "Invalid time ~a, replacement? " time)
60
-                                        (finish-output *query-io*)
61
-                                        (let ((replacement (read-line)))
62
-                                          (format t "~&Replacing ~s with ~s.~%---~%" time replacement)
63
-                                          (smug:replace-invalid time replacement)))
64
-                                      (progn
65
-                                        (format t "~&Time ~a is invalid.~%" time)
66
-                                        (abort))))))))
67
-             (smug:parse (timesheet.parser::.date-records) string))))
68
-    (multiple-value-bind (parsed leftovers) (parse-string (read-file-into-string file))
69
-      (loop
70
-          (if (or (null leftovers) (string= leftovers ""))
71
-            (return parsed)
72
-            (cerror "Continue?" 'parsing-error :leftovers leftovers)))
73
-      parsed)))
74
-
75
-(defun unroll-date (date-obj)
76
-  (with-slots (year month day) date-obj
77
-    (list day month year)))
78
-
79 23
 (defun combine-date-time (time-obj day month year)
80 24
   (declare (optimize (debug 3)))
81 25
   (with-slots (second minute hour) time-obj
... ...
@@ -97,15 +41,15 @@
97 41
                  (funcall #'local-time-duration:duration unit amount)))))
98 42
     (with-slots (year month day) date
99 43
       (loop with complete = nil
100
-            with partial = nil
101
-            for (start-obj end-obj mod) in ranges
102
-            for start = (combine-date-time start-obj day month year)
103
-            for end = (when end-obj (combine-date-time end-obj day month year))
104
-            for time-mod = (when mod (make-mod mod))
105
-            if end do (push (local-time-duration:timestamp-difference end start) complete)
106
-            else do (push start partial)
107
-            when time-mod do (push time-mod complete)
108
-            finally (return (values complete partial))))))
44
+        with partial = nil
45
+        for (start-obj end-obj mod) in ranges
46
+        for start = (combine-date-time start-obj day month year)
47
+        for end = (when end-obj (combine-date-time end-obj day month year))
48
+        for time-mod = (when mod (make-mod mod))
49
+        if end do (push (local-time-duration:timestamp-difference end start) complete)
50
+        else do (push start partial)
51
+        when time-mod do (push time-mod complete)
52
+        finally (return (values complete partial))))))
109 53
 
110 54
 (defun calculate-duration-in-15mins (duration)
111 55
   (let ((duration-in-minutes (local-time-duration:duration-as duration :minute)))
... ...
@@ -136,14 +80,9 @@
136 80
       (mapcan #'make-entry *))))
137 81
 
138 82
 (defun get-log (&optional (file *default-time-sheet-file*) ignore-whitespace)
139
-  (let* ((entries (parse-file file ignore-whitespace)))
83
+  (let* ((entries (timesheet.cli::parse-file file ignore-whitespace)))
140 84
     (mapcan #'get-entry-ranges entries)))
141 85
 
142
-(defun sort-by-date (results)
143
-  (stable-sort results #'local-time:timestamp<
144
-               :key (alambda (apply #'local-time:encode-timestamp
145
-                                    (list* 0 0 0 0 (unroll-date (date it)))))))
146
-
147 86
 (defun group-by-client (incompletes)
148 87
   (let ((results (make-hash-table :test 'equalp)))
149 88
     (loop for incomplete in incompletes
... ...
@@ -168,11 +107,12 @@
168 107
 
169 108
 (define-message status-line-format (client duration rate cost)
170 109
   (:own-line ()
171
-    (:titlecase () (:rjust (26) :str))
172
-    ": " (:float 7 2) " hours @ " (:float 7 2) " $/hr = $" (:float 7 2)))
110
+   (:titlecase () (:rjust (26) :str))
111
+   ": " (:float 7 2) " hours @ " (:float 7 2) " $/hr = $" (:float 7 2)))
173 112
 
174 113
 (defun print-status (results)
175
-  (let* ((status-calculator (calculate-results results)))
114
+  (let* ((status-calculator (calculate-results results))
115
+         (client-totals (client-totals status-calculator)))
176 116
     (labels ((print-status-line (status-line)
177 117
                (with-slots (client duration) status-line
178 118
                  (status-line-format t client duration
... ...
@@ -180,24 +120,14 @@
180 120
                                      (calculate-cost status-calculator status-line))))
181 121
              (print-separator ()
182 122
                (format t "~&~120,1,0,'-<~>~%")))
183
-      (let ((client-totals (client-totals status-calculator)))
123
+      (let-each (:be *)
184 124
         (print-separator)
185
-        (let-each (:be *)
186
-          (hash-table-keys client-totals)
187
-          (sort * #'string-lessp)
188
-          (dolist (client *)
189
-            (print-status-line (gethash client client-totals)))))
190
-      (format t (total-line status-calculator *rate*)))))
191
-
192
-(defun pprint-results (results incompletes status)
193
-  (print-entries results)
125
+        (hash-table-keys client-totals)
126
+        (sort * #'string-lessp)
127
+        (dolist (client *)
128
+          (print-status-line (gethash client client-totals)))
129
+        (format t (total-line status-calculator *rate*))))))
194 130
 
195
-  (when incompletes
196
-    (format t "~&~120,1,0,'-<~>~%Partial Entries:~%")
197
-    (print-entries incompletes))
198
-
199
-  (when status
200
-    (print-status results)))
201 131
 
202 132
 (defun group-by-class (list &optional accum1 accum2)
203 133
   (tagbody ; Let's do some TCO ...
... ...
@@ -211,37 +141,6 @@
211 141
         (setf list tail) ; Here we step towards the terminating condition
212 142
         (go start))))) ; Recurse
213 143
 
214
-(defun pprint-log (args &key client reverse status help version ignore-whitespace interactive)
215
-  (when help
216
-    (show-help)
217
-    (return-from pprint-log))
218
-
219
-  (when version
220
-    (show-version)
221
-    (return-from pprint-log))
222
-
223
-  (flet ((sort-results (results &optional (client client))
224
-           (setf results (sort-by-date results))
225
-           (when client
226
-             (setf results (stable-sort results #'string-lessp :key #'client)))
227
-           (when reverse
228
-             (setf results (nreverse results)))
229
-           results)
230
-         (get-logs (files)
231
-           (loop for file in (ensure-list files)
232
-                 append (get-log file ignore-whitespace)) ))
233
-
234
-    (let ((*default-time-sheet-file* (or args *default-time-sheet-file*))
235
-          (*interactive* interactive)
236
-          (*print-pretty* t))
237
-      (let-each (:be *)
238
-        (get-logs *default-time-sheet-file*)
239
-        (group-by-class *)
240
-        (destructuring-bind (complete-ranges incomplete-ranges) *
241
-          (let ((complete-results (sort-results complete-ranges client))
242
-                (incomplete-results (sort-results incomplete-ranges t)))
243
-            (pprint-results complete-results incomplete-results status)))))))
244
-
245 144
 (defmacro with-timesheet-configuration (() &body body)
246 145
   `(progn
247 146
      (ubiquitous:restore 'timesheet)