;;; -*- Mode:LISP; Package:USER; Readtable:CL; Base:10 -*- ;;;; Checkbook (defstruct checkbook entries balance pathname) (defstruct (entry (:print-function print-entry)) credit-date date number (memo "") (type 'misc) (checked nil) amount balance) (defun create-entry (credit-date date number memo type checked amount) (make-entry :credit-date credit-date :date date :number number :memo memo :type type :checked checked :amount amount)) (defun print-entry (entry &optional (stream t) depth) (declare (ignore depth)) (format stream "[~A]" (entry-string entry))) (defun entry-string (entry) (format nil "~A ~A ~A ~4D ~36A ~A" (date-string (entry-credit-date entry)) (date-string (entry-date entry)) (if (entry-checked entry) #\* #\space) (if (entry-number entry) (entry-number entry) "") (entry-memo entry) (amount-string (entry-amount entry)))) (defun date-string (universal-time) (multiple-value-bind (nil nil nil day month year) (decode-universal-time universal-time) (format nil "~2,'0D/~2,'0D/~2,'0D" month day (mod year 100.)))) (defun amount-string (amount) (multiple-value-bind (dollars cents) (truncate amount 100.) (format nil "~5D.~2,'0D" dollars (abs cents)))) (defun entry-earlier-p (entry1 entry2) (let ((cdate1 (entry-credit-date entry1)) (cdate2 (entry-credit-date entry2))) (cond ((< cdate1 cdate2) t) ((> cdate1 cdate2) nil) (t (let ((date1 (entry-date entry1)) (date2 (entry-date entry2))) (cond ((< date1 date2) t) ((> date1 date2) nil) (t (and (entry-number entry1) (entry-number entry2) (< (entry-number entry1) (entry-number entry2)))))))))) (defun insert-entry (entry checkbook) (setf (checkbook-entries checkbook) (merge 'list (list entry) (checkbook-entries checkbook) #'entry-earlier-p))) ;;; fix this up (defun input-entry () (let ((date (prompt-and-read :date "~&Date: ")) (number (prompt-and-read '(:number :or-nil t) "Number: ")) (memo (prompt-and-read :string-trim "Memo: ")) (amount (floor (* 100. (prompt-and-read :number "Amount: "))))) (make-entry :credit-date date :date date :number number :memo memo :amount amount))) (defun input-and-insert-entry (checkbook) (insert-entry (input-entry) checkbook)) (defun doit (checkbook) (do () (()) (send-if-handles *standard-output* :clear-screen) (print-checkbook checkbook) (input-and-insert-entry checkbook))) (defun print-checkbook (checkbook &optional (stream t)) (let ((balance 0)) (dolist (entry (checkbook-entries checkbook)) (setq balance (+ balance (entry-amount entry))) (format stream "~&~A ~A" (entry-string entry) (amount-string balance))) (terpri stream))) (defun print-checkbook-to-file (checkbook file) (with-open-file (stream file :out) (print-checkbook checkbook stream))) (defun save-entry (entry &optional (stream *standard-output*)) (prin1 (list (entry-credit-date entry) (entry-date entry) (entry-number entry) (entry-memo entry) (entry-type entry) (entry-checked entry) (entry-amount entry)) stream) (terpri stream)) (defun save-checkbook (checkbook file) (with-open-file (stream file :direction :output) (dolist (entry (checkbook-entries checkbook)) (save-entry entry stream)))) (defflavor scroll-mouse-window () (tv:scroll-mouse-mixin tv:scroll-window)) (defvar *checkbook* nil) (defvar *checkbooks* nil) (defvar *start-date* 0) (defvar *end-date* (time:parse-universal-time "Jan 1 2000")) (defvar *filter-list* '()) (defvar *filter-function* 'all) (defmacro def-filter (name namestring doc &body body) `(progn (defun ,name (entry) ,@body) (push '(,namestring :eval (setq *filter-function* #',name) :documentation ,doc) *filter-list* ))) (def-filter food " Food " "Show Food" (eq (entry-type entry) 'food)) (def-filter electric " Electric " "Show Electric Payments" (eq (entry-type entry) 'electric)) (def-filter gas " Gas " "Show Gas Payments" (eq (entry-type entry) 'Gas)) (def-filter mortgage " Mortgage " "Show Mortgage Payments" (eq (entry-type entry) 'mortgage)) (def-filter deposits " Deposits " "Show deposits" (plusp (entry-amount entry))) (def-filter debits " Withdrawals/Debits" "Show withdrawals and debits" (and (minusp (entry-amount entry)) (not (entry-number entry)))) (def-filter checks " Checks " "Show checks" (entry-number entry)) (def-filter all " All " "Show all entries" entry) (defun get-entry-list () (let ((balance 0)) (remove-if-not *filter-function* (remove-if-not #'(lambda (entry) (and (>= (entry-credit-date entry) *start-date*) (<= (entry-credit-date entry) *end-date*))) (mapc #'(lambda (entry) (setf (entry-balance entry) (incf balance (entry-amount entry)))) (if *checkbook* (checkbook-entries *checkbook*))))))) (defun checkbook-items () (tv:scroll-maintain-list #'get-entry-list #'(lambda (entry) (tv:scroll-parse-item :mouse `(nil :eval (do-in-command-loop '(:delete-entry ,entry)) :documentation "Delete Entry") `(:mouse (nil :eval (do-in-command-loop '(:change-credit-date ,entry)) :documentation "Credit Date") :function ,#'(lambda () (date-string (entry-credit-date entry))) nil 10) `(:mouse (nil :eval (do-in-command-loop '(:change-date ,entry)) :documentation "Date") :function ,#'(lambda () (date-string (entry-date entry))) nil 10) `(:mouse (nil :eval (do-in-command-loop '(:toggle-checked ,entry)) :documentation "Checked") :function ,#'(lambda () (if (entry-checked entry) #\* #\.)) nil 2) `(:mouse (nil :eval (tv:beep) :documentation "Number") :string ,(if (entry-number entry) (format nil "~4D" (entry-number entry)) " ") 5) `(:mouse (nil :eval (do-in-command-loop '(:change-memo ,entry)) :documentation "Memo") :function ,#'(lambda () (entry-memo entry)) nil 36.) `(:mouse (nil :eval (do-in-command-loop '(:change-amount ,entry)) :documentation "Amount") :function ,#'(lambda () (amount-string (entry-amount entry))) nil 10.) `(:function ,#'(lambda () (amount-string (entry-balance entry))) nil 10.))))) (defflavor ledger-interactor () (tv:process-mixin tv:lisp-interactor) (:default-init-plist :process '(command-loop))) (defun title-item-list () `(("From:" :no-select nil :font fonts:cptfontb) (,(date-string *start-date*) start-date) (" To:" :no-select nil :font fonts:cptfontb) (,(date-string *end-date*) end-date))) (defflavor ledger-frame ((checkbook) (checkbooks nil)) (tv:bordered-constraint-frame-with-shared-io-buffer) :gettable-instance-variables :settable-instance-variables (:default-init-plist :panes `((from-to tv:command-menu :item-list ,(title-item-list)) (title tv:window :blinker-p nil :label nil) (menu tv:command-menu :item-list ,(append '(("File " :no-select nil :font fonts:cptfontb) (" Load " load-checkbook-file) (" Save " save-checkbook-file) (" Save To " save-checkbook-file-to) (" Save Text " write-file) ("" :no-select nil) ("Add Entry " add-entry) ("" :no-select nil) ("Show Only " :no-select nil :font fonts:cptfontb) ) *filter-list*)) (display scroll-mouse-window :display-item ,(checkbook-items) :label "none") (interactor ledger-interactor)) :constraints `((main . ((title-strip middle interactor) ((title-strip :horizontal (2 :lines from-to) (from-to title) ((from-to .2) (title :even)))) ((middle :horizontal (40 :lines display) (display menu) ((display .8) (menu :even)))) ((interactor :even))))))) (defmethod (ledger-frame :after :init) (&rest ignore) (send self :set-selection-substitute (send self :get-pane 'interactor))) (tv:add-system-key #\x 'ledger-frame "Checkbook") (defun update (interactor-window) (let ((frame (send interactor-window :superior))) (send frame :send-pane 'display :redisplay) (send frame :send-pane 'display :set-label (format nil "~a" (if *checkbook* (checkbook-pathname *checkbook*) "None"))) (send frame :send-pane 'from-to :set-item-list (title-item-list)))) (defvar *interaction-window*) (defun do-in-command-loop (blip) (tv:io-buffer-put (send *interaction-window* :io-buffer) blip)) (defun command-loop (window) (setq *interaction-window* window) (setq *filter-function* #'all) (let ((*terminal-io* window)) (do ((blip (send window :any-tyi) (send window :any-tyi))) (()) (cond ((consp blip) (case (car blip) (:menu (if (eq (second (second blip)) :eval) (eval (third (second blip))) (funcall (second (second blip))))) (:change-credit-date (setf (entry-credit-date (second blip)) (prompt-and-read :date "~&Credit Date: ")) (resort-checkbook)) (:change-date (setf (entry-date (second blip)) (prompt-and-read :date "~&Date: ")) (resort-checkbook)) (:toggle-checked (setf (entry-checked (second blip)) (not (entry-checked (second blip))))) (:change-memo (setf (entry-memo (second blip)) (prompt-and-read :string-trim "Memo: "))) (:change-amount (setf (entry-amount (second blip)) (truncate (* 100. (prompt-and-read :number "Amount: "))))) (:delete-entry (delete-entry (second blip) *checkbook*)) (t (tv:beep)))) (t )) (update window)))) (defun resort-checkbook () (setf (checkbook-entries *checkbook*) (sort (checkbook-entries *checkbook*) #'entry-earlier-p))) (defun load-checkbook-file () (let ((entries '()) (pathname (prompt-and-read :pathname "File: "))) (with-open-file (stream pathname :direction :input) (do ((next (read stream nil) (read stream nil))) ((null next)) (push (apply #'create-entry next) entries))) (if *checkbook* (pushnew *checkbook* *checkbooks*)) (pushnew (setq *checkbook* (make-checkbook :entries (nreverse entries) :pathname pathname)) *checkbooks*))) (defun save-checkbook-file () (save-checkbook *checkbook* (or (checkbook-pathname *checkbook*) (setf (checkbook-pathname *checkbook*) (prompt-and-read :pathname "~&Save to file: "))))) (defun save-checkbook-file-to () (save-checkbook *checkbook* (setf (checkbook-pathname *checkbook*) (prompt-and-read `(:pathname :defaults ,(checkbook-pathname *checkbook*)) "~&Save to file (default ~a): " (checkbook-pathname *checkbook*))))) (defun print-entries (entries &optional (stream t)) (dolist (entry entries) (format stream "~&~A ~A" (entry-string entry) (amount-string (entry-balance entry)))) (terpri stream)) (defun print-entries-to-file (entries file) (with-open-file (stream file :out) (print-entries entries stream))) (defun write-file () (print-entries-to-file (get-entry-list) (prompt-and-read `(:pathname :defaults ,(checkbook-pathname *checkbook*)) "~&Write Text File (default ~a): " (checkbook-pathname *checkbook*)))) (defun delete-entry (entry &optional (checkbook *checkbook*)) (setf (checkbook-entries checkbook) (delete entry (checkbook-entries checkbook)))) (defun add-entry () (insert-entry (get-entry) *checkbook*)) (defun get-entry () (let ((*credit-date* (time:get-universal-time)) (*date* (time:get-universal-time)) (*number* nil) (*memo* "") (*checked* nil) (*type* 'misc) (*amount* 0)) (declare (special *credit-date* *date* *number* *memo* *checked* *type* *amount*)) (tv:choose-variable-values '((*credit-date* "Credit Date" :date) (*date* "Date" :date) (*number* "Number" :number-or-nil) (*memo* "Memo" :string) (*checked* "Checked" :boolean) (*type* "Type" :choose (misc deposit food gas elec mort phone ins-car ins-house)) ;:menu-alist allows mouse doc (*amount* "Amount" :number))) (create-entry *credit-date* *date* *number* *memo* *type* *checked* (floor (* 100. *amount*))))) (defun start-date () (setq *start-date* (prompt-and-read :date "~&Start Date: "))) (defun end-date () (setq *end-date* (prompt-and-read :date "~&End Date: ")))