Browse code
Rename from timesheet to tempores
fiddlerwoaroof authored on 28/04/2016 01:12:18
Showing 12 changed files
Showing 12 changed files
- freshbooks.lisp
- list-query.lisp
- macros.lisp
- main-classes.lisp
- mvc.lisp
- package.lisp
- parser.lisp
- timesheet.asd
- test-parser.lisp
- timesheet
- timesheet-client.lisp
- timesheet.lisp
... | ... |
@@ -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,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*) |
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: |
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 |
- |