source: release/1.5/source/contrib/krueger/InterfaceProjects/Loan Document/loan-doc.lisp

Last change on this file was 13646, checked in by R. Matthew Emerson, 15 years ago

Merge r13631, r13636 from trunk. (Paul Krueger's updated InterfaceProjects
contrib; fix for ticket:652)

File size: 20.7 KB
RevLine 
[13631]1;; loan-doc.lisp
2
3#|
4The MIT license.
5
6Copyright (c) 2010 Paul L. Krueger
7
8Permission is hereby granted, free of charge, to any person obtaining a copy of this software
9and associated documentation files (the "Software"), to deal in the Software without restriction,
10including without limitation the rights to use, copy, modify, merge, publish, distribute,
11sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
12furnished to do so, subject to the following conditions:
13
14The above copyright notice and this permission notice shall be included in all copies or substantial
15portions of the Software.
16
17THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
18LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
23|#
24
25(eval-when (:compile-toplevel :load-toplevel :execute)
26 (require :menu-utils)
27 (require :decimal)
28 (require :date)
29 (require :lisp-doc-controller)
30 (require :loan-win-cntrl)
31 (require :loan-pr-view)
32 (require :ns-string-utils))
33
34(defpackage :loan-document
35 (:nicknames :lnd)
36 (:use :iu :ccl :common-lisp))
37
38(in-package :lnd)
39
40;; demonstration code for creating a Cocoa document class in lisp that can be required
41;; and loaded into a standard running CCL IDE (i.e. does not require a stand-alone program).
42
43(defconstant $max-interest-rate$ .5)
44
45;; Some loan utility functions
46
47;; all equations are derived from a basic loan equation for an N month loan:
48;; LoanPrinciple = MonthlyPayment * ( (1 / (1 + MonthlyInterest)) +
49;; (1 / (1 + MonthlyInterest)^2) + ... +
50;; (1 / (1 + MonthlyInterest)^N) )
51;; which can be manipulated to derive:
52;; MonthlyPayment = LoanPrinciple * (MonthlyInterest + (MonthlyInterest / ((1 + MonthlyInterest)^LoanDuration - 1)))
53
54(defun pay-to-loan-ratio (mo-int loan-dur)
55 ;; Just computes the MonthlyPayment/Loan ratio from the basic loan equation given above
56 (if (zerop mo-int) 0
57 (+ mo-int (/ mo-int (1- (expt (1+ mo-int) loan-dur))))))
58
59;; The loan class
60
61(defclass loan-doc (ns:ns-document)
62 ((loan-amount :accessor loan-amount :initform 0)
63 (interest-rate :accessor interest-rate :initform 0)
64 (loan-duration :accessor loan-duration :initform 0)
65 (monthly-payment :accessor monthly-payment :initform 0)
66 (origination-date :accessor origination-date :initform (now))
67 (first-payment :accessor first-payment :initform (next-month (now)))
68 (pay-schedule :accessor pay-schedule :initform nil)
69 (compute-mode :accessor compute-mode :initform 0)
70 (last-set-param :accessor last-set-param :initform nil)
71 (max-dur :foreign-type #>BOOL :accessor max-dur)
72 (min-dur :foreign-type #>BOOL :accessor min-dur)
73 (min-pay :foreign-type #>BOOL :accessor min-pay))
74 (:default-initargs
75 :doc-class nil
76 :menu-name nil
77 :file-ext nil)
78 (:metaclass ns:+ns-object))
79
80(defvar *loan-doc-controller* (make-instance 'lisp-doc-controller
81 :doc-class (find-class 'loan-doc)
82 :menu-class "Loan"
83 :file-ext "loan"))
84
85;; Normal lisp methods for our class
86
87(defmethod get-loan-state ((self loan-doc))
88 ;; returns a list of loan state values suitable for use by set-loan-state
89 (list (loan-amount self)
90 (interest-rate self)
91 (loan-duration self)
92 (monthly-payment self)
93 (origination-date self)
94 (first-payment self)
95 (max-dur self)
96 (min-dur self)
97 (min-pay self)))
98
99(defmethod set-loan-state ((self loan-doc) state-list)
100 (setf (last-set-param self) nil)
101 (#/willChangeValueForKey: self #@"loanAmt")
102 (setf (loan-amount self) (pop state-list))
103 (#/didChangeValueForKey: self #@"loanAmt")
104 (#/willChangeValueForKey: self #@"interestRate")
105 (setf (interest-rate self) (pop state-list))
106 (#/didChangeValueForKey: self #@"interestRate")
107 (#/willChangeValueForKey: self #@"loanDuration")
108 (setf (loan-duration self) (pop state-list))
109 (#/didChangeValueForKey: self #@"loanDuration")
110 (#/willChangeValueForKey: self #@"monthlyPayment")
111 (setf (monthly-payment self) (pop state-list))
112 (#/didChangeValueForKey: self #@"monthlyPayment")
113 (#/willChangeValueForKey: self #@"originationDate")
114 (setf (origination-date self) (pop state-list))
115 (#/didChangeValueForKey: self #@"originationDate")
116 (#/willChangeValueForKey: self #@"firstPayment")
117 (setf (first-payment self) (pop state-list))
118 (#/didChangeValueForKey: self #@"firstPayment")
119 (#/willChangeValueForKey: self #@"maxDur")
120 (setf (max-dur self) (pop state-list))
121 (#/didChangeValueForKey: self #@"maxDur")
122 (#/willChangeValueForKey: self #@"minDur")
123 (setf (min-dur self) (pop state-list))
124 (#/didChangeValueForKey: self #@"minDur")
125 (#/willChangeValueForKey: self #@"minPay")
126 (setf (min-pay self) (pop state-list))
127 (#/didChangeValueForKey: self #@"minPay")
128 (#/willChangeValueForKey: self #@"totInterest")
129 (setf (pay-schedule self) nil)
130 (#/didChangeValueForKey: self #@"totInterest")
131 (compute-new-loan-values self))
132
133(defmethod create-undo ((self loan-doc) undo-name &optional (always-undo nil))
134 (when (or always-undo (not (eq (last-set-param self) undo-name)))
135 (let ((undo (#/undoManager self))
136 (st (lisp-object-to-ns-data (get-loan-state self))))
137 (#/setLoanState: (#/prepareWithInvocationTarget: undo self)
138 st)
139 (unless (#/isUndoing undo)
140 (#/setActionName: undo undo-name))
141 (setf (last-set-param self) undo-name))))
142
143(defmethod set-pay-schedule ((self loan-doc))
144 ;; create a detailed payment schedule for the loan using daily compounding of interest
145 ;; Payments are on the same date of each month, but the number of days between payments
146 ;; varies because the number of days in each month varies.
147 ;; We compute accurate interest compounded daily for the actual number of days.
148 (let ((monthly-interest (/ (interest-rate self) 12))
149 (payment (monthly-payment self))
150 (sched nil)
151 (display-min-pay-banner nil))
152 (prog1
153 (do* ((begin (loan-amount self) end)
154 (begin-date (first-payment self) end-date)
155 (end-date (next-month begin-date) (next-month begin-date))
156 (int (round (* begin monthly-interest))
157 (round (* begin monthly-interest)))
158 (end (- (+ begin int) payment) (- (+ begin int) payment)))
159 ((not (plusp end))
160 (progn
161 (push (list (short-date-string begin-date)
162 (/ begin 100)
163 (/ int 100)
164 (/ payment 100)
165 (short-date-string end-date)
166 (/ end 100)
167 int)
168 sched)
169 (setf (pay-schedule self) (nreverse sched))))
170 (when (>= end begin)
171 ;; oops, with this combination of values the loan will never
172 ;; be paid off, so let's set a minimum payment required
173 ;; Display a field that tells user the minimum payment was reached
174 (setf display-min-pay-banner t)
175 (#/willChangeValueForKey: self #@"monthlyPayment")
176 (setf (monthly-payment self) (1+ int))
177 (#/didChangeValueForKey: self #@"monthlyPayment")
178 ;; now patch up our loop variables and keep going
179 (setf payment (monthly-payment self))
180 (setf end (1- begin)))
181 ;; put the last payment into the list
182 (push (list (short-date-string begin-date)
183 (/ begin 100)
184 (/ int 100)
185 (/ payment 100)
186 (short-date-string end-date)
187 (/ end 100)
188 int)
189 sched))
190 (#/willChangeValueForKey: self #@"totInterest")
191 ;; we'll make the total interest field call our accessor
192 ;; to generate a new amount
193 (#/didChangeValueForKey: self #@"totInterest")
194 (if display-min-pay-banner
195 (progn
196 ;; Set a condition that says the minimum payment was reached
197 (setf display-min-pay-banner t)
198 (#/willChangeValueForKey: self #@"minPay")
199 (setf (min-pay self) #$YES)
200 (#/didChangeValueForKey: self #@"minPay"))
201 (progn
202 ;; otherwise reset that condition
203 (#/willChangeValueForKey: self #@"minPay")
204 (setf (min-pay self) #$NO)
205 (#/didChangeValueForKey: self #@"minPay")))
206 ;; If we happen to be computing the interest rate, then
207 ;; the combination of loan-amount and monthly payment will
208 ;; determine a maximum interest rate. This, in turn,
209 ;; determines a maximum loan duration. If the duration was set
210 ;; longer than this by the user, we will reset the
211 ;; lone duration value to the maximum needed.
212 ;; If, on the other hand, the monthly payment is set so low that
213 ;; the interest rate approaches 0, then we may have to adjust the
214 ;; loan duration up to the minimum needed to pay the loan.
215 ;; Let's start by resetting our two "duration" conditions and then we'll
216 ;; set them if conditions dictate.
217 ;; Reset a condition that indicates the max duration was reached
218 (#/willChangeValueForKey: self #@"maxDur")
219 (setf (max-dur self) #$NO)
220 (#/didChangeValueForKey: self #@"maxDur")
221 ;; Reset a condition that indicates the min duration was reached
222 (#/willChangeValueForKey: self #@"minDur")
223 (setf (min-dur self) #$NO)
224 (#/didChangeValueForKey: self #@"minDur"))
225 (let ((duration-diff (- (loan-duration self) (list-length (pay-schedule self)))))
226 (unless (or (eql (compute-mode self) 2) (zerop duration-diff))
227 ;; i.e. we're not calling this function just to determine the loan duration
228 ;; and we have to adjust the loan duration
229 (if (plusp duration-diff)
230 (progn
231 ;; change the loan-duration value to what it must be
232 (#/willChangeValueForKey: self #@"loanDuration")
233 (setf (loan-duration self) (list-length (pay-schedule self)))
234 (#/didChangeValueForKey: self #@"loanDuration")
235 (when (> duration-diff 2)
236 ;; If we're one-off just fix it and don't post a message
237 ;; This can occur almost anytime because of numerical issues
238 ;; Display a field that tells user the max duration was reached
239 (#/willChangeValueForKey: self #@"maxDur")
240 (setf (max-dur self) #$YES)
241 (#/didChangeValueForKey: self #@"maxDur")))
242 (progn
243 ;; change the oan-duration value to what it must be
244 (#/willChangeValueForKey: self #@"loanDuration")
245 (setf (loan-duration self) (list-length (pay-schedule self)))
246 (#/didChangeValueForKey: self #@"loanDuration")
247 (when (< duration-diff -2)
248 ;; If we're one-off just fix it and don't post a message
249 ;; This can occur almost anytime because of numerical issues
250 ;; Display a field that tells user the min duration was reached
251 (#/willChangeValueForKey: self #@"minDur")
252 (setf (min-dur self) #$YES)
253 (#/didChangeValueForKey: self #@"minDur"))))))))
254
255(defmethod print-pay-schedule ((self loan-doc) &optional (strm t))
256 (format strm
257 "~:{~%On ~a balance = $~$ + interest of $~$ - payment of $~$ = ~a balance of $~$~}"
258 (pay-schedule self)))
259
260(defmethod compute-int-rate ((self loan-doc))
261 ;; Find a monthly interest rate that makes the rest of the values work.
262 ;; There isn't an algebraic solution for the interest rate, so let's search for it.
263 ;; Find a suitable search range and do a binary search for it. Even for large interest
264 ;; rates the number of search iterations should be minimal.
265
266 (with-slots (loan-amount monthly-payment loan-duration interest-rate) self
267
268 ;; First we'll check to see whether the monthly payment is great than the loan amount.
269 ;; If so we'll set the interest rate directly so that the loan is paid off in one month.
270 ;; This avoids some ugly arithmetic overflow things that can happen when interest rates
271 ;; go off the charts
272 (let ((max-monthly-rate (/ $max-interest-rate$ 12)))
273 (if (>= monthly-payment loan-amount)
274 (min max-monthly-rate (1- (/ monthly-payment loan-amount)))
275 (let ((imin (max 0 (min max-monthly-rate
276 (/ (- (* monthly-payment loan-duration) loan-amount)
277 (* loan-duration loan-amount)))))
278 ;; imin is basically a rate that would result in the first month's interest as
279 ;; the average interest paid for all months. Since we know it must be greater
280 ;; than this, we have a guaranteed lower bound. But we cap it at our allowed
281 ;; monthly maximum interest.
282 (imax (min max-monthly-rate
283 (- (/ monthly-payment loan-amount) .000008333)))
284 ;; imax is a rate that would result in the first month's interest being
285 ;; minimally smaller than the payment. Since we must pay off in a finite
286 ;; duration, this is a guaranteed maximum. We cap it the allowed maximum
287 ;; monthly rate.
288 (target-p-l-ratio (/ monthly-payment loan-amount)))
289 (unless (>= imax imin)
290 (error "Max int = ~8,4f, Min int = ~8,4f" imax imin))
291 (do* ((i (/ (+ imin imax) 2)
292 (/ (+ imin imax) 2))
293 (p-l-ratio (pay-to-loan-ratio i loan-duration)
294 (pay-to-loan-ratio i loan-duration)))
295 ((<= (- imax imin) .000001) imax)
296 (if (>= target-p-l-ratio p-l-ratio)
297 (setf imin i)
298 (setf imax i))))))))
299
300(defmethod compute-new-loan-values ((self loan-doc))
301 ;; For the sake of expediency we assume monthly componding
302 ;; The basic equation governing these computations is
303 (with-slots (compute-mode interest-rate loan-duration monthly-payment
304 loan-amount pay-schedule) self
305 (case compute-mode
306 (0
307 ;; compute the loan amount
308 (unless (or (zerop interest-rate)
309 (zerop loan-duration)
310 (zerop monthly-payment))
311 (#/willChangeValueForKey: self #@"loanAmt")
312 (setf loan-amount
313 (round (/ monthly-payment
314 (pay-to-loan-ratio (/ interest-rate 12)
315 loan-duration))))
316 (set-pay-schedule self)
317 (#/didChangeValueForKey: self #@"loanAmt")))
318 (1
319 ;; compute the interest rate
320 (unless (or (zerop loan-amount)
321 (zerop loan-duration)
322 (zerop monthly-payment))
323 (#/willChangeValueForKey: self #@"interestRate")
324 (setf interest-rate
325 (* 12 (/ (floor (* 1000000 (compute-int-rate self)))
326 1000000)))
327 (set-pay-schedule self)
328 (#/didChangeValueForKey: self #@"interestRate")))
329 (2
330 ;; compute the loan duration
331 (unless (or (zerop interest-rate)
332 (zerop loan-amount)
333 (zerop monthly-payment))
334 (#/willChangeValueForKey: self #@"loanDuration")
335 (set-pay-schedule self)
336 (setf loan-duration
337 (list-length pay-schedule))
338 (#/didChangeValueForKey: self #@"loanDuration")))
339 (3
340 ;; compute the monthly payment
341 (unless (or (zerop interest-rate)
342 (zerop loan-amount)
343 (zerop loan-duration))
344 (#/willChangeValueForKey: self #@"monthlyPayment")
345 (setf monthly-payment
346 (round (* loan-amount
347 (pay-to-loan-ratio (/ interest-rate 12)
348 loan-duration))))
349 (set-pay-schedule self)
350 (#/didChangeValueForKey: self #@"monthlyPayment"))))))
351
352;; Accessor functions used by display objects to retrieve and set values as dictated
353;; by the bindings we set up in IB
354
355(objc:defmethod (#/loanAmt :id)
356 ((self loan-doc))
357 (lisp-to-ns-decimal (loan-amount self)))
358
359(objc:defmethod (#/interestRate :id)
360 ((self loan-doc))
361 (#/numberWithFloat: ns:ns-number (float (interest-rate self))))
362
363(objc:defmethod (#/loanDuration :id)
364 ((self loan-doc))
365 (#/numberWithInt: ns:ns-number (loan-duration self)))
366
367(objc:defmethod (#/monthlyPayment :id)
368 ((self loan-doc))
369 (lisp-to-ns-decimal (monthly-payment self)))
370
371(objc:defmethod (#/originationDate :id)
372 ((self loan-doc))
373 (lisp-to-ns-date (origination-date self)))
374
375(objc:defmethod (#/firstPayment :id)
376 ((self loan-doc))
377 (lisp-to-ns-date (first-payment self)))
378
379(objc:defmethod (#/totInterest :id)
380 ((self loan-doc))
381 (lisp-to-ns-decimal (reduce #'+ (pay-schedule self)
382 :key #'seventh
383 :initial-value 0)))
384
385(objc:defmethod (#/setLoanAmt: :void)
386 ((self loan-doc) (amt :id))
387 (create-undo self #@"set loan amount")
388 (setf (loan-amount self) (lisp-from-ns-decimal amt))
389 (compute-new-loan-values self))
390
391(objc:defmethod (#/setInterestRate: :void)
392 ((self loan-doc) (rate :id))
393 (create-undo self #@"set interest rate")
394 (setf (interest-rate self) (#/floatValue rate))
395 (compute-new-loan-values self))
396
397(objc:defmethod (#/setLoanDuration: :void)
398 ((self loan-doc) (dur :id))
399 (create-undo self #@"set loan duration")
400 (setf (loan-duration self) (#/longValue dur))
401 (compute-new-loan-values self))
402
403(objc:defmethod (#/setMonthlyPayment: :void)
404 ((self loan-doc) (pay :id))
405 (create-undo self #@"set monthly payment")
406 (setf (monthly-payment self) (lisp-from-ns-decimal pay))
407 (compute-new-loan-values self))
408
409(objc:defmethod (#/setOriginationDate: :void)
410 ((self loan-doc) (dt :id))
411 (create-undo self #@"set origination date")
412 (let ((new-dt (ns-to-lisp-date dt)))
413 (setf (origination-date self) new-dt)
414 (#/willChangeValueForKey: self #@"firstPayment")
415 (setf (first-payment self) (next-month new-dt))
416 (#/didChangeValueForKey: self #@"firstPayment"))
417 (compute-new-loan-values self))
418
419(objc:defmethod (#/setFirstPayment: :void)
420 ((self loan-doc) (pay :id))
421 (create-undo self #@"set first payment date")
422 (let ((new-pay (ns-to-lisp-date pay)))
423 (setf (first-payment self) new-pay))
424 (compute-new-loan-values self))
425
426;; Necessary overrides of NSDocument methods
427;; Methods called to read data from and write data to external files
428
429(objc:defmethod (#/readFromData:ofType:error: #>BOOL)
430 ((self loan-doc) (data :id) (dtype :id) (err (:* :id)))
431 (declare (ignore err dtype))
432 (set-loan-state self (ns-data-to-lisp-object data))
433 #$YES)
434
435(objc:defmethod (#/dataOfType:error: :id)
436 ((self loan-doc) (dtype :id) (err (:* :id)))
437 (declare (ignore dtype err))
438 (lisp-object-to-ns-data (get-loan-state self)))
439
440(objc:defmethod (#/setLoanState: :void)
441 ((self loan-doc) (st :id))
442 (create-undo self nil t)
443 ;; called when user does an "undo"
444 (set-loan-state self (ns-data-to-lisp-object st)))
445
446;; Methods to manage windows
447
448(objc:defmethod (#/makeWindowControllers :void)
449 ((self loan-doc))
450 (let ((lwc (make-instance 'loan-win-controller
451 :with-loan self)))
452 (#/setShouldCloseDocument: lwc #$YES)
453 (#/addWindowController: self lwc)))
454
455(objc:defmethod (#/validateMenuItem: #>BOOL)
456 ((self loan-doc) (item :id))
457 (let* ((action (#/action item)))
458 (cond ((eql action (ccl::@selector #/saveDocument:))
459 (#/isDocumentEdited self))
460 (t (call-next-method item)))))
461
462(objc:defmethod (#/prepareSavePanel: #>BOOL)
463 ((self loan-doc) (panel :id))
464 (#/setRequiredFileType: panel #@"loan")
465 #$YES)
466
467;; Printing Support
468
469(objc:defmethod (#/printLoan: :void)
470 ((self loan-doc) (sender :id))
471 (declare (ignore sender))
472 (#/printDocument: self self))
473
474(objc:defmethod (#/printOperationWithSettings:error: :id)
475 ((self loan-doc) (settings :id) (err (:* :id)))
476 (declare (ignore err settings))
477 (let ((pr-view (make-instance 'loan-print-view
478 :loan self)))
479 (#/printOperationWithView:printInfo: ns:ns-print-operation
480 pr-view
481 (#/printInfo self))))
482
483(provide :loan-doc)
Note: See TracBrowser for help on using the repository browser.