reptile7's JavaScript blog
Thursday, December 24, 2015
Adventures in Amortization, Part 4
Blog Entry #356

In the last two entries, we've used the Loan Amount script's CalcPayment( ), CalcLoanAmt( ), and CalcPeriod( ) functions to calculate an amortizing loan's monthly payment (Payment), principal (LoanAmt), and period in months (Period), respectively.

The script also features a corresponding CalcRate( ) function for calculating an amortizing loan's yearly interest rate. The CalcRate( ) function is called by clicking the Rate field's button, and it preliminarily gets and vets an inputted principal, period in months, and monthly payment à la the CalcPayment( )/CalcLoanAmt( )/CalcPeriod( ) functions - so far, so normal. Unfortunately, the amortization formula we've been using

Payment = LoanAmt * Rate * Math.pow(1 + Rate, Period) / (Math.pow(1 + Rate, Period) - 1);

cannot be solved explicitly for the monthly interest Rate; consequently, the CalcRate( ) function determines the Rate via an iterative numerical method that incorporates elements of successive substitution and a binary search.

var TempPayment, Increment; TempPayment = -9999; Rate = 50 / 1200; // Yearly rate of 50% Increment = Rate / 2; with (Math) { while (abs(Payment - TempPayment) > 0.005) { TempPayment = LoanAmt * Rate * pow(1 + Rate, Period) / (pow(1 + Rate, Period) - 1); if (TempPayment > Payment) { Rate -= Increment; } else if (TempPayment < Payment) { Rate += Increment; } Increment /= 2; } } form.Rate.value = (parseInt(Rate * 1200000) / 1000).toString( );

With LoanAmt/Period/Payment data in hand, CalcRate( ) guesses the yearly percentage rate to be 50, which is divided by 1200 to give an initial Rate. It zeroes in on the actual Rate by iteratively
(a) decreasing or increasing the Rate by a shrinking Increment after
(b) comparing the Payment to a transitional TempPayment.
The TempPayment and Increment are initialized to -9999 and Rate / 2, respectively.

A while loop takes us from the initial Rate to the actual Rate. The while condition tests if the | Payment - TempPayment | separation is greater than 0.005; the abs( ) method of the Math object is documented here. If the condition comparison returns true:
(1) The LoanAmt, Rate, and Period are plugged into the amortization formula to give a new TempPayment.
(2) If the new TempPayment is greater than the Payment, then the Rate is too high and is therefore decreased by the Increment; vice versa if the TempPayment is less than the Payment.
(3) The Increment is halved.
Steps (1)-(3) repeat until the condition is false.

The TempPayment initialization is unnecessary if we recast the while loop as a do...while loop:

do { /* ...Loop body statements... */ }
while (abs(Payment - TempPayment) > 0.005)

Lastly, the actual Rate is converted to a yearly rate, specifically,
(i) Rate is multiplied by 1200000,
(ii) the resulting product is parseInt( )ed, and
(iii) the parseInt( ) return is divided by 1000;
the * 1200000 / 1000 scaling causes the yearly rate to have three or fewer post-decimal point digits. The yearly rate is
(iv) unnecessarily toString( )ed and
(v) subsequently displayed in the Rate field.

Zero and below

It can be shown that the Payment approaches LoanAmt / Period as the Rate approaches 0.
(Thanks to mathbff and her "How to Find Any Limit" video - in particular the Open Up Parentheses (Expand Then Simplify) section starting at 12:01 - for help in this regard.)

(1) On the right-hand side (RHS) of the amortization equation, expand the (1 + Rate)Period terms.

Payment = LoanAmt * Rate * (1 + Period*Rate + Period*Rate2 + ... + RatePeriod) / ((1 + Period*Rate + Period*Rate2 + ... + RatePeriod) - 1)

(2) In the RHS denominator, carry out the 1 - 1 subtraction and then factor out the Rate.

Payment = LoanAmt * Rate * (1 + Period*Rate + Period*Rate2 + ... + RatePeriod) / Rate * (Period + Period*Rate + ... + RatePeriod-1)

(3) Re the RHS numerator/denominator, cancel the outer Rate factors and then plug 0 into the remaining Rate terms, and you're there.

The CalcRate( ) function does not flag Payments that are at or below the LoanAmt / Period threshold: in these cases, the while loop
(a) continues to push the TempPayment toward the Payment and
(b) pushes the Rate closer and closer to 0 as the TempPayment reaches/crosses the threshold.
Another complication arises when converting a near-0 Rate to a yearly rate: parseInt( ) stops flooring an number to 0 and instead extracts the x part thereof if z is 7 or higher, e.g., parseInt(7.673982976852402e-10) returns 7 - this strange result occurs with all of the OS X browsers on my computer.

We can intercept too-low Payments and the erroneous rates they produce via the conditional below:

// Place this code just before the while or do...while loop: var minPayment = LoanAmt / Period; if (Payment <= minPayment) { Payment = minPayment != Math.ceil(minPayment) ? Math.ceil(minPayment) : minPayment + 1; window.alert("Your payment must be at least $" + Payment + " to pay off the loan by the end of the given period."); return; }

We used an analogous snippet to flag CalcPeriod( ) Payments that are at or below LoanAmt * Rate - see the You bet your loan life section of the previous post.

Toward the infinite loop, part 2

If there is too much distance between the initial Rate and the actual Rate, then the Increment eventually becomes too small to push the TempPayment within 0.005 of the Payment, and consequently the loop runs indefinitely.

If we borrow $1000 and repay it in twelve monthly installments, then our minPayment is 83.33. In this case, the original CalcRate( ) function will accept a PaymentAmt.value as low as 55.56, which effectively corresponds to a negative interest rate, but anything lower than that causes the browser to hang; the browser similarly hangs if the Payment is any higher than 135, which corresponds to a 100% yearly interest rate, even though there is in theory no upper limit on what an interest rate can be. (>719,000%, anyone?)
FWIW: I've also found the Loan Amount calculator here, here, and here, and in fairness to Dave and Joe, none of these other guys sorts this problem out.

The Payment <= minPayment conditional puts the kibosh on the Rate0 situation but that still leaves us the high-rate situation to deal with.

I track down a Missing Term Loan Calculator (MTLC) that satisfactorily calculates >100% interest rates. Created by Dan Peterson in 2010, the MTLC - specifically, the cir( ) function thereof - determines an interest rate in much the same way that the Loan Amount script does, namely, it
(a) takes an initial guess at the rate (myDecRate), and then
(b) calculates a temporary payment (myNewPmtAmt) by plugging an inputted principal (myPrin), an inputted number of payments (myNumPmts), and myDecRate into the amortization equation, and then
(c) iteratively advances myDecRate toward the actual rate and myNewPmtAmt toward an inputted payment (myPmtAmt) via a series of shrinking increments.

Significantly, the Δ(myDecRate) increments don't go any lower than 0.000001 / 12 - that's one ten-thousandth of a percent, APR-wise. I go back to the CalcRate( ) function and recast the Increment /= 2; assignment as a

if (Increment > 0.000001 / 12) Increment /= 2;

conditional to see if similarly bounding the Increment staves off the generation of infinite loops as described above. It does!

With the preceding statement in place, the CalcRate( ) function nevertheless requires many more loop iterations to zero in on a >100% interest rate than the cir( ) function does, and it is tempting to import some of the latter into the former. But do we really want to be accommodating such high interest rates in the first place? I like the idea of intercepting them with:

// This would also go just before the loop: var Rate100 = 100 / 1200; var Payment100 = LoanAmt * Rate100 * Math.pow(1 + Rate100, Period) / (Math.pow(1 + Rate100, Period) - 1); if (Payment >= Payment100) { window.alert("Your yearly interest rate is going to be \u2265100%.\nMaybe you should seek out a better lender."); return; }

\u2265 is the Unicode escape sequence for a ≥ symbol.

We'll discuss the script's remaining functions, ShowAmoritazation( ) and ValToMoney( ), in the following entry.

Sunday, December 06, 2015
Adventures in Amortization, Part 3
Blog Entry #355

We return now to our ongoing deconstruction of the Loan Amount script's JavaScript. In our last episode, we used the script's CalcPayment( ) function to calculate the monthly payment (Payment) for an amortizing loan from the loan's principal (LoanAmt), interest rate (Rate), and period (Period) via the amortization formula below:

Payment = LoanAmt * (Rate * Math.pow(1 + Rate, Period)) / (Math.pow(1 + Rate, Period) - 1);

In today's post, we'll see how to calculate an unknown LoanAmt or Period if we have values for the remaining three variables.

And we got no principals

Suppose we want to borrow at a yearly interest rate of 5% for one year and can only afford a $50 monthly payment. How much loan can we afford?

We respectively enter 5, 12, and 50 into the left frame's Rate, Period, and PaymentAmt fields and then click the LoanAmt field's button, thereby calling a CalcLoanAmt( ) function.

<label>Loan Amount ($)<br><input type="text" name="LoanAmt" size="10"></label>
<input type="button" value="Calculate" onclick="CalcLoanAmt(this.form);">

function CalcLoanAmt(form) { ... }

CalcLoanAmt( ) initially converts the Rate.value, Period.value, and PaymentAmt.value strings to numbers

Rate = parseFloat(form.Rate.value);
Period = parseInt(form.Period.value);
Payment = parseFloat(form.PaymentAmt.value);

and makes sure that there aren't any NaNs in the resulting Rate/Period/Payment data

if (isNaN(Rate)) { window.alert("Missing Interest Rate"); return; }
if (isNaN(Period)) { window.alert("Missing Period Value"); return; }
if (isNaN(Payment)) { window.alert("Missing Payment Amount"); return; }

and adjusts the Rate

if (Rate >= 1) { Rate /= 100; }
Rate /= 12;

à la the CalcPayment( ) function.

The amortization formula is easily solved for the LoanAmt: just divide both sides of the equation by
Rate * (1 + Rate)Period / ((1 + Rate)Period - 1)
and you're there.

LoanAmt = Payment / ((Rate * Math.pow(1 + Rate, Period)) / (Math.pow(1 + Rate, Period) - 1));

Plugging our Payment, Rate, and Period into the preceding equation gives us a LoanAmt of 584.0611002149129, which is truncated to 584.06 and then displayed in the LoanAmt field.

form.LoanAmt.value = (Math.floor(LoanAmt * 100) / 100).toString( );
// Alternatively and preferably: form.LoanAmt.value = LoanAmt.toFixed(2);
return; } // That's it for CalcLoanAmt( ).

You bet your loan life

Suppose we borrow $1000 at a yearly interest rate of 5% and can only afford a $50 monthly payment. How many months will it take to pay off our loan?

We respectively enter 1000, 5, and 50 into the left frame's LoanAmt, Rate, and PaymentAmt fields and then click the Period field's button, thereby calling a CalcPeriod( ) function. CalcPeriod( ) initially converts the LoanAmt.value, Rate.value, and PaymentAmt.value strings to numbers, makes sure that there aren't any NaNs in the resulting LoanAmt/Rate/Payment data, and adjusts the Rate à la the CalcPayment( ) and CalcLoanAmt( ) functions.

<label>Loan Period (in months)<br><input type="text" name="Period" size="10"></label>
<input type="button" value="Calculate" onclick="CalcPeriod(this.form);">

function CalcPeriod(form) { LoanAmt = parseFloat(form.LoanAmt.value); Rate = parseFloat(form.Rate.value); Payment = parseFloat(form.PaymentAmt.value); if (isNaN(LoanAmt)) { window.alert("Missing Loan Amount"); return; } if (isNaN(Rate)) { window.alert("Missing Interest Rate"); return; } if (isNaN(Payment)) { window.alert("Missing Payment Amount"); return; } if (Rate >= 1) { Rate /= 100; } Rate /= 12;

We can convert the solved-for-Payment amortization equation to a solved-for-Period equation as follows:

(1) Multiply both sides of the equation by (1 + Rate)Period - 1.

Payment * (1 + Rate)Period - Payment = LoanAmt * Rate * (1 + Rate)Period

(2) Rearrange the resulting equation to put the (1 + Rate)Period terms on the same side and a Payment term by itself on the opposite side, and then combine/factorize the (1 + Rate)Period terms.

(Payment - LoanAmt * Rate) * (1 + Rate)Period = Payment

(3) Divide both sides of the resulting equation by Payment - LoanAmt * Rate.

(1 + Rate)Period = Payment / (Payment - LoanAmt * Rate)

(4) Take the logarithm of both sides of the resulting equation and, recalling that log(xy) = y * log(x), divide both sides of the logarithmed equation by log(1 + Rate).

Period = log(Payment / (Payment - LoanAmt * Rate)) / log(1 + Rate)

As it happens, setting the principal and interest rate of an amortizing loan places a constraint on the loan payment: to pay off the loan (to reduce the principal), the Payment must be greater than the LoanAmt * Rate product. You can see why this is so via the solved-for-Period amortization equation:
(1) If the Payment is less than LoanAmt * Rate, then Payment / (Payment - LoanAmt * Rate) will be negative, and you can't take the logarithm of a negative number (at least in the real number system, and when you're working with loans, you're definitely restricted to the real number system).
(2) If the Payment is equal to LoanAmt * Rate, then Payment / (Payment - LoanAmt * Rate) will be infinity/undefined, i.e., it'll take an infinite number of months to pay off the loan (and I don't think we've got that long).

CalcPeriod( ) flags the Payment < LoanAmt * Rate situation via the conditional below but it doesn't confront the Payment == LoanAmt * Rate situation.

if (Payment - LoanAmt * Rate < 0) { window.alert("Payment amount too small.\nIt must be at least $" + (Math.ceil(LoanAmt * Rate)).toString( )); return; }

If the if condition is true:
(1) An alert( ) pop-up informs the lendee that the payment is too small and must be at least Math.ceil(LoanAmt * Rate). (It's not necessary to use the toString( ) method to append the payment number to the Payment amount too small ... string - see the Adding Strings and Numbers section of W3Schools' "JavaScript Operators" page.)
(2) A return statement causes CalcPeriod( ) to exit.

For specifying a minimum payment on the alert( ) box, the LoanAmt * Rate baseline is ceil( )ed, which fits the bill as long as LoanAmt * Rate is not an integer. If LoanAmt * Rate is an integer - as would be the case if we increased the yearly interest rate to 6% - then it is unaffected by the ceil( ) operation and consequently becomes the at least payment, which as detailed above isn't going to cut it.

We can handle all too-low Payments in one go with:

if (Payment <= LoanAmt * Rate) { Payment = LoanAmt * Rate != Math.ceil(LoanAmt * Rate) ? Math.ceil(LoanAmt * Rate) : (LoanAmt * Rate) + 1; window.alert("Your payment is too small to reduce the principal.\nBeef it up to at least $" + Payment); return; }

• The LoanAmt * Rate != Math.ceil(LoanAmt * Rate) condition returns true if LoanAmt * Rate is not an integer.
• Following the author's lead, I push insufficient Payments up to the next whole dollar amount, although we could get away with setting them to one cent higher than the LoanAmt * Rate baseline if we wanted to.

Once an acceptable Payment is in hand, CalcPeriod( ) calculates the Period with:

with (Math) { Period = ceil(log(Payment / (Payment - LoanAmt * Rate)) / log(1 + Rate)); }

We want the Period to be a round number of months. Toward this end, we could floor( ), round( ), or ceil( ) the Period: the author chose to ceil( ) it. We'll see below that, besides being one month longer, a ceil( )ed Period results in a lower Payment for the lendee and more total interest for the lender vis-à-vis a floor( )ed Period (unless Period is an integer, which is possible but unlikely).

Other coding points:
• The with statement, whose use Mozilla frowns on, is documented here.
Math.log( ) returns a base-e logarithm although the logarithm base for the preceding statement actually doesn't matter.

The ceil( )ed Period for our example is 21, which is displayed in the Period field.

form.Period.value = Period.toString( );
// Better: form.Period.value = Period;

As the corresponding Payments for an un-ceil( )ed Period and a ceil( )ed Period are slightly different (again assuming that Period is not an integer), CalcPeriod( ) calls the CalcPayment( ) function in order to sync the original Payment with the ceil( )ed Period.

return; } // That's it for CalcPeriod( ).

CalcPayment( ) outputs a Payment = 49.84 for the 21-month Period, i.e., we'll be paying 16¢/month less than originally planned. BTW, the un-ceil( )ed Period would have been 20.92620601649899: had we floor( )ed it to 20, the CalcPayment( ) Payment would be 52.22.

In the next entry we'll go through the script's CalcRate( ) function, which calculates the Rate from the LoanAmt, Period, and Payment; depending on how long that takes, we may also start discussing the script's functionality.

Powered by Blogger

Actually, reptile7's JavaScript blog is powered by Café La Llave. ;-)