git.fiddlerwoaroof.com
Browse code

Finished a minimal command line interface

At this point, it just scans through the file and prints out a nicely
formatted log.

fiddlerwoaroof authored on 21/02/2016 00:17:47
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))