diff --git a/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java b/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java index 9be8a72fb..a343d759b 100644 --- a/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java +++ b/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java @@ -110,6 +110,8 @@ public final class FunctionEval { retval[59] = FinanceFunction.PMT; retval[60] = new Rate(); + retval[61] = new Mirr(); + retval[62] = new Irr(); retval[63] = NumericFunction.RAND; retval[64] = new Match(); diff --git a/src/java/org/apache/poi/ss/formula/functions/Mirr.java b/src/java/org/apache/poi/ss/formula/functions/Mirr.java new file mode 100644 index 000000000..aa9ec5892 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/functions/Mirr.java @@ -0,0 +1,109 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + + +package org.apache.poi.ss.formula.functions; + +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.EvaluationException; + +/** + * Calculates Modified internal rate of return. Syntax is MIRR(cash_flow_values, finance_rate, reinvest_rate) + * + *

Returns the modified internal rate of return for a series of periodic cash flows. MIRR considers both the cost + * of the investment and the interest received on reinvestment of cash.

+ * + * Values is an array or a reference to cells that contain numbers. These numbers represent a series of payments (negative values) and income (positive values) occurring at regular periods. + * + * + * Finance_rate is the interest rate you pay on the money used in the cash flows. + * Reinvest_rate is the interest rate you receive on the cash flows as you reinvest them. + * + * @author Carlos Delgado (carlos dot del dot est at gmail dot com) + * @author Cédric Walter (cedric dot walter at gmail dot com) + * + * @see Wikipedia on MIRR + * @see Excel MIRR + * @see {@link Irr} + */ +public class Mirr extends MultiOperandNumericFunction { + + public Mirr() { + super(false, false); + } + + @Override + protected int getMaxNumOperands() { + return 3; + } + + @Override + protected double evaluate(double[] values) throws EvaluationException { + + double financeRate = values[values.length-1]; + double reinvestRate = values[values.length-2]; + + double[] mirrValues = new double[values.length - 2]; + System.arraycopy(values, 0, mirrValues, 0, mirrValues.length); + + boolean mirrValuesAreAllNegatives = true; + for (double mirrValue : mirrValues) { + mirrValuesAreAllNegatives &= mirrValue < 0; + } + if (mirrValuesAreAllNegatives) { + return -1.0d; + } + + boolean mirrValuesAreAllPositives = true; + for (double mirrValue : mirrValues) { + mirrValuesAreAllPositives &= mirrValue > 0; + } + if (mirrValuesAreAllPositives) { + throw new EvaluationException(ErrorEval.DIV_ZERO); + } + + return mirr(mirrValues, financeRate, reinvestRate); + } + + private static double mirr(double[] in, double financeRate, double reinvestRate) { + double value = 0; + int numOfYears = in.length - 1; + double pv = 0; + double fv = 0; + + int indexN = 0; + for (double anIn : in) { + if (anIn < 0) { + pv += anIn / Math.pow(1 + financeRate + reinvestRate, indexN++); + } + } + + for (double anIn : in) { + if (anIn > 0) { + fv += anIn * Math.pow(1 + financeRate, numOfYears - indexN++); + } + } + + if (fv != 0 && pv != 0) { + value = Math.pow(-fv / pv, 1d / numOfYears) - 1; + } + return value; + } +} diff --git a/src/testcases/org/apache/poi/ss/formula/functions/TestMirr.java b/src/testcases/org/apache/poi/ss/formula/functions/TestMirr.java new file mode 100644 index 000000000..6e1963b69 --- /dev/null +++ b/src/testcases/org/apache/poi/ss/formula/functions/TestMirr.java @@ -0,0 +1,162 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.ss.formula.functions; + +import junit.framework.AssertionFailedError; +import junit.framework.TestCase; +import org.apache.poi.hssf.HSSFTestDataSamples; +import org.apache.poi.hssf.usermodel.*; +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.EvaluationException; +import org.apache.poi.ss.usermodel.CellValue; + +/** + * Tests for {@link org.apache.poi.ss.formula.functions.Mirr} + * + * @author Carlos Delgado (carlos dot del dot est at gmail dot com) + * @author Cédric Walter (cedric dot walter at gmail dot com) + * @see {@link org.apache.poi.ss.formula.functions.TestIrr} + */ +public final class TestMirr extends TestCase { + + public void testMirr() { + Mirr mirr = new Mirr(); + double mirrValue; + + double financeRate = 0.12; + double reinvestRate = 0.1; + double[] values = {-120000d, 39000d, 30000d, 21000d, 37000d, 46000d, reinvestRate, financeRate}; + try { + mirrValue = mirr.evaluate(values); + } catch (EvaluationException e) { + throw new AssertionFailedError("MIRR should not failed with these parameters" + e); + } + assertEquals("mirr", 0.126094130366, mirrValue, 0.0000000001); + + reinvestRate = 0.05; + financeRate = 0.08; + values = new double[]{-7500d, 3000d, 5000d, 1200d, 4000d, reinvestRate, financeRate}; + try { + mirrValue = mirr.evaluate(values); + } catch (EvaluationException e) { + throw new AssertionFailedError("MIRR should not failed with these parameters" + e); + } + assertEquals("mirr", 0.18736225093, mirrValue, 0.0000000001); + + reinvestRate = 0.065; + financeRate = 0.1; + values = new double[]{-10000, 3400d, 6500d, 1000d, reinvestRate, financeRate}; + try { + mirrValue = mirr.evaluate(values); + } catch (EvaluationException e) { + throw new AssertionFailedError("MIRR should not failed with these parameters" + e); + } + assertEquals("mirr", 0.07039493966, mirrValue, 0.0000000001); + + reinvestRate = 0.07; + financeRate = 0.01; + values = new double[]{-10000d, -3400d, -6500d, -1000d, reinvestRate, financeRate}; + try { + mirrValue = mirr.evaluate(values); + } catch (EvaluationException e) { + throw new AssertionFailedError("MIRR should not failed with these parameters" + e); + } + assertEquals("mirr", -1, mirrValue, 0.0); + + } + + public void testMirrErrors_expectDIV0() { + Mirr mirr = new Mirr(); + + double reinvestRate = 0.05; + double financeRate = 0.08; + double[] incomes = {120000d, 39000d, 30000d, 21000d, 37000d, 46000d, reinvestRate, financeRate}; + try { + mirr.evaluate(incomes); + } catch (EvaluationException e) { + assertEquals(ErrorEval.DIV_ZERO, e.getErrorEval()); + return; + } + throw new AssertionFailedError("MIRR should failed with all these positives values"); + } + + + public void testEvaluateInSheet() { + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet sheet = wb.createSheet("Sheet1"); + HSSFRow row = sheet.createRow(0); + + row.createCell(0).setCellValue(-7500d); + row.createCell(1).setCellValue(3000d); + row.createCell(2).setCellValue(5000d); + row.createCell(3).setCellValue(1200d); + row.createCell(4).setCellValue(4000d); + + row.createCell(5).setCellValue(0.05d); + row.createCell(6).setCellValue(0.08d); + + HSSFCell cell = row.createCell(7); + cell.setCellFormula("MIRR(A1:E1, F1, G1)"); + + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + fe.clearAllCachedResultValues(); + fe.evaluateFormulaCell(cell); + double res = cell.getNumericCellValue(); + assertEquals(0.18736225093, res, 0.00000001); + } + + public void testMirrFromSpreadsheet() { + HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook("mirrTest.xls"); + HSSFSheet sheet = wb.getSheet("Mirr"); + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + StringBuilder failures = new StringBuilder(); + int failureCount = 0; + int[] resultRows = {9, 19, 29, 45}; + + for (int rowNum : resultRows) { + HSSFRow row = sheet.getRow(rowNum); + HSSFCell cellA = row.getCell(0); + try { + CellValue cv = fe.evaluate(cellA); + assertFormulaResult(cv, cellA); + } catch (Throwable e) { + if (failures.length() > 0) failures.append('\n'); + failures.append("Row[").append(cellA.getRowIndex() + 1).append("]: ").append(cellA.getCellFormula()).append(" "); + failures.append(e.getMessage()); + failureCount++; + } + } + + HSSFRow row = sheet.getRow(37); + HSSFCell cellA = row.getCell(0); + CellValue cv = fe.evaluate(cellA); + assertEquals(ErrorEval.DIV_ZERO.getErrorCode(), cv.getErrorValue()); + + if (failures.length() > 0) { + throw new AssertionFailedError(failureCount + " IRR assertions failed:\n" + failures.toString()); + } + + } + + private static void assertFormulaResult(CellValue cv, HSSFCell cell) { + double actualValue = cv.getNumberValue(); + double expectedValue = cell.getNumericCellValue(); // cached formula result calculated by Excel + assertEquals("Invalid formula result: " + cv.toString(), HSSFCell.CELL_TYPE_NUMERIC, cv.getCellType()); + assertEquals(expectedValue, actualValue, 1E-8); + } +} diff --git a/test-data/spreadsheet/FormulaEvalTestData.xls b/test-data/spreadsheet/FormulaEvalTestData.xls index afe3af10a..29924ae6d 100644 Binary files a/test-data/spreadsheet/FormulaEvalTestData.xls and b/test-data/spreadsheet/FormulaEvalTestData.xls differ diff --git a/test-data/spreadsheet/mirrTest.xls b/test-data/spreadsheet/mirrTest.xls new file mode 100644 index 000000000..a8a0b0b12 Binary files /dev/null and b/test-data/spreadsheet/mirrTest.xls differ