Browse code
Finished a minimal command line interface
At this point, it just scans through the file and prints out a nicely
formatted log.
Showing 6 changed files
1 | 1 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,15 @@ |
1 |
+(in-package #:generic-equals) |
|
2 |
+ |
|
3 |
+(defgeneric == (a b) |
|
4 |
+ (:method (a b) (eql a b)) |
|
5 |
+ (:method ((a list) (b list)) |
|
6 |
+ (declare (optimize (speed 3) (space 3))) |
|
7 |
+ (if (or (null a) (null b)) |
|
8 |
+ (and (null a) (null b)) |
|
9 |
+ (and (== (car a) (car b)) |
|
10 |
+ (== (cdr a) (cdr b))))) |
|
11 |
+ (:method ((a vector) (b vector)) |
|
12 |
+ (declare (optimize (speed 3) (space 3))) |
|
13 |
+ (every #'identity (map 'vector #'== a b)))) |
|
14 |
+ |
|
15 |
+ |
0 | 16 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,41 @@ |
1 |
+(in-package #:timesheet.macros) |
|
2 |
+ |
|
3 |
+(defmacro make-equality (class &body test-defs) |
|
4 |
+ `(defmethod == ((a ,class) (b ,class)) |
|
5 |
+ (declare (optimize (speed 3))) |
|
6 |
+ (and ,@(loop for (slot . test) in test-defs |
|
7 |
+ with test-val = (or (car test) 'eql) |
|
8 |
+ collect `(,test-val (slot-value a ',slot) |
|
9 |
+ (slot-value b ',slot)))))) |
|
10 |
+ |
|
11 |
+(defmacro make-simple-equality (class &key (test 'eql) &environment env) |
|
12 |
+ (let ((class-def (find-class class t env))) |
|
13 |
+ `(defmethod == ((a ,class) (b ,class)) |
|
14 |
+ (declare (optimize (speed 3))) |
|
15 |
+ (and ,@(loop for slot in (closer-mop:class-direct-slots class-def) |
|
16 |
+ collect (let ((slot (closer-mop:slot-definition-name slot))) |
|
17 |
+ `(,test (slot-value a ',slot) |
|
18 |
+ (slot-value b ',slot)))))))) |
|
19 |
+ |
|
20 |
+(defmacro defmethod-and-inverse (name (arga argb) &body body) |
|
21 |
+ `(progn |
|
22 |
+ (defmethod ,name (,arga ,argb) |
|
23 |
+ (declare (optimize (speed 3))) |
|
24 |
+ ,@body) |
|
25 |
+ (defmethod ,name (,argb ,arga) |
|
26 |
+ (declare (optimize (speed 3))) |
|
27 |
+ ,@body))) |
|
28 |
+ |
|
29 |
+(defmacro define-printer ((obj stream &key (type t) (identity t)) (&body pretty) (&body normal)) |
|
30 |
+ `(defmethod print-object ((,obj ,obj) ,stream) |
|
31 |
+ (if *print-pretty* |
|
32 |
+ (progn |
|
33 |
+ ,@pretty) |
|
34 |
+ (print-unreadable-object (,obj ,stream :type ,type :identity ,identity) |
|
35 |
+ ,@normal)))) |
|
36 |
+ |
|
37 |
+(defmacro quick-equalities (&body defs) |
|
38 |
+ `(progn |
|
39 |
+ ,@(loop for (name test) in defs |
|
40 |
+ collect (list 'make-equality name :test test)))) |
|
41 |
+ |
... | ... |
@@ -1,13 +1,26 @@ |
1 | 1 |
;;;; package.lisp |
2 |
+(defpackage #:generic-equals |
|
3 |
+ (:use #:cl) |
|
4 |
+ (:export #:==)) |
|
2 | 5 |
|
3 |
-(defpackage #:timesheet.parser |
|
4 |
- (:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils #:smug) |
|
5 |
- ) |
|
6 |
+(defpackage #:timesheet.macros |
|
7 |
+ (:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils #:generic-equals) |
|
8 |
+ (:export #:make-equality #:make-simple-equality #:defmethod-and-inverse |
|
9 |
+ #:define-printer #:quick-equalities)) |
|
6 | 10 |
|
7 |
-(defpackage #:timesheet |
|
8 |
- (:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils) |
|
9 |
- ) |
|
11 |
+ |
|
12 |
+(defpackage #:timesheet.parser |
|
13 |
+ (:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils #:smug |
|
14 |
+ #:timesheet.macros #:generic-equals) |
|
15 |
+ (:shadow #:parse) |
|
16 |
+ (:export #:parse #:unparse #:date #:records #:client #:ranges #:memo #:hour #:minute #:second |
|
17 |
+ #:day-of-week #:year #:month #:day #:amount #:unit)) |
|
10 | 18 |
|
11 | 19 |
(defpackage #:timesheet.mvc |
12 | 20 |
(:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils) |
13 | 21 |
(:export #:model #:view #:controller #:display #:operate #:has-changed)) |
22 |
+ |
|
23 |
+(defpackage #:timesheet |
|
24 |
+ (:use #:cl #:anaphora #:alexandria #:serapeum #:fwoar.lisputils |
|
25 |
+ #:timesheet.parser)) |
|
26 |
+ |
... | ... |
@@ -1,38 +1,7 @@ |
1 | 1 |
(in-package #:timesheet.parser) |
2 | 2 |
|
3 |
-(defgeneric == (a b) |
|
4 |
- (:method (a b) (eql a b)) |
|
5 |
- (:method ((a list) (b list)) |
|
6 |
- (if (or (null a) (null b)) |
|
7 |
- (and (null a) (null b)) |
|
8 |
- (and (== (car a) (car b)) |
|
9 |
- (== (cdr a) (cdr b))))) |
|
10 |
- (:method ((a vector) (b vector)) (every #'identity (map 'vector #'== a b)))) |
|
11 |
- |
|
12 |
-(defmacro make-equality (class &body test-defs &environment env) |
|
13 |
- `(defmethod == ((a ,class) (b ,class)) |
|
14 |
- (declare (optimize (speed 3))) |
|
15 |
- (and ,@(loop for (slot . test) in test-defs |
|
16 |
- with test-val = (or (car test) 'eql) |
|
17 |
- collect `(,test-val (slot-value a ',slot) |
|
18 |
- (slot-value b ',slot)))))) |
|
19 |
- |
|
20 |
-(defmacro make-simple-equality (class &key (test 'eql) &environment env) |
|
21 |
- (let ((class-def (find-class class t env))) |
|
22 |
- `(defmethod == ((a ,class) (b ,class)) |
|
23 |
- (declare (optimize (speed 3))) |
|
24 |
- (and ,@(loop for slot in (closer-mop:class-direct-slots class-def) |
|
25 |
- collect (let ((slot (closer-mop:slot-definition-name slot))) |
|
26 |
- `(,test (slot-value a ',slot) |
|
27 |
- (slot-value b ',slot)))))))) |
|
28 |
- |
|
29 |
-#|(defmacro make-equalities (&body defs) |# |
|
30 |
-#| `(progn |# |
|
31 |
-#| ,@(loop for (name test) in defs |# |
|
32 |
-#| collect (list 'make-equality name :test test)))) |# |
|
33 |
- |
|
34 | 3 |
(eval-when (:compile-toplevel :load-toplevel :execute) |
35 |
- |
|
4 |
+ ;; make sure these classes am ready to go! |
|
36 | 5 |
(defclass day-entry () |
37 | 6 |
((date :initarg :date) |
38 | 7 |
(records :initarg :records))) |
... | ... |
@@ -92,26 +61,6 @@ |
92 | 61 |
(make-simple-equality time-mod :test equal) |
93 | 62 |
|
94 | 63 |
|
95 |
-(st:deftest == () |
|
96 |
- (st:should be eql t (== #\1 #\1)) |
|
97 |
- (st:should be eql t (== 1 1)) |
|
98 |
- (st:should be eql t (== "1" "1")) |
|
99 |
- (st:should be eql t (== '("1") '("1"))) |
|
100 |
- (st:should be eql t (== #("1") #("1"))) |
|
101 |
- (st:should be eql t (== '(1 . 2) '(1 . 2))) |
|
102 |
- (st:should be eql t (== '((1 . 2)) '((1 . 2)))) |
|
103 |
- (st:should be eql t |
|
104 |
- (== (make-time-mod 3 "mins") |
|
105 |
- (make-time-mod 3 "mins"))) |
|
106 |
- (st:should be eql t |
|
107 |
- (== (list (make-time-mod 3 "mins")) |
|
108 |
- (list (make-time-mod 3 "mins")))) |
|
109 |
- (st:should be eql t |
|
110 |
- (== #((make-time-mod 3 "mins")) |
|
111 |
- #((make-time-mod 3 "mins"))))) |
|
112 |
- |
|
113 |
- |
|
114 |
- |
|
115 | 64 |
(defun make-day-entry (date records) |
116 | 65 |
(make-instance 'day-entry :date date :records records)) |
117 | 66 |
|
... | ... |
@@ -135,15 +84,6 @@ |
135 | 84 |
(defun make-time-obj (hour minute &optional second) |
136 | 85 |
(make-instance 'time-obj :hour hour :minute minute :second second)) |
137 | 86 |
|
138 |
-(defmacro defmethod-and-inverse (name (arga argb) &body body) |
|
139 |
- `(progn |
|
140 |
- (defmethod ,name (,arga ,argb) |
|
141 |
- (declare (optimize (speed 3))) |
|
142 |
- ,@body) |
|
143 |
- (defmethod ,name (,argb ,arga) |
|
144 |
- (declare (optimize (speed 3))) |
|
145 |
- ,@body))) |
|
146 |
- |
|
147 | 87 |
(defmethod-and-inverse == ((date-obj date-obj) (list list)) |
148 | 88 |
(with-slots (day-of-week year month day) date-obj |
149 | 89 |
(every #'== (list day-of-week year month day) list))) |
... | ... |
@@ -152,40 +92,35 @@ |
152 | 92 |
(with-slots (hour minute second) time-obj |
153 | 93 |
(every #'== (list hour minute second) list))) |
154 | 94 |
|
155 |
-(defmacro define-printer ((obj stream &key (type t) (identity t)) &body body) |
|
156 |
- `(defmethod print-object ((,obj ,obj) ,stream) |
|
157 |
- (print-unreadable-object (,obj ,stream :type ,type :identity ,identity) |
|
158 |
- ,@body))) |
|
159 |
- |
|
160 | 95 |
(define-printer (date-obj s) |
161 |
- (with-slots (day-of-week year month day) date-obj |
|
162 |
- (format s "~a, ~2,'0d/~2,'0d/~2,'0d" (subseq |
|
163 |
- (string-capitalize day-of-week) |
|
164 |
- 0 3) |
|
165 |
- year month day))) |
|
96 |
+ ((with-slots (day-of-week year month day) date-obj |
|
97 |
+ (format s "~a, ~2,'0d/~2,'0d/~2,'0d" |
|
98 |
+ (subseq (string-capitalize day-of-week) 0 3) |
|
99 |
+ year month day))) |
|
100 |
+ ((with-slots (day-of-week year month day) date-obj |
|
101 |
+ (format s "~a, ~2,'0d/~2,'0d/~2,'0d" |
|
102 |
+ (subseq (string-capitalize day-of-week) 0 3) |
|
103 |
+ year month day)))) |
|
166 | 104 |
|
167 | 105 |
(define-printer (time-obj s) |
168 |
- (with-slots (hour minute second) time-obj |
|
169 |
- (format s "~2,'0d:~2,'0d:~2,'0d" hour minute second))) |
|
106 |
+ () |
|
107 |
+ ((with-slots (hour minute second) time-obj |
|
108 |
+ (format s "~2,'0d:~2,'0d:~2,'0d" hour minute second)))) |
|
170 | 109 |
|
171 |
-(defmethod print-object ((obj day-entry) s) |
|
172 |
- (print-unreadable-object (obj s :type t :identity t) |
|
173 |
- (with-slots (date records) obj |
|
174 |
- (format s "~d records for ~s" (length records) date)))) |
|
110 |
+(define-printer (day-entry s) |
|
111 |
+ () |
|
112 |
+ ((with-slots (date records) day-entry |
|
113 |
+ (format s "~d records for ~s" (length records) date)))) |
|
175 | 114 |
|
176 |
-(defmethod print-object ((obj time-record) s) |
|
177 |
- (print-unreadable-object (obj s :type t :identity t) |
|
178 |
- (with-slots (client) obj |
|
179 |
- (format s "For ~s" client)))) |
|
115 |
+(define-printer (time-record s) |
|
116 |
+ () |
|
117 |
+ ((with-slots (client) time-record |
|
118 |
+ (format s "For ~s" client)))) |
|
180 | 119 |
|
181 |
-(defmethod print-object ((obj time-mod) s) |
|
182 |
- (print-unreadable-object (obj s :type t :identity t) |
|
183 |
- (with-slots (amount unit) obj |
|
184 |
- (format s "~s ~s" amount unit)))) |
|
185 |
- |
|
186 |
-;; Time: |
|
187 |
-;; 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
188 |
-;; [0-2][0-9]:[0-6][0-8]:[0-6][0-6] |
|
120 |
+(define-printer (time-mod s) |
|
121 |
+ () |
|
122 |
+ ((with-slots (amount unit) time-mod |
|
123 |
+ (format s "~s ~s" amount unit)))) |
|
189 | 124 |
|
190 | 125 |
(defun .digit () |
191 | 126 |
(.is #'digit-char-p)) |
... | ... |
@@ -386,7 +321,10 @@ |
386 | 321 |
(month (.month)) |
387 | 322 |
(_ (.date-separator)) |
388 | 323 |
(day (.day))) |
389 |
- (.identity (make-date-obj dow year month day)))) |
|
324 |
+ (let ((year (parse-integer year)) |
|
325 |
+ (month (parse-integer month)) |
|
326 |
+ (day (parse-integer day))) |
|
327 |
+ (.identity (make-date-obj dow year month day))))) |
|
390 | 328 |
|
391 | 329 |
(defun .date-start () |
392 | 330 |
(.string= "-- ")) |
... | ... |
@@ -571,3 +509,22 @@ |
571 | 509 |
|
572 | 510 |
(st:should be == `((1 . "")) |
573 | 511 |
(run (.prog1 (.hour) (.not (.item))) "01"))) |
512 |
+ |
|
513 |
+(st:deftest == () |
|
514 |
+ (st:should be eql t (== #\1 #\1)) |
|
515 |
+ (st:should be eql t (== 1 1)) |
|
516 |
+ (st:should be eql t (== "1" "1")) |
|
517 |
+ (st:should be eql t (== '("1") '("1"))) |
|
518 |
+ (st:should be eql t (== #("1") #("1"))) |
|
519 |
+ (st:should be eql t (== '(1 . 2) '(1 . 2))) |
|
520 |
+ (st:should be eql t (== '((1 . 2)) '((1 . 2)))) |
|
521 |
+ (st:should be eql t |
|
522 |
+ (== (make-time-mod 3 "mins") |
|
523 |
+ (make-time-mod 3 "mins"))) |
|
524 |
+ (st:should be eql t |
|
525 |
+ (== (list (make-time-mod 3 "mins")) |
|
526 |
+ (list (make-time-mod 3 "mins")))) |
|
527 |
+ (st:should be eql t |
|
528 |
+ (== #((make-time-mod 3 "mins")) |
|
529 |
+ #((make-time-mod 3 "mins"))))) |
|
530 |
+ |
... | ... |
@@ -14,9 +14,15 @@ |
14 | 14 |
#:fwoar.lisputils |
15 | 15 |
#:smug |
16 | 16 |
#:cells |
17 |
- #:manardb) |
|
17 |
+ #:ubiquitous |
|
18 |
+ #:command-line-arguments |
|
19 |
+ #:manardb |
|
20 |
+ #:local-time-duration |
|
21 |
+ ) |
|
18 | 22 |
:serial t |
19 | 23 |
:components ((:file "package") |
24 |
+ (:file "generic-equals") |
|
25 |
+ (:file "macros") |
|
20 | 26 |
(:file "parser") |
21 | 27 |
(:file "mvc") |
22 | 28 |
(:file "timesheet"))) |
... | ... |
@@ -2,5 +2,130 @@ |
2 | 2 |
|
3 | 3 |
(in-package #:timesheet) |
4 | 4 |
|
5 |
+(ubiquitous:restore 'timesheet) |
|
6 |
+ |
|
5 | 7 |
;;; "timesheet" goes here. Hacks and glory await! |
6 | 8 |
|
9 |
+(defvar *default-time-sheet-file*) |
|
10 |
+(defvar *rate*) |
|
11 |
+ |
|
12 |
+(defun parse-file (&optional (file *default-time-sheet-file*)) |
|
13 |
+ (with-open-file (s *default-time-sheet-file* :direction :input) |
|
14 |
+ (let ((dest (make-string (file-length s)))) |
|
15 |
+ (read-sequence dest s) |
|
16 |
+ (caar (smug:run (timesheet.parser::.date-records) dest))))) |
|
17 |
+ |
|
18 |
+(defun unroll-date (date-obj) |
|
19 |
+ (with-slots (year month day) date-obj |
|
20 |
+ (list day month year))) |
|
21 |
+ |
|
22 |
+(defun calculate-ranges (ranges year month day) |
|
23 |
+ (loop for (start-obj end-obj mod) in ranges |
|
24 |
+ for start = (local-time:encode-timestamp 0 |
|
25 |
+ (slot-value start-obj 'second) |
|
26 |
+ (slot-value start-obj 'minute) |
|
27 |
+ (slot-value start-obj 'hour) |
|
28 |
+ day month year) |
|
29 |
+ for end = (local-time:encode-timestamp 0 |
|
30 |
+ (slot-value end-obj 'second) |
|
31 |
+ (slot-value end-obj 'minute) |
|
32 |
+ (slot-value end-obj 'hour) |
|
33 |
+ day month year ) |
|
34 |
+ for time-mod = (when time-mod |
|
35 |
+ (let ((unit (make-keyword |
|
36 |
+ (string-upcase |
|
37 |
+ (if (string= (slot-value time-mod 'timesheet.parser::unit) "mins") |
|
38 |
+ "minute" |
|
39 |
+ "hour")))) |
|
40 |
+ (amount (slot-value time-mod 'timesheet.parser:amount))) |
|
41 |
+ (funcall #'local-time-duration:duration unit amount))) |
|
42 |
+ nconc (list (local-time-duration:timestamp-difference end start) |
|
43 |
+ (or time-mod (local-time-duration:duration))))) |
|
44 |
+ |
|
45 |
+(defun calculate-rounded-ranges (ranges) |
|
46 |
+ (flet ((calc-duration-in-15mins (duration) |
|
47 |
+ (let ((duration-in-minutes (local-time-duration:duration-as duration :minute))) |
|
48 |
+ (coerce (/ (round duration-in-minutes 15) 4) |
|
49 |
+ 'float)))) |
|
50 |
+ (calc-duration-in-15mins |
|
51 |
+ (reduce #'local-time-duration:duration+ ranges |
|
52 |
+ :initial-value (local-time-duration:duration))))) |
|
53 |
+ |
|
54 |
+(defun get-log (&optional (file *default-time-sheet-file*)) |
|
55 |
+ (block nil |
|
56 |
+ (let* ((entries (parse-file file))) |
|
57 |
+ (loop for entry in entries |
|
58 |
+ for date = (slot-value entry 'date) |
|
59 |
+ nconc (with-slots (year month day) date |
|
60 |
+ (loop for record in (slot-value entry 'records) |
|
61 |
+ collect (with-slots (client memo ranges) record |
|
62 |
+ `(,date |
|
63 |
+ ,client |
|
64 |
+ ,(calculate-rounded-ranges |
|
65 |
+ (calculate-ranges ranges year month day)) |
|
66 |
+ ,memo)))))))) |
|
67 |
+ |
|
68 |
+(defparameter +pprint-log-option-spec+ |
|
69 |
+ '((("client" #\c) :type boolean :optional t :documentation "sort by client") |
|
70 |
+ (("reverse" #\r) :type boolean :optional t :documentation "reverse") |
|
71 |
+ (("status" #\s) :type boolean :optional t :documentation "status"))) |
|
72 |
+ |
|
73 |
+(defparameter *version* "0:1") |
|
74 |
+(defun show-version () |
|
75 |
+ (format t "timesheet, common-lisp version ~a~%" *version*)) |
|
76 |
+ |
|
77 |
+(defun show-help () |
|
78 |
+ (show-version) |
|
79 |
+ (command-line-arguments:show-option-help +pprint-log-option-spec+ :sort-names t)) |
|
80 |
+ |
|
81 |
+(defun pprint-log (args &key client reverse status help) |
|
82 |
+ (when help |
|
83 |
+ (show-help) |
|
84 |
+ (return-from pprint-log)) |
|
85 |
+ |
|
86 |
+ (let* ((*default-time-sheet-file* (or (cadr args) *default-time-sheet-file*)) |
|
87 |
+ (*print-pretty* t) |
|
88 |
+ (results (get-log *default-time-sheet-file*)) |
|
89 |
+ (clients (make-hash-table)) |
|
90 |
+ (total-cost 0)) |
|
91 |
+ (setf results (stable-sort results #'local-time:timestamp< |
|
92 |
+ :key (alambda (apply #'local-time:encode-timestamp |
|
93 |
+ (append '(0 0 0 0) |
|
94 |
+ (unroll-date (car it))))))) |
|
95 |
+ (when client |
|
96 |
+ (setf results (stable-sort results #'string-lessp :key #'cadr))) |
|
97 |
+ (when reverse |
|
98 |
+ (setf results (nreverse results))) |
|
99 |
+ (format t "~&~:{~4a ~10<~:(~a~)~> ~7,2F hrs ~a~%~}" results) |
|
100 |
+ (flet ((record-client (client hours) |
|
101 |
+ (let ((client (make-keyword (string-upcase client)))) |
|
102 |
+ (incf (gethash client clients 0) hours)))) |
|
103 |
+ (when status |
|
104 |
+ (format t "~120,1,0,'-<~>") |
|
105 |
+ (let ((total (format nil "~26<Total~>:~7,2F hours @ ~7,2F $/hr = $~7,2F" |
|
106 |
+ (loop for (_ client time ___) in results |
|
107 |
+ sum time |
|
108 |
+ do (record-client client time) |
|
109 |
+ do (incf total-cost (* time *rate*))) |
|
110 |
+ *rate* |
|
111 |
+ total-cost))) |
|
112 |
+ (flet ((fix-assoc (alist) |
|
113 |
+ (mapcar (destructuring-lambda ((client . time)) |
|
114 |
+ (list client time *rate* (* time *rate*))) |
|
115 |
+ alist))) |
|
116 |
+ (format t "~&~:{~:(~26<~a~>~):~7,2F hours @ ~7,2F $/hr = $~7,2F~%~}" |
|
117 |
+ (stable-sort |
|
118 |
+ (fix-assoc (hash-table-alist clients)) |
|
119 |
+ #'string< |
|
120 |
+ :key (alambda (car it))))) |
|
121 |
+ (format t total)))))) |
|
122 |
+ |
|
123 |
+(defun pprint-log-main (argv) |
|
124 |
+ (setf *default-time-sheet-file* (ubiquitous:defaulted-value "" :timesheet :file)) |
|
125 |
+ (setf *rate* (ubiquitous:defaulted-value 40 :rate)) |
|
126 |
+ (command-line-arguments:handle-command-line |
|
127 |
+ +pprint-log-option-spec+ |
|
128 |
+ 'pprint-log |
|
129 |
+ :command-line (cdr argv) |
|
130 |
+ :name "timesheet" |
|
131 |
+ :rest-arity t)) |