Validate XIRR inputs and return correct error values
Fix: Return #NUM! if values and dates contain a different number of values Fix: Return #NUM! if there is not at least one positive cash flow and one negative cash flow Fix: Return #NUM! if any number in dates precedes the starting date Fix: Return #NUM! if a result that works cannot be found after max iteration tries Fix: Correct DocBlocks for XIRR & XNPV Add: Validate XIRR with unit tests Closes #1177
This commit is contained in:
parent
3fc2fa47de
commit
788f79c1bb
|
@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
|
||||||
- Fix branch pruning handling of non boolean conditions [#1167](https://github.com/PHPOffice/PhpSpreadsheet/pull/1167)
|
- Fix branch pruning handling of non boolean conditions [#1167](https://github.com/PHPOffice/PhpSpreadsheet/pull/1167)
|
||||||
- Fix ODS Reader when no DC namespace are defined [#1182](https://github.com/PHPOffice/PhpSpreadsheet/pull/1182)
|
- Fix ODS Reader when no DC namespace are defined [#1182](https://github.com/PHPOffice/PhpSpreadsheet/pull/1182)
|
||||||
- Fixed Functions->ifCondition for allowing <> and empty condition [#1206](https://github.com/PHPOffice/PhpSpreadsheet/pull/1206)
|
- Fixed Functions->ifCondition for allowing <> and empty condition [#1206](https://github.com/PHPOffice/PhpSpreadsheet/pull/1206)
|
||||||
|
- Validate XIRR inputs and return correct error values [#1120](https://github.com/PHPOffice/PhpSpreadsheet/issues/1120)
|
||||||
|
|
||||||
## [1.9.0] - 2019-08-17
|
## [1.9.0] - 2019-08-17
|
||||||
|
|
||||||
|
|
|
@ -2148,7 +2148,7 @@ class Financial
|
||||||
* The maturity date is the date when the Treasury bill expires.
|
* The maturity date is the date when the Treasury bill expires.
|
||||||
* @param int $price The Treasury bill's price per $100 face value
|
* @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)
|
public static function TBILLYIELD($settlement, $maturity, $price)
|
||||||
{
|
{
|
||||||
|
@ -2183,6 +2183,23 @@ class Financial
|
||||||
return Functions::VALUE();
|
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)
|
public static function XIRR($values, $dates, $guess = 0.1)
|
||||||
{
|
{
|
||||||
if ((!is_array($values)) && (!is_array($dates))) {
|
if ((!is_array($values)) && (!is_array($dates))) {
|
||||||
|
@ -2195,11 +2212,28 @@ class Financial
|
||||||
return Functions::NAN();
|
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
|
// create an initial range, with a root somewhere between 0 and guess
|
||||||
$x1 = 0.0;
|
$x1 = 0.0;
|
||||||
$x2 = $guess;
|
$x2 = $guess;
|
||||||
$f1 = self::XNPV($x1, $values, $dates);
|
$f1 = self::XNPV($x1, $values, $dates);
|
||||||
|
if (!is_numeric($f1)) {
|
||||||
|
return $f1;
|
||||||
|
}
|
||||||
$f2 = self::XNPV($x2, $values, $dates);
|
$f2 = self::XNPV($x2, $values, $dates);
|
||||||
|
if (!is_numeric($f2)) {
|
||||||
|
return $f2;
|
||||||
|
}
|
||||||
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
|
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
|
||||||
if (($f1 * $f2) < 0.0) {
|
if (($f1 * $f2) < 0.0) {
|
||||||
break;
|
break;
|
||||||
|
@ -2210,7 +2244,7 @@ class Financial
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (($f1 * $f2) > 0.0) {
|
if (($f1 * $f2) > 0.0) {
|
||||||
return Functions::VALUE();
|
return Functions::NAN();
|
||||||
}
|
}
|
||||||
|
|
||||||
$f = self::XNPV($x1, $values, $dates);
|
$f = self::XNPV($x1, $values, $dates);
|
||||||
|
@ -2247,15 +2281,15 @@ class Financial
|
||||||
* =XNPV(rate,values,dates)
|
* =XNPV(rate,values,dates)
|
||||||
*
|
*
|
||||||
* @param float $rate the discount rate to apply to the cash flows
|
* @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.
|
* 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.
|
* 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.
|
* 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.
|
* 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.
|
* 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)
|
public static function XNPV($rate, $values, $dates)
|
||||||
{
|
{
|
||||||
|
@ -2273,7 +2307,7 @@ class Financial
|
||||||
return Functions::NAN();
|
return Functions::NAN();
|
||||||
}
|
}
|
||||||
if ((min($values) > 0) || (max($values) < 0)) {
|
if ((min($values) > 0) || (max($values) < 0)) {
|
||||||
return Functions::VALUE();
|
return Functions::NAN();
|
||||||
}
|
}
|
||||||
|
|
||||||
$xnpv = 0.0;
|
$xnpv = 0.0;
|
||||||
|
|
|
@ -501,13 +501,12 @@ class FinancialTest extends TestCase
|
||||||
* @dataProvider providerXIRR
|
* @dataProvider providerXIRR
|
||||||
*
|
*
|
||||||
* @param mixed $expectedResult
|
* @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);
|
$result = Financial::XIRR(...$args);
|
||||||
self::assertEquals($expectedResult, $result, '', 1E-8);
|
self::assertEquals($expectedResult, $result, $message, Financial::FINANCIAL_PRECISION);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function providerXIRR()
|
public function providerXIRR()
|
||||||
|
|
|
@ -1,71 +1,62 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
// values, dates, guess, Result
|
// result, message, values, dates, guess
|
||||||
|
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
[
|
'#NUM!',
|
||||||
-10000,
|
'If values and dates contain a different number of values, returns the #NUM! error value',
|
||||||
[
|
[4000, -46000],
|
||||||
2750,
|
['01/04/2015'],
|
||||||
4250,
|
0.1
|
||||||
3250,
|
|
||||||
2750,
|
|
||||||
46000,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'2008-01-01',
|
|
||||||
[
|
|
||||||
'2008-03-01',
|
|
||||||
'2008-10-30',
|
|
||||||
'2009-02-15',
|
|
||||||
'2009-04-01',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
0.10000000000000001,
|
|
||||||
0.373362535,
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
'#NUM!',
|
||||||
-100,
|
'Expects at least one positive cash flow and one negative cash flow; otherwise returns the #NUM! error value',
|
||||||
[
|
[-4000, -46000],
|
||||||
20,
|
['01/04/2015', '2019-06-27'],
|
||||||
40,
|
0.1
|
||||||
25,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'2010-01-01',
|
|
||||||
[
|
|
||||||
'2010-04-01',
|
|
||||||
'2010-10-01',
|
|
||||||
'2011-02-01',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
-0.3024,
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
[
|
'#NUM!',
|
||||||
-100,
|
'Expects at least one positive cash flow and one negative cash flow; otherwise returns the #NUM! error value',
|
||||||
[
|
[4000, 46000],
|
||||||
20,
|
['01/04/2015', '2019-06-27'],
|
||||||
40,
|
0.1
|
||||||
25,
|
],
|
||||||
8,
|
[
|
||||||
15,
|
'#VALUE!',
|
||||||
],
|
'If any number in dates is not a valid date, returns the #VALUE! error value',
|
||||||
],
|
[4000, -46000],
|
||||||
[
|
['01/04/2015', '2019X06-27'],
|
||||||
'2010-01-01',
|
0.1
|
||||||
[
|
],
|
||||||
'2010-04-01',
|
[
|
||||||
'2010-10-01',
|
'#NUM!',
|
||||||
'2011-02-01',
|
'If any number in dates precedes the starting date, XIRR returns the #NUM! error value',
|
||||||
'2011-03-01',
|
[1893.67, 139947.43, 52573.25, 48849.74, 26369.16, -273029.18],
|
||||||
'2011-06-01',
|
['2019-06-27', '2019-06-20', '2019-06-21', '2019-06-24', '2019-06-27', '2019-07-27'],
|
||||||
],
|
0.1
|
||||||
],
|
],
|
||||||
0.20949999999999999,
|
[
|
||||||
|
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
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
Loading…
Reference in New Issue