diff --git a/poi/src/main/java/org/apache/poi/ss/formula/functions/Rate.java b/poi/src/main/java/org/apache/poi/ss/formula/functions/Rate.java index 90c3684cde..70905b4edd 100644 --- a/poi/src/main/java/org/apache/poi/ss/formula/functions/Rate.java +++ b/poi/src/main/java/org/apache/poi/ss/formula/functions/Rate.java @@ -73,42 +73,49 @@ public class Rate implements Function { return new NumberEval(rate); } - private double calculateRate(double nper, double pmt, double pv, double fv, double type, double guess) { - //FROM MS http://office.microsoft.com/en-us/excel-help/rate-HP005209232.aspx - int FINANCIAL_MAX_ITERATIONS = 20;//Bet accuracy with 128 - double FINANCIAL_PRECISION = 0.0000001;//1.0e-8 + private static double _g_div_gp(double r, double n, double p, double x, double y, double w) { + double t1 = Math.pow(r+1, n); + double t2 = Math.pow(r+1, n-1); + return (y + t1*x + p*(t1 - 1)*(r*w + 1)/r) / + (n*t2*x - p*(t1 - 1)*(r*w + 1)/(Math.pow(r, 2) + n*p*t2*(r*w + 1)/r + + p*(t1 - 1)*w/r)); + } + + /** + * Compute the rate of interest per period. + * + * The implementation was ported from the NumPy library, + * see https://github.com/numpy/numpy-financial/blob/d02edfb65dcdf23bd571c2cded7fcd4a0528c6af/numpy_financial/_financial.py#L602 + * + * + * @param nper Number of compounding periods + * @param pmt Payment + * @param pv Present Value + * @param fv Future value + * @param type When payments are due ('begin' (1) or 'end' (0)) + * @param guess Starting guess for solving the rate of interest + * @return rate of interest per period or NaN if the solution didn't converge + */ + static double calculateRate(double nper, double pmt, double pv, double fv, double type, double guess){ + double tol = 1e-8; + double maxiter = 100; + + double rn = guess; + int iter = 0; + boolean close = false; + while (iter < maxiter && !close){ + double rnp1 = rn - _g_div_gp(rn, nper, pmt, pv, fv, type); + double diff = Math.abs(rnp1 - rn); + close = diff < tol; + iter += 1; + rn = rnp1; - double y, y0, y1, x0, x1, f = 0, i; - double rate = guess; - if (Math.abs(rate) < FINANCIAL_PRECISION) { - y = pv * (1 + nper * rate) + pmt * (1 + rate * type) * nper + fv; - } else { - f = Math.pow(1 + rate, nper); - y = pv * f + pmt * (1 / rate + type) * (f - 1) + fv; } - y0 = pv + pmt * nper + fv; - y1 = pv * f + pmt * (1 / rate + type) * (f - 1) + fv; - - // find root by Newton secant method - i = x0 = 0.0; - x1 = rate; - while ((Math.abs(y0 - y1) > FINANCIAL_PRECISION) && (i < FINANCIAL_MAX_ITERATIONS)) { - rate = (y1 * x0 - y0 * x1) / (y1 - y0); - x0 = x1; - x1 = rate; - - if (Math.abs(rate) < FINANCIAL_PRECISION) { - y = pv * (1 + nper * rate) + pmt * (1 + rate * type) * nper + fv; - } else { - f = Math.exp(nper * Math.log(1 + rate)); - y = pv * f + pmt * (1 / rate + type) * (f - 1) + fv; - } - - y0 = y1; - y1 = y; - ++i; + if(!close) + return Double.NaN; + else { + return rn; } - return rate; } /** diff --git a/poi/src/test/java/org/apache/poi/ss/formula/functions/TestRate.java b/poi/src/test/java/org/apache/poi/ss/formula/functions/TestRate.java index 3e7bdbfe35..945b044904 100644 --- a/poi/src/test/java/org/apache/poi/ss/formula/functions/TestRate.java +++ b/poi/src/test/java/org/apache/poi/ss/formula/functions/TestRate.java @@ -24,11 +24,13 @@ import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; import org.apache.poi.hssf.usermodel.HSSFRow; import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.FormulaError; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.apache.poi.ss.util.Utils.addRow; import static org.apache.poi.ss.util.Utils.assertDouble; +import static org.apache.poi.ss.util.Utils.assertError; /** * Test cases for RATE() @@ -51,7 +53,110 @@ final class TestRate { } } + // https://wiki.documentfoundation.org/Documentation/Calc_Functions/RATE + @Test + void testLibreOfficeExample1() throws Exception { + try (HSSFWorkbook wb = new HSSFWorkbook()) { + HSSFSheet sheet = wb.createSheet(); + HSSFRow row = sheet.createRow(0); + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + HSSFCell cell = row.createCell(0); + assertDouble(fe, cell, "RATE(3,-10,900,1,0,0.5)", -0.7634, 0.0001); + } + } + + // https://wiki.documentfoundation.org/Documentation/Calc_Functions/RATE + @Test + void testLibreOfficeExample2() throws Exception { + try (HSSFWorkbook wb = new HSSFWorkbook()) { + HSSFSheet sheet = wb.createSheet(); + HSSFRow row = sheet.createRow(0); + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + HSSFCell cell = row.createCell(0); + assertDouble(fe, cell, "RATE(3,-10,900)", -0.7563, 0.0001); + } + } + + /** + * LibreOffice and Excel return #!NUM, but POI and NumPy return a result + */ + // https://wiki.documentfoundation.org/Documentation/Calc_Functions/RATE @Disabled("test fails - see related issue") + @Test + void testLibreOfficeInfeasibleSolution() throws Exception { + try (HSSFWorkbook wb = new HSSFWorkbook()) { + HSSFSheet sheet = wb.createSheet(); + HSSFRow row = sheet.createRow(0); + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + HSSFCell cell = row.createCell(0); + assertDouble(fe, cell, "RATE(3,10,900,1,0,0.5)", -0.7634, 0.0001); + } + } + + /** + * See https://github.com/numpy/numpy-financial/blob/d02edfb65dcdf23bd571c2cded7fcd4a0528c6af/numpy_financial/tests/test_financial.py#L126 + */ + @Test + void testNumPyExample1() throws Exception { + double[] expected = {-0.39920185, -0.02305873, -0.41818459, 0.26513414}; + double nper = 2; + double pmt = 0; + double[] pv = {-593.06, -4725.38, -662.05, -428.78}; + double[] fv = {214.07, 4509.97, 224.11, 686.29}; + try (HSSFWorkbook wb = new HSSFWorkbook()) { + HSSFSheet sheet = wb.createSheet(); + HSSFRow row = sheet.createRow(0); + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + for(int i = 0; i < pv.length; i++){ + String fmla = String.format("RATE(%2f, %2f, %2f, %2f, 0, 0.1)", nper, pmt, pv[i], fv[i]); + HSSFCell cell = row.createCell(i); + assertDouble(fe, cell, fmla, expected[i], 1e-8); + } + } + } + + @Test + void testNumPyExample2() throws Exception { + try (HSSFWorkbook wb = new HSSFWorkbook()) { + HSSFSheet sheet = wb.createSheet(); + HSSFRow row = sheet.createRow(0); + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + HSSFCell cell = row.createCell(0); + assertDouble(fe, cell, "RATE(10, 0, -3500, 10000.0, 0, 0.1)", 0.1106908537142689284704528100, 1E-6); + } + } + + /** + * RATE will return NaN, if the Newton Raphson method cannot find a + * feasible rate within the required tolerance or number of iterations. + * This can occur if both `pmt` and `pv` have the same sign, as it is + * impossible to repay a loan by making further withdrawls. + * + * See https://github.com/numpy/numpy-financial/blob/d02edfb65dcdf23bd571c2cded7fcd4a0528c6af/numpy_financial/tests/test_financial.py#L113 + */ + @Test + void testNumPyInfeasibleSolution1() throws Exception { + try (HSSFWorkbook wb = new HSSFWorkbook()) { + HSSFSheet sheet = wb.createSheet(); + HSSFRow row = sheet.createRow(0); + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + HSSFCell cell = row.createCell(0); + assertError(fe, cell, "RATE(12, 400, 10000, 5000.0, 0, 0.1)", FormulaError.NUM); + } + } + + // https://github.com/numpy/numpy-financial/blob/d02edfb65dcdf23bd571c2cded7fcd4a0528c6af/numpy_financial/tests/test_financial.py#L126 + @Test + void testNumPyInfeasibleSolution2() throws Exception { + try (HSSFWorkbook wb = new HSSFWorkbook()) { + HSSFSheet sheet = wb.createSheet(); + HSSFRow row = sheet.createRow(0); + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + HSSFCell cell = row.createCell(0); + assertError(fe, cell, "RATE(2, 0, -13.65, -329.67, 0, 0.1)", FormulaError.NUM); + } + } + @Test void testBug65988() throws Exception { try (HSSFWorkbook wb = new HSSFWorkbook()) {