From 574810c8aa85d5f2e6cfb9e476f01584fa7b860b Mon Sep 17 00:00:00 2001 From: Yegor Kozlov Date: Fri, 10 Dec 2010 14:20:35 +0000 Subject: [PATCH] Support for IRR() function, see Bugzilla 50409. Includes fix for Bugzilla 50437 git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1044370 13f79535-47bb-0310-9956-ffa450edef68 --- src/documentation/content/xdocs/index.xml | 2 +- src/documentation/content/xdocs/status.xml | 4 +- .../poi/ss/formula/eval/FunctionEval.java | 1 + .../formula/functions/AggregateFunction.java | 2 +- .../poi/ss/formula/functions/FinanceLib.java | 2 +- .../apache/poi/ss/formula/functions/Irr.java | 120 ++++++++++++++++ .../apache/poi/ss/formula/functions/Npv.java | 76 ++--------- .../poi/ss/formula/functions/TestIrr.java | 128 ++++++++++++++++++ .../poi/ss/formula/functions/TestNpv.java | 104 ++++++++++++++ test-data/spreadsheet/IrrNpvTestCaseData.xls | Bin 0 -> 28672 bytes 10 files changed, 373 insertions(+), 66 deletions(-) create mode 100644 src/java/org/apache/poi/ss/formula/functions/Irr.java create mode 100644 src/testcases/org/apache/poi/ss/formula/functions/TestIrr.java create mode 100644 src/testcases/org/apache/poi/ss/formula/functions/TestNpv.java create mode 100644 test-data/spreadsheet/IrrNpvTestCaseData.xls diff --git a/src/documentation/content/xdocs/index.xml b/src/documentation/content/xdocs/index.xml index 5b77ebaa1f..2db376ab2d 100644 --- a/src/documentation/content/xdocs/index.xml +++ b/src/documentation/content/xdocs/index.xml @@ -35,7 +35,7 @@
29 October 2010 - POI 3.7 available

The Apache POI team is pleased to announce the release of 3.7. This includes a large number of bug fixes, and some enhancements (especially text extraction). See the - full release notes for more details. + full release notes for more details.

A full list of changes is available in the change log. People interested should also follow the dev mailing list to track further progress.

diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index 7854a2adc7..f120102386 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -34,8 +34,10 @@ + 50437 - Support passing ranges to NPV() + 50409 - Added implementation for IRR() 47405 - Improved performance of RowRecordsAggregate.getStartRowNumberForBlock / getEndRowNumberForBlock - 50315 - Avoid crashing Excel when sorting XSSFSheet autofilter + 50315 - Avoid crashing Excel when sorting XSSFSheet autofilter 50076 - Allow access from XSSFReader to sheet comments and headers/footers 50076 - Refactor XSSFEventBasedExcelExtractor to make it easier for you to have control over outputting the cell contents 50258 - avoid corruption of XSSFWorkbook after applying XSSFRichTextRun#applyFont 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 85957dad48..01fc068fe9 100644 --- a/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java +++ b/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java @@ -100,6 +100,7 @@ public final class FunctionEval { retval[58] = FinanceFunction.NPER; retval[59] = FinanceFunction.PMT; + retval[62] = new Irr(); retval[63] = NumericFunction.RAND; retval[64] = new Match(); retval[65] = DateFunc.instance; diff --git a/src/java/org/apache/poi/ss/formula/functions/AggregateFunction.java b/src/java/org/apache/poi/ss/formula/functions/AggregateFunction.java index a8cff4e130..7a047dc70a 100644 --- a/src/java/org/apache/poi/ss/formula/functions/AggregateFunction.java +++ b/src/java/org/apache/poi/ss/formula/functions/AggregateFunction.java @@ -67,7 +67,7 @@ public abstract class AggregateFunction extends MultiOperandNumericFunction { return new NumberEval(result); } } - private static final class ValueCollector extends MultiOperandNumericFunction { + static final class ValueCollector extends MultiOperandNumericFunction { private static final ValueCollector instance = new ValueCollector(); public ValueCollector() { super(false, false); diff --git a/src/java/org/apache/poi/ss/formula/functions/FinanceLib.java b/src/java/org/apache/poi/ss/formula/functions/FinanceLib.java index d61e7e020f..2a10fc9c39 100644 --- a/src/java/org/apache/poi/ss/formula/functions/FinanceLib.java +++ b/src/java/org/apache/poi/ss/formula/functions/FinanceLib.java @@ -56,7 +56,7 @@ package org.apache.poi.ss.formula.functions; * ny + p + f=0 ...{when r=0} * */ -final class FinanceLib { +public final class FinanceLib { private FinanceLib() { // no instances of this class diff --git a/src/java/org/apache/poi/ss/formula/functions/Irr.java b/src/java/org/apache/poi/ss/formula/functions/Irr.java new file mode 100644 index 0000000000..27f7232eb2 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/functions/Irr.java @@ -0,0 +1,120 @@ +/* ==================================================================== + 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.*; + +/** + * Calculates the internal rate of return. + * + * Syntax is IRR(values) or IRR(values,guess) + * + * @author Marcel May + * @author Yegor Kozlov + * + * @see Wikipedia on IRR + * @see Excel IRR + */ +public final class Irr implements Function { + + + public ValueEval evaluate(final ValueEval[] args, final int srcRowIndex, final int srcColumnIndex) { + if(args.length == 0 || args.length > 2) { + // Wrong number of arguments + return ErrorEval.VALUE_INVALID; + } + + try { + double[] values = AggregateFunction.ValueCollector.collectValues(args[0]); + double guess; + if(args.length == 2) { + guess = NumericFunction.singleOperandEvaluate(args[1], srcRowIndex, srcColumnIndex); + } else { + guess = 0.1d; + } + double result = irr(values, guess); + NumericFunction.checkValue(result); + return new NumberEval(result); + } catch (EvaluationException e){ + return e.getErrorEval(); + } + } + + /** + * Computes the internal rate of return using an estimated irr of 10 percent. + * + * @param income the income values. + * @return the irr. + */ + public static double irr(double[] income) { + return irr(income, 0.1d); + } + + + /** + * Calculates IRR using the Newton-Raphson Method. + *

+ * Starting with the guess, the method cycles through the calculation until the result + * is accurate within 0.00001 percent. If IRR can't find a result that works + * after 20 tries, the Double.NaN<> is returned. + *

+ *

+ * The implementation is inspired by the NewtonSolver from the Apache Commons-Math library, + * @see {http://commons.apache.org/} + *

+ * + * @param values the income values. + * @param guess the initial guess of irr. + * @return the irr value. The method returns Double.NaN + * if the maximum iteration count is exceeded + * + * @see {http://en.wikipedia.org/wiki/Internal_rate_of_return#Numerical_solution} + * @see {http://en.wikipedia.org/wiki/Newton%27s_method} + */ + public static double irr(double[] values, double guess) { + int maxIterationCount = 20; + double absoluteAccuracy = 1E-7; + + double x0 = guess; + double x1; + + int i = 0; + while (i < maxIterationCount) { + + // the value of the function (NPV) and its derivate can be calculated in the same loop + double fValue = 0; + double fDerivative = 0; + for (int k = 0; k < values.length; k++) { + fValue += values[k] / Math.pow(1.0 + x0, k); + fDerivative += -k * values[k] / Math.pow(1.0 + x0, k + 1); + } + + // the essense of the Newton-Raphson Method + x1 = x0 - fValue/fDerivative; + + if (Math.abs(x1 - x0) <= absoluteAccuracy) { + return x1; + } + + x0 = x1; + ++i; + } + // maximum number of iterations is exceeded + return Double.NaN; + } +} diff --git a/src/java/org/apache/poi/ss/formula/functions/Npv.java b/src/java/org/apache/poi/ss/formula/functions/Npv.java index 3d86ad6dd6..8ca7f8555c 100644 --- a/src/java/org/apache/poi/ss/formula/functions/Npv.java +++ b/src/java/org/apache/poi/ss/formula/functions/Npv.java @@ -17,11 +17,14 @@ package org.apache.poi.ss.formula.functions; +import org.apache.poi.ss.formula.TwoDEval; import org.apache.poi.ss.formula.eval.ErrorEval; import org.apache.poi.ss.formula.eval.EvaluationException; import org.apache.poi.ss.formula.eval.NumberEval; import org.apache.poi.ss.formula.eval.ValueEval; +import java.util.Arrays; + /** * Calculates the net present value of an investment by using a discount rate * and a series of future payments (negative values) and income (positive @@ -30,78 +33,27 @@ import org.apache.poi.ss.formula.eval.ValueEval; * income. * * @author SPetrakovsky + * @author Marcel May */ -public final class Npv implements Function2Arg, Function3Arg, Function4Arg { - - - public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1) { - double result; - try { - double rate = NumericFunction.singleOperandEvaluate(arg0, srcRowIndex, srcColumnIndex); - double d1 = NumericFunction.singleOperandEvaluate(arg1, srcRowIndex, srcColumnIndex); - result = evaluate(rate, d1); - NumericFunction.checkValue(result); - } catch (EvaluationException e) { - return e.getErrorEval(); - } - return new NumberEval(result); - } - public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1, - ValueEval arg2) { - double result; - try { - double rate = NumericFunction.singleOperandEvaluate(arg0, srcRowIndex, srcColumnIndex); - double d1 = NumericFunction.singleOperandEvaluate(arg1, srcRowIndex, srcColumnIndex); - double d2 = NumericFunction.singleOperandEvaluate(arg2, srcRowIndex, srcColumnIndex); - result = evaluate(rate, d1, d2); - NumericFunction.checkValue(result); - } catch (EvaluationException e) { - return e.getErrorEval(); - } - return new NumberEval(result); - } - public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1, - ValueEval arg2, ValueEval arg3) { - double result; - try { - double rate = NumericFunction.singleOperandEvaluate(arg0, srcRowIndex, srcColumnIndex); - double d1 = NumericFunction.singleOperandEvaluate(arg1, srcRowIndex, srcColumnIndex); - double d2 = NumericFunction.singleOperandEvaluate(arg2, srcRowIndex, srcColumnIndex); - double d3 = NumericFunction.singleOperandEvaluate(arg3, srcRowIndex, srcColumnIndex); - result = evaluate(rate, d1, d2, d3); - NumericFunction.checkValue(result); - } catch (EvaluationException e) { - return e.getErrorEval(); - } - return new NumberEval(result); - } +public final class Npv implements Function { public ValueEval evaluate(ValueEval[] args, int srcRowIndex, int srcColumnIndex) { int nArgs = args.length; - if (nArgs<2) { + if (nArgs < 2) { return ErrorEval.VALUE_INVALID; } - int np = nArgs-1; - double[] ds = new double[np]; - double result; - try { + + try { double rate = NumericFunction.singleOperandEvaluate(args[0], srcRowIndex, srcColumnIndex); - for (int i = 0; i < ds.length; i++) { - ds[i] = NumericFunction.singleOperandEvaluate(args[i+1], srcRowIndex, srcColumnIndex); - } - result = evaluate(rate, ds); + // convert tail arguments into an array of doubles + ValueEval[] vargs = Arrays.copyOfRange(args, 1 , args.length); + double[] values = AggregateFunction.ValueCollector.collectValues(vargs); + + double result = FinanceLib.npv(rate, values); NumericFunction.checkValue(result); + return new NumberEval(result); } catch (EvaluationException e) { return e.getErrorEval(); } - return new NumberEval(result); - } - - private static double evaluate(double rate, double...ds) { - double sum = 0; - for (int i = 0; i < ds.length; i++) { - sum += ds[i] / Math.pow(rate + 1, i); - } - return sum; } } diff --git a/src/testcases/org/apache/poi/ss/formula/functions/TestIrr.java b/src/testcases/org/apache/poi/ss/formula/functions/TestIrr.java new file mode 100644 index 0000000000..7606d3fefa --- /dev/null +++ b/src/testcases/org/apache/poi/ss/formula/functions/TestIrr.java @@ -0,0 +1,128 @@ +/* ==================================================================== + 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.TestCase; +import junit.framework.AssertionFailedError; +import org.apache.poi.hssf.usermodel.*; +import org.apache.poi.hssf.HSSFTestDataSamples; +import org.apache.poi.ss.usermodel.CellValue; + +/** + * Tests for {@link Irr} + * + * @author Marcel May + */ +public final class TestIrr extends TestCase { + + public void testIrr() { + // http://en.wikipedia.org/wiki/Internal_rate_of_return#Example + double[] incomes = {-4000d, 1200d, 1410d, 1875d, 1050d}; + double irr = Irr.irr(incomes); + double irrRounded = Math.round(irr * 1000d) / 1000d; + assertEquals("irr", 0.143d, irrRounded); + + // http://www.techonthenet.com/excel/formulas/irr.php + incomes = new double[]{-7500d, 3000d, 5000d, 1200d, 4000d}; + irr = Irr.irr(incomes); + irrRounded = Math.round(irr * 100d) / 100d; + assertEquals("irr", 0.28d, irrRounded); + + incomes = new double[]{-10000d, 3400d, 6500d, 1000d}; + irr = Irr.irr(incomes); + irrRounded = Math.round(irr * 100d) / 100d; + assertEquals("irr", 0.05, irrRounded); + + incomes = new double[]{100d, -10d, -110d}; + irr = Irr.irr(incomes); + irrRounded = Math.round(irr * 100d) / 100d; + assertEquals("irr", 0.1, irrRounded); + + incomes = new double[]{-70000d, 12000, 15000}; + irr = Irr.irr(incomes, -0.1); + irrRounded = Math.round(irr * 100d) / 100d; + assertEquals("irr", -0.44, irrRounded); + } + + public void testEvaluateInSheet() { + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet sheet = wb.createSheet("Sheet1"); + HSSFRow row = sheet.createRow(0); + + row.createCell(0).setCellValue(-4000d); + row.createCell(1).setCellValue(1200d); + row.createCell(2).setCellValue(1410d); + row.createCell(3).setCellValue(1875d); + row.createCell(4).setCellValue(1050d); + + HSSFCell cell = row.createCell(5); + cell.setCellFormula("IRR(A1:E1)"); + + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + fe.clearAllCachedResultValues(); + fe.evaluateFormulaCell(cell); + double res = cell.getNumericCellValue(); + assertEquals(0.143d, Math.round(res * 1000d) / 1000d); + } + + public void testIrrFromSpreadsheet(){ + HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook("IrrNpvTestCaseData.xls"); + HSSFSheet sheet = wb.getSheet("IRR-NPV"); + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + StringBuffer failures = new StringBuffer(); + int failureCount = 0; + // TODO YK: Formulas in rows 16 and 17 operate with ArrayPtg which isn't yet supported + // FormulaEvaluator as of r1041407 throws "Unexpected ptg class (org.apache.poi.ss.formula.ptg.ArrayPtg)" + for(int rownum = 9; rownum <= 15; rownum++){ + 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[" + (cellA.getRowIndex() + 1)+ "]: " + cellA.getCellFormula() + " "); + failures.append(e.getMessage()); + failureCount++; + } + + HSSFCell cellC = row.getCell(2); //IRR-NPV relationship: NPV(IRR(values), values) = 0 + try { + CellValue cv = fe.evaluate(cellC); + assertEquals(0, cv.getNumberValue(), 0.0001); // should agree within 0.01% + } catch (Throwable e){ + if(failures.length() > 0) failures.append('\n'); + failures.append("Row[" + (cellC.getRowIndex() + 1)+ "]: " + cellC.getCellFormula() + " "); + failures.append(e.getMessage()); + failureCount++; + } + } + + 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-4); // should agree within 0.01% + } +} diff --git a/src/testcases/org/apache/poi/ss/formula/functions/TestNpv.java b/src/testcases/org/apache/poi/ss/formula/functions/TestNpv.java new file mode 100644 index 0000000000..247138a4b6 --- /dev/null +++ b/src/testcases/org/apache/poi/ss/formula/functions/TestNpv.java @@ -0,0 +1,104 @@ +/* ==================================================================== + 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.TestCase; +import junit.framework.AssertionFailedError; +import org.apache.poi.hssf.usermodel.*; +import org.apache.poi.hssf.HSSFTestDataSamples; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.CellValue; + +/** + * Tests for {@link Npv} + * + * @author Marcel May + * @see Excel Help + */ +public final class TestNpv extends TestCase { + + public void testEvaluateInSheetExample2() { + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet sheet = wb.createSheet("Sheet1"); + HSSFRow row = sheet.createRow(0); + + sheet.createRow(1).createCell(0).setCellValue(0.08d); + sheet.createRow(2).createCell(0).setCellValue(-40000d); + sheet.createRow(3).createCell(0).setCellValue(8000d); + sheet.createRow(4).createCell(0).setCellValue(9200d); + sheet.createRow(5).createCell(0).setCellValue(10000d); + sheet.createRow(6).createCell(0).setCellValue(12000d); + sheet.createRow(7).createCell(0).setCellValue(14500d); + + HSSFCell cell = row.createCell(8); + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + + // Enumeration + cell.setCellFormula("NPV(A2, A4,A5,A6,A7,A8)+A3"); + fe.clearAllCachedResultValues(); + fe.evaluateFormulaCell(cell); + double res = cell.getNumericCellValue(); + assertEquals(1922.06d, Math.round(res * 100d) / 100d); + + // Range + cell.setCellFormula("NPV(A2, A4:A8)+A3"); + + fe.clearAllCachedResultValues(); + fe.evaluateFormulaCell(cell); + res = cell.getNumericCellValue(); + assertEquals(1922.06d, Math.round(res * 100d) / 100d); + } + + /** + * evaluate formulas with NPV and compare the result with + * the cached formula result pre-calculated by Excel + */ + public void testNpvFromSpreadsheet(){ + HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook("IrrNpvTestCaseData.xls"); + HSSFSheet sheet = wb.getSheet("IRR-NPV"); + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + StringBuffer failures = new StringBuffer(); + int failureCount = 0; + // TODO YK: Formulas in rows 16 and 17 operate with ArrayPtg which isn't yet supported + // FormulaEvaluator as of r1041407 throws "Unexpected ptg class (org.apache.poi.ss.formula.ptg.ArrayPtg)" + for(int rownum = 9; rownum <= 15; rownum++){ + HSSFRow row = sheet.getRow(rownum); + HSSFCell cellB = row.getCell(1); + try { + CellValue cv = fe.evaluate(cellB); + assertFormulaResult(cv, cellB); + } catch (Throwable e){ + if(failures.length() > 0) failures.append('\n'); + failures.append("Row[" + (cellB.getRowIndex() + 1)+ "]: " + cellB.getCellFormula() + " "); + failures.append(e.getMessage()); + failureCount++; + } + } + + if(failures.length() > 0) { + throw new AssertionFailedError(failureCount + " IRR evaluations 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(expectedValue, actualValue, 1E-4); // should agree within 0.01% + } +} diff --git a/test-data/spreadsheet/IrrNpvTestCaseData.xls b/test-data/spreadsheet/IrrNpvTestCaseData.xls new file mode 100644 index 0000000000000000000000000000000000000000..667493b46362a51252c6714fb00de49291453323 GIT binary patch literal 28672 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