source: release/1.5/source/contrib/krueger/InterfaceProjects/Loan Calc/loan-calc.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: 19.6 KB
RevLine 
[13390]1;;; loan-calc.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;;; Sample lisp/Cocoa interface that uses a NIB file defined with interface builder;
26;;; A definition is provided for the "SpeechController" class that was specified to interface builder
27;;; as the class of the NIB file owner.
28;;; We manually create an instance of SpeechController and specify it as the owner for the NIB file.
29
[13646]30(eval-when (:compile-toplevel :load-toplevel :execute)
31 (require :nib)
32 (require :date)
33 (require :decimal))
[13390]34
35(defpackage :loan-calc
36 (:nicknames :lnc)
37 (:use :iu :ccl :common-lisp)
38 (:export test-loan))
39
40(in-package :lnc)
41
42;; The loan-controller class
43
44(defclass loan-controller (ns:ns-window-controller)
45 ((loan :foreign-type :id :accessor loan)
46 (loan-text :foreign-type :id :accessor loan-text)
47 (int-text :foreign-type :id :accessor int-text)
48 (dur-text :foreign-type :id :accessor dur-text)
49 (pay-text :foreign-type :id :accessor pay-text)
50 (int-slider :foreign-type :id :accessor int-slider)
51 (dur-slider :foreign-type :id :accessor dur-slider)
52 (pay-slider :foreign-type :id :accessor pay-slider))
53 (:metaclass ns:+ns-object))
54
55(objc:defmethod (#/initWithLoan: :id)
56 ((self loan-controller) (ln :id))
57 (setf (loan self) ln)
58 (let* ((nib-name (ccl::%make-nsstring
59 (namestring (truename "ip:Loan Calc;loan.nib"))))
60 (init-self (#/initWithWindowNibPath:owner: self nib-name self)))
61 init-self))
62
63;; Action methods that are called when controls do something
64
65(objc:defmethod (#/buttonPushed: :void)
66 ((self loan-controller) (button-matrix :id))
67 (with-slots (loan loan-text int-text dur-text pay-text int-slider
68 dur-slider pay-slider) self
69 (let ((cm (#/selectedRow button-matrix)))
70 (unless (eql cm (compute-mode loan))
71 (case (compute-mode loan)
72 (0 (#/setEnabled: loan-text #$YES))
73 (1 (#/setEnabled: int-text #$YES)
74 (#/setEnabled: int-slider #$YES))
75 (2 (#/setEnabled: dur-text #$YES)
76 (#/setEnabled: dur-slider #$YES))
77 (3 (#/setEnabled: pay-text #$YES)
78 (#/setEnabled: pay-slider #$YES)))
79 (setf (compute-mode loan) cm)
80 (case cm
81 (0 (#/setEnabled: loan-text #$NO))
82 (1 (#/setEnabled: int-text #$NO)
83 (#/setEnabled: int-slider #$NO))
84 (2 (#/setEnabled: dur-text #$NO)
85 (#/setEnabled: dur-slider #$NO))
86 (3 (#/setEnabled: pay-text #$NO)
87 (#/setEnabled: pay-slider #$NO)))
88 (compute-new-loan-values loan)))))
89
90(objc:defmethod (#/awakeFromNib :void)
91 ((self loan-controller))
92 (#/setEnabled: (loan-text self) #$NO)
93 ;; set the sliders to update continuously so that the text boxes reflect the current value
94 ;; Note that we can set this in IB for text boxes, but not, apparently, for sliders
95 (#/setContinuous: (int-slider self) #$YES)
96 (#/setContinuous: (dur-slider self) #$YES)
97 (#/setContinuous: (pay-slider self) #$YES))
98
99(objc:defmethod (#/windowWillClose: :void)
100 ((self loan-controller) (notif :id))
101 (declare (ignore notif))
102 (when (loan self)
103 ;; Tell the loan that the window is closing
104 ;; It will #/autorelease this window-controller)
105 (window-closed (loan self))))
106
107
108;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
109;;;; Everything below is related to the loan data model
110
111(defconstant $max-interest-rate$ .5)
112
113;; Some loan utility functions
114
115;; all equations are derived from a basic loan equation for an N month loan:
116;; LoanPrinciple = MonthlyPayment * ( (1 / (1 + MonthlyInterest)) +
117;; (1 / (1 + MonthlyInterest)^2) + ... +
118;; (1 / (1 + MonthlyInterest)^N) )
119;; which can be manipulated to derive:
120;; MonthlyPayment = LoanPrinciple * (MonthlyInterest + (MonthlyInterest / ((1 + MonthlyInterest)^LoanDuration - 1)))
121
122(defun pay-to-loan-ratio (mo-int loan-dur)
123 ;; Just computes the MonthlyPayment/Loan ratio from the basic loan equation given above
124 (if (zerop mo-int) 0
125 (+ mo-int (/ mo-int (1- (expt (1+ mo-int) loan-dur))))))
126
127;; The loan class
128
129(defclass loan (ns:ns-object)
130 ((loan-amount :accessor loan-amount :initform 0)
131 (interest-rate :accessor interest-rate :initform 0)
132 (loan-duration :accessor loan-duration :initform 0)
133 (monthly-payment :accessor monthly-payment :initform 0)
134 (origination-date :accessor origination-date :initform (now))
135 (first-payment :accessor first-payment :initform (next-month (now)))
136 (pay-schedule :accessor pay-schedule :initform nil)
137 (window-controller :accessor window-controller :initform nil)
138 (compute-mode :accessor compute-mode :initform 0)
139 (max-dur :foreign-type #>BOOL :accessor max-dur)
140 (min-dur :foreign-type #>BOOL :accessor min-dur)
141 (min-pay :foreign-type #>BOOL :accessor min-pay))
142 (:metaclass ns:+ns-object))
143
144
145;; Normal lisp methods for our class
146
147(defmethod initialize-instance :after ((self loan)
148 &key &allow-other-keys)
149 (setf (window-controller self)
150 (make-instance (find-class 'loan-controller)
151 :with-loan self))
152 (#/showWindow: (window-controller self) self))
153
154(defmethod window-closed ((self loan))
155 ;; called by the window-controller to tell us that the window closed
156 (when (window-controller self)
157 (#/autorelease (window-controller self))
158 (setf (window-controller self) nil)))
159
160(defmethod close-loan ((self loan))
161 ;; just tell the window to close
162 (when (window-controller self)
163 (let ((win (#/window (window-controller self))))
164 ;; for reasons mysterious to me, calling #/window seems to
165 ;; also #/retain the window, so we'll #/release it
166 (#/release win)
167 (#/performClose: win self))))
168
169(defmethod set-pay-schedule ((self loan))
170 ;; create a detailed payment schedule for the loan using daily compounding of interest
171 ;; Payments are on the same date of each month, but the number of days between payments
172 ;; varies because the number of days in each month varies.
173 ;; We compute accurate interest compounded daily for the actual number of days.
174 (let ((monthly-interest (/ (interest-rate self) 12))
175 (payment (monthly-payment self))
176 (sched nil)
177 (display-min-pay-banner nil))
178 (prog1
179 (do* ((begin (loan-amount self) end)
180 (begin-date (first-payment self) end-date)
181 (end-date (next-month begin-date) (next-month begin-date))
182 (int (round (* begin monthly-interest))
183 (round (* begin monthly-interest)))
184 (end (- (+ begin int) payment) (- (+ begin int) payment)))
185 ((not (plusp end))
186 (progn
187 (push (list (short-date-string begin-date)
188 (/ begin 100)
189 (/ int 100)
190 (/ payment 100)
191 (short-date-string end-date)
192 (/ end 100)
193 int)
194 sched)
195 (setf (pay-schedule self) (nreverse sched))))
196 (when (>= end begin)
197 ;; oops, with this combination of values the loan will never
198 ;; be paid off, so let's set a minimum payment required
199 ;; Display a field that tells user the minimum payment was reached
200 (setf display-min-pay-banner t)
201 (#/willChangeValueForKey: self #@"monthlyPayment")
202 (setf (monthly-payment self) (1+ int))
203 (#/didChangeValueForKey: self #@"monthlyPayment")
204 ;; now patch up our loop variables and keep going
205 (setf payment (monthly-payment self))
206 (setf end (1- begin)))
207 ;; put the last payment into the list
208 (push (list (short-date-string begin-date)
209 (/ begin 100)
210 (/ int 100)
211 (/ payment 100)
212 (short-date-string end-date)
213 (/ end 100)
214 int)
215 sched))
216 (#/willChangeValueForKey: self #@"totInterest")
217 ;; we'll make the total interest field call our accessor
218 ;; to generate a new amount
219 (#/didChangeValueForKey: self #@"totInterest")
220 (if display-min-pay-banner
221 (progn
222 ;; Set a condition that says the minimum payment was reached
223 (setf display-min-pay-banner t)
224 (#/willChangeValueForKey: self #@"minPay")
225 (setf (min-pay self) #$YES)
226 (#/didChangeValueForKey: self #@"minPay"))
227 (progn
228 ;; otherwise reset that condition
229 (#/willChangeValueForKey: self #@"minPay")
230 (setf (min-pay self) #$NO)
231 (#/didChangeValueForKey: self #@"minPay")))
232 ;; If we happen to be computing the interest rate, then
233 ;; the combination of loan-amount and monthly payment will
234 ;; determine a maximum interest rate. This, in turn,
235 ;; determines a maximum loan duration. If the duration was set
236 ;; longer than this by the user, we will reset the
237 ;; lone duration value to the maximum needed.
238 ;; If, on the other hand, the monthly payment is set so low that
239 ;; the interest rate approaches 0, then we may have to adjust the
240 ;; loan duration up to the minimum needed to pay the loan.
241 ;; Let's start by resetting our two "duration" conditions and then we'll
242 ;; set them if conditions dictate.
243 ;; Reset a condition that indicates the max duration was reached
244 (#/willChangeValueForKey: self #@"maxDur")
245 (setf (max-dur self) #$NO)
246 (#/didChangeValueForKey: self #@"maxDur")
247 ;; Reset a condition that indicates the min duration was reached
248 (#/willChangeValueForKey: self #@"minDur")
249 (setf (min-dur self) #$NO)
250 (#/didChangeValueForKey: self #@"minDur"))
251 (let ((duration-diff (- (loan-duration self) (list-length (pay-schedule self)))))
252 (unless (or (eql (compute-mode self) 2) (zerop duration-diff))
253 ;; i.e. we're not calling this function just to determine the loan duration
254 ;; and we have to adjust the loan duration
255 (if (plusp duration-diff)
256 (progn
257 ;; change the loan-duration value to what it must be
258 (#/willChangeValueForKey: self #@"loanDuration")
259 (setf (loan-duration self) (list-length (pay-schedule self)))
260 (#/didChangeValueForKey: self #@"loanDuration")
261 (when (> duration-diff 2)
262 ;; If we're one-off just fix it and don't post a message
263 ;; This can occur almost anytime because of numerical issues
264 ;; Display a field that tells user the max duration was reached
265 (#/willChangeValueForKey: self #@"maxDur")
266 (setf (max-dur self) #$YES)
267 (#/didChangeValueForKey: self #@"maxDur")))
268 (progn
269 ;; change the oan-duration value to what it must be
270 (#/willChangeValueForKey: self #@"loanDuration")
271 (setf (loan-duration self) (list-length (pay-schedule self)))
272 (#/didChangeValueForKey: self #@"loanDuration")
273 (when (< duration-diff -2)
274 ;; If we're one-off just fix it and don't post a message
275 ;; This can occur almost anytime because of numerical issues
276 ;; Display a field that tells user the min duration was reached
277 (#/willChangeValueForKey: self #@"minDur")
278 (setf (min-dur self) #$YES)
279 (#/didChangeValueForKey: self #@"minDur"))))))))
280
281(defmethod print-pay-schedule ((self loan) &optional (strm t))
282 (format strm
283 "~:{~%On ~a balance = $~$ + interest of $~$ - payment of $~$ = ~a balance of $~$~}"
284 (pay-schedule self)))
285
286(defmethod compute-int-rate ((self loan))
287 ;; Find a monthly interest rate that makes the rest of the values work.
288 ;; There isn't an algebraic solution for the interest rate, so let's search for it.
289 ;; Find a suitable search range and do a binary search for it. Even for large interest
290 ;; rates the number of search iterations should be minimal.
291
292 (with-slots (loan-amount monthly-payment loan-duration interest-rate) self
293
294 ;; First we'll check to see whether the monthly payment is great than the loan amount.
295 ;; If so we'll set the interest rate directly so that the loan is paid off in one month.
296 ;; This avoids some ugly arithmetic overflow things that can happen when interest rates
297 ;; go off the charts
298 (let ((max-monthly-rate (/ $max-interest-rate$ 12)))
299 (if (>= monthly-payment loan-amount)
300 (min max-monthly-rate (1- (/ monthly-payment loan-amount)))
301 (let ((imin (max 0 (min max-monthly-rate
302 (/ (- (* monthly-payment loan-duration) loan-amount)
303 (* loan-duration loan-amount)))))
304 ;; imin is basically a rate that would result in the first month's interest as
305 ;; the average interest paid for all months. Since we know it must be greater
306 ;; than this, we have a guaranteed lower bound. But we cap it at our allowed
307 ;; monthly maximum interest.
308 (imax (min max-monthly-rate
309 (- (/ monthly-payment loan-amount) .000008333)))
310 ;; imax is a rate that would result in the first month's interest being
311 ;; minimally smaller than the payment. Since we must pay off in a finite
312 ;; duration, this is a guaranteed maximum. We cap it the allowed maximum
313 ;; monthly rate.
314 (target-p-l-ratio (/ monthly-payment loan-amount)))
315 (unless (>= imax imin)
316 (error "Max int = ~8,4f, Min int = ~8,4f" imax imin))
317 (do* ((i (/ (+ imin imax) 2)
318 (/ (+ imin imax) 2))
319 (p-l-ratio (pay-to-loan-ratio i loan-duration)
320 (pay-to-loan-ratio i loan-duration)))
321 ((<= (- imax imin) .000001) imax)
322 (if (>= target-p-l-ratio p-l-ratio)
323 (setf imin i)
324 (setf imax i))))))))
325
326(defmethod compute-new-loan-values ((self loan))
327 ;; For the sake of expediency we assume monthly componding
328 ;; The basic equation governing these computations is
329 (with-slots (compute-mode interest-rate loan-duration monthly-payment
330 loan-amount pay-schedule) self
331 (case compute-mode
332 (0
333 ;; compute the loan amount
334 (unless (or (zerop interest-rate)
335 (zerop loan-duration)
336 (zerop monthly-payment))
337 (#/willChangeValueForKey: self #@"loanAmt")
338 (setf loan-amount
339 (round (/ monthly-payment
340 (pay-to-loan-ratio (/ interest-rate 12)
341 loan-duration))))
342 (set-pay-schedule self)
343 (#/didChangeValueForKey: self #@"loanAmt")))
344 (1
345 ;; compute the interest rate
346 (unless (or (zerop loan-amount)
347 (zerop loan-duration)
348 (zerop monthly-payment))
349 (#/willChangeValueForKey: self #@"interestRate")
350 (setf interest-rate
351 (* 12 (/ (floor (* 1000000 (compute-int-rate self)))
352 1000000)))
353 (set-pay-schedule self)
354 (#/didChangeValueForKey: self #@"interestRate")))
355 (2
356 ;; compute the loan duration
357 (unless (or (zerop interest-rate)
358 (zerop loan-amount)
359 (zerop monthly-payment))
360 (#/willChangeValueForKey: self #@"loanDuration")
361 (set-pay-schedule self)
362 (setf loan-duration
363 (list-length pay-schedule))
364 (#/didChangeValueForKey: self #@"loanDuration")))
365 (3
366 ;; compute the monthly payment
367 (unless (or (zerop interest-rate)
368 (zerop loan-amount)
369 (zerop loan-duration))
370 (#/willChangeValueForKey: self #@"monthlyPayment")
371 (setf monthly-payment
372 (round (* loan-amount
373 (pay-to-loan-ratio (/ interest-rate 12)
374 loan-duration))))
375 (set-pay-schedule self)
376 (#/didChangeValueForKey: self #@"monthlyPayment"))))))
377
378;; Accessor functions used by display objects to retrieve and set values as dictated
379;; by the bindings we set up in IB
380
381(objc:defmethod (#/loanAmt :id)
382 ((self loan))
383 (lisp-to-ns-decimal (loan-amount self)))
384
385(objc:defmethod (#/interestRate :id)
386 ((self loan))
387 (#/numberWithFloat: ns:ns-number (float (interest-rate self))))
388
389(objc:defmethod (#/loanDuration :id)
390 ((self loan))
391 (#/numberWithInt: ns:ns-number (loan-duration self)))
392
393(objc:defmethod (#/monthlyPayment :id)
394 ((self loan))
395 (lisp-to-ns-decimal (monthly-payment self)))
396
397(objc:defmethod (#/originationDate :id)
398 ((self loan))
399 (lisp-to-ns-date (origination-date self)))
400
401(objc:defmethod (#/firstPayment :id)
402 ((self loan))
403 (lisp-to-ns-date (first-payment self)))
404
405(objc:defmethod (#/totInterest :id)
406 ((self loan))
407 (lisp-to-ns-decimal (reduce #'+ (pay-schedule self)
408 :key #'seventh
409 :initial-value 0)))
410
411(objc:defmethod (#/setLoanAmt: :void)
412 ((self loan) (amt :id))
413 (setf (loan-amount self) (lisp-from-ns-decimal amt))
414 (compute-new-loan-values self))
415
416(objc:defmethod (#/setInterestRate: :void)
417 ((self loan) (rate :id))
418 (setf (interest-rate self) (#/floatValue rate))
419 (compute-new-loan-values self))
420
421(objc:defmethod (#/setLoanDuration: :void)
422 ((self loan) (dur :id))
423 (setf (loan-duration self) (#/longValue dur))
424 (compute-new-loan-values self))
425
426(objc:defmethod (#/setMonthlyPayment: :void)
427 ((self loan) (pay :id))
428 (setf (monthly-payment self) (lisp-from-ns-decimal pay))
429 (compute-new-loan-values self))
430
431(objc:defmethod (#/setOriginationDate: :void)
432 ((self loan) (dt :id))
433 (let ((new-dt (ns-to-lisp-date dt)))
434 (setf (origination-date self) new-dt)
435 (#/willChangeValueForKey: self #@"firstPayment")
436 (setf (first-payment self) (next-month new-dt))
437 (#/didChangeValueForKey: self #@"firstPayment"))
438 (compute-new-loan-values self))
439
440(objc:defmethod (#/setFirstPayment: :void)
441 ((self loan) (pay :id))
442 (let ((new-pay (ns-to-lisp-date pay)))
443 (setf (first-payment self) new-pay))
444 (compute-new-loan-values self))
445
446
447
448;; test by
449(defun test-loan ()
450 ;; up to caller to #/release the returned loan instance
451 ;; but only after window is closed or crash will occur
452 (make-instance 'loan))
453
454(provide :loan-calc)
Note: See TracBrowser for help on using the repository browser.