Skip to content

Commit 788f79c

Browse files
blacknellPowerKiKi
authored andcommitted
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
1 parent 3fc2fa4 commit 788f79c

File tree

4 files changed

+95
-70
lines changed

4 files changed

+95
-70
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
2626
- Fix branch pruning handling of non boolean conditions [#1167](https://github.com/PHPOffice/PhpSpreadsheet/pull/1167)
2727
- Fix ODS Reader when no DC namespace are defined [#1182](https://github.com/PHPOffice/PhpSpreadsheet/pull/1182)
2828
- Fixed Functions->ifCondition for allowing <> and empty condition [#1206](https://github.com/PHPOffice/PhpSpreadsheet/pull/1206)
29+
- Validate XIRR inputs and return correct error values [#1120](https://github.com/PHPOffice/PhpSpreadsheet/issues/1120)
2930

3031
## [1.9.0] - 2019-08-17
3132

src/PhpSpreadsheet/Calculation/Financial.php

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2148,7 +2148,7 @@ public static function TBILLPRICE($settlement, $maturity, $discount)
21482148
* The maturity date is the date when the Treasury bill expires.
21492149
* @param int $price The Treasury bill's price per $100 face value
21502150
*
2151-
* @return float
2151+
* @return float|mixed|string
21522152
*/
21532153
public static function TBILLYIELD($settlement, $maturity, $price)
21542154
{
@@ -2183,6 +2183,23 @@ public static function TBILLYIELD($settlement, $maturity, $price)
21832183
return Functions::VALUE();
21842184
}
21852185

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

2215+
$datesCount = count($dates);
2216+
for ($i = 0; $i < $datesCount; ++$i) {
2217+
$dates[$i] = DateTime::getDateValue($dates[$i]);
2218+
if (!is_numeric($dates[$i])) {
2219+
return Functions::VALUE();
2220+
}
2221+
}
2222+
if (min($dates) != $dates[0]) {
2223+
return Functions::NAN();
2224+
}
2225+
21982226
// create an initial range, with a root somewhere between 0 and guess
21992227
$x1 = 0.0;
22002228
$x2 = $guess;
22012229
$f1 = self::XNPV($x1, $values, $dates);
2230+
if (!is_numeric($f1)) {
2231+
return $f1;
2232+
}
22022233
$f2 = self::XNPV($x2, $values, $dates);
2234+
if (!is_numeric($f2)) {
2235+
return $f2;
2236+
}
22032237
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
22042238
if (($f1 * $f2) < 0.0) {
22052239
break;
@@ -2210,7 +2244,7 @@ public static function XIRR($values, $dates, $guess = 0.1)
22102244
}
22112245
}
22122246
if (($f1 * $f2) > 0.0) {
2213-
return Functions::VALUE();
2247+
return Functions::NAN();
22142248
}
22152249

22162250
$f = self::XNPV($x1, $values, $dates);
@@ -2247,15 +2281,15 @@ public static function XIRR($values, $dates, $guess = 0.1)
22472281
* =XNPV(rate,values,dates)
22482282
*
22492283
* @param float $rate the discount rate to apply to the cash flows
2250-
* @param array of float $values A series of cash flows that corresponds to a schedule of payments in dates.
2284+
* @param float[] $values A series of cash flows that corresponds to a schedule of payments in dates.
22512285
* The first payment is optional and corresponds to a cost or payment that occurs at the beginning of the investment.
22522286
* 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.
22532287
* The series of values must contain at least one positive value and one negative value.
2254-
* @param array of mixed $dates A schedule of payment dates that corresponds to the cash flow payments.
2288+
* @param mixed[] $dates A schedule of payment dates that corresponds to the cash flow payments.
22552289
* The first payment date indicates the beginning of the schedule of payments.
22562290
* All other dates must be later than this date, but they may occur in any order.
22572291
*
2258-
* @return float
2292+
* @return float|mixed|string
22592293
*/
22602294
public static function XNPV($rate, $values, $dates)
22612295
{
@@ -2273,7 +2307,7 @@ public static function XNPV($rate, $values, $dates)
22732307
return Functions::NAN();
22742308
}
22752309
if ((min($values) > 0) || (max($values) < 0)) {
2276-
return Functions::VALUE();
2310+
return Functions::NAN();
22772311
}
22782312

22792313
$xnpv = 0.0;

tests/PhpSpreadsheetTests/Calculation/FinancialTest.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -501,13 +501,12 @@ public function providerRATE()
501501
* @dataProvider providerXIRR
502502
*
503503
* @param mixed $expectedResult
504+
* @param mixed $message
504505
*/
505-
public function testXIRR($expectedResult, ...$args)
506+
public function testXIRR($expectedResult, $message, ...$args)
506507
{
507-
$this->markTestIncomplete('TODO: This test should be fixed');
508-
509508
$result = Financial::XIRR(...$args);
510-
self::assertEquals($expectedResult, $result, '', 1E-8);
509+
self::assertEquals($expectedResult, $result, $message, Financial::FINANCIAL_PRECISION);
511510
}
512511

513512
public function providerXIRR()
Lines changed: 51 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,62 @@
11
<?php
22

3-
// values, dates, guess, Result
3+
// result, message, values, dates, guess
44

55
return [
66
[
7-
[
8-
-10000,
9-
[
10-
2750,
11-
4250,
12-
3250,
13-
2750,
14-
46000,
15-
],
16-
],
17-
[
18-
'2008-01-01',
19-
[
20-
'2008-03-01',
21-
'2008-10-30',
22-
'2009-02-15',
23-
'2009-04-01',
24-
],
25-
],
26-
0.10000000000000001,
27-
0.373362535,
7+
'#NUM!',
8+
'If values and dates contain a different number of values, returns the #NUM! error value',
9+
[4000, -46000],
10+
['01/04/2015'],
11+
0.1
2812
],
2913
[
30-
[
31-
-100,
32-
[
33-
20,
34-
40,
35-
25,
36-
],
37-
],
38-
[
39-
'2010-01-01',
40-
[
41-
'2010-04-01',
42-
'2010-10-01',
43-
'2011-02-01',
44-
],
45-
],
46-
-0.3024,
14+
'#NUM!',
15+
'Expects at least one positive cash flow and one negative cash flow; otherwise returns the #NUM! error value',
16+
[-4000, -46000],
17+
['01/04/2015', '2019-06-27'],
18+
0.1
4719
],
4820
[
49-
[
50-
-100,
51-
[
52-
20,
53-
40,
54-
25,
55-
8,
56-
15,
57-
],
58-
],
59-
[
60-
'2010-01-01',
61-
[
62-
'2010-04-01',
63-
'2010-10-01',
64-
'2011-02-01',
65-
'2011-03-01',
66-
'2011-06-01',
67-
],
68-
],
69-
0.20949999999999999,
21+
'#NUM!',
22+
'Expects at least one positive cash flow and one negative cash flow; otherwise returns the #NUM! error value',
23+
[4000, 46000],
24+
['01/04/2015', '2019-06-27'],
25+
0.1
26+
],
27+
[
28+
'#VALUE!',
29+
'If any number in dates is not a valid date, returns the #VALUE! error value',
30+
[4000, -46000],
31+
['01/04/2015', '2019X06-27'],
32+
0.1
33+
],
34+
[
35+
'#NUM!',
36+
'If any number in dates precedes the starting date, XIRR returns the #NUM! error value',
37+
[1893.67, 139947.43, 52573.25, 48849.74, 26369.16, -273029.18],
38+
['2019-06-27', '2019-06-20', '2019-06-21', '2019-06-24', '2019-06-27', '2019-07-27'],
39+
0.1
40+
],
41+
[
42+
0.137963527441025,
43+
'XIRR calculation #1 is incorrect',
44+
[139947.43, 1893.67, 52573.25, 48849.74, 26369.16, -273029.18],
45+
['2019-06-20', '2019-06-27', '2019-06-21', '2019-06-24', '2019-06-27', '2019-07-27'],
46+
0.1
47+
],
48+
[
49+
0.09999999,
50+
'XIRR calculation #2 is incorrect',
51+
[100.0, -110.0],
52+
['2019-06-12', '2020-06-11'],
53+
0.1
54+
],
55+
[
56+
'#NUM!',
57+
'Can\'t find a result that works after FINANCIAL_MAX_ITERATIONS tries, the #NUM! error value is returned',
58+
[139947.43, 1893.67, 52573.25, 48849.74, 26369.16, -273029.18],
59+
['2019-06-20', '2019-06-27', '2019-06-21', '2019-06-24', '2019-06-27', '2019-07-27'],
60+
0.00000
7061
],
7162
];

0 commit comments

Comments
 (0)