Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate XIRR inputs and return correct error values #1177

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
- Trying to remove a column that doesn't exist deletes the latest column
- Keep big integer as integer instead of lossely casting to float [#874](https://github.com/PHPOffice/PhpSpreadsheet/pull/874)
- Fix branch pruning handling of non boolean conditions [#1167](https://github.com/PHPOffice/PhpSpreadsheet/pull/1167)
- Validate XIRR inputs and return correct error values [#1120](https://github.com/PHPOffice/PhpSpreadsheet/issues/1120)

## [1.9.0] - 2019-08-17

Expand Down
46 changes: 40 additions & 6 deletions src/PhpSpreadsheet/Calculation/Financial.php
Original file line number Diff line number Diff line change
Expand Up @@ -2148,7 +2148,7 @@ public static function TBILLPRICE($settlement, $maturity, $discount)
* The maturity date is the date when the Treasury bill expires.
* @param int $price The Treasury bill's price per $100 face value
*
* @return float
* @return float|mixed|string
*/
public static function TBILLYIELD($settlement, $maturity, $price)
{
Expand Down Expand Up @@ -2183,6 +2183,23 @@ public static function TBILLYIELD($settlement, $maturity, $price)
return Functions::VALUE();
}

/**
* XIRR.
*
* Returns the internal rate of return for a schedule of cash flows that is not necessarily periodic.
*
* Excel Function:
* =XIRR(values,dates,guess)
*
* @param float[] $values A series of cash flow payments
* The series of values must contain at least one positive value & one negative value
* @param mixed[] $dates A series of payment dates
* The first payment date indicates the beginning of the schedule of payments
* All other dates must be later than this date, but they may occur in any order
* @param float $guess An optional guess at the expected answer
*
* @return float|mixed|string
*/
public static function XIRR($values, $dates, $guess = 0.1)
{
if ((!is_array($values)) && (!is_array($dates))) {
Expand All @@ -2195,11 +2212,28 @@ public static function XIRR($values, $dates, $guess = 0.1)
return Functions::NAN();
}

$datesCount = count($dates);
for ($i = 0; $i < $datesCount; ++$i) {
$dates[$i] = DateTime::getDateValue($dates[$i]);
if (!is_numeric($dates[$i])) {
return Functions::VALUE();
}
}
if (min($dates) != $dates[0]) {
return Functions::NAN();
}

// create an initial range, with a root somewhere between 0 and guess
$x1 = 0.0;
$x2 = $guess;
$f1 = self::XNPV($x1, $values, $dates);
if (!is_numeric($f1)) {
return $f1;
}
$f2 = self::XNPV($x2, $values, $dates);
if (!is_numeric($f2)) {
return $f2;
}
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
if (($f1 * $f2) < 0.0) {
break;
Expand All @@ -2210,7 +2244,7 @@ public static function XIRR($values, $dates, $guess = 0.1)
}
}
if (($f1 * $f2) > 0.0) {
return Functions::VALUE();
return Functions::NAN();
}

$f = self::XNPV($x1, $values, $dates);
Expand Down Expand Up @@ -2247,15 +2281,15 @@ public static function XIRR($values, $dates, $guess = 0.1)
* =XNPV(rate,values,dates)
*
* @param float $rate the discount rate to apply to the cash flows
* @param array of float $values A series of cash flows that corresponds to a schedule of payments in dates.
* @param float[] $values A series of cash flows that corresponds to a schedule of payments in dates.
* The first payment is optional and corresponds to a cost or payment that occurs at the beginning of the investment.
* If the first value is a cost or payment, it must be a negative value. All succeeding payments are discounted based on a 365-day year.
* The series of values must contain at least one positive value and one negative value.
* @param array of mixed $dates A schedule of payment dates that corresponds to the cash flow payments.
* @param mixed[] $dates A schedule of payment dates that corresponds to the cash flow payments.
* The first payment date indicates the beginning of the schedule of payments.
* All other dates must be later than this date, but they may occur in any order.
*
* @return float
* @return float|mixed|string
*/
public static function XNPV($rate, $values, $dates)
{
Expand All @@ -2273,7 +2307,7 @@ public static function XNPV($rate, $values, $dates)
return Functions::NAN();
}
if ((min($values) > 0) || (max($values) < 0)) {
return Functions::VALUE();
return Functions::NAN();
}

$xnpv = 0.0;
Expand Down
7 changes: 3 additions & 4 deletions tests/PhpSpreadsheetTests/Calculation/FinancialTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -501,13 +501,12 @@ public function providerRATE()
* @dataProvider providerXIRR
*
* @param mixed $expectedResult
* @param mixed $message
*/
public function testXIRR($expectedResult, ...$args)
public function testXIRR($expectedResult, $message, ...$args)
{
$this->markTestIncomplete('TODO: This test should be fixed');

$result = Financial::XIRR(...$args);
self::assertEquals($expectedResult, $result, '', 1E-8);
self::assertEquals($expectedResult, $result, $message, Financial::FINANCIAL_PRECISION);
}

public function providerXIRR()
Expand Down
111 changes: 51 additions & 60 deletions tests/data/Calculation/Financial/XIRR.php
Original file line number Diff line number Diff line change
@@ -1,71 +1,62 @@
<?php

// values, dates, guess, Result
// result, message, values, dates, guess

return [
[
[
-10000,
[
2750,
4250,
3250,
2750,
46000,
],
],
[
'2008-01-01',
[
'2008-03-01',
'2008-10-30',
'2009-02-15',
'2009-04-01',
],
],
0.10000000000000001,
0.373362535,
'#NUM!',
'If values and dates contain a different number of values, returns the #NUM! error value',
[4000, -46000],
['01/04/2015'],
0.1
],
[
[
-100,
[
20,
40,
25,
],
],
[
'2010-01-01',
[
'2010-04-01',
'2010-10-01',
'2011-02-01',
],
],
-0.3024,
'#NUM!',
'Expects at least one positive cash flow and one negative cash flow; otherwise returns the #NUM! error value',
[-4000, -46000],
['01/04/2015', '2019-06-27'],
0.1
],
[
[
-100,
[
20,
40,
25,
8,
15,
],
],
[
'2010-01-01',
[
'2010-04-01',
'2010-10-01',
'2011-02-01',
'2011-03-01',
'2011-06-01',
],
],
0.20949999999999999,
'#NUM!',
'Expects at least one positive cash flow and one negative cash flow; otherwise returns the #NUM! error value',
[4000, 46000],
['01/04/2015', '2019-06-27'],
0.1
],
[
'#VALUE!',
'If any number in dates is not a valid date, returns the #VALUE! error value',
[4000, -46000],
['01/04/2015', '2019X06-27'],
0.1
],
[
'#NUM!',
'If any number in dates precedes the starting date, XIRR returns the #NUM! error value',
[1893.67, 139947.43, 52573.25, 48849.74, 26369.16, -273029.18],
['2019-06-27', '2019-06-20', '2019-06-21', '2019-06-24', '2019-06-27', '2019-07-27'],
0.1
],
[
0.137963527441025,
'XIRR calculation #1 is incorrect',
[139947.43, 1893.67, 52573.25, 48849.74, 26369.16, -273029.18],
['2019-06-20', '2019-06-27', '2019-06-21', '2019-06-24', '2019-06-27', '2019-07-27'],
0.1
],
[
0.09999999,
'XIRR calculation #2 is incorrect',
[100.0, -110.0],
['2019-06-12', '2020-06-11'],
0.1
],
[
'#NUM!',
'Can\'t find a result that works after FINANCIAL_MAX_ITERATIONS tries, the #NUM! error value is returned',
[139947.43, 1893.67, 52573.25, 48849.74, 26369.16, -273029.18],
['2019-06-20', '2019-06-27', '2019-06-21', '2019-06-24', '2019-06-27', '2019-07-27'],
0.00000
],
];