Skip to content

COUPNUM should not return zero when settlement is in the last period #1020

@myfonj

Description

@myfonj

Apparently what docs say ("rounded up to the nearest whole coupon") actually means that COUPNUM can never return 0.

=COUPNUM(DATE(2000;12;24);DATE(2000;12;24);4;0)
echo Financial::COUPNUM(date_create('2000-12-24'),date_create('2000-12-24'),4,0);
// settlement = maturity → Excel 2016 gives #NUMBER,  PHPOffice -1

=COUPNUM(DATE(2000;12;23);DATE(2000;12;24);4;0)
echo Financial::COUPNUM(date_create('2000-12-23'),date_create('2000-12-24'),4,0);
// settlement is one day before maturity → Excel 2016 gives 1, PHPOffice 0

=COUPNUM(DATE(2000;9;24);DATE(2000;12;24);4;0)
echo Financial::COUPNUM(date_create('2000-09-24'),date_create('2000-12-24'),4,0);
// settlement is on first coupon day before maturity → Excel 2016 gives 1, PHPOffice 0 

=COUPNUM(DATE(2000;9;23);DATE(2000;12;24);4;0)
echo Financial::COUPNUM(date_create('2000-09-23'),date_create('2000-12-24'),4,0);
// settlement is on day before first coupon day before maturity → Excel 2016 gives 2, PHPOffice 2 

Also, upon investigating it turned out current COUPNUM implementation somewhat lives in 365 year, or something.

/**
* COUPNUM.
*
* Returns the number of coupons payable between the settlement date and maturity date,
* rounded up to the nearest whole coupon.
*
* Excel Function:
* COUPNUM(settlement,maturity,frequency[,basis])
*
* @category Financial Functions
*
* @param mixed $settlement The security's settlement date.
* The security settlement date is the date after the issue
* date when the security is traded to the buyer.
* @param mixed $maturity The security's maturity date.
* The maturity date is the date when the security expires.
* @param mixed $frequency the number of coupon payments per year.
* Valid frequency values are:
* 1 Annual
* 2 Semi-Annual
* 4 Quarterly
* If working in Gnumeric Mode, the following frequency options are
* also available
* 6 Bimonthly
* 12 Monthly
* @param int $basis The type of day count to use.
* 0 or omitted US (NASD) 30/360
* 1 Actual/actual
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
*
* @return int|string
*/
public static function COUPNUM($settlement, $maturity, $frequency, $basis = 0)
{
$settlement = Functions::flattenSingleValue($settlement);
$maturity = Functions::flattenSingleValue($maturity);
$frequency = (int) Functions::flattenSingleValue($frequency);
$basis = ($basis === null) ? 0 : (int) Functions::flattenSingleValue($basis);
if (is_string($settlement = DateTime::getDateValue($settlement))) {
return Functions::VALUE();
}
if (is_string($maturity = DateTime::getDateValue($maturity))) {
return Functions::VALUE();
}
if (($settlement > $maturity) ||
(!self::isValidFrequency($frequency)) ||
(($basis < 0) || ($basis > 4))) {
return Functions::NAN();
}
$settlement = self::couponFirstPeriodDate($settlement, $maturity, $frequency, true);
$daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis) * 365;
switch ($frequency) {
case 1: // annual payments
return ceil($daysBetweenSettlementAndMaturity / 360);
case 2: // half-yearly
return ceil($daysBetweenSettlementAndMaturity / 180);
case 4: // quarterly
return ceil($daysBetweenSettlementAndMaturity / 90);
case 6: // bimonthly
return ceil($daysBetweenSettlementAndMaturity / 60);
case 12: // monthly
return ceil($daysBetweenSettlementAndMaturity / 30);
}
return Functions::VALUE();
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions