// Emacs settings: -*- mode: Fundamental; tab-width: 4; -*-

////////////////////////////////////////////////////////////////////////////
//                                                                        //
// Financial Calculator                                                   //
//                                                                        //
// Copyright (c) 2010, Andrew Birrell                                     //
//                                                                        //
////////////////////////////////////////////////////////////////////////////


//
// Display
//

var stack = new Array();

function numFromId(id) {
	var value = document.getElementById(id).value;
	if (value == "") return "";
	if (!value.match(/^\s*-?\s*[0-9,]*(\.[0-9]*)?(e[+-]?[0-9]+)?\s*$/i)) {
		return parseInt('a');
	}
	value = value.replace(/,/g, "");
	return parseFloat(value); // might deliver NaN
}

function showValue(id, s) {
	// Display string in given spot
	document.getElementById(id).innerHTML = s;
}

function showNumber(id, num, places) {
	// Show "num", rounded to given number of places, if possible
	var res;
	if (isNaN(num)) {
		res = "Undefined";
	} else if (!isFinite(num)) {
		res = "Not possible";
	} else {
		res = num.toFixed(places);
	}
	showValue(id, res);
}

function push(num) {
	// If "num" is a normal number, not equal to top of stack, push it.
	// In any case, return "num".
	if (!isNaN(num) && isFinite(num) &&
			(stack.length == 0 || num != stack[0])) {
		stack.unshift(num);
		if (stack.length > 15) stack.pop();
		var scratch = "";
		for (var x = 0; x < stack.length; x++) {
			if (x > 0) scratch += "<br>\n";
			scratch += stack[x].toFixed(6);
		}
		document.getElementById("scratchpad").innerHTML = scratch;
	}
	return num;
}


//
// Calculations
//

// Using notation:
//
//   pv = present (initial) value
//   i  = interest rate, as fraction (10% = 0.1)
//   y  = periodic payment
//   n  = number of periods
//   fv = future (final) value
//   r  = 1 + i;
//
// The basic formula is:
//
//   fv = pv * r^n + y * (1 + r + r^2 + r^3 + ... + r^(n-1))
//      = pv * r^n + y * (r^n - 1) / i (iff "i" is non-zero)
//      = (pv + y/i) * r^n - y/i
//
// Simple algebra gives:
//
//   pv = (fv - y * (r^n - 1) / i) / r^n
//
//   y  = (fv - pv * r^n) * i / (r^n - 1)
//
//   r^n = (fv + y/i) / (pv + y/i)
//   n  = log((fv + y/i) / (pv + y/i)) / log(r)
//
// As i --> 0 then (r^n-1)/i --> n and r^n --> 1, so:
//
//   fv = pv + y * n
//   pv = fv - y * n
//   y  = (fv - pv) / n
//   n  = (fv - pv) / y
//
// I know of no closed-form formula for computing "i".
//
// Note that the calculations below become very inaccurate if "i" is very
// close to zero.  The UI prevents that happening.

function pvCalc(i, y, n, fv) {
	var rn = Math.pow(1 + i, n);
	var rni = (i == 0 ? n : (rn - 1) / i);
	return (fv - y * rni) / rn;
}

function iCalc(pv, y, n, fv) {
	// Compute requisite interest rate, using the bisection method.
	// Convergence is slower than, say, Newton-Raphson, but more reliable.
	if (n == 0) return parseInt("a");
	var high = 1; // "i" that produces result > fv
	var low = -1; // "i" that produces result < fv
	for (;;) {
		var highFV = fvCalc(pv, high, y, n);
		var lowFV = fvCalc(pv, low, y, n);
		if (highFV > fv && lowFV < fv) {
			break;
		} else if (highFV < fv && lowFV > fv) {
			var foo = high; high = low; low = foo;
			break;
		} else {
			high = high * 2;
			low = low * 2;
			if (!isFinite(high) || !isFinite(low)) return 1/0;
		}
	}
	for (var x = 0; x < 100; x++) {
		var i = (high + low) / 2;
		if (Math.abs(high - low) < 0.000000001) return i;
		var cur = fvCalc(pv, i, y, n);
		if (cur > fv) high = i; else low = i;
	}
	return 1/0;
}

function yCalc(pv, i, n, fv) {
	var rn = Math.pow(1 + i, n);
	var rni = (i == 0 ? n : (rn - 1) / i);
	return (fv - pv * rn) / rni;
}

function nCalc(pv, i, y, fv) {
	var r = 1 + i;
	if (i==0) return (fv - pv) / y;
	var n = Math.log((fv + y / i) / (pv + y/i)) / Math.log(r);
	return (n < 0 ? 1/0 : n);
}

function fvCalc(pv, i, y, n) {
	var rn = Math.pow(1 + i, n);
	var rni = (i == 0 ? n : (rn - 1) / i);
	return pv * rn + y * rni;
}


//
// Main program
//

function compute() {
	var pv = numFromId('initial');
	var pct = numFromId('interest');
	var i = (typeof(pct) == "number" ? pct / 100 : pct);
	var y = numFromId('payment');
	var n = numFromId('periods');
	var fv = numFromId('final');
	var blanks = 0;
	if (pv === "") blanks++;
	if (i === "") blanks++;
	if (y === "") blanks++;
	if (n === "") blanks++;
	if (fv === "") blanks++;
	showValue('resInitial', "&nbsp;");
	showValue('resInterest', "");
	showValue('resPayment', "");
	showValue('resPeriods', "");
	showValue('resFinal', "");
	if (blanks == 0) {
		alert("Too many inputs");
	} else if (blanks > 1) {
		alert("Too few inputs");
	} else if (isNaN(pv) || isNaN(i) || isNaN(y) || isNaN(n) || isNaN(fv)) {
		alert("Inputs should be numbers");
	} else if (typeof(pct) == "number" && Math.abs(pct) < 0.000000001 &&
					pct != 0) {
		alert("Interest rate is too small: use 0 if you want");
	} else if (typeof(n) == 'number' && n < 0) {
		alert("\"Periods\" should be positive");
	} else if (pv === "") {
		showNumber('resInitial', push(pvCalc(i, y, n, fv)), 2);
	} else if (i === "") {
		showNumber('resInterest', push(iCalc(pv, y, n, fv) * 100), 2);
	} else if (y === "") {
		showNumber('resPayment', push(yCalc(pv, i, n, fv)), 2);
	} else if (n === "") {
		showNumber('resPeriods', push(nCalc(pv, i, y, fv)), 2);
	} else if (fv === "") {
		showNumber('resFinal', push(fvCalc(pv, i, y, n)), 2);
	}
	return false;
}


//
// Initialization
//

function init() {
	document.getElementById("initial").focus();
}

