How to create a Loan Amortisation and Interest Calculation routine

By Tony Marston

17th May 2003

Intended Audience
Prerequisites
Sample output
Building the form
- Form elements
- Validate user input
- Inform user of any errors
- Perform calculations
-- 'Binary Chop' technique
- Produce payment schedule

Intended Audience

This tutorial is intended for developers who wish to perform interest rate calculations and produce loan amortisation schedules. It will also show how to use the 'binary chop' technique in order to calculate the interest rate. This is a program which I originally wrote in COBOL many years ago, and have subsequently rewritten in UNIFACE, Visual Basic, and now PHP.

Prerequisites

This code does not require any particular PHP modules to be installed, nor does it require access to any database or file system.

Sample output

Here is what the initial screen will look like:

amortise1 (7K)

This displays each of the four elements in the loan calculation, which are:

This screen will allow the user to enter any 3 of the 4 values. After calculating the missing 4th value the user can produce a schedule of payments.

You can run the program from here and view the complete source code from here.

Building the form

Now we shall look at the various components in this program.

Form elements

We first need to define the area where the user can enter data and press one of the buttons to perform a certain action. You will notice that the action attribute of the <form> tag is $_SERVER['PHP_SELF'], which causes the same script to be executed again.

<form action="<?php echo $_SERVER['PHP_SELF'] ?>" method="POST"> 
<table border="1"> 
<tr> 
    <td>Principal</td><td><input type="text" name="principal" value="<?= $principal ?>" /></td>
    <td><input type="submit" name="button1" value="calculate Principal" /></td> 
</tr> 
<tr> 
    <td>Number of Payments</td><td><input type="text" name="number" value="<?= $number ?>" /></td>
    <td><input type="submit" name="button2" value="calculate Number " /></td> 
</tr> 
<tr> 
    <td>Interest Rate (%) per Payment</td><td><input type="text" name="rate" value="<?= $rate ?>" /></td>
    <td><input type="submit" name="button3" value="calculate Interest   " /></td> 
</tr> 
<tr> 
    <td>Payment</td><td><input type="text" name="payment" value="<?= $payment ?>" /></td>
    <td><input type="submit" name="button4" value="calculate Payment" /></td> 
</tr> 
</table> 
<p><input type="submit" name="reset" value="reset" />&nbsp;&nbsp;&nbsp;
   <input type="submit" name="button5" value ="Payment Schedule" /></p> 
</form>

Validate user input

This first piece of code deals with three possible conditions:

// look for no POST entries, or the RESET button 
if (count($_POST) == 0 or @$_POST['reset']) { 
    // POST array is empty - set initial values 
    $principal = null; 
    $number    = null; 
    $rate      = null; 
    $payment   = null; 
} else { 
    // retrieve values from POST array 
    $principal = $_POST['principal']; 
    $number    = $_POST['number']; 
    $rate      = $_POST['rate']; 
    $payment   = $_POST['payment']; 
} // if

As usual we cannot process the user's input without first making sure that it is valid. Notice here that we only validate the contents of a field if it contains something to be validated.

// validate all fields 
$error = array(); 
if (!empty($principal)) { 
   if (!is_numeric($principal)) { 
      $error['principal'] = "must be numeric"; 
   } elseif ($principal < 0) { 
      $error['principal'] = "must be > zero"; 
   } else { 
      $principal = (float)$principal;    // convert to floating point 
   } // if 
} // if 

if (!empty($number)) { 
   if (!ereg('^[[:digit:]]+$', $number)) { 
      $error['number'] = "must be an integer"; 
   } else { 
      $number = (int)$number;    // convert to integer 
   } // if 
} // if 

if (!empty($rate)) { 
   if (!is_numeric($rate)) { 
      $error['rate'] = "must be numeric"; 
   } elseif ($rate < 0) { 
      $error['rate'] = "must be > zero"; 
   } else { 
      $rate = (float)$rate;    // convert to floating point 
   } // if 
} // if 

if (!empty($payment)) { 
   if (!is_numeric($payment)) { 
      $error['payment'] = "must be numeric"; 
   } elseif ($payment < 0) { 
      $error['payment'] = "must be > zero"; 
   } else { 
      $payment = (float)$payment;    // convert to floating point 
   } // if 
} // if 

Inform user of any errors

I have often seen messages in the PHP newsgroup from novices who want to know how to tell the user that some of his input has failed in its validation. Basically you have to show the user the same page, with the values that were entered, and with any error messages which were generated during the validation process. The technique that I use has two simple steps:

This is a simple technique, but it works every time.

Perform calculations

Once the user has entered 3 of the values he will want the system to calculate the 4th. As each different calculation is driven by a different button the code to achieve this is relatively straight forward.

if (count($error) == 0) { 
   // no errors - perform requested action 
   if (isset($_POST['button1'])) { 
      $principal = calc_principal($number, $rate, $payment); 
   } // if 
   if (isset($_POST['button2'])) { 
      $number = calc_number($principal , $rate, $payment); 
   } // if 
   if (isset($_POST['button3'])) { 
      $rate = calc_rate($principal, $number, $payment); 
   } // if 
   if (isset($_POST['button4'])) { 
      $payment = calc_payment($principal, $number, $rate ,2); 
   } // if 
} // if 

The formulae that I use for the calculations were obtained from the instruction manual for a Casio BF-100 calculator that I bought in 1984 when working on a Lease Accounting System. Note that before I can perform the calculation to provide the 4th value I must first ensure that the other three values have actually been provided.

function calc_principal ($payno, $int, $pmt) 
{ 
// check that required values have been supplied 
if (empty($payno)) { 
   echo "<p class='error'>a value for NUMBER of PAYMENTS is required</p>"; 
   exit; 
} // if 
if (empty($int)) { 
   echo "<p class='error'>a value for INTEREST RATE is required</p>"; 
   exit; 
} // if 
if (empty($pmt)) { 
   echo "<p class='error'>a value for PAYMENT is required</p>"; 
   exit; 
} // if 

// now do the calculation using this formula: 

//****************************************** 
//             ((1 + INT) ** PAYNO) - 1 
// PV = PMT * -------------------------- 
//            INT * ((1 + INT) ** PAYNO) 
//****************************************** 

$int    = $int / 100;        //convert to percentage 
$value1 = (pow((1 + $int), $payno)) - 1; 
$value2 = $int * pow((1 + $int), $payno); 
$pv     = $pmt * ($value1 / $value2); 
$pv     = number_format($pv, 2, ".", ""); 

return $pv; 

} // calc_principal ================================================================== 

'Binary Chop' technique

While the calculations for number of payments and payment amount are simple enough (once you know the formulae), there is no published formula to provide the interest rate per period. All my calculator manual had to say on the subject was Approximation formulas by Newton's Law. Roughly translated this means that you have to guess a value for $rate, then use one of the other formulae to calculate a value that you already know. If this calculation produces the right value then you know that your guess was correct. If it was not correct then you have to take another guess and try again.

Let me ask a simple question - how many guesses will it take to find the right number between 0 and 100 when this number must have 9 decimal places? If you start from 0.000000001 and end with 99.999999999 you should realise that you will have to take an enormous number of guesses until you happen you hit the right one. Fortunately there is a quicker way which I remembered from my early days when programming in Assembler and COBOL. It is called the 'binary chop technique', and it will help you extract a value from a range of possible values provided that the range has been sorted. It is called the 'binary chop technique' because it involves splitting the range of possible values into 2 halves, determining which half contains the answer and discarding the other half. You then take the remaining half, split it into 2, and keep repeating the process until you come up with the right value.

In the following code snippet you will see that I set my low point to 0, my high point to 100, but as I know the interest rate will be quite small I set the first guess value at 2.5 instead of 50.

function calc_rate($pv, $payno, $pmt) 
{ 

// try and guess the value using binary chop technique 
$GuessHigh   = (float)100;    // maximum value 
$GuessMiddle = (float)2.5;    // first guess 
$GuessLow    = (float)0;      // minimum value 
$GuessPMT    = (float)0;      // result of test calculation 

I have a control loop which keeps calculating values for $GuessPMT until it matches the known value in $pmt.

do {
   ...
   ...
} while ($GuessPMT !== $pmt); 

return $int; 

} // calc_rate =======================================================================

I perform the calculation for $GuessPMT using the current value of $GuessMiddle. Note that I require the calculation to yield a result to 6 decimal places for additional accuracy.

   // use current value for GuessMiddle as the interest rate,
   // and set level of accuracy to 6 decimal places
   $GuessPMT = (float)calc_payment($pv, $payno, $GuessMiddle, 6); 

Now I need to look at the result of this calculation to see how it compares with the known value. If it is too high or too low I must split the difference and generate a new value for $GuessMiddle.

   if ($GuessPMT > $pmt) {    // guess is too high 
      $GuessHigh   = $GuessMiddle; 
      $GuessMiddle = $GuessMiddle + $GuessLow; 
      $GuessMiddle = $GuessMiddle / 2; 
   } // if 
    
   if ($GuessPMT < $pmt) {    // guess is too low 
      $GuessLow    = $GuessMiddle; 
      $GuessMiddle = $GuessMiddle + $GuessHigh; 
      $GuessMiddle = $GuessMiddle / 2; 
   } // if 

If we have run out of guesses before hitting the right answer then we must assume that the last guess was close enough.

   if ($GuessMiddle == $GuessHigh) break; 
   if ($GuessMiddle == $GuessLow) break; 

We must round the current guess to 9 decimal places. Note that if this yields a result that is all zeros then there is a fault with the input data.

   $int = number_format($GuessMiddle, 9, ".", "");    // round it to 9 decimal places 
   if ($int == 0) { 
      echo "<p class='error'>Interest rate has reached zero - calculation error</p>"; 
      exit; 
   } // if 

Using this method it is quite common to achieve a result in 30-35 guesses.

Produce payment schedule

After having performed the necessary calculation the final step is to produce a payment schedule. For each payment it will show the split between interest and principal and the outstanding balance. The principal which is paid off in each period is used to decrease the balance before interest is calculated for the next period. With each successive payment the amount of interest gets smaller while the principal gets bigger, thus reducing the balance faster and faster until it is reduced to zero with the last payment. The final line in the schedule shows the total amount paid in the life of the loan, which should equal the total interest paid plus the total principal paid.

amortise2 (12K)

The payment schedule is not produced until the relevant button is pressed, at which point it first checks that the required values are available.

if (isset($_POST['button5'])) { 
   print_schedule($principal, $rate, $payment); 
} // if

function print_schedule($balance, $rate, $payment) 
{
// check that required values have been supplied
if (empty($balance)) { 
   echo "<p class='error'>a value for PRINCIPAL is required</p>"; 
   exit; 
} // if 
if (empty($rate)) { 
   echo "<p class='error'>a value for INTEREST RATE is required</p>"; 
   exit; 
} // if 
if (empty($payment)) { 
   echo "<p class='error'>a value for PAYMENT is required</p>"; 
   exit; 
} // if 

Next we start the table and create the table headings.

echo '<table border="1">'; 
echo '<tr><th>#</th><th>PAYMENT</th><th>INTEREST</th><th>PRINCIPAL</th><th>BALANCE</th></tr>'; 

Then comes the control loop which keeps on going until the balance has been cleared. We keep a count and hope that it matches the number of payments when the loop has finished.

$count = 0; 
do { 
   $count++;
   ...
   ...
} while ($balance > 0);

Within this loop the first task is to calculate the interest to be paid on the outstanding balance, then get the amount which is left over to pay off the principal.

   // calculate interest on outstanding balance 
   $interest = $balance * $rate/100; 
   
   // what portion of payment applies to principal? 
   $principal = $payment - $interest; 

Sometimes there is a rounding error and the outstanding balance becomes 0.01 less than the payment - this code should take care that situation:

   // watch out for balance < payment 
   if ($balance < $payment) { 
      $principal = $balance; 
      $payment   = $interest + $principal; 
   } // if 

Here we reduce the amount of the balance outstanding by that portion of the payment which was not interest.

   // reduce balance by principal paid 
   $balance = $balance - $principal; 

Here is another situation that may crop up in certain circumstances:

   // watch for rounding error that leaves a tiny balance 
   if ($balance < 0) { 
      $principal = $principal + $balance; 
      $interest  = $interest - $balance; 
      $balance   = 0; 
   } // if 

The calculations for the current payment are complete, so output the details. Each value is formatted to 2 decimal places.

   echo "<tr>"; 
   echo "<td>$count</td>"; 
   echo "<td>" .number_format($payment,   2, ".", ",") ."</td>"; 
   echo "<td>" .number_format($interest,  2, ".", ",") ."</td>"; 
   echo "<td>" .number_format($principal, 2, ".", ",") ."</td>"; 
   echo "<td>" .number_format($balance,   2, ".", ",") ."</td>"; 
   echo "</tr>"; 

Here we increment the totals for the last line:

   @$totPayment   = $totPayment + $payment; 
   @$totInterest  = $totInterest + $interest; 
   @$totPrincipal = $totPrincipal + $principal; 

This traps the situation where the payment amount entered is not enough to pay the interest, so the loan will never be repaid.

   if ($payment < $interest) { 
      echo "</table>"; 
      echo "<p>Payment < Interest amount - rate is too high, or payment is too low</p>"; 
      exit; 
   } // if 

When the loop has completed the last act is to print the final totals.

echo "<tr>"; 
echo "<td>&nbsp;</td>"; 
echo "<td><b>" .number_format($totPayment,   2, ".", ",") ."</b></td>"; 
echo "<td><b>" .number_format($totInterest,  2, ".", ",") ."</b></td>"; 
echo "<td><b>" .number_format($totPrincipal, 2, ".", ",") ."</b></td>"; 
echo "<td>&nbsp;</td>"; 
echo "</tr>"; 
echo "</table>";
   
} // print_schedule ================================================================== 

© Tony Marston
17th May 2003

http://www.tonymarston.net
http://www.radicore.org

counter