diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 05fe8a81..36f5efe4 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -1983,17 +1983,17 @@ class Calculation ], 'PRICE' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'PRICE'], + 'functionCall' => [Financial\Securities::class, 'price'], 'argumentCount' => '6,7', ], 'PRICEDISC' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'PRICEDISC'], + 'functionCall' => [Financial\Securities::class, 'discounted'], 'argumentCount' => '4,5', ], 'PRICEMAT' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'PRICEMAT'], + 'functionCall' => [Financial\Securities::class, 'maturity'], 'argumentCount' => '5,6', ], 'PROB' => [ diff --git a/src/PhpSpreadsheet/Calculation/Financial.php b/src/PhpSpreadsheet/Calculation/Financial.php index f6f5c775..9735e9f4 100644 --- a/src/PhpSpreadsheet/Calculation/Financial.php +++ b/src/PhpSpreadsheet/Calculation/Financial.php @@ -11,15 +11,6 @@ class Financial const FINANCIAL_PRECISION = 1.0e-08; - private static function isValidFrequency($frequency) - { - if (($frequency == 1) || ($frequency == 2) || ($frequency == 4)) { - return true; - } - - return false; - } - private static function interestAndPrincipal($rate = 0, $per = 0, $nper = 0, $pv = 0, $fv = 0, $type = 0) { $pmt = self::PMT($rate, $nper, $pv, $fv, $type); @@ -1370,79 +1361,39 @@ class Financial return $interestAndPrincipal[1]; } - private static function validatePrice($settlement, $maturity, $rate, $yield, $redemption, $frequency, $basis) - { - if (is_string($settlement)) { - return Functions::VALUE(); - } - if (is_string($maturity)) { - return Functions::VALUE(); - } - if (!is_numeric($rate)) { - return Functions::VALUE(); - } - if (!is_numeric($yield)) { - return Functions::VALUE(); - } - if (!is_numeric($redemption)) { - return Functions::VALUE(); - } - if (!is_numeric($frequency)) { - return Functions::VALUE(); - } - if (!is_numeric($basis)) { - return Functions::VALUE(); - } - - return ''; - } - + /** + * PRICE. + * + * Returns the price per $100 face value of a security that pays periodic interest. + * + * @Deprecated 1.18.0 + * + * @see Use the price() method in the Financial\Securities class instead + * + * @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 float $rate the security's annual coupon rate + * @param float $yield the security's annual yield + * @param float $redemption The number of coupon payments per year. + * For annual payments, frequency = 1; + * for semiannual, frequency = 2; + * for quarterly, frequency = 4. + * @param int $frequency + * @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 float|string Result, or a string containing an error + */ public static function PRICE($settlement, $maturity, $rate, $yield, $redemption, $frequency, $basis = 0) { - $settlement = Functions::flattenSingleValue($settlement); - $maturity = Functions::flattenSingleValue($maturity); - $rate = Functions::flattenSingleValue($rate); - $yield = Functions::flattenSingleValue($yield); - $redemption = Functions::flattenSingleValue($redemption); - $frequency = Functions::flattenSingleValue($frequency); - $basis = Functions::flattenSingleValue($basis); - - $settlement = DateTime::getDateValue($settlement); - $maturity = DateTime::getDateValue($maturity); - $rslt = self::validatePrice($settlement, $maturity, $rate, $yield, $redemption, $frequency, $basis); - if ($rslt) { - return $rslt; - } - $rate = (float) $rate; - $yield = (float) $yield; - $redemption = (float) $redemption; - $frequency = (int) $frequency; - $basis = (int) $basis; - - if ( - ($settlement > $maturity) || - (!self::isValidFrequency($frequency)) || - (($basis < 0) || ($basis > 4)) - ) { - return Functions::NAN(); - } - - $dsc = self::COUPDAYSNC($settlement, $maturity, $frequency, $basis); - $e = self::COUPDAYS($settlement, $maturity, $frequency, $basis); - $n = self::COUPNUM($settlement, $maturity, $frequency, $basis); - $a = self::COUPDAYBS($settlement, $maturity, $frequency, $basis); - - $baseYF = 1.0 + ($yield / $frequency); - $rfp = 100 * ($rate / $frequency); - $de = $dsc / $e; - - $result = $redemption / $baseYF ** (--$n + $de); - for ($k = 0; $k <= $n; ++$k) { - $result += $rfp / ($baseYF ** ($k + $de)); - } - $result -= $rfp * ($a / $e); - - return $result; + return Financial\Securities::price($settlement, $maturity, $rate, $yield, $redemption, $frequency, $basis); } /** @@ -1450,6 +1401,10 @@ class Financial * * Returns the price per $100 face value of a discounted security. * + * @Deprecated 1.18.0 + * + * @see Use the discounted() method in the Financial\Securities class instead + * * @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. @@ -1467,27 +1422,7 @@ class Financial */ public static function PRICEDISC($settlement, $maturity, $discount, $redemption, $basis = 0) { - $settlement = Functions::flattenSingleValue($settlement); - $maturity = Functions::flattenSingleValue($maturity); - $discount = (float) Functions::flattenSingleValue($discount); - $redemption = (float) Functions::flattenSingleValue($redemption); - $basis = (int) Functions::flattenSingleValue($basis); - - // Validate - if ((is_numeric($discount)) && (is_numeric($redemption)) && (is_numeric($basis))) { - if (($discount <= 0) || ($redemption <= 0)) { - return Functions::NAN(); - } - $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis); - if (!is_numeric($daysBetweenSettlementAndMaturity)) { - // return date error - return $daysBetweenSettlementAndMaturity; - } - - return $redemption * (1 - $discount * $daysBetweenSettlementAndMaturity); - } - - return Functions::VALUE(); + return Financial\Securities::discounted($settlement, $maturity, $discount, $redemption, $basis); } /** @@ -1495,6 +1430,10 @@ class Financial * * Returns the price per $100 face value of a security that pays interest at maturity. * + * @Deprecated 1.18.0 + * + * @see Use the maturity() method in the Financial\Securities class instead + * * @param mixed $settlement The security's settlement date. * The security's 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. @@ -1513,47 +1452,7 @@ class Financial */ public static function PRICEMAT($settlement, $maturity, $issue, $rate, $yield, $basis = 0) { - $settlement = Functions::flattenSingleValue($settlement); - $maturity = Functions::flattenSingleValue($maturity); - $issue = Functions::flattenSingleValue($issue); - $rate = Functions::flattenSingleValue($rate); - $yield = Functions::flattenSingleValue($yield); - $basis = (int) Functions::flattenSingleValue($basis); - - // Validate - if (is_numeric($rate) && is_numeric($yield)) { - if (($rate <= 0) || ($yield <= 0)) { - return Functions::NAN(); - } - $daysPerYear = Financial\Helpers::daysPerYear(DateTime::YEAR($settlement), $basis); - if (!is_numeric($daysPerYear)) { - return $daysPerYear; - } - $daysBetweenIssueAndSettlement = DateTime::YEARFRAC($issue, $settlement, $basis); - if (!is_numeric($daysBetweenIssueAndSettlement)) { - // return date error - return $daysBetweenIssueAndSettlement; - } - $daysBetweenIssueAndSettlement *= $daysPerYear; - $daysBetweenIssueAndMaturity = DateTime::YEARFRAC($issue, $maturity, $basis); - if (!is_numeric($daysBetweenIssueAndMaturity)) { - // return date error - return $daysBetweenIssueAndMaturity; - } - $daysBetweenIssueAndMaturity *= $daysPerYear; - $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis); - if (!is_numeric($daysBetweenSettlementAndMaturity)) { - // return date error - return $daysBetweenSettlementAndMaturity; - } - $daysBetweenSettlementAndMaturity *= $daysPerYear; - - return (100 + (($daysBetweenIssueAndMaturity / $daysPerYear) * $rate * 100)) / - (1 + (($daysBetweenSettlementAndMaturity / $daysPerYear) * $yield)) - - (($daysBetweenIssueAndSettlement / $daysPerYear) * $rate * 100); - } - - return Functions::VALUE(); + return Financial\Securities::maturity($settlement, $maturity, $issue, $rate, $yield, $basis); } /** diff --git a/src/PhpSpreadsheet/Calculation/Financial/Coupons.php b/src/PhpSpreadsheet/Calculation/Financial/Coupons.php index ff99c839..835ef633 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Coupons.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Coupons.php @@ -419,7 +419,7 @@ class Coupons return $frequency; } - private static function validateBasis($basis) + private static function validateBasis($basis): int { if (!is_numeric($basis)) { throw new Exception(Functions::NAN()); diff --git a/src/PhpSpreadsheet/Calculation/Financial/Securities.php b/src/PhpSpreadsheet/Calculation/Financial/Securities.php new file mode 100644 index 00000000..761fc18a --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Financial/Securities.php @@ -0,0 +1,321 @@ +getMessage(); + } + + $dsc = Coupons::COUPDAYSNC($settlement, $maturity, $frequency, $basis); + $e = Coupons::COUPDAYS($settlement, $maturity, $frequency, $basis); + $n = Coupons::COUPNUM($settlement, $maturity, $frequency, $basis); + $a = Coupons::COUPDAYBS($settlement, $maturity, $frequency, $basis); + + $baseYF = 1.0 + ($yield / $frequency); + $rfp = 100 * ($rate / $frequency); + $de = $dsc / $e; + + $result = $redemption / $baseYF ** (--$n + $de); + for ($k = 0; $k <= $n; ++$k) { + $result += $rfp / ($baseYF ** ($k + $de)); + } + $result -= $rfp * ($a / $e); + + return $result; + } + + /** + * PRICEDISC. + * + * Returns the price per $100 face value of a discounted security. + * + * @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 float $discount The security's discount rate + * @param float $redemption The security's redemption value per $100 face value + * @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 float|string Result, or a string containing an error + */ + public static function discounted($settlement, $maturity, $discount, $redemption, $basis = 0) + { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $discount = Functions::flattenSingleValue($discount); + $redemption = Functions::flattenSingleValue($redemption); + $basis = Functions::flattenSingleValue($basis); + + try { + $settlement = self::validateSettlementDate($settlement); + $maturity = self::validateMaturityDate($maturity); + self::validateSecurityPeriod($settlement, $maturity); + $discount = self::validateDiscount($discount); + $redemption = self::validateRedemption($redemption); + $basis = self::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis); + if (!is_numeric($daysBetweenSettlementAndMaturity)) { + // return date error + return $daysBetweenSettlementAndMaturity; + } + + return $redemption * (1 - $discount * $daysBetweenSettlementAndMaturity); + } + + /** + * PRICEMAT. + * + * Returns the price per $100 face value of a security that pays interest at maturity. + * + * @param mixed $settlement The security's settlement date. + * The security's 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 $issue The security's issue date + * @param float $rate The security's interest rate at date of issue + * @param float $yield The security's annual yield + * @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 float|string Result, or a string containing an error + */ + public static function maturity($settlement, $maturity, $issue, $rate, $yield, $basis = 0) + { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $issue = Functions::flattenSingleValue($issue); + $rate = Functions::flattenSingleValue($rate); + $yield = Functions::flattenSingleValue($yield); + $basis = Functions::flattenSingleValue($basis); + + try { + $settlement = self::validateSettlementDate($settlement); + $maturity = self::validateMaturityDate($maturity); + self::validateSecurityPeriod($settlement, $maturity); + $issue = self::validateIssueDate($issue); + $rate = self::validateRate($rate); + $yield = self::validateYield($yield); + $basis = self::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + $daysPerYear = Helpers::daysPerYear(DateTime::YEAR($settlement), $basis); + if (!is_numeric($daysPerYear)) { + return $daysPerYear; + } + $daysBetweenIssueAndSettlement = DateTime::YEARFRAC($issue, $settlement, $basis); + if (!is_numeric($daysBetweenIssueAndSettlement)) { + // return date error + return $daysBetweenIssueAndSettlement; + } + $daysBetweenIssueAndSettlement *= $daysPerYear; + $daysBetweenIssueAndMaturity = DateTime::YEARFRAC($issue, $maturity, $basis); + if (!is_numeric($daysBetweenIssueAndMaturity)) { + // return date error + return $daysBetweenIssueAndMaturity; + } + $daysBetweenIssueAndMaturity *= $daysPerYear; + $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis); + if (!is_numeric($daysBetweenSettlementAndMaturity)) { + // return date error + return $daysBetweenSettlementAndMaturity; + } + $daysBetweenSettlementAndMaturity *= $daysPerYear; + + return (100 + (($daysBetweenIssueAndMaturity / $daysPerYear) * $rate * 100)) / + (1 + (($daysBetweenSettlementAndMaturity / $daysPerYear) * $yield)) - + (($daysBetweenIssueAndSettlement / $daysPerYear) * $rate * 100); + } + + private static function validateInputDate($date) + { + $date = DateTime::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 validateIssueDate($issue) + { + return self::validateInputDate($issue); + } + + private static function validateSecurityPeriod($settlement, $maturity): void + { + if ($settlement >= $maturity) { + throw new Exception(Functions::NAN()); + } + } + + private static function validateRate($rate): float + { + if (!is_numeric($rate)) { + throw new Exception(Functions::VALUE()); + } + + $rate = (float) $rate; + if ($rate < 0.0) { + throw new Exception(Functions::NAN()); + } + + return $rate; + } + + private static function validateYield($yield): float + { + if (!is_numeric($yield)) { + throw new Exception(Functions::VALUE()); + } + + $yield = (float) $yield; + if ($yield < 0.0) { + throw new Exception(Functions::NAN()); + } + + return $yield; + } + + private static function validateRedemption($redemption): float + { + if (!is_numeric($redemption)) { + throw new Exception(Functions::VALUE()); + } + + $redemption = (float) $redemption; + if ($redemption <= 0.0) { + throw new Exception(Functions::NAN()); + } + + return $redemption; + } + + private static function validateDiscount($discount): float + { + if (!is_numeric($discount)) { + throw new Exception(Functions::VALUE()); + } + + $discount = (float) $discount; + if ($discount <= 0.0) { + throw new Exception(Functions::NAN()); + } + + return $discount; + } + + private static function validateFrequency($frequency): int + { + if (!is_numeric($frequency)) { + throw new Exception(Functions::VALUE()); + } + + $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::VALUE()); + } + + $basis = (int) $basis; + if (($basis < 0) || ($basis > 4)) { + throw new Exception(Functions::NAN()); + } + + return $basis; + } +} diff --git a/tests/data/Calculation/Financial/PRICEDISC.php b/tests/data/Calculation/Financial/PRICEDISC.php index 88a0d94e..9413ca39 100644 --- a/tests/data/Calculation/Financial/PRICEDISC.php +++ b/tests/data/Calculation/Financial/PRICEDISC.php @@ -9,4 +9,36 @@ return [ 97.6311475409836, ['2008-02-15', '2008-11-30', 0.03, 100, 1], ], + [ + '#VALUE!', + ['Invalid Date', '2008-11-30', 0.03, 100, 1], + ], + [ + '#VALUE!', + ['2008-02-15', 'Invalid Date', 0.03, 100, 1], + ], + [ + '#VALUE!', + ['2008-02-15', '2008-11-30', 'NaN', 100, 1], + ], + [ + '#NUM!', + ['2008-02-15', '2008-11-30', -0.03, 100, 1], + ], + [ + '#VALUE!', + ['2008-02-15', '2008-11-30', 0.03, 'NaN', 1], + ], + [ + '#NUM!', + ['2008-02-15', '2008-11-30', 0.03, -100, 1], + ], + [ + '#VALUE!', + ['2008-02-15', '2008-11-30', 0.03, 100, 'NaN'], + ], + [ + '#NUM!', + ['2008-02-15', '2008-11-30', 0.03, 100, -1], + ], ]; diff --git a/tests/data/Calculation/Financial/PRICEMAT.php b/tests/data/Calculation/Financial/PRICEMAT.php index ac5c242e..2de50927 100644 --- a/tests/data/Calculation/Financial/PRICEMAT.php +++ b/tests/data/Calculation/Financial/PRICEMAT.php @@ -13,4 +13,32 @@ return [ 93.0909090909091, '1-Jul-2017', '1-Jan-2027', '1-Jan-2017', 0.07, 0.08, ], + [ + '#VALUE!', + 'Invalid Date', '1-Jan-2027', '1-Jan-2017', 0.07, 0.08, + ], + [ + '#VALUE!', + '1-Jul-2017', 'Invalid Date', '1-Jan-2017', 0.07, 0.08, + ], + [ + '#VALUE!', + '1-Jul-2017', '1-Jan-2027', 'Invalid Date', 0.07, 0.08, + ], + [ + '#VALUE!', + '1-Jul-2017', '1-Jan-2027', '1-Jan-2017', 'NaN', 0.08, + ], + [ + '#NUM!', + '1-Jul-2017', '1-Jan-2027', '1-Jan-2017', -0.07, 0.08, + ], + [ + '#VALUE!', + '1-Jul-2017', '1-Jan-2027', '1-Jan-2017', 0.07, 'NaN', + ], + [ + '#NUM!', + '1-Jul-2017', '1-Jan-2027', '1-Jan-2017', 0.07, -0.08, + ], ];