Extract ACCRINT() and ACCRINTM() Financial functions into their own class (#1956)

* Extract ACCRINT() and ACCRINTM() Financial functions into their own class
Implement additional validations, with additional unit tests
Add support for the new calculation method argument for ACCRINT()
* Additional tests for Amortization functions
This commit is contained in:
Mark Baker 2021-03-26 22:49:16 +01:00 committed by GitHub
parent d36f9d5a23
commit c699d144e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 625 additions and 370 deletions

View File

@ -233,12 +233,12 @@ class Calculation
],
'ACCRINT' => [
'category' => Category::CATEGORY_FINANCIAL,
'functionCall' => [Financial::class, 'ACCRINT'],
'argumentCount' => '4-7',
'functionCall' => [Financial\Securities\AccruedInterest::class, 'periodic'],
'argumentCount' => '4-8',
],
'ACCRINTM' => [
'category' => Category::CATEGORY_FINANCIAL,
'functionCall' => [Financial::class, 'ACCRINTM'],
'functionCall' => [Financial\Securities\AccruedInterest::class, 'atMaturity'],
'argumentCount' => '3-5',
],
'ACOS' => [

View File

@ -0,0 +1,27 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
trait BaseValidations
{
protected static function validateFloat($value): float
{
if (!is_numeric($value)) {
throw new Exception(Functions::VALUE());
}
return (float) $value;
}
protected static function validateInt($value): int
{
if (!is_numeric($value)) {
throw new Exception(Functions::VALUE());
}
return (int) floor($value);
}
}

View File

@ -2,10 +2,13 @@
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class BesselI
{
use BaseValidations;
/**
* BESSELI.
*
@ -32,20 +35,22 @@ class BesselI
$x = ($x === null) ? 0.0 : Functions::flattenSingleValue($x);
$ord = ($ord === null) ? 0 : Functions::flattenSingleValue($ord);
if ((is_numeric($x)) && (is_numeric($ord))) {
$ord = (int) floor($ord);
try {
$x = self::validateFloat($x);
$ord = self::validateInt($ord);
} catch (Exception $e) {
return $e->getMessage();
}
if ($ord < 0) {
return Functions::NAN();
}
$fResult = self::calculate((float) $x, $ord);
$fResult = self::calculate($x, $ord);
return (is_nan($fResult)) ? Functions::NAN() : $fResult;
}
return Functions::VALUE();
}
private static function calculate(float $x, int $ord): float
{
// special cases

View File

@ -2,10 +2,13 @@
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class BesselJ
{
use BaseValidations;
/**
* BESSELJ.
*
@ -30,20 +33,22 @@ class BesselJ
$x = ($x === null) ? 0.0 : Functions::flattenSingleValue($x);
$ord = ($ord === null) ? 0.0 : Functions::flattenSingleValue($ord);
if ((is_numeric($x)) && (is_numeric($ord))) {
$ord = (int) floor($ord);
try {
$x = self::validateFloat($x);
$ord = self::validateInt($ord);
} catch (Exception $e) {
return $e->getMessage();
}
if ($ord < 0) {
return Functions::NAN();
}
$fResult = self::calculate((float) $x, $ord);
$fResult = self::calculate($x, $ord);
return (is_nan($fResult)) ? Functions::NAN() : $fResult;
}
return Functions::VALUE();
}
private static function calculate(float $x, int $ord): float
{
// special cases

View File

@ -2,10 +2,13 @@
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class BesselK
{
use BaseValidations;
/**
* BESSELK.
*
@ -28,9 +31,13 @@ class BesselK
$x = ($x === null) ? 0.0 : Functions::flattenSingleValue($x);
$ord = ($ord === null) ? 0 : Functions::flattenSingleValue($ord);
if ((is_numeric($x)) && (is_numeric($ord))) {
$ord = (int) floor($ord);
$x = (float) $x;
try {
$x = self::validateFloat($x);
$ord = self::validateInt($ord);
} catch (Exception $e) {
return $e->getMessage();
}
if (($ord < 0) || ($x <= 0.0)) {
return Functions::NAN();
}
@ -40,13 +47,10 @@ class BesselK
return (is_nan($fBk)) ? Functions::NAN() : $fBk;
}
return Functions::VALUE();
}
private static function calculate($x, $ord): float
private static function calculate(float $x, int $ord): float
{
// special cases
switch (floor($ord)) {
switch ($ord) {
case 0:
return self::besselK0($x);
case 1:

View File

@ -2,10 +2,13 @@
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class BesselY
{
use BaseValidations;
/**
* BESSELY.
*
@ -27,9 +30,13 @@ class BesselY
$x = ($x === null) ? 0.0 : Functions::flattenSingleValue($x);
$ord = ($ord === null) ? 0 : Functions::flattenSingleValue($ord);
if ((is_numeric($x)) && (is_numeric($ord))) {
$ord = (int) floor($ord);
$x = (float) $x;
try {
$x = self::validateFloat($x);
$ord = self::validateInt($ord);
} catch (Exception $e) {
return $e->getMessage();
}
if (($ord < 0) || ($x <= 0.0)) {
return Functions::NAN();
}
@ -39,13 +46,10 @@ class BesselY
return (is_nan($fBy)) ? Functions::NAN() : $fBy;
}
return Functions::VALUE();
}
private static function calculate($x, $ord): float
private static function calculate(float $x, int $ord): float
{
// special cases
switch (floor($ord)) {
switch ($ord) {
case 0:
return self::besselY0($x);
case 1:

View File

@ -2,10 +2,13 @@
namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class Compare
{
use BaseValidations;
/**
* DELTA.
*
@ -27,8 +30,11 @@ class Compare
$a = Functions::flattenSingleValue($a);
$b = Functions::flattenSingleValue($b);
if (!is_numeric($a) || !is_numeric($b)) {
return Functions::VALUE();
try {
$a = self::validateFloat($a);
$b = self::validateFloat($b);
} catch (Exception $e) {
return $e->getMessage();
}
return (int) ($a == $b);
@ -54,8 +60,11 @@ class Compare
$number = Functions::flattenSingleValue($number);
$step = Functions::flattenSingleValue($step);
if (!is_numeric($number) || !is_numeric($step)) {
return Functions::VALUE();
try {
$number = self::validateFloat($number);
$step = self::validateFloat($step);
} catch (Exception $e) {
return $e->getMessage();
}
return (int) ($number >= $step);

View File

@ -4,10 +4,13 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use Complex\Complex as ComplexObject;
use Complex\Exception as ComplexException;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class Complex
{
use BaseValidations;
/**
* COMPLEX.
*
@ -29,10 +32,14 @@ class Complex
$imaginary = ($imaginary === null) ? 0.0 : Functions::flattenSingleValue($imaginary);
$suffix = ($suffix === null) ? 'i' : Functions::flattenSingleValue($suffix);
if (
((is_numeric($realNumber)) && (is_numeric($imaginary))) &&
(($suffix == 'i') || ($suffix == 'j') || ($suffix == ''))
) {
try {
$realNumber = self::validateFloat($realNumber);
$imaginary = self::validateFloat($imaginary);
} catch (Exception $e) {
return $e->getMessage();
}
if (($suffix == 'i') || ($suffix == 'j') || ($suffix == '')) {
$complex = new ComplexObject($realNumber, $imaginary, $suffix);
return (string) $complex;

View File

@ -21,8 +21,8 @@ class Erf
* Excel Function:
* ERF(lower[,upper])
*
* @param float $lower lower bound for integrating ERF
* @param float $upper upper bound for integrating ERF.
* @param mixed (float) $lower lower bound for integrating ERF
* @param mixed (float) $upper upper bound for integrating ERF.
* If omitted, ERF integrates between zero and lower_limit
*
* @return float|string
@ -52,7 +52,7 @@ class Erf
* Excel Function:
* ERF.PRECISE(limit)
*
* @param float $limit bound for integrating ERF
* @param mixed (float) $limit bound for integrating ERF
*
* @return float|string
*/

View File

@ -35,7 +35,12 @@ class Financial
* Returns the accrued interest for a security that pays periodic interest.
*
* Excel Function:
* ACCRINT(issue,firstinterest,settlement,rate,par,frequency[,basis])
* ACCRINT(issue,firstinterest,settlement,rate,par,frequency[,basis][,calc_method])
*
* @Deprecated 1.18.0
*
* @see Financial\Securities\AccruedInterest::periodic()
* Use the periodic() method in the Financial\Securities\AccruedInterest class instead
*
* @param mixed $issue the security's issue date
* @param mixed $firstinterest the security's first interest date
@ -45,7 +50,7 @@ class Financial
* @param mixed (float) $rate the security's annual coupon rate
* @param mixed (float) $par The security's par value.
* If you omit par, ACCRINT uses $1,000.
* @param mixed (int) $frequency the number of coupon payments per year.
* @param mixed (int) $frequency The number of coupon payments per year.
* Valid frequency values are:
* 1 Annual
* 2 Semi-Annual
@ -56,36 +61,32 @@ class Financial
* 2 Actual/360
* 3 Actual/365
* 4 European 30/360
* @param mixed (bool) $calcMethod
* If true, use Issue to Settlement
* If false, use FirstInterest to Settlement
*
* @return float|string Result, or a string containing an error
*/
public static function ACCRINT($issue, $firstinterest, $settlement, $rate, $par = 1000, $frequency = 1, $basis = 0)
{
$issue = Functions::flattenSingleValue($issue);
$firstinterest = Functions::flattenSingleValue($firstinterest);
$settlement = Functions::flattenSingleValue($settlement);
$rate = Functions::flattenSingleValue($rate);
$par = ($par === null) ? 1000 : Functions::flattenSingleValue($par);
$frequency = ($frequency === null) ? 1 : Functions::flattenSingleValue($frequency);
$basis = ($basis === null) ? 0 : Functions::flattenSingleValue($basis);
// Validate
if ((is_numeric($rate)) && (is_numeric($par))) {
$rate = (float) $rate;
$par = (float) $par;
if (($rate <= 0) || ($par <= 0)) {
return Functions::NAN();
}
$daysBetweenIssueAndSettlement = DateTime::YEARFRAC($issue, $settlement, $basis);
if (!is_numeric($daysBetweenIssueAndSettlement)) {
// return date error
return $daysBetweenIssueAndSettlement;
}
return $par * $rate * $daysBetweenIssueAndSettlement;
}
return Functions::VALUE();
public static function ACCRINT(
$issue,
$firstinterest,
$settlement,
$rate,
$par = 1000,
$frequency = 1,
$basis = 0,
$calcMethod = true
) {
return Securities\AccruedInterest::periodic(
$issue,
$firstinterest,
$settlement,
$rate,
$par,
$frequency,
$basis,
$calcMethod
);
}
/**
@ -96,6 +97,11 @@ class Financial
* Excel Function:
* ACCRINTM(issue,settlement,rate[,par[,basis]])
*
* @Deprecated 1.18.0
*
* @see Financial\Securities\AccruedInterest::atMaturity()
* Use the atMaturity() method in the Financial\Securities\AccruedInterest class instead
*
* @param mixed $issue The security's issue date
* @param mixed $settlement The security's settlement (or maturity) date
* @param mixed (float) $rate The security's annual coupon rate
@ -112,29 +118,7 @@ class Financial
*/
public static function ACCRINTM($issue, $settlement, $rate, $par = 1000, $basis = 0)
{
$issue = Functions::flattenSingleValue($issue);
$settlement = Functions::flattenSingleValue($settlement);
$rate = Functions::flattenSingleValue($rate);
$par = ($par === null) ? 1000 : Functions::flattenSingleValue($par);
$basis = ($basis === null) ? 0 : Functions::flattenSingleValue($basis);
// Validate
if ((is_numeric($rate)) && (is_numeric($par))) {
$rate = (float) $rate;
$par = (float) $par;
if (($rate <= 0) || ($par <= 0)) {
return Functions::NAN();
}
$daysBetweenIssueAndSettlement = DateTime::YEARFRAC($issue, $settlement, $basis);
if (!is_numeric($daysBetweenIssueAndSettlement)) {
// return date error
return $daysBetweenIssueAndSettlement;
}
return $par * $rate * $daysBetweenIssueAndSettlement;
}
return Functions::VALUE();
return Securities\AccruedInterest::atMaturity($issue, $settlement, $rate, $par, $basis);
}
/**

View File

@ -3,10 +3,13 @@
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial;
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class Amortization
{
use BaseValidations;
/**
* AMORDEGRC.
*
@ -47,6 +50,18 @@ class Amortization
$rate = Functions::flattenSingleValue($rate);
$basis = ($basis === null) ? 0 : (int) Functions::flattenSingleValue($basis);
try {
$cost = self::validateFloat($cost);
$purchased = self::validateDate($purchased);
$firstPeriod = self::validateDate($firstPeriod);
$salvage = self::validateFloat($salvage);
$period = self::validateFloat($period);
$rate = self::validateFloat($rate);
$basis = self::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
$yearFrac = DateTimeExcel\YearFrac::funcYearFrac($purchased, $firstPeriod, $basis);
if (is_string($yearFrac)) {
return $yearFrac;
@ -113,6 +128,18 @@ class Amortization
$rate = Functions::flattenSingleValue($rate);
$basis = ($basis === null) ? 0 : (int) Functions::flattenSingleValue($basis);
try {
$cost = self::validateFloat($cost);
$purchased = self::validateDate($purchased);
$firstPeriod = self::validateDate($firstPeriod);
$salvage = self::validateFloat($salvage);
$period = self::validateFloat($period);
$rate = self::validateFloat($rate);
$basis = self::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
$fOneRate = $cost * $rate;
$fCostDelta = $cost - $salvage;
// Note, quirky variation for leap years on the YEARFRAC for this function

View File

@ -0,0 +1,72 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial;
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities\Constants as SecuritiesConstants;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
trait BaseValidations
{
protected static function validateDate($date)
{
return DateTimeExcel\Helpers::getDateValue($date);
}
protected static function validateSettlementDate($settlement)
{
return self::validateDate($settlement);
}
protected static function validateMaturityDate($maturity)
{
return self::validateDate($maturity);
}
protected static function validateFloat($value): float
{
if (!is_numeric($value)) {
throw new Exception(Functions::VALUE());
}
return (float) $value;
}
protected static function validateInt($value): int
{
if (!is_numeric($value)) {
throw new Exception(Functions::VALUE());
}
return (int) floor($value);
}
protected static function validateFrequency($frequency): int
{
$frequency = self::validateInt($frequency);
if (
($frequency !== SecuritiesConstants::FREQUENCY_ANNUAL) &&
($frequency !== SecuritiesConstants::FREQUENCY_SEMI_ANNUAL) &&
($frequency !== SecuritiesConstants::FREQUENCY_QUARTERLY)
) {
throw new Exception(Functions::NAN());
}
return $frequency;
}
protected static function validateBasis($basis): int
{
if (!is_numeric($basis)) {
throw new Exception(Functions::VALUE());
}
$basis = (int) $basis;
if (($basis < 0) || ($basis > 4)) {
throw new Exception(Functions::NAN());
}
return $basis;
}
}

View File

@ -2,7 +2,6 @@
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial;
use DateTime;
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
@ -10,6 +9,8 @@ use PhpOffice\PhpSpreadsheet\Shared\Date;
class Coupons
{
use BaseValidations;
public const FREQUENCY_ANNUAL = 1;
public const FREQUENCY_SEMI_ANNUAL = 2;
public const FREQUENCY_QUARTERLY = 4;
@ -62,6 +63,9 @@ class Coupons
}
$daysPerYear = Helpers::daysPerYear(DateTimeExcel\Year::funcYear($settlement), $basis);
if (is_string($daysPerYear)) {
return Functions::VALUE();
}
$prev = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_PREVIOUS);
if ($basis === Helpers::DAYS_PER_YEAR_ACTUAL) {
@ -185,7 +189,7 @@ class Coupons
if ($basis === Helpers::DAYS_PER_YEAR_NASD) {
$settlementDate = Date::excelToDateTimeObject($settlement);
$settlementEoM = self::isLastDayOfMonth($settlementDate);
$settlementEoM = Helpers::isLastDayOfMonth($settlementDate);
if ($settlementEoM) {
++$settlement;
}
@ -340,26 +344,12 @@ class Coupons
return self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_PREVIOUS);
}
/**
* isLastDayOfMonth.
*
* Returns a boolean TRUE/FALSE indicating if this date is the last date of the month
*
* @param DateTime $testDate The date for testing
*
* @return bool
*/
private static function isLastDayOfMonth(DateTime $testDate)
{
return $testDate->format('d') === $testDate->format('t');
}
private static function couponFirstPeriodDate($settlement, $maturity, int $frequency, $next)
{
$months = 12 / $frequency;
$result = Date::excelToDateTimeObject($maturity);
$maturityEoM = self::isLastDayOfMonth($result);
$maturityEoM = Helpers::isLastDayOfMonth($result);
while ($settlement < Date::PHPToExcel($result)) {
$result->modify('-' . $months . ' months');
@ -375,62 +365,10 @@ class Coupons
return Date::PHPToExcel($result);
}
private static function validateInputDate($date)
{
$date = DateTimeExcel\Helpers::getDateValue($date);
if (is_string($date)) {
throw new Exception(Functions::VALUE());
}
return $date;
}
private static function validateSettlementDate($settlement)
{
return self::validateInputDate($settlement);
}
private static function validateMaturityDate($maturity)
{
return self::validateInputDate($maturity);
}
private static function validateCouponPeriod($settlement, $maturity): void
{
if ($settlement >= $maturity) {
throw new Exception(Functions::NAN());
}
}
private static function validateFrequency($frequency): int
{
if (!is_numeric($frequency)) {
throw new Exception(Functions::NAN());
}
$frequency = (int) $frequency;
if (
($frequency !== self::FREQUENCY_ANNUAL) &&
($frequency !== self::FREQUENCY_SEMI_ANNUAL) &&
($frequency !== self::FREQUENCY_QUARTERLY)
) {
throw new Exception(Functions::NAN());
}
return $frequency;
}
private static function validateBasis($basis): int
{
if (!is_numeric($basis)) {
throw new Exception(Functions::NAN());
}
$basis = (int) $basis;
if (($basis < 0) || ($basis > 4)) {
throw new Exception(Functions::NAN());
}
return $basis;
}
}

View File

@ -7,6 +7,8 @@ use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class Depreciation
{
use BaseValidations;
/**
* DB.
*
@ -203,11 +205,7 @@ class Depreciation
private static function validateCost($cost, bool $negativeValueAllowed = false): float
{
if (!is_numeric($cost)) {
throw new Exception(Functions::VALUE());
}
$cost = (float) $cost;
$cost = self::validateFloat($cost);
if ($cost < 0.0 && $negativeValueAllowed === false) {
throw new Exception(Functions::NAN());
}
@ -217,11 +215,7 @@ class Depreciation
private static function validateSalvage($salvage, bool $negativeValueAllowed = false): float
{
if (!is_numeric($salvage)) {
throw new Exception(Functions::VALUE());
}
$salvage = (float) $salvage;
$salvage = self::validateFloat($salvage);
if ($salvage < 0.0 && $negativeValueAllowed === false) {
throw new Exception(Functions::NAN());
}
@ -231,11 +225,7 @@ class Depreciation
private static function validateLife($life, bool $negativeValueAllowed = false): float
{
if (!is_numeric($life)) {
throw new Exception(Functions::VALUE());
}
$life = (float) $life;
$life = self::validateFloat($life);
if ($life < 0.0 && $negativeValueAllowed === false) {
throw new Exception(Functions::NAN());
}
@ -245,11 +235,7 @@ class Depreciation
private static function validatePeriod($period, bool $negativeValueAllowed = false): float
{
if (!is_numeric($period)) {
throw new Exception(Functions::VALUE());
}
$period = (float) $period;
$period = self::validateFloat($period);
if ($period <= 0.0 && $negativeValueAllowed === false) {
throw new Exception(Functions::NAN());
}
@ -259,11 +245,7 @@ class Depreciation
private static function validateMonth($month): int
{
if (!is_numeric($month)) {
throw new Exception(Functions::VALUE());
}
$month = (int) $month;
$month = self::validateInt($month);
if ($month < 1) {
throw new Exception(Functions::NAN());
}
@ -273,11 +255,7 @@ class Depreciation
private static function validateFactor($factor): float
{
if (!is_numeric($factor)) {
throw new Exception(Functions::VALUE());
}
$factor = (float) $factor;
$factor = self::validateFloat($factor);
if ($factor <= 0.0) {
throw new Exception(Functions::NAN());
}

View File

@ -2,6 +2,7 @@
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial;
use DateTimeInterface;
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
@ -47,4 +48,18 @@ class Helpers
return Functions::NAN();
}
/**
* isLastDayOfMonth.
*
* Returns a boolean TRUE/FALSE indicating if this date is the last date of the month
*
* @param DateTimeInterface $date The date for testing
*
* @return bool
*/
public static function isLastDayOfMonth(DateTimeInterface $date)
{
return $date->format('d') === $date->format('t');
}
}

View File

@ -2,10 +2,13 @@
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class InterestRate
{
use BaseValidations;
/**
* EFFECT.
*
@ -25,16 +28,17 @@ class InterestRate
$nominalRate = Functions::flattenSingleValue($nominalRate);
$periodsPerYear = Functions::flattenSingleValue($periodsPerYear);
// Validate parameters
if (!is_numeric($nominalRate) || !is_numeric($periodsPerYear)) {
return Functions::VALUE();
try {
$nominalRate = self::validateFloat($nominalRate);
$periodsPerYear = self::validateInt($periodsPerYear);
} catch (Exception $e) {
return $e->getMessage();
}
if ($nominalRate <= 0 || $periodsPerYear < 1) {
return Functions::NAN();
}
$periodsPerYear = (int) $periodsPerYear;
return ((1 + $nominalRate / $periodsPerYear) ** $periodsPerYear) - 1;
}
@ -53,16 +57,17 @@ class InterestRate
$effectiveRate = Functions::flattenSingleValue($effectiveRate);
$periodsPerYear = Functions::flattenSingleValue($periodsPerYear);
// Validate parameters
if (!is_numeric($effectiveRate) || !is_numeric($periodsPerYear)) {
return Functions::VALUE();
try {
$effectiveRate = self::validateFloat($effectiveRate);
$periodsPerYear = self::validateInt($periodsPerYear);
} catch (Exception $e) {
return $e->getMessage();
}
if ($effectiveRate <= 0 || $periodsPerYear < 1) {
return Functions::NAN();
}
$periodsPerYear = (int) $periodsPerYear;
// Calculate
return $periodsPerYear * (($effectiveRate + 1) ** (1 / $periodsPerYear) - 1);
}

View File

@ -0,0 +1,143 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities;
use PhpOffice\PhpSpreadsheet\Calculation\DateTime;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class AccruedInterest
{
use BaseValidations;
public const ACCRINT_CALCMODE_ISSUE_TO_SETTLEMENT = true;
public const ACCRINT_CALCMODE_FIRST_INTEREST_TO_SETTLEMENT = false;
/**
* ACCRINT.
*
* Returns the accrued interest for a security that pays periodic interest.
*
* Excel Function:
* ACCRINT(issue,firstinterest,settlement,rate,par,frequency[,basis][,calc_method])
*
* @param mixed $issue the security's issue date
* @param mixed $firstinterest the security's first interest date
* @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 (float) $rate The security's annual coupon rate
* @param mixed (float) $par The security's par value.
* If you omit par, ACCRINT uses $1,000.
* @param mixed (int) $frequency The number of coupon payments per year.
* Valid frequency values are:
* 1 Annual
* 2 Semi-Annual
* 4 Quarterly
* @param mixed (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
* @param mixed $parValue
* @param mixed $calcMethod
*
* @return float|string Result, or a string containing an error
*/
public static function periodic(
$issue,
$firstinterest,
$settlement,
$rate,
$parValue = 1000,
$frequency = 1,
$basis = 0,
$calcMethod = self::ACCRINT_CALCMODE_ISSUE_TO_SETTLEMENT
) {
$issue = Functions::flattenSingleValue($issue);
$firstinterest = Functions::flattenSingleValue($firstinterest);
$settlement = Functions::flattenSingleValue($settlement);
$rate = Functions::flattenSingleValue($rate);
$parValue = ($parValue === null) ? 1000 : Functions::flattenSingleValue($parValue);
$frequency = ($frequency === null) ? 1 : Functions::flattenSingleValue($frequency);
$basis = ($basis === null) ? 0 : Functions::flattenSingleValue($basis);
try {
$issue = self::validateIssueDate($issue);
$settlement = self::validateSettlementDate($settlement);
self::validateSecurityPeriod($issue, $settlement);
$rate = self::validateRate($rate);
$parValue = self::validateParValue($parValue);
$frequency = self::validateFrequency($frequency);
$basis = self::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
$daysBetweenIssueAndSettlement = DateTime::YEARFRAC($issue, $settlement, $basis);
if (!is_numeric($daysBetweenIssueAndSettlement)) {
// return date error
return $daysBetweenIssueAndSettlement;
}
$daysBetweenFirstInterestAndSettlement = DateTime::YEARFRAC($firstinterest, $settlement, $basis);
if (!is_numeric($daysBetweenFirstInterestAndSettlement)) {
// return date error
return $daysBetweenFirstInterestAndSettlement;
}
return $parValue * $rate * $daysBetweenIssueAndSettlement;
}
/**
* ACCRINTM.
*
* Returns the accrued interest for a security that pays interest at maturity.
*
* Excel Function:
* ACCRINTM(issue,settlement,rate[,par[,basis]])
*
* @param mixed $issue The security's issue date
* @param mixed $settlement The security's settlement (or maturity) date
* @param mixed (float) $rate The security's annual coupon rate
* @param mixed (float) $par The security's par value.
* If you omit par, ACCRINT uses $1,000.
* @param mixed (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
* @param mixed $parValue
*
* @return float|string Result, or a string containing an error
*/
public static function atMaturity($issue, $settlement, $rate, $parValue = 1000, $basis = 0)
{
$issue = Functions::flattenSingleValue($issue);
$settlement = Functions::flattenSingleValue($settlement);
$rate = Functions::flattenSingleValue($rate);
$parValue = ($parValue === null) ? 1000 : Functions::flattenSingleValue($parValue);
$basis = ($basis === null) ? 0 : Functions::flattenSingleValue($basis);
try {
$issue = self::validateIssueDate($issue);
$settlement = self::validateSettlementDate($settlement);
self::validateSecurityPeriod($issue, $settlement);
$rate = self::validateRate($rate);
$parValue = self::validateParValue($parValue);
$basis = self::validateBasis($basis);
} catch (Exception $e) {
return $e->getMessage();
}
$daysBetweenIssueAndSettlement = DateTime::YEARFRAC($issue, $settlement, $basis);
if (!is_numeric($daysBetweenIssueAndSettlement)) {
// return date error
return $daysBetweenIssueAndSettlement;
}
return $parValue * $rate * $daysBetweenIssueAndSettlement;
}
}

View File

@ -7,31 +7,35 @@ use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities\Constants as SecuritiesConstants;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
abstract class BaseValidations
trait BaseValidations
{
protected static function validateInputDate($date)
protected static function validateDate($date)
{
$date = DateTimeExcel\Helpers::getDateValue($date);
if (is_string($date)) {
return DateTimeExcel\Helpers::getDateValue($date);
}
protected static function validateFloat($value): float
{
if (!is_numeric($value)) {
throw new Exception(Functions::VALUE());
}
return $date;
return (float) $value;
}
protected static function validateSettlementDate($settlement)
{
return self::validateInputDate($settlement);
return self::validateDate($settlement);
}
protected static function validateMaturityDate($maturity)
{
return self::validateInputDate($maturity);
return self::validateDate($maturity);
}
protected static function validateIssueDate($issue)
{
return self::validateInputDate($issue);
return self::validateDate($issue);
}
protected static function validateSecurityPeriod($settlement, $maturity): void
@ -43,11 +47,7 @@ abstract class BaseValidations
protected static function validateRate($rate): float
{
if (!is_numeric($rate)) {
throw new Exception(Functions::VALUE());
}
$rate = (float) $rate;
$rate = self::validateFloat($rate);
if ($rate < 0.0) {
throw new Exception(Functions::NAN());
}
@ -55,13 +55,19 @@ abstract class BaseValidations
return $rate;
}
protected static function validatePrice($price): float
protected static function validateParValue($parValue): float
{
if (!is_numeric($price)) {
throw new Exception(Functions::VALUE());
$parValue = self::validateFloat($parValue);
if ($parValue < 0.0) {
throw new Exception(Functions::NAN());
}
$price = (float) $price;
return $parValue;
}
protected static function validatePrice($price): float
{
$price = self::validateFloat($price);
if ($price < 0.0) {
throw new Exception(Functions::NAN());
}
@ -71,11 +77,7 @@ abstract class BaseValidations
protected static function validateYield($yield): float
{
if (!is_numeric($yield)) {
throw new Exception(Functions::VALUE());
}
$yield = (float) $yield;
$yield = self::validateFloat($yield);
if ($yield < 0.0) {
throw new Exception(Functions::NAN());
}
@ -85,11 +87,7 @@ abstract class BaseValidations
protected static function validateRedemption($redemption): float
{
if (!is_numeric($redemption)) {
throw new Exception(Functions::VALUE());
}
$redemption = (float) $redemption;
$redemption = self::validateFloat($redemption);
if ($redemption <= 0.0) {
throw new Exception(Functions::NAN());
}
@ -99,11 +97,7 @@ abstract class BaseValidations
protected static function validateDiscount($discount): float
{
if (!is_numeric($discount)) {
throw new Exception(Functions::VALUE());
}
$discount = (float) $discount;
$discount = self::validateFloat($discount);
if ($discount <= 0.0) {
throw new Exception(Functions::NAN());
}

View File

@ -8,8 +8,10 @@ use PhpOffice\PhpSpreadsheet\Calculation\Financial\Coupons;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Helpers;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class Price extends BaseValidations
class Price
{
use BaseValidations;
/**
* PRICE.
*

View File

@ -7,8 +7,10 @@ use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Helpers;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class Yields extends BaseValidations
class Yields
{
use BaseValidations;
/**
* YIELDDISC.
*

View File

@ -8,6 +8,8 @@ use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class TreasuryBill
{
use BaseValidations;
/**
* TBILLEQ.
*
@ -29,8 +31,8 @@ class TreasuryBill
$discount = Functions::flattenSingleValue($discount);
try {
$maturity = DateTimeExcel\Helpers::getDateValue($maturity);
$settlement = DateTimeExcel\Helpers::getDateValue($settlement);
$settlement = self::validateSettlementDate($settlement);
$maturity = self::validateMaturityDate($maturity);
} catch (Exception $e) {
return $e->getMessage();
}
@ -75,8 +77,8 @@ class TreasuryBill
$discount = Functions::flattenSingleValue($discount);
try {
$maturity = DateTimeExcel\Helpers::getDateValue($maturity);
$settlement = DateTimeExcel\Helpers::getDateValue($settlement);
$settlement = self::validateSettlementDate($settlement);
$maturity = self::validateMaturityDate($maturity);
} catch (Exception $e) {
return $e->getMessage();
}
@ -126,8 +128,8 @@ class TreasuryBill
$price = Functions::flattenSingleValue($price);
try {
$maturity = DateTimeExcel\Helpers::getDateValue($maturity);
$settlement = DateTimeExcel\Helpers::getDateValue($settlement);
$settlement = self::validateSettlementDate($settlement);
$maturity = self::validateMaturityDate($maturity);
} catch (Exception $e) {
return $e->getMessage();
}

View File

@ -21,7 +21,7 @@ class AccrintMTest extends TestCase
public function testACCRINTM($expectedResult, ...$args): void
{
$result = Financial::ACCRINTM(...$args);
self::assertEqualsWithDelta($expectedResult, $result, 1E-8);
self::assertEqualsWithDelta($expectedResult, $result, 1E-12);
}
public function providerACCRINTM()

View File

@ -21,7 +21,7 @@ class AccrintTest extends TestCase
public function testACCRINT($expectedResult, ...$args): void
{
$result = Financial::ACCRINT(...$args);
self::assertEqualsWithDelta($expectedResult, $result, 1E-8);
self::assertEqualsWithDelta($expectedResult, $result, 1E-12);
}
public function providerACCRINT()

View File

@ -4,72 +4,83 @@
return [
[
16.666666666666998,
'2008-03-01',
'2008-08-31',
'2008-05-01',
0.10000000000000001,
1000,
2,
0,
16.6666666666666,
'2008-03-01', '2008-08-31', '2008-05-01', 0.10, 1000, 2, 0,
],
[
15.555555555555999,
'2008-03-05',
'2008-08-31',
'2008-05-01',
0.10000000000000001,
1000,
2,
0,
15.5555555555559,
'2008-03-05', '2008-08-31', '2008-05-01', 0.10, 1000, 2, 0,
],
[
15.5555555555559,
'2008-03-05', '2008-08-31', '2008-05-01', 0.10, 1000, 2, 0, true,
],
[
7.22222222222222,
'2008-04-05', '2008-08-31', '2008-05-01', 0.10, 1000, 2, 0, true,
],
[
200,
'2010-01-01',
'2010-06-30',
'2010-04-01',
0.080000000000000002,
10000,
4,
'2010-01-01', '2010-06-30', '2010-04-01', 0.08, 10000, 4,
],
[
1600,
'2012-01-01', '2012-04-01', '2013-12-31', 0.08, 10000, 4,
],
[
32.363013698630134,
'2012-01-01', '2012-03-31', '2012-02-15', 0.0525, 5000, 4, 3, 1,
],
[
6.472602739726027,
'2012-01-01', '2012-03-31', '2012-02-15', 0.0525, 1000, 4, 3, 1,
],
[
18.05555555555555,
'2017-08-05', '2017-11-10', '2017-10-10', 0.05, 2000, 4, 0, 1,
],
[
'#NUM!',
'2008-03-05',
'2008-08-31',
'2008-05-01',
-0.10000000000000001,
1000,
2,
0,
'2008-03-05', '2008-08-31', '2008-05-01', -0.10, 1000, 2, 0,
],
[
'#VALUE!',
'Invalid Date',
'2008-08-31',
'2008-05-01',
0.10000000000000001,
1000,
2,
0,
'Invalid Date', '2008-08-31', '2008-05-01', 0.10, 1000, 2, 0,
],
[
'#VALUE!',
'2008-03-01',
'2008-08-31',
'2008-05-01',
'ABC',
1000,
2,
0,
'2008-03-01', '2008-08-31', '2008-05-01', 'ABC', 1000, 2, 0,
],
[
'Non-numeric Rate' => [
'#VALUE!',
'2008-03-01',
'2008-08-31',
'2008-05-01',
0.10000000000000001,
1000,
2,
'ABC',
'2008-03-01', '2008-08-31', '2008-05-01', 'NaN', 1000, 2, 0,
],
'Invalid Rate' => [
'#NUM!',
'2008-03-01', '2008-08-31', '2008-05-01', -0.10, 1000, 2, 0,
],
'Non-numeric Par Value' => [
'#VALUE!',
'2008-03-01', '2008-08-31', '2008-05-01', 0.10, 'NaN', 2, 0,
],
'Invalid Par Value' => [
'#NUM!',
'2008-03-01', '2008-08-31', '2008-05-01', 0.10, -1000, 2, 0,
],
'Non-numeric Frequency' => [
'#VALUE!',
'2008-03-01', '2008-08-31', '2008-05-01', 0.10, 1000, 'NaN', 0,
],
'Invalid Frequency' => [
'#NUM!',
'2008-03-01', '2008-08-31', '2008-05-01', 0.10, -1000, 3, 0,
],
'Non-numeric Basis' => [
'#VALUE!',
'2008-03-01', '2008-08-31', '2008-05-01', 0.10, 1000, 2, 'ABC',
],
'Invalid Basis' => [
'#NUM!',
'2008-03-01', '2008-08-31', '2008-05-01', 0.10, 1000, 2, -2,
],
];

View File

@ -5,41 +5,50 @@
return [
[
20.547945205478999,
'2008-04-01',
'2008-06-15',
0.10000000000000001,
1000,
3,
'2008-04-01', '2008-06-15', 0.10, 1000, 3,
],
[
800,
'2010-01-01',
'2010-12-31',
0.080000000000000002,
10000,
'2010-01-01', '2010-12-31', 0.08, 10000,
],
[
365.958904109589,
'2012-01-01', '2013-02-15', 0.065, 5000, 3,
],
[
73.1917808219178,
'2012-01-01', '2013-02-15', 0.065, 1000, 3,
],
[
'#NUM!',
'2008-03-05',
'2008-08-31',
-0.10000000000000001,
1000,
2,
'2008-03-05', '2008-08-31', -0.10, 1000, 2,
],
[
'#VALUE!',
'Invalid Date',
'2008-08-31',
0.10000000000000001,
1000,
2,
'Invalid Date', '2008-08-31', 0.10, 1000, 2,
],
[
'Non-numeric Rate' => [
'#VALUE!',
'2008-03-01',
'2008-08-31',
'ABC',
1000,
2,
'2008-03-01', '2008-08-31', 'NaN', 1000, 2,
],
'Invalid Rate' => [
'#NUM!',
'2008-03-01', '2008-08-31', -0.10, 1000, 2,
],
'Non-numeric Par Value' => [
'#VALUE!',
'2008-03-01', '2008-08-31', 0.10, 'NaN', 2,
],
'Invalid Par Value' => [
'#NUM!',
'2008-03-01', '2008-08-31', 0.10, -1000, 2,
],
'Non-numeric Basis' => [
'#VALUE!',
'2008-03-01', '2008-08-31', 0.10, 1000, 'NaN',
],
'Invalid Basis' => [
'#NUM!',
'2008-03-01', '2008-08-31', 0.10, 1000, 99,
],
];

View File

@ -47,6 +47,14 @@ return [
42,
150, '2011-01-01', '2011-09-30', 20, 1, 0.4, 4,
],
[
2813,
10000, '2012-03-01', '2012-12-31', 1500, 1, 0.3, 1,
],
[
'#VALUE!',
'NaN', '2012-03-01', '2020-12-25', 20, 1, 0.2, 4,
],
[
'#VALUE!',
550, 'notADate', '2020-12-25', 20, 1, 0.2, 4,
@ -55,4 +63,8 @@ return [
'#VALUE!',
550, '2011-01-01', 'notADate', 20, 1, 0.2, 4,
],
[
'#VALUE!',
550, '2012-03-01', '2020-12-25', 'NaN', 1, 0.2, 4,
],
];

View File

@ -45,7 +45,7 @@ return [
1,
],
'Non-Numeric Frequency' => [
'#NUM!',
'#VALUE!',
'25-Jan-2007',
'15-Nov-2008',
'NaN',
@ -59,7 +59,7 @@ return [
-1,
],
'Non-Numeric Basis' => [
'#NUM!',
'#VALUE!',
'25-Jan-2007',
'15-Nov-2008',
4,

View File

@ -59,7 +59,7 @@ return [
1,
],
'Non-Numeric Frequency' => [
'#NUM!',
'#VALUE!',
'25-Jan-2007',
'15-Nov-2008',
'NaN',
@ -73,7 +73,7 @@ return [
-1,
],
'Non-Numeric Basis' => [
'#NUM!',
'#VALUE!',
'25-Jan-2007',
'15-Nov-2008',
4,

View File

@ -38,7 +38,7 @@ return [
1,
],
'Non-Numeric Frequency' => [
'#NUM!',
'#VALUE!',
'25-Jan-2007',
'15-Nov-2008',
'NaN',
@ -52,7 +52,7 @@ return [
-1,
],
'Non-Numeric Basis' => [
'#NUM!',
'#VALUE!',
'25-Jan-2007',
'15-Nov-2008',
4,

View File

@ -38,7 +38,7 @@ return [
1,
],
'Non-Numeric Frequency' => [
'#NUM!',
'#VALUE!',
'25-Jan-2007',
'15-Nov-2008',
'NaN',
@ -52,7 +52,7 @@ return [
-1,
],
'Non-Numeric Basis' => [
'#NUM!',
'#VALUE!',
'25-Jan-2007',
'15-Nov-2008',
4,

View File

@ -39,7 +39,7 @@ return [
1,
],
'Non-Numeric Frequency' => [
'#NUM!',
'#VALUE!',
'25-Jan-2007',
'15-Nov-2008',
'NaN',
@ -53,7 +53,7 @@ return [
-1,
],
'Non-Numeric Basis' => [
'#NUM!',
'#VALUE!',
'25-Jan-2007',
'15-Nov-2008',
4,

View File

@ -38,7 +38,7 @@ return [
1,
],
'Non-Numeric Frequency' => [
'#NUM!',
'#VALUE!',
'25-Jan-2007',
'15-Nov-2008',
'NaN',
@ -52,7 +52,7 @@ return [
-1,
],
'Non-Numeric Basis' => [
'#NUM!',
'#VALUE!',
'25-Jan-2007',
'15-Nov-2008',
4,