// Financial Calculator
// based on work from http://www.arachnoid.com/lutusp/finance.html


//Gets a value from a form field and cleans it up into a floating point number
//
//Params:
//form - the form the field is in
//field - the name of the field to grab the info from
function getValueFloat( form, field ){
	var value = eval( "form." + field + ".value" );
	
	//remove non-digit non decimal characters
	value = value.replace( /[^-\d.]/g, "" );
	
	//check to see if we have a valid number
	var pattern = /^-?\d*(\.\d+)?$/;
	if( !pattern.test( value ) ){
		alert( fieldNames[field] + " is not a valid number" );
		return null;
	} else if( value == "" ){
		return null;
	}
	
	return parseFloat( value );
}


//Set a floating point value into a form field with a specified number of
//decimal places
//
//Params:
//form - the form the field is in
//field - the name of the field to put the value into
//value - the value to put in the field
//decimals - the number of decimal places to round to
function setValueFloat( form, field, value, decimals ){
	//Round number
	var factor = Math.pow(10, decimals); 
	value *= factor;
	value = Math.round(value);
	value /= factor;
	
	//Set field
	eval( "form." + field + ".value = value" );
}

//Set an integer value into a form field with a specified number of
//decimal places
//
//Params:
//form - the form the field is in
//field - the name of the field to put the value into
//value - the value to put in the field
function setValueInteger( form, field, value ){
	//Round number
	value = Math.round(value);
	
	//Set field
	eval( "form." + field + ".value = value" );
}

//Set floating point value into a form field with a US dollar format
//
//Params:
//form - the form the field is in
//field - the name of the field to put the value into
//value - the value to put in the field
function setValueCurrency( form, field, value ){
	//Round number
	value *= 100;
	value = Math.round(value);
	value /= 100;
	
	//Add commas
	value = value.toString();
	var pos = value.indexOf(".");
	if( pos == -1 ){
		pos = value.length;
	}
	
	// To prevent commas being placed after a -
	var stop = 0;
	if( value.indexOf("-") != -1 ){
		stop = 1;
	}
	
	for( var i = pos - 3; i > stop; i -= 3 ){
		value = value.substr(0, i) + "," + value.substr(i);
	}
	
	//Make sure we have 2 decimal places
	pos = value.indexOf(".");
	if( pos == -1 ){
		value += ".00";
	} else if( pos == value.length - 2 ){
		value += "0";
	}
	
	//Set field
	eval( "form." + field + ".value = value" );
}

// Fills in the missing 5th field or the default field of a given
// financial calc form
//
// Params:
// form - the form to complete
function completeForm( form ){
	var pv, fv, np, pmt, ir;
	
	var blank = "";	//The field we will be calculating the value for
	var err = false; //Indicates more than one field is invalid/empty
	
	//Get value of pv from the form, if this is null then it is
	//invalid/empty
	if( ( pv = getValueFloat( form, "pv" ) ) == null ){
		blank = "pv";
	}

	if( negateFields['pv'] ){
		pv *= -1;
	}

	//Get value of fv from the form, if this is null then it is
	//invalid/empty
	if( ( fv = getValueFloat( form, "fv" ) ) == null ){
		//If blank is already set then we have more than one blank/invalid
		//field and can not calculate anything
		if( blank != "" ){
			err = true;
		} else {
			blank = "fv";
		}
	}
	
	//Negate or no?
	if( negateFields['fv'] ){
		fv *= -1;
	}
	
	//Get value of np from the form, if this is null then it is
	//invalid/empty
	if( ( np = getValueFloat( form, "np" ) ) == null ){
		//If blank is already set then we have more than one blank/invalid
		//field and can not calculate anything
		if( blank != "" ){
			err = true;
		} else {
			blank = "np";
		}
	}

	//Negate or no?
	if( negateFields['np'] ){
		np *= -1;
	}
	
	//Get value of pmt from the form, if this is null then it is
	//invalid/empty
	if( ( pmt = getValueFloat( form, "pmt" ) ) == null ){
		//If blank is already set then we have more than one blank/invalid
		//field and can not calculate anything
		if( blank != "" ){
			err = true;
		} else {
			blank = "pmt";
		}
	}
	
	//Negate or no?
	if( negateFields['pmt'] ){
		pmt *= -1;
	}
	
	//Get value of ir from the form, if this is null then it is
	//invalid/empty
	if( ( ir = getValueFloat( form, "ir" ) ) == null ){
		//If blank is already set then we have more than one blank/invalid
		//field and can not calculate anything
		if( blank != "" ){
			err = true;
		} else {
			blank = "ir";
		}
	}
	
	//Negate or no?
	if( negateFields['ir'] ){
		ir *= -1;
	}
	
	if( err ){
		alert("You may only leave one field blank!");
	} else {
		//If all fields are filled in, use the default blank
		if( blank == "" ){
			blank = default_blank;
		}
		
		//Convert ir into a use able format for the equation
		ir /= 12.0;
		ir /= 100;
		
		
		if( blank == "pv" ){
			//Solve for pv
			value = calcPV( fv, np, pmt, ir );
			
			//Negate or no?
			if( negateFields['pv'] ){
				value *= -1
			}
			
			//update the form element
			setValueCurrency( form, "pv", value );
		} else if( blank == "fv" ){
			//Solve for fv
			value = calcFV( pv, np, pmt, ir );
			
			//Negate or no?
			if( negateFields['fv'] ){
				value *= -1
			}
			
			//update the form element
			setValueCurrency( form, "fv", value );
		} else if( blank == "np" ){
			//Solve for np
			value = calcNP( pv, fv, pmt, ir );
			
			//Negate or no?
			if( negateFields['np'] ){
				value *= -1
			}
			
			//update the form element
			setValueFloat( form, "np", value, 2 );
		} else if( blank == "pmt" ){
			//Solve for pmt
			value = calcPMT( pv, fv, np, ir );
			
			//Negate or no?
			if( negateFields['pmt'] ){
				value *= -1
			}
			
			//update the form element
			setValueCurrency( form, "pmt", value );
		} else if( blank == "ir" ){
			//Solve for ir
			value = calcIR( pv, fv, np, pmt );

			//Negate or no?
			if( negateFields['ir'] ){
				value *= -1
			}
			
			//update the form element
			setValueFloat( form, "ir", value, 3 );
		}
	}
}

// Calculates the present value
//
// Params:
// fv - future value
// np - number of payments
// pmt - payment amount
// ir - interest rate
function calcPV( fv, np, pmt, ir ){
	if( ir == 0 ){
		value = -( fv + ( pmt * np ) );
	} else {
		value = ( Math.pow( 1 + ir, -np ) * ( - pmt * Math.pow( 1 + ir, np ) - fv * ir + pmt ) ) / ir;
	}
	
	return value;
}

// Calculates the future value
//
// Params:
// pv - present value
// np - number of payments
// pmt - payment amount
// ir - interest rate
function calcFV( pv, np, pmt, ir ){
	if( ir == 0 ){
		value = -( pv + ( pmt * np ) );
	} else {
		value = ( pmt - Math.pow( 1 + ir, np ) * (pmt + ir * pv ) ) / ir;
	}
	
	return value;
}

// Calculates the number of payments
//
// Params:
// pv - present value
// fv - future value
// pmt - payment amount
// ir - interest rate
function calcNP( pv, fv, pmt, ir){
	if( ir == 0 ){
		if( pmt == 0 ){
			alert( "When calculating the " + fieldNames['np'] + " the " +
			fieldNames['pmt'] + " and " +  fieldNames['ir'] + " cannot both" +
				"be 0" );
		} else {
			value = -( ( fv + pv ) / pmt );
		}
	} else {
		value = Math.log( (pmt - fv * ir ) / ( pmt + pv * ir ) ) / 
			Math.log( ir + 1 );
	}
	
	return value;
}

// Calculates the payment amount
//
// Params:
// pv - present value
// fv - future value
// np - number of payments
// ir - interest rate
function calcPMT( pv, fv, np, ir ){
	if( ir == 0 ){
		if( np == 0 ){
			alert( "When calculating the " + fieldNames['pmt'] + " the " +
				fieldNames['np'] + " and " +  fieldNames['ir'] + " cannot both" +
				"be 0" );
		} else {
			value = -( ( fv + pv ) / np );
		}
	} else {
		value = ( ir * ( -pv * Math.pow( ir + 1, np ) + -fv ) ) / 
			( Math.pow( ir + 1, np ) - 1 );
	}
	
	return value;
}

// Calculates the interest rate
//
// Params:
// pv - present value
// fv - future value
// np - number of payments
// pmt - payment amount
function calcIR( pv, fv, np, pmt ){
	return realCalcIR( pv, fv, np, pmt, false );
}

// Calculates the interest rate going from either 0 to 1 or -1 to 0
// if you are not going in reverse and the interest rate is not found,
// it will then try again going in reverse
//
// Params:
// pv - present value
// fv - future value
// np - number of payments
// pmt - payment amount
// rev - are we going in reverse? (ie -1 to 0)
function realCalcIR( pv, fv, np, pmt, rev ){
	var p_err = .001; // The tolerance value. If a guess interest rate
										// gives us a value with a percent error less than
										// this it is good enough for us and we'll use it
										
	var i = 0; // number of iterations through the loop
	var low, high, high_err, low_err;


	// Modified binary search.
	if( rev ){
		low = -1;
		high = 0;
	}else{
		low = 0;
		high = 1;
	}

	while( i < 200 ){ //prevents a possible infinite loop
		// Make guess the average of high and low
		guess = ( low + high ) / 2;
		
		// If fv is zero we need to use pv instead to avoid division by zero in
		// the percent error calculation
		if( fv != 0 ){
			if( Math.abs( ( calcFV( pv, np, pmt, guess / 12  ) - fv ) / fv ) <= p_err ){
				//If the percent error on the guess is less than the tolerance return it
				return guess * 100;
			}
		} else {
		  if( Math.abs( ( calcPV( fv, np, pmt, guess / 12 ) - pv ) / pv ) <= p_err ){
				//If the percent error on the guess is less than the tolerance return it
				return guess * 100;
			}
		}
		
		// Calculate the percent error for high and low so we can decide which
		// way we need to move. If fv is zero we use pv for the same reason stated
		// above
		if( fv != 0 ){
			high_err = Math.abs( ( calcFV( pv, np, pmt, high / 12 ) - fv ) / fv );
			low_err =  Math.abs( ( calcFV( pv, np, pmt, low / 12 ) - fv ) / fv );
		} else {
			high_err = Math.abs( ( calcPV( fv, np, pmt, high / 12 ) - pv ) / pv );
			low_err =  Math.abs( ( calcPV( fv, np, pmt, low / 12 ) - pv ) / pv );				
		}
		
		// Since we already have the % error for high and low why not check if the
		// are valid answers?
		if( high_err < p_err ){
			return high * 100;
		} else if( low_err < p_err ){
			return low * 100;
		// This is one of the biggest differences from a normal binary search
		// Instead of changing high or low to guess we are using the average 
		// instead. Without this sometimes the algorithm will miss the right
		// value. This fact makes me wonder how optimal this method is, but for
		// the most part it works!
		} else if( high_err < low_err ){
			low = ( low + guess ) / 2;
		} else {
			high = ( high + guess ) / 2;
		}
		
		i++;
	}
	
	//If we are here we didn't find a value
	if( !rev ){
		//If we are not going in reverse try it before giving up
		return realCalcIR( pv, fv, np, pmt, true );
	}else{
		//Throw in the towel!
		alert( "Could not calculate interest" );
	}
}
