package org.geogebra.common.kernel.advanced;
import java.util.ArrayList;
import org.apache.commons.math3.analysis.solvers.BrentSolver;
import org.apache.commons.math3.analysis.solvers.NewtonSolver;
import org.geogebra.common.kernel.Construction;
import org.geogebra.common.kernel.Kernel;
import org.geogebra.common.kernel.algos.AlgoElement;
import org.geogebra.common.kernel.algos.AlgoRootNewton;
import org.geogebra.common.kernel.commands.Commands;
import org.geogebra.common.kernel.geos.GeoElement;
import org.geogebra.common.kernel.geos.GeoNumeric;
import org.geogebra.common.util.debug.Log;
/**
* Computes values corresponding to Excel's financial functions Rate, Nper, PMT,
* PV and FV for problems involving compound interest with periodic payments.
* Results are found by solving this fundamental formula: <br>
* <br>
*
* pmt * (1 + rate * pmtType) * ((1 + rate)^n - 1) / (rate) + pv * (1 + rate)^n
* + fv = 0
*
* https://support.office.com/en-us/article/PV-function-23879d31-0e02-4321-be01-
* da16e8168cbd
*
* <br>
* If rate is 0, then: (pmt * nper) + pv + fv = 0 <br>
* <br>
* rate = interest rate for a compounding period <br>
* n = number of periods (nper) <br>
* pmt = payment amount made each period <br>
* pv = present value <br>
* fv = future value <br>
* pmtType specifies whether payments are due at the beginning (pmtType = 0) or
* end of each period (pmtType = 1). <br>
* <br>
* The formula (from Excel's documentation) is solved directly for the values
* pmt, nper, pv and fv. Rate cannot be found directly and is found instead by
* an iterative method.
*
* Also see Appendix E Formulas Used p251 hp 12c platinum financial calculator
* User Guide http://h10032.www1.hp.com/ctg/Manual/bpia5184.pdf
*
* @author G. Sturr
*
*/
public class AlgoFinancial extends AlgoElement {
public enum CalculationType {
RATE, NPER, PMT, PV, FV
}
// input
private GeoNumeric geoRate, geoNper, geoPmt, geoPV, geoFV, geoPmtType,
geoGuess;
// output
private GeoNumeric result;
// compute
private CalculationType calcType;
private double rate, nper, pmt, pv, fv, pmtType, guess;
/**
* @param cons
* @param label
* @param rate
* @param nper
* @param pmt
* @param pv
* @param fv
* @param pmtType
* @param calcType
*/
public AlgoFinancial(Construction cons, String label, GeoNumeric rate,
GeoNumeric nper, GeoNumeric pmt, GeoNumeric pv, GeoNumeric fv,
GeoNumeric pmtType, CalculationType calcType) {
this(cons, label, rate, nper, pmt, pv, fv, pmtType, null, calcType);
}
/**
* @param cons
* @param label
* @param rate
* @param nper
* @param pmt
* @param pv
* @param fv
* @param pmtType
* @param guess
* @param calcType
*/
public AlgoFinancial(Construction cons, String label, GeoNumeric rate,
GeoNumeric nper, GeoNumeric pmt, GeoNumeric pv, GeoNumeric fv,
GeoNumeric pmtType, GeoNumeric guess, CalculationType calcType) {
super(cons);
this.geoRate = rate;
this.geoNper = nper;
this.geoPmt = pmt;
this.geoPV = pv;
this.geoFV = fv;
this.geoPmtType = pmtType;
this.geoGuess = guess;
this.calcType = calcType;
result = new GeoNumeric(cons);
setInputOutput();
compute();
result.setLabel(label);
}
@Override
public Commands getClassName() {
switch (calcType) {
case RATE:
return Commands.Rate;
case NPER:
return Commands.Periods;
case PMT:
return Commands.Payment;
case PV:
return Commands.PresentValue;
case FV:
return Commands.FutureValue;
default:
return Commands.Rate;
}
}
// for AlgoElement
@Override
protected void setInputOutput() {
ArrayList<GeoElement> tempList = new ArrayList<GeoElement>();
if (geoRate != null) {
tempList.add(geoRate);
}
if (geoNper != null) {
tempList.add(geoNper);
}
if (geoPmt != null) {
tempList.add(geoPmt);
}
if (geoPV != null) {
tempList.add(geoPV);
}
if (geoFV != null) {
tempList.add(geoFV);
}
if (geoPmtType != null) {
tempList.add(geoPmtType);
}
if (geoGuess != null) {
tempList.add(geoGuess);
}
input = new GeoElement[tempList.size()];
input = tempList.toArray(input);
setOutputLength(1);
setOutput(0, result);
setDependencies(); // done by AlgoElement
}
public GeoNumeric getResult() {
return result;
}
@Override
public final void compute() {
switch (calcType) {
case RATE:
if (!(setNper() && setPmt() && setPV() && setFV() && setPmtType()
&& setGuess())) {
result.setUndefined();
return;
}
if (computeRate()) {
result.setValue(rate);
} else {
result.setUndefined();
}
break;
case NPER:
if (!(setRate() && setPmt() && setPV() && setFV()
&& setPmtType())) {
result.setUndefined();
return;
}
if (Kernel.isZero(rate)) {
nper = Kernel.checkInteger(-(pv + fv) / pmt);
} else {
double pmt2 = pmt * (1 + rate * pmtType);
nper = Kernel.checkInteger(
Math.log((pmt2 - rate * fv) / (pmt2 + rate * pv))
/ Math.log(1 + rate));
}
if (nper <= 0) {
nper = Double.NaN;
}
result.setValue(nper);
break;
case PMT:
if (!(setRate() && setNper() && setPV() && setFV()
&& setPmtType())) {
result.setUndefined();
return;
}
if (rate == 0) {
pmt = -(pv + fv) / nper;
} else {
pmt = (-fv - pv * Math.pow(1 + rate, nper)) / pmtFactor();
}
result.setValue(pmt);
break;
case PV:
if (!(setRate() && setNper() && setPmt() && setFV()
&& setPmtType())) {
result.setUndefined();
return;
}
if (rate == 0) {
pv = -pmt * nper - fv;
} else {
pv = (-fv - pmt * pmtFactor()) / Math.pow(1 + rate, nper);
}
result.setValue(pv);
break;
case FV:
if (!(setRate() && setNper() && setPmt() && setPV()
&& setPmtType())) {
result.setUndefined();
return;
}
if (rate == 0) {
fv = -pmt * nper - pv;
} else {
fv = -pmt * pmtFactor() - pv * Math.pow(1 + rate, nper);
}
result.setValue(fv);
break;
}
}
// ================================================
// Utility functions for compute
// ================================================
private double pmtFactor() {
return (1 + rate * pmtType) * (Math.pow(1 + rate, nper) - 1) / (rate);
}
/**
* Uses Brent's method to find rate then Newton to polish the root
*
* adapted from AlgoRootInterval
*
* @return true if calculation successful
*/
private boolean computeRate() {
BrentSolver rootFinder = new BrentSolver();
NewtonSolver rootPolisher = new NewtonSolver();
RateFunction fun = new RateFunction(nper, pv, fv, pmt, pmtType);
double min = 0;
double max = 0.001;
double newtonRoot = Double.NaN;
if (geoGuess != null) {
rate = geoGuess.getValue();
double dx = 0.001;
// look for sign-change around interval for Brent
// within [-1,1]
min = Math.max(-1, rate - dx);
max = Math.min(1, rate + dx);
double minSign = Math.signum(value(fun, min));
double maxSign = Math.signum(value(fun, max));
// sensible bound on rate (between 0 and 1)
while (minSign == maxSign && dx < 1) {
dx *= 2;
min = Math.max(0, rate - dx);
max = Math.min(1, rate + dx);
minSign = Math.signum(value(fun, min));
maxSign = Math.signum(value(fun, max));
}
} else {
// default starting value if Brent fails
rate = 0.1;
// quick and dirty look for interval with sign change
double minSign = Math.signum(value(fun, min));
double maxSign = Math.signum(value(fun, max));
// sensible bound on rate (1)
while (minSign == maxSign && max < 1) {
max += 0.05;
maxSign = Math.signum(value(fun, max));
}
}
// Brent's method (Apache 2.2)
try {
// App.error("min = " + min + " max = " + max);
rate = rootFinder.solve(AlgoRootNewton.MAX_ITERATIONS, fun, min,
max);
// App.error("brent rate = " + rate);
} catch (Exception e) {
// we will still try Newton in this case
Log.debug("problem with Brent Solver" + e.getMessage());
}
if (Kernel.isEqual(rate, 1) || Double.isInfinite(rate)
|| Double.isNaN(rate)) {
rate = 0.1;
}
try {
// Log.debug("trying Newton with starting value " + rate);
newtonRoot = rootPolisher.solve(AlgoRootNewton.MAX_ITERATIONS, fun,
min, max, rate);
if (Math.abs(fun.value(newtonRoot)) < Math.abs(fun.value(rate))) {
// App.error("polished result from Newton is better: \n" + rate
// + "\n" + newtonRoot);
rate = newtonRoot;
}
} catch (Exception e) {
Log.debug("problem with Newton: " + e.getMessage());
return false;
}
return true;
}
// =============================================
// Test/set parameter values
// =============================================
private static double value(RateFunction fun, double min) {
try {
return fun.value(min);
} catch (RuntimeException e) {
// catches ArithmeticException, IllegalStateException and
// ArithmeticException
return Double.NaN;
}
}
private boolean setRate() {
if (geoRate == null || !geoRate.isDefined()) {
return false;
}
rate = geoRate.evaluateDouble();
return !Double.isNaN(rate);
}
private boolean setNper() {
if (geoNper == null || !geoNper.isDefined()) {
return false;
}
nper = Kernel.checkInteger(geoNper.evaluateDouble());
// number of periods must be positive
// check for NaN not needed as NaN > 0 returns false
return nper > 0;
}
private boolean setPmt() {
if (geoPmt == null || !geoPmt.isDefined()) {
return false;
}
pmt = geoPmt.evaluateDouble();
return !Double.isNaN(pmt);
}
private boolean setPV() {
if (geoPV == null) {
if (calcType == CalculationType.FV) {
pv = 0;
return true;
}
return false;
}
if (geoPV.isDefined()) {
pv = geoPV.evaluateDouble();
return !Double.isNaN(pv);
}
return false;
}
private boolean setFV() {
if (geoFV == null) {
fv = 0;
return true;
}
if (geoFV.isDefined()) {
fv = geoFV.evaluateDouble();
return !Double.isNaN(fv);
}
return false;
}
private boolean setPmtType() {
if (geoPmtType == null) {
pmtType = 0;
return true;
}
if (geoPmtType.isDefined()) {
if (geoPmtType.getDouble() == 1 || geoPmtType.getDouble() == 0) {
pmtType = geoPmtType.getDouble();
return true;
}
}
return false;
}
private boolean setGuess() {
if (geoGuess == null) {
guess = 0.1;
return true;
}
if (geoGuess.isDefined()) {
guess = geoGuess.evaluateDouble();
return !Double.isNaN(guess);
}
return false;
}
}