diff options
| -rw-r--r-- | mortgage.html | 68 | ||||
| -rw-r--r-- | mortgage.js | 74 | ||||
| -rw-r--r-- | mortgage.py | 55 | ||||
| -rw-r--r-- | requirements.txt | 1 |
4 files changed, 198 insertions, 0 deletions
diff --git a/mortgage.html b/mortgage.html new file mode 100644 index 0000000..07de66b --- /dev/null +++ b/mortgage.html | |||
| @@ -0,0 +1,68 @@ | |||
| 1 | <!DOCTYPE html> | ||
| 2 | <html> | ||
| 3 | |||
| 4 | <head> | ||
| 5 | <title>Mortgage Calculator</title> | ||
| 6 | <style> | ||
| 7 | #parameters { | ||
| 8 | margin-top: 2em; | ||
| 9 | display: grid; | ||
| 10 | grid-template-columns: 10em min-content; | ||
| 11 | column-gap: 1em; | ||
| 12 | row-gap: 0.5em; | ||
| 13 | } | ||
| 14 | #parameters div { | ||
| 15 | text-align: right; | ||
| 16 | } | ||
| 17 | #parameters input { | ||
| 18 | width: 12em; | ||
| 19 | text-align: right; | ||
| 20 | } | ||
| 21 | #results { | ||
| 22 | margin-top: 2em; | ||
| 23 | } | ||
| 24 | #results-table { | ||
| 25 | border: 1px solid; | ||
| 26 | border-collapse: collapse; | ||
| 27 | } | ||
| 28 | #results-table th { | ||
| 29 | padding-top: 0.5em; | ||
| 30 | padding-bottom: 0.5em; | ||
| 31 | } | ||
| 32 | #results-table th, td { | ||
| 33 | text-align: center; | ||
| 34 | padding-left: 1em; | ||
| 35 | padding-right: 1em; | ||
| 36 | } | ||
| 37 | #results-table tr:nth-child(even){ | ||
| 38 | background-color: #f2f2f2; | ||
| 39 | } | ||
| 40 | #results-table tr:hover {background-color: #ddd;} | ||
| 41 | </style> | ||
| 42 | </head> | ||
| 43 | |||
| 44 | <body> | ||
| 45 | <h1>Mortgage Calculator</h1> | ||
| 46 | <div id="parameters"> | ||
| 47 | <div>Principal</div> | ||
| 48 | <div><input id="principal" type="number" value="100000" onchange="update()"></div> | ||
| 49 | <div>Annual Interest (%)</div> | ||
| 50 | <div><input id="interest" type="number" value="5.0" onchange="update()"></div> | ||
| 51 | <div>Installments</div> | ||
| 52 | <div><input id="installments" type="number" value="300" onchange="update()"></div> | ||
| 53 | </div> | ||
| 54 | <div id="results"> | ||
| 55 | <table id="results-table"> | ||
| 56 | <tr> | ||
| 57 | <th>Time</th> | ||
| 58 | <th>Outstanding Debt</th> | ||
| 59 | <th>Monthly Installment</th> | ||
| 60 | <th>Interest Paid</th> | ||
| 61 | <th>Amount Amortised</th> | ||
| 62 | </tr> | ||
| 63 | </table> | ||
| 64 | </div> | ||
| 65 | <script type="text/javascript" src="mortgage.js"></script> | ||
| 66 | </body> | ||
| 67 | |||
| 68 | </html> | ||
diff --git a/mortgage.js b/mortgage.js new file mode 100644 index 0000000..ffc1b1d --- /dev/null +++ b/mortgage.js | |||
| @@ -0,0 +1,74 @@ | |||
| 1 | /// Round up the given number two the nearest two decimal places. | ||
| 2 | function round(x) { | ||
| 3 | return Math.ceil(x*100)/100; | ||
| 4 | } | ||
| 5 | |||
| 6 | /// Format the float as a string using two decimal places. | ||
| 7 | function fmt(x) { | ||
| 8 | return round(x).toFixed(2); | ||
| 9 | } | ||
| 10 | |||
| 11 | /// Clear all rows in the table except for the first row (header). | ||
| 12 | function clear_table(table) { | ||
| 13 | while (table.rows.length > 1) { | ||
| 14 | table.deleteRow(1); | ||
| 15 | } | ||
| 16 | } | ||
| 17 | |||
| 18 | /// Insert the values into a new row in the table. | ||
| 19 | function insert_row(table, values) { | ||
| 20 | var row = table.insertRow(table.rows.length); | ||
| 21 | values.forEach((value) => { | ||
| 22 | row.insertCell(row.cells.length).innerHTML = value; | ||
| 23 | }); | ||
| 24 | } | ||
| 25 | |||
| 26 | /// Compute the mortgage. | ||
| 27 | function compute_mortgage(principal, annual_interest, installments, table) { | ||
| 28 | const p = principal; | ||
| 29 | const i = annual_interest / 100 / 12; // Monthly equivalent. | ||
| 30 | const n = installments; | ||
| 31 | |||
| 32 | const monthly_installment = p * (i * (1 + i)**n) / ((1 + i)**n - 1); | ||
| 33 | |||
| 34 | var debt = principal; | ||
| 35 | var total_interest = 0; | ||
| 36 | var total_amortised = 0; | ||
| 37 | |||
| 38 | clear_table(table); | ||
| 39 | |||
| 40 | for (k = 0; k < n; ++k) { | ||
| 41 | const interest = i * debt; | ||
| 42 | const amortised = monthly_installment - interest; | ||
| 43 | |||
| 44 | const values = [k.toString(), fmt(debt), fmt(monthly_installment), fmt(interest), fmt(amortised)]; | ||
| 45 | insert_row(table, values); | ||
| 46 | |||
| 47 | total_interest += interest; | ||
| 48 | total_amortised += amortised; | ||
| 49 | debt -= amortised; | ||
| 50 | } | ||
| 51 | |||
| 52 | const total_interest_pct = total_interest / principal * 100; | ||
| 53 | |||
| 54 | insert_row(table, ["", "", "", "", ""]); // Separator. | ||
| 55 | |||
| 56 | insert_row(table, ["Total", "", "Amount Paid", "Interest Paid", "Amount Amortised"]); | ||
| 57 | insert_row(table, [ | ||
| 58 | "", | ||
| 59 | "", | ||
| 60 | fmt(monthly_installment * n), | ||
| 61 | fmt(total_interest) + " (" + fmt(total_interest_pct) + "%)", | ||
| 62 | fmt(total_amortised)]); | ||
| 63 | } | ||
| 64 | |||
| 65 | function update() { | ||
| 66 | const principal = parseFloat(document.getElementById("principal").value); | ||
| 67 | const interest = parseFloat(document.getElementById("interest").value); | ||
| 68 | const installments = parseInt(document.getElementById("installments").value); | ||
| 69 | const results = document.getElementById("results-table"); | ||
| 70 | |||
| 71 | compute_mortgage(principal, interest, installments, results); | ||
| 72 | } | ||
| 73 | |||
| 74 | update(); | ||
diff --git a/mortgage.py b/mortgage.py new file mode 100644 index 0000000..3b9b958 --- /dev/null +++ b/mortgage.py | |||
| @@ -0,0 +1,55 @@ | |||
| 1 | import argparse | ||
| 2 | import math | ||
| 3 | import sys | ||
| 4 | from tabulate import tabulate, SEPARATING_LINE | ||
| 5 | |||
| 6 | |||
| 7 | def calc_payments(principal, annual_interest, installments): | ||
| 8 | p = principal | ||
| 9 | i = annual_interest / 100 / 12 # Monthly equivalent. | ||
| 10 | n = installments | ||
| 11 | |||
| 12 | def fmt(x): | ||
| 13 | return f"{math.ceil(x*100)/100:.2f}" | ||
| 14 | |||
| 15 | table = [["Time", "Outstanding Debt", "Monthly Installment", "Interest Paid", "Amount Amortised"]] | ||
| 16 | |||
| 17 | monthly_installment = p * (i * (1 + i)**n) / ((1 + i)**n - 1) | ||
| 18 | debt = principal | ||
| 19 | total_interest = 0 | ||
| 20 | total_amortised = 0 | ||
| 21 | |||
| 22 | for k in range(n): | ||
| 23 | interest = i * debt | ||
| 24 | amortised = monthly_installment - interest | ||
| 25 | table.append([k] + [fmt(x) for x in [debt, monthly_installment, interest, amortised]]) | ||
| 26 | |||
| 27 | total_interest += interest | ||
| 28 | total_amortised += amortised | ||
| 29 | debt -= amortised | ||
| 30 | |||
| 31 | total_interest_pct = total_interest / principal * 100 | ||
| 32 | |||
| 33 | table.append(SEPARATING_LINE) | ||
| 34 | table.append(["Total", "", "Amount Paid", "Interest Paid", "Amount Amortised"]) | ||
| 35 | table.append(["", ""] + [ | ||
| 36 | fmt(monthly_installment * n), | ||
| 37 | fmt(total_interest) + f" ({fmt(total_interest_pct)}%)", | ||
| 38 | fmt(total_amortised)]) | ||
| 39 | return table | ||
| 40 | |||
| 41 | |||
| 42 | def main(): | ||
| 43 | parser = argparse.ArgumentParser() | ||
| 44 | parser.add_argument("-p", "--principal", type=float, default=100000, help="Principal") | ||
| 45 | parser.add_argument("-i", "--interest", type=float, default=5, help="Annual interest %") | ||
| 46 | parser.add_argument("-n", "--installments", type=int, default=300, help="Number of installments") | ||
| 47 | args = parser.parse_args() | ||
| 48 | |||
| 49 | table = calc_payments(args.principal, args.interest, args.installments) | ||
| 50 | print(tabulate(table)) | ||
| 51 | return 0 | ||
| 52 | |||
| 53 | |||
| 54 | if __name__ == "__main__": | ||
| 55 | sys.exit(main()) | ||
diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a5d5159 --- /dev/null +++ b/requirements.txt | |||
| @@ -0,0 +1 @@ | |||
| tabulate | |||
