git.fiddlerwoaroof.com
Browse code

Rename from timesheet to tempores

fiddlerwoaroof authored on 28/04/2016 01:12:18
Showing 12 changed files
... ...
@@ -1,16 +1,16 @@
1
-(defpackage #:timesheet.freshbooks
1
+(defpackage #:tempores.freshbooks
2 2
   (:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils
3
-        #:timesheet.parser)
4
-  (:import-from #:timesheet #:timesheet)
5
-  (:export #:timesheet))
3
+        #:tempores.parser)
4
+  (:import-from #:tempores #:tempores)
5
+  (:export #:tempores))
6 6
 
7
-(in-package :timesheet.freshbooks)
7
+(in-package :tempores.freshbooks)
8 8
 
9 9
 (defvar *api-key*)
10 10
 (defvar *endpoint*)
11 11
 
12 12
 (defun init ()
13
-  (ubiquitous:restore 'timesheet)
13
+  (ubiquitous:restore 'tempores)
14 14
   (ubiquitous:value :freshbooks :api-key)
15 15
   (ubiquitous:value :freshbooks :endpoint))
16 16
 
... ...
@@ -76,13 +76,13 @@
76 76
   project_id name description rate bill_method client_id hour_budget
77 77
   tasks staff)
78 78
 
79
-(timesheet.macros:define-printer (task s)
79
+(tempores.macros:define-printer (task s)
80 80
   ((with-slots (task_id name) task
81 81
     (format s "~i~a (~a):" name task_id)))
82 82
   ((with-slots (task_id name) task
83 83
     (format s "~a (~a)" name task_id))))
84 84
 
85
-(timesheet.macros:define-printer (project s)
85
+(tempores.macros:define-printer (project s)
86 86
   ((with-slots (project_id name tasks) project
87 87
     (format s "~i~a (~a):~%~{~a~%~}" name project_id tasks)))
88 88
   ((with-slots (project_id name tasks) project
... ...
@@ -170,19 +170,19 @@
170 170
                  (hash-table-alist *task-registry*))))
171 171
     (cdr (assoc name tasks :test #'string-equal))))
172 172
 
173
-(defun timesheet-to-entries (timesheet-log)
173
+(defun tempores-to-entries (tempores-log)
174 174
   (let ((task-id (get-task-by-name "General")))
175
-    (loop for entry in timesheet-log
176
-          for date = (timesheet::date entry)
177
-          for project = (timesheet::client entry)
178
-          for note = (timesheet::memo entry)
179
-          for hours = (timesheet::duration entry)
175
+    (loop for entry in tempores-log
176
+          for date = (tempores::date entry)
177
+          for project = (tempores::client entry)
178
+          for note = (tempores::memo entry)
179
+          for hours = (tempores::duration entry)
180 180
           for fmt-date = (format nil "~:@{~2,'0d-~2,'0d-~2,'0d 00:00:00~}"
181
-                                 (reverse (timesheet.cli::unroll-date date)))
181
+                                 (reverse (tempores.cli::unroll-date date)))
182 182
           collect (make-time-entry project task-id fmt-date hours note))))
183 183
 
184 184
 (defun make-entry-updates ()
185
-  (let ((updates (timesheet-to-entries (timesheet::get-log #p"/home/edwlan/bucket/time.md"))))
185
+  (let ((updates (tempores-to-entries (tempores::get-log #p"/home/edwlan/bucket/time.md"))))
186 186
     (loop for update in updates
187 187
           collect (<:request (:method "time_entry.create") update))))
188 188
 
... ...
@@ -1,9 +1,9 @@
1 1
 
2
-(defpackage #:timesheet.ql
2
+(defpackage #:tempores.ql
3 3
   (:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils
4
-        #:timesheet.macros #:generic-equals))
4
+        #:tempores.macros #:generic-equals))
5 5
 
6
-(in-package #:timesheet.ql)
6
+(in-package #:tempores.ql)
7 7
 
8 8
 (defstruct (ql-clause (:type vector))
9 9
   clause-type clause-parts)
... ...
@@ -1,4 +1,4 @@
1
-(in-package #:timesheet.macros)
1
+(in-package #:tempores.macros)
2 2
 
3 3
 (defmacro make-equality (class &body test-defs)
4 4
   `(defmethod == ((a ,class) (b ,class))
... ...
@@ -1,4 +1,4 @@
1
-(in-package #:timesheet)
1
+(in-package #:tempores)
2 2
 
3 3
 (defclass status-calculator ()
4 4
   ((rate :initarg :rate :accessor rate)
... ...
@@ -1,6 +1,6 @@
1 1
 #|
2 2
 
3
-(in-package :timesheet.mvc)
3
+(in-package :tempores.mvc)
4 4
 
5 5
 (defclass model () ())
6 6
 
... ...
@@ -1,45 +1,45 @@
1 1
 ;;;; package.lisp
2
-(defpackage #:timesheet.packages
2
+(defpackage #:tempores.packages
3 3
   (:use #:cl))
4
-(in-package #:timesheet.packages)
4
+(in-package #:tempores.packages)
5 5
 
6 6
 
7 7
 (defpackage #:generic-equals
8 8
   (:use #:cl)
9 9
   (:export #:==))
10 10
 
11
-(defpackage #:timesheet.macros
11
+(defpackage #:tempores.macros
12 12
   (:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils #:generic-equals)
13 13
   (:export #:make-equality #:make-simple-equality #:defmethod-and-inverse
14 14
            #:define-printer #:quick-equalities))
15 15
 
16 16
 
17
-(defpackage #:timesheet.parser
17
+(defpackage #:tempores.parser
18 18
   (:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils #:smug
19
-        #:timesheet.macros #:generic-equals)
19
+        #:tempores.macros #:generic-equals)
20 20
   (:shadow #:parse)
21 21
   (:export #:parse #:unparse #:date #:records #:client #:ranges #:memo #:hour #:minute #:second
22 22
            #:day-of-week #:year #:month #:day #:amount #:unit))
23 23
 
24
-(defpackage #:timesheet.mvc
24
+(defpackage #:tempores.mvc
25 25
   (:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils)
26 26
   (:export #:model #:view #:controller #:display #:operate #:has-changed))
27 27
 
28
-(defpackage #:timesheet
28
+(defpackage #:tempores
29 29
   (:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils
30
-        #:timesheet.parser)
30
+        #:tempores.parser)
31 31
   (:import-from #:format-string-builder #:define-message)
32
-  (:export #:with-timesheet-configuration #:pprint-log #:get-log #:timesheet
32
+  (:export #:with-tempores-configuration #:pprint-log #:get-log #:tempores
33 33
            #:*default-time-sheet-file* #:*rate* #:group-by-class #:print-status
34 34
            #:print-entries #:autocorrect-warning))
35 35
 
36
-(defpackage #:timesheet.cli
36
+(defpackage #:tempores.cli
37 37
   (:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils
38
-        #:timesheet.parser #:timesheet #:net.didierverna.clon
38
+        #:tempores.parser #:tempores #:net.didierverna.clon
39 39
         #:plambda)
40 40
   (:import-from #:format-string-builder #:define-message))
41 41
 
42
-(in-package #:timesheet)
42
+(in-package #:tempores)
43 43
 
44 44
 (defvar *default-time-sheet-file*)
45 45
 (defvar *rate*)
... ...
@@ -1,4 +1,4 @@
1
-(in-package #:timesheet.parser)
1
+(in-package #:tempores.parser)
2 2
 
3 3
 (eval-when (:compile-toplevel :load-toplevel :execute)
4 4
   ;; make sure these classes am ready to go!
5 5
similarity index 88%
6 6
rename from timesheet.asd
7 7
rename to tempores.asd
... ...
@@ -1,8 +1,8 @@
1 1
 (in-package :asdf-user)
2
-;;;; timesheet.asd
2
+;;;; tempores.asd
3 3
 
4
-(asdf:defsystem #:timesheet
5
-  :description "Describe timesheet here"
4
+(asdf:defsystem #:tempores
5
+  :description "Describe tempores here"
6 6
   :author "fiddlerwoaroof"
7 7
   :license "MIT"
8 8
   :depends-on (#:alexandria
... ...
@@ -33,7 +33,7 @@
33 33
                (:file "mvc")
34 34
                (:file "main-classes")  
35 35
                (:file "freshbooks")  
36
-               (:file "timesheet")))
36
+               (:file "tempores")))
37 37
 
38 38
 
39 39
 ;; vim: set ft=lisp:
... ...
@@ -1,4 +1,4 @@
1
-(in-package #:timesheet.parser)
1
+(in-package #:tempores.parser)
2 2
 
3 3
 ;; This will help make sure everything is consumed when
4 4
 ;; we don't care about the parser's output.
5 5
deleted file mode 100755
6 6
Binary files a/timesheet and /dev/null differ
7 7
deleted file mode 100644
... ...
@@ -1,229 +0,0 @@
1
-(in-package #:timesheet.cli)
2
-
3
-(defparameter *interactive* nil)
4
-(defparameter *version* "0:7")
5
-
6
-(defun unroll-date (date-obj)
7
-  (with-slots (year month day) date-obj
8
-    (list day month year)))
9
-
10
-(defun split-time (time)
11
-  (let ((time-parts (split-sequence #\: time)))
12
-    (destructuring-bind (hours minutes . optional-seconds) time-parts
13
-      (let ((hours (parse-integer hours))
14
-            (minutes (parse-integer minutes))
15
-            (seconds (parse-integer (or (car optional-seconds) "0")))
16
-            (extra (cdr optional-seconds)))
17
-        (values hours minutes seconds extra)))))
18
-
19
-(defun try-fix-time (failed-time)
20
-  (handler-case
21
-    (multiple-value-bind (hours minutes seconds extra) (split-time failed-time)
22
-      (if (and (< hours 24) (< minutes 60) (< seconds 60) (null extra))
23
-        (values (format nil "~2,'0d:~2,'0d:~2,'0d" hours minutes seconds) t)
24
-        (values nil nil)))
25
-    (parse-error (c) c (values nil nil))))
26
-
27
-(defun call-with-prompt (stream prompt args cb)
28
-  (apply #'format stream prompt args)
29
-  (finish-output *query-io*)
30
-  (funcall cb (read-line *query-io*)))
31
-
32
-(defmacro with-prompt ((result-sym stream prompt &rest args) &body body)
33
-  `(call-with-prompt ,stream ,prompt (list ,@args)
34
-                     (lambda (,result-sym)
35
-                       ,@body)))
36
-
37
-(defun abort-with-message (stream message &rest args)
38
-  (apply #'format stream message args)
39
-  (abort))
40
-
41
-(define-condition parse-time-error (parse-error)
42
-  ((time-string :initarg :time-string :accessor time-string))
43
-  (:report (lambda (condition stream)
44
-             (format stream "Time input did not parse correctly: ~s" (time-string condition)))))
45
-
46
-(defun call-with-prompt-for-time (stream prompt args cb)
47
-  (call-with-prompt
48
-    stream prompt args
49
-    (lambda (time-string)
50
-      (multiple-value-bind (hours minutes seconds extra) (split-time time-string)
51
-        (funcall cb hours minutes seconds extra)))))
52
-
53
-(defmacro with-prompt-for-time ((result-syms stream prompt &rest args) &body body)
54
-  `(call-with-prompt-for-time ,stream ,prompt (list ,@args)
55
-                              (lambda (,@result-syms)
56
-                                ,@body)))
57
-
58
-(define-message format-time (hours minutes &optional (seconds 0))
59
-  (:decimal 2 '0) #\: (:decimal 2 '0) #\: (:decimal 2 '0))
60
-
61
-(defun handle-invalid-time (c) c
62
-  (let ((time (timesheet.parser::failed-chunk c)))
63
-    (multiple-value-bind (new-value success) (try-fix-time time)
64
-      (when success
65
-        (progn (warn 'timesheet::autocorrect-warning
66
-                     :old-value time
67
-                     :new-value new-value)
68
-               (smug:replace-invalid time new-value))))
69
-    (if *interactive*
70
-      (loop
71
-        (handler-case
72
-          (with-prompt-for-time ((hours minutes seconds &rest rest)
73
-                                 *query-io* "Invalid time ~a, replacement? " time)
74
-            (declare (ignore rest))
75
-            (let ((replacement (format-time nil hours minutes seconds)))
76
-              (format *query-io* "~&Replacing ~s with ~s.~%---~%" time replacement)
77
-              (smug:replace-invalid time replacement)))
78
-          (parse-error (c) c (format t "~&Invalid entry.~%"))))
79
-      (abort-with-message t "~&Time ~a is invalid.~%" time))))
80
-
81
-(defun handle-invalid-whitespace (ignore-whitespace-errors)
82
-  (lambda (c) c
83
-    (let ((extra-whitespace (timesheet.parser::failed-chunk c)))
84
-      (if (or ignore-whitespace-errors
85
-              (when *interactive*
86
-                (y-or-n-p "Invalid extra whitespace ~s, truncate?" extra-whitespace)))
87
-        (smug:replace-invalid extra-whitespace "")
88
-        (abort-with-message t "~&Whitespace errors~%")))))
89
-
90
-(defun parse-file (&optional (file *default-time-sheet-file*) ignore-whitespace-errors)
91
-  (flet ((parse-string (string)
92
-           (handler-bind ((timesheet.parser::invalid-whitespace
93
-                            (handle-invalid-whitespace ignore-whitespace-errors))
94
-                          (parse-error #'handle-invalid-time)
95
-                          (timesheet.parser::invalid-time #'handle-invalid-time) )
96
-             (smug:parse (timesheet.parser::.date-records) string))))
97
-    (multiple-value-bind (parsed leftovers) (parse-string (read-file-into-string file))
98
-      (if (or (null leftovers) (string= leftovers ""))
99
-        parsed
100
-        (cerror "Continue?" 'parsing-error :leftovers leftovers)))))
101
-
102
-(defun pprint-results (results incompletes status)
103
-  (print-entries results)
104
-
105
-  (when incompletes
106
-    (format t "~&~120,1,0,'-<~>~%Partial Entries:~%")
107
-    (print-entries incompletes))
108
-
109
-  (when status
110
-    (print-status results)))
111
-
112
-(defun sort-by-date (results)
113
-  (stable-sort results #'local-time:timestamp<
114
-               :key (alambda (apply #'local-time:encode-timestamp
115
-                                    (list* 0 0 0 0 (unroll-date (date it)))))))
116
-
117
-(defun maybe-nreverse (flag list)
118
-  (if flag
119
-    (nreverse list)
120
-    list))
121
-
122
-(define-modify-macro maybe-nreversef (flag)
123
-                     (lambda (place flag)
124
-                       (maybe-nreverse flag place)))
125
-
126
-(defun list-without-nulls (&rest items)
127
-  (loop for item in items
128
-        when item collect item))
129
-
130
-(defun pprint-log (args &key client reverse status ignore-whitespace interactive)
131
-  (labels ((sort-func (client)
132
-             (apply #'compose
133
-                    (list-without-nulls
134
-                      (when reverse #'nreverse)
135
-                      (when client
136
-                        (plambda (stable-sort :1 #'string-lessp :key #'client)))
137
-                      #'sort-by-date)))
138
-           (sort-results (results &optional (client client))
139
-             (funcall (sort-func client) results))
140
-           (get-logs (files)
141
-             (loop for file in (ensure-list files)
142
-                   append (timesheet:get-log file ignore-whitespace)) ))
143
-
144
-    (let ((*default-time-sheet-file* (or args *default-time-sheet-file*))
145
-          (*interactive* interactive)
146
-          (*print-pretty* t))
147
-      (let-each (:be *)
148
-        (get-logs *default-time-sheet-file*)
149
-        (group-by-class *)
150
-        (destructuring-bind (complete-ranges incomplete-ranges) *
151
-          (let ((complete-results (sort-results complete-ranges client))
152
-                (incomplete-results (sort-results incomplete-ranges t)))
153
-            (pprint-results complete-results incomplete-results status)))))))
154
-
155
-(defsynopsis (:postfix "TIMESHEETS ...")
156
-  (text :contents "A program for managing logs of hours worked")
157
-  (group (:header "Display options")
158
-         (flag :short-name "s" :long-name "status"
159
-               :description "Print a short summary of work status")
160
-         (flag :short-name "W"
161
-               :long-name "ignore-whitespace"
162
-               :description "Ignore whitespace errors in input")
163
-         (flag :short-name "i" :long-name "interactive"
164
-               :description "Run interactively"))
165
-  (group (:header "Sort options")
166
-         (flag :short-name "r"
167
-               :long-name "reverse"
168
-               :description "Reverse the sort direction")
169
-         (flag :short-name "c"
170
-               :long-name "client"
171
-               :description "Sort records by client"))
172
-  (group (:header "Freshbooks")
173
-         (flag :long-name "post-hours"
174
-               :description "Post hours to freshbooks (requires manual setup of Freshbooks keys)"))
175
-  (group (:header "Self-test options")
176
-         (flag :long-name "run-tests"
177
-               :description "Run the tests")
178
-         (enum :long-name "output-style"
179
-               :description "The kind of output to produce"
180
-               :default-value :normal
181
-               :enum '(:xunit :normal)))
182
-  (group (:header "Generic options")
183
-         (flag :short-name "v" :long-name "version"
184
-               :description "Show the program version")
185
-         (flag :short-name "h" :long-name "help"
186
-               :description "Show this help")))
187
-
188
-(eval-when (:load-toplevel :compile-toplevel :execute)
189
-  (define-message version-message (version)
190
-    (:own-line () "timesheet file parser, version " :str)))
191
-
192
-(defun show-version ()
193
-  (version-message t *version*))
194
-
195
-(defun tests-main (&optional (output-style nil output-style-p))
196
-  (let ((should-test:*verbose* t))
197
-    (ecase output-style
198
-      (:xunit (should-test:test-for-xunit *standard-output* :package :timesheet.parser))
199
-      (:normal (should-test:test :package :timesheet.parser)))))
200
-
201
-(defun pprint-log-main ()
202
-  (make-context)
203
-  (tagbody
204
-    start
205
-    (restart-case
206
-      (cond
207
-        ((getopt :long-name "help") (help))
208
-        ((getopt :long-name "version") (show-version))
209
-        ((getopt :long-name "post-hours") (let ((*print-pretty* nil))
210
-                                            (loop for item in (timesheet.freshbooks::post-time-entries-main)
211
-                                                  do (format t "Posted an entry")
212
-                                                  do (plump:serialize item)
213
-                                                  finally (format t "Don't forget to archive time file."))))
214
-        ((getopt :long-name "run-tests") (tests-main (getopt :long-name "output-style")))
215
-        (t (with-timesheet-configuration ()
216
-             (pprint-log
217
-               (remainder)
218
-               :client (getopt :long-name "client")
219
-               :interactive (getopt :long-name "interactive")
220
-               :ignore-whitespace (getopt :long-name "ignore-whitespace")
221
-               :status (getopt :long-name "status")
222
-               :reverse (getopt :long-name "reverse")))))
223
-      (retry () (go start))
224
-      (abort ()))))
225
-
226
-(defun make-executable ()
227
-  (dump "timesheet" pprint-log-main
228
-        :compression 8
229
-        :purify t))
230 0
deleted file mode 100644
... ...
@@ -1,151 +0,0 @@
1
-;; timesheet.lisp
2
-
3
-(in-package #:timesheet)
4
-
5
-;;; "timesheet" goes here. Hacks and glory await!
6
-
7
-(defmacro maybe-list (test &optional val)
8
-  "If both arguments passed, when test is true, return a list containing val
9
-   or, when test is false, return nil.  If one argument passed, when test names
10
-   something that is not a list, return a list containing it, otherwise
11
-   return nil."
12
-  (once-only (test)
13
-    (let ((test (if val test `(not (listp ,test))))
14
-          (val (if val val test)))
15
-      `(when ,test
16
-         (list ,val)))))
17
-
18
-(defclass report ()
19
-  ((status-calculator :initarg :status-calculator :accessor status-calculator)
20
-   (status-lines :initform nil :accessor :status-lines)
21
-   (entries :initform nil :accessor :entries)))
22
-
23
-(defun combine-date-time (time-obj day month year)
24
-  (declare (optimize (debug 3)))
25
-  (with-slots (second minute hour) time-obj
26
-    (local-time:encode-timestamp 0 second minute hour
27
-                                 day month year)))
28
-
29
-(defun calculate-ranges (ranges date)
30
-  (declare (optimize (debug 3)))
31
-  (labels ((time-mod-unit-keyword (time-mod)
32
-             (make-keyword
33
-               (string-upcase
34
-                 (if (string= (slot-value time-mod 'unit) "mins")
35
-                   "minute"
36
-                   "hour"))))
37
-           (make-mod (mod)
38
-             (when mod
39
-               (let ((unit (time-mod-unit-keyword mod))
40
-                     (amount (slot-value mod 'timesheet.parser:amount)))
41
-                 (funcall #'local-time-duration:duration unit amount)))))
42
-    (with-slots (year month day) date
43
-      (loop with complete = nil
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))))))
53
-
54
-(defun calculate-duration-in-15mins (duration)
55
-  (let ((duration-in-minutes (local-time-duration:duration-as duration :minute)))
56
-    (coerce (/ (round duration-in-minutes 15) 4)
57
-            'float)))
58
-
59
-(defun calculate-rounded-ranges (ranges)
60
-  (let-each (:be *)
61
-    (local-time-duration:duration)
62
-    (reduce #'local-time-duration:duration+ ranges :initial-value *)
63
-    (calculate-duration-in-15mins *)))
64
-
65
-(defclass log-entry ()
66
-  ((complete :initarg :complete)
67
-   (incomplete :initarg :incomplete)))
68
-
69
-(defun get-entry-ranges (entry)
70
-  (flet ((make-entry (record)
71
-           (let ((date (slot-value entry 'date)))
72
-             (with-slots (client memo ranges) record
73
-               (multiple-value-bind (complete partial) (calculate-ranges ranges date)
74
-                 (list*
75
-                   (make-complete-entry date client memo (calculate-rounded-ranges complete))
76
-                   (maybe-list partial
77
-                               (make-partial-entry date client memo partial))))))))
78
-    (let-each (:be *)
79
-      (slot-value entry 'records)
80
-      (mapcan #'make-entry *))))
81
-
82
-(defun get-log (&optional (file *default-time-sheet-file*) ignore-whitespace)
83
-  (let* ((entries (timesheet.cli::parse-file file ignore-whitespace)))
84
-    (mapcan #'get-entry-ranges entries)))
85
-
86
-(defun group-by-client (incompletes)
87
-  (let ((results (make-hash-table :test 'equalp)))
88
-    (loop for incomplete in incompletes
89
-          for client = (client incomplete)
90
-          do (push incomplete (gethash client results)))
91
-    (hash-table-alist results)))
92
-
93
-(defun update-clients (status-calculator entry)
94
-  (flet ((ensure-client (client)
95
-           (ensure-gethash client
96
-                           (client-totals status-calculator)
97
-                           (make-instance 'status-line :client client))))
98
-    (with-accessors ((client client)) entry
99
-      (let ((client-hash-table (ensure-client client)))
100
-        (update client-hash-table entry)))))
101
-
102
-(defun calculate-results (results &optional (rate *rate*))
103
-  (let-first (:be status-calculator) (make-status-calculator rate)
104
-    (dolist (result results)
105
-      (update-clients status-calculator result)
106
-      (update status-calculator result))))
107
-
108
-(define-message status-line-format (client duration rate cost)
109
-  (:own-line ()
110
-   (:titlecase () (:rjust (26) :str))
111
-   ": " (:float 7 2) " hours @ " (:float 7 2) " $/hr = $" (:float 7 2)))
112
-
113
-(defun print-status (results)
114
-  (let* ((status-calculator (calculate-results results))
115
-         (client-totals (client-totals status-calculator)))
116
-    (labels ((print-status-line (status-line)
117
-               (with-slots (client duration) status-line
118
-                 (status-line-format t client duration
119
-                                     (rate status-calculator)
120
-                                     (calculate-cost status-calculator status-line))))
121
-             (print-separator ()
122
-               (format t "~&~120,1,0,'-<~>~%")))
123
-      (let-each (:be *)
124
-        (print-separator)
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*))))))
130
-
131
-
132
-(defun group-by-class (list &optional accum1 accum2)
133
-  (tagbody ; Let's do some TCO ...
134
-    start
135
-    (if (null list)
136
-      (return-from group-by-class (list accum1 accum2))
137
-      (destructuring-bind (head . tail) list
138
-        (etypecase head
139
-          (complete-entry (setf accum1 (cons head accum1))) ; Here we set the accumulators
140
-          (partial-entry (setf accum2 (cons head accum2)))) ;  to the appropriate values.
141
-        (setf list tail) ; Here we step towards the terminating condition
142
-        (go start))))) ; Recurse
143
-
144
-(defmacro with-timesheet-configuration (() &body body)
145
-  `(progn
146
-     (ubiquitous:restore 'timesheet)
147
-     (let ((*rate* (ubiquitous:defaulted-value 0 :rate))
148
-           (*default-time-sheet-file*
149
-             (ubiquitous:defaulted-value #p"~/time.md" :timesheet :file)))
150
-       ,@body)))
151
-