Skip to content

Commit 7e4331e

Browse files
authored
Error in COUPNCD (#2119)
See issue #2116. Code for handling end of month (method couponFirstPeriodDate) needed a fix. Fixed it, confirmed it covered the reported issue with no regression problems. Then added some extra similar tests to all the callers of couponFirstPeriodDate, and ... One new test, in COUPDAYSNC, does not agree with Excel. It also does not agree with LibreOffice. It does, however, agree with Gnumeric, and with my (hardly guaranteed) hand calculation of what the result should be. So, I'm going with it (and have added an appropriate comment to the test data). I'm glad to discuss the matter with anyone more familiar than I with how this is supposed to work - those 360-day years are killers.
1 parent 0b0f022 commit 7e4331e

File tree

7 files changed

+106
-53
lines changed

7 files changed

+106
-53
lines changed

phpstan-baseline.neon

-40
Original file line numberDiff line numberDiff line change
@@ -635,46 +635,6 @@ parameters:
635635
count: 1
636636
path: src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/Periodic.php
637637

638-
-
639-
message: "#^Binary operation \"\\*\" between float\\|string and int results in an error\\.$#"
640-
count: 2
641-
path: src/PhpSpreadsheet/Calculation/Financial/Coupons.php
642-
643-
-
644-
message: "#^Binary operation \"\\*\" between float\\|string and int\\|string results in an error\\.$#"
645-
count: 1
646-
path: src/PhpSpreadsheet/Calculation/Financial/Coupons.php
647-
648-
-
649-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Financial\\\\Coupons\\:\\:couponFirstPeriodDate\\(\\) has no return typehint specified\\.$#"
650-
count: 1
651-
path: src/PhpSpreadsheet/Calculation/Financial/Coupons.php
652-
653-
-
654-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Financial\\\\Coupons\\:\\:couponFirstPeriodDate\\(\\) has parameter \\$maturity with no typehint specified\\.$#"
655-
count: 1
656-
path: src/PhpSpreadsheet/Calculation/Financial/Coupons.php
657-
658-
-
659-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Financial\\\\Coupons\\:\\:couponFirstPeriodDate\\(\\) has parameter \\$next with no typehint specified\\.$#"
660-
count: 1
661-
path: src/PhpSpreadsheet/Calculation/Financial/Coupons.php
662-
663-
-
664-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Financial\\\\Coupons\\:\\:couponFirstPeriodDate\\(\\) has parameter \\$settlement with no typehint specified\\.$#"
665-
count: 1
666-
path: src/PhpSpreadsheet/Calculation/Financial/Coupons.php
667-
668-
-
669-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Financial\\\\Coupons\\:\\:validateCouponPeriod\\(\\) has parameter \\$maturity with no typehint specified\\.$#"
670-
count: 1
671-
path: src/PhpSpreadsheet/Calculation/Financial/Coupons.php
672-
673-
-
674-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Financial\\\\Coupons\\:\\:validateCouponPeriod\\(\\) has parameter \\$settlement with no typehint specified\\.$#"
675-
count: 1
676-
path: src/PhpSpreadsheet/Calculation/Financial/Coupons.php
677-
678638
-
679639
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Financial\\\\Depreciation\\:\\:validateCost\\(\\) has parameter \\$cost with no typehint specified\\.$#"
680640
count: 1

src/PhpSpreadsheet/Calculation/Financial/Coupons.php

+19-13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial;
44

5+
use DateTime;
56
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
67
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
78
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants;
@@ -73,7 +74,7 @@ public static function COUPDAYBS(
7374
return abs((float) DateTimeExcel\Days::between($prev, $settlement));
7475
}
7576

76-
return DateTimeExcel\YearFrac::fraction($prev, $settlement, $basis) * $daysPerYear;
77+
return (float) DateTimeExcel\YearFrac::fraction($prev, $settlement, $basis) * $daysPerYear;
7778
}
7879

7980
/**
@@ -208,7 +209,7 @@ public static function COUPDAYSNC(
208209
}
209210
}
210211

211-
return DateTimeExcel\YearFrac::fraction($settlement, $next, $basis) * $daysPerYear;
212+
return (float) DateTimeExcel\YearFrac::fraction($settlement, $next, $basis) * $daysPerYear;
212213
}
213214

214215
/**
@@ -322,7 +323,7 @@ public static function COUPNUM(
322323
FinancialConstants::BASIS_DAYS_PER_YEAR_NASD
323324
);
324325

325-
return (int) ceil($yearsBetweenSettlementAndMaturity * $frequency);
326+
return (int) ceil((float) $yearsBetweenSettlementAndMaturity * $frequency);
326327
}
327328

328329
/**
@@ -379,28 +380,33 @@ public static function COUPPCD(
379380
return self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_PREVIOUS);
380381
}
381382

382-
private static function couponFirstPeriodDate($settlement, $maturity, int $frequency, $next)
383+
private static function monthsDiff(DateTime $result, int $months, string $plusOrMinus, int $day, bool $lastDayFlag): void
384+
{
385+
$result->setDate((int) $result->format('Y'), (int) $result->format('m'), 1);
386+
$result->modify("$plusOrMinus $months months");
387+
$daysInMonth = (int) $result->format('t');
388+
$result->setDate((int) $result->format('Y'), (int) $result->format('m'), $lastDayFlag ? $daysInMonth : min($day, $daysInMonth));
389+
}
390+
391+
private static function couponFirstPeriodDate(float $settlement, float $maturity, int $frequency, bool $next): float
383392
{
384393
$months = 12 / $frequency;
385394

386395
$result = Date::excelToDateTimeObject($maturity);
387-
$maturityEoM = Helpers::isLastDayOfMonth($result);
396+
$day = (int) $result->format('d');
397+
$lastDayFlag = Helpers::isLastDayOfMonth($result);
388398

389399
while ($settlement < Date::PHPToExcel($result)) {
390-
$result->modify('-' . $months . ' months');
400+
self::monthsDiff($result, $months, '-', $day, $lastDayFlag);
391401
}
392402
if ($next === true) {
393-
$result->modify('+' . $months . ' months');
394-
}
395-
396-
if ($maturityEoM === true) {
397-
$result->modify('-1 day');
403+
self::monthsDiff($result, $months, '+', $day, $lastDayFlag);
398404
}
399405

400-
return Date::PHPToExcel($result);
406+
return (float) Date::PHPToExcel($result);
401407
}
402408

403-
private static function validateCouponPeriod($settlement, $maturity): void
409+
private static function validateCouponPeriod(float $settlement, float $maturity): void
404410
{
405411
if ($settlement >= $maturity) {
406412
throw new Exception(Functions::NAN());

tests/data/Calculation/Financial/COUPDAYBS.php

+14
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,18 @@
205205
4,
206206
4,
207207
],
208+
[
209+
5,
210+
'05-Apr-2019',
211+
'30-Sep-2021',
212+
2,
213+
0,
214+
],
215+
[
216+
5,
217+
'05-Oct-2019',
218+
'31-Mar-2022',
219+
2,
220+
0,
221+
],
208222
];

tests/data/Calculation/Financial/COUPDAYS.php

+14
Original file line numberDiff line numberDiff line change
@@ -219,4 +219,18 @@
219219
4,
220220
4,
221221
],
222+
[
223+
180,
224+
'05-Apr-2019',
225+
'30-Sep-2021',
226+
2,
227+
0,
228+
],
229+
[
230+
180,
231+
'05-Oct-2019',
232+
'31-Mar-2022',
233+
2,
234+
0,
235+
],
222236
];

tests/data/Calculation/Financial/COUPDAYSNC.php

+17
Original file line numberDiff line numberDiff line change
@@ -219,4 +219,21 @@
219219
4,
220220
4,
221221
],
222+
[
223+
175,
224+
'05-Apr-2019',
225+
'30-Sep-2021',
226+
2,
227+
0,
228+
],
229+
// Excel and LibreOffice return 175 for the following calculation.
230+
// Gnumeric returns 176.
231+
// My hand calculation, hardly guaranteed, agrees with Gnumeric.
232+
[
233+
176,
234+
'05-Oct-2019',
235+
'31-Mar-2022',
236+
2,
237+
0,
238+
],
222239
];

tests/data/Calculation/Financial/COUPNCD.php

+28
Original file line numberDiff line numberDiff line change
@@ -219,4 +219,32 @@
219219
4,
220220
4,
221221
],
222+
[
223+
44651,
224+
'30-Sep-2021',
225+
'31-Mar-2022',
226+
2,
227+
0,
228+
],
229+
[
230+
44834,
231+
'31-Mar-2022',
232+
'30-Sep-2022',
233+
2,
234+
0,
235+
],
236+
[
237+
43738,
238+
'05-Apr-2019',
239+
'30-Sep-2021',
240+
2,
241+
0,
242+
],
243+
[
244+
43921,
245+
'05-Oct-2019',
246+
'31-Mar-2022',
247+
2,
248+
0,
249+
],
222250
];

tests/data/Calculation/Financial/COUPPCD.php

+14
Original file line numberDiff line numberDiff line change
@@ -219,4 +219,18 @@
219219
4,
220220
4,
221221
],
222+
[
223+
43555,
224+
'05-Apr-2019',
225+
'30-Sep-2021',
226+
2,
227+
0,
228+
],
229+
[
230+
43738,
231+
'05-Oct-2019',
232+
'31-Mar-2022',
233+
2,
234+
0,
235+
],
222236
];

0 commit comments

Comments
 (0)