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
This commit is contained in:
Yegor Kozlov 2010-12-10 14:20:35 +00:00
parent 49dd8f1ca0
commit 574810c8aa
10 changed files with 373 additions and 66 deletions

View File

@ -35,7 +35,7 @@
<section><title>29 October 2010 - POI 3.7 available</title>
<p>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
<link href="http://www.apache.org/dist/poi/release/bin/RELEASE-NOTES.txt">full release notes</link> for more details.
<link href="http://www.apache.org/dist/poi/release/bin/RELEASE_NOTES.txt">full release notes</link> for more details.
</p>
<p>A full list of changes is available in the <link href="changes.html">change log</link>.
People interested should also follow the <link href="mailinglists.html">dev mailing list</link> to track further progress.</p>

View File

@ -34,8 +34,10 @@
<changes>
<release version="3.8-beta1" date="2010-??-??">
<action dev="POI-DEVELOPERS" type="add">50437 - Support passing ranges to NPV()</action>
<action dev="POI-DEVELOPERS" type="add">50409 - Added implementation for IRR()</action>
<action dev="poi-developers" type="add">47405 - Improved performance of RowRecordsAggregate.getStartRowNumberForBlock / getEndRowNumberForBlock</action>
<action dev="poi-developers" type="add">50315 - Avoid crashing Excel when sorting XSSFSheet autofilter</action>
<action dev="poi-developers" type="fix">50315 - Avoid crashing Excel when sorting XSSFSheet autofilter</action>
<action dev="poi-developers" type="add">50076 - Allow access from XSSFReader to sheet comments and headers/footers</action>
<action dev="poi-developers" type="add">50076 - Refactor XSSFEventBasedExcelExtractor to make it easier for you to have control over outputting the cell contents</action>
<action dev="poi-developers" type="fix">50258 - avoid corruption of XSSFWorkbook after applying XSSFRichTextRun#applyFont</action>

View File

@ -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;

View File

@ -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);

View File

@ -56,7 +56,7 @@ package org.apache.poi.ss.formula.functions;
* ny + p + f=0 ...{when r=0}
* </pre>
*/
final class FinanceLib {
public final class FinanceLib {
private FinanceLib() {
// no instances of this class

View File

@ -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 <a href="http://en.wikipedia.org/wiki/Internal_rate_of_return#Numerical_solution">Wikipedia on IRR</a>
* @see <a href="http://office.microsoft.com/en-us/excel-help/irr-HP005209146.aspx">Excel IRR</a>
*/
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.
* <p>
* 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.
* </p>
* <p>
* The implementation is inspired by the NewtonSolver from the Apache Commons-Math library,
* @see {http://commons.apache.org/}
* </p>
*
* @param values the income values.
* @param guess the initial guess of irr.
* @return the irr value. The method returns <code>Double.NaN</code>
* 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;
}
}

View File

@ -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;
}
}

View File

@ -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%
}
}

View File

@ -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 <a href="http://office.microsoft.com/en-us/excel-help/npv-HP005209199.aspx">Excel Help</a>
*/
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%
}
}

Binary file not shown.