Financial start refactoring cash flow functions (#1986)

* Start extracting CashFlow functions from Financial, beginning with the simple Single Rate flows
* Extracting Variable Periodic and NonPeriodic CashFlow functions from Financial
* Some more unit tests for exception cases
This commit is contained in:
Mark Baker 2021-04-04 14:44:06 +02:00 committed by GitHub
parent 6446039f4f
commit 42761f90b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 680 additions and 337 deletions

View File

@ -756,21 +756,19 @@ class Financial
* Excel Function:
* FVSCHEDULE(principal,schedule)
*
* @Deprecated 1.18.0
*
* @see Financial\CashFlow\Single::futureValue()
* Use the futureValue() method in the Financial\CashFlow\Single class instead
*
* @param float $principal the present value
* @param float[] $schedule an array of interest rates to apply
*
* @return float
* @return float|string
*/
public static function FVSCHEDULE($principal, $schedule)
{
$principal = Functions::flattenSingleValue($principal);
$schedule = Functions::flattenArray($schedule);
foreach ($schedule as $rate) {
$principal *= 1 + $rate;
}
return $principal;
return Financial\CashFlow\Single::futureValue($principal, $schedule);
}
/**
@ -876,6 +874,8 @@ class Financial
* Excel Function:
* IRR(values[,guess])
*
* @Deprecated 1.18.0
*
* @param mixed $values An array or a reference to cells that contain numbers for which you want
* to calculate the internal rate of return.
* Values must contain at least one positive value and one negative value to
@ -883,56 +883,13 @@ class Financial
* @param mixed $guess A number that you guess is close to the result of IRR
*
* @return float|string
*
*@see Financial\CashFlow\Variable\Periodic::rate()
* Use the IRR() method in the Financial\CashFlow\Variable\Periodic class instead
*/
public static function IRR($values, $guess = 0.1)
{
if (!is_array($values)) {
return Functions::VALUE();
}
$values = Functions::flattenArray($values);
$guess = Functions::flattenSingleValue($guess);
// create an initial range, with a root somewhere between 0 and guess
$x1 = 0.0;
$x2 = $guess;
$f1 = self::NPV($x1, $values);
$f2 = self::NPV($x2, $values);
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
if (($f1 * $f2) < 0.0) {
break;
}
if (abs($f1) < abs($f2)) {
$f1 = self::NPV($x1 += 1.6 * ($x1 - $x2), $values);
} else {
$f2 = self::NPV($x2 += 1.6 * ($x2 - $x1), $values);
}
}
if (($f1 * $f2) > 0.0) {
return Functions::VALUE();
}
$f = self::NPV($x1, $values);
if ($f < 0.0) {
$rtb = $x1;
$dx = $x2 - $x1;
} else {
$rtb = $x2;
$dx = $x1 - $x2;
}
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
$dx *= 0.5;
$x_mid = $rtb + $dx;
$f_mid = self::NPV($x_mid, $values);
if ($f_mid <= 0.0) {
$rtb = $x_mid;
}
if ((abs($f_mid) < self::FINANCIAL_PRECISION) || (abs($dx) < self::FINANCIAL_PRECISION)) {
return $x_mid;
}
}
return Functions::VALUE();
return Financial\CashFlow\Variable\Periodic::rate($values, $guess);
}
/**
@ -986,6 +943,11 @@ class Financial
* Excel Function:
* MIRR(values,finance_rate, reinvestment_rate)
*
* @Deprecated 1.18.0
*
* @see Financial\CashFlow\Variable\Periodic::modifiedRate()
* Use the MIRR() method in the Financial\CashFlow\Variable\Periodic class instead
*
* @param mixed $values An array or a reference to cells that contain a series of payments and
* income occurring at regular intervals.
* Payments are negative value, income is positive values.
@ -996,34 +958,7 @@ class Financial
*/
public static function MIRR($values, $finance_rate, $reinvestment_rate)
{
if (!is_array($values)) {
return Functions::VALUE();
}
$values = Functions::flattenArray($values);
$finance_rate = Functions::flattenSingleValue($finance_rate);
$reinvestment_rate = Functions::flattenSingleValue($reinvestment_rate);
$n = count($values);
$rr = 1.0 + $reinvestment_rate;
$fr = 1.0 + $finance_rate;
$npv_pos = $npv_neg = 0.0;
foreach ($values as $i => $v) {
if ($v >= 0) {
$npv_pos += $v / $rr ** $i;
} else {
$npv_neg += $v / $fr ** $i;
}
}
if (($npv_neg == 0) || ($npv_pos == 0) || ($reinvestment_rate <= -1)) {
return Functions::VALUE();
}
$mirr = ((-$npv_pos * $rr ** $n)
/ ($npv_neg * ($rr))) ** (1.0 / ($n - 1)) - 1.0;
return is_finite($mirr) ? $mirr : Functions::VALUE();
return Financial\CashFlow\Variable\Periodic::modifiedRate($values, $finance_rate, $reinvestment_rate);
}
/**
@ -1094,28 +1029,16 @@ class Financial
*
* Returns the Net Present Value of a cash flow series given a discount rate.
*
* @Deprecated 1.18.0
*
* @return float
*
*@see Financial\CashFlow\Variable\Periodic::presentValue()
* Use the NPV() method in the Financial\CashFlow\Variable\Periodic class instead
*/
public static function NPV(...$args)
{
// Return value
$returnValue = 0;
// Loop through arguments
$aArgs = Functions::flattenArray($args);
// Calculate
$rate = array_shift($aArgs);
$countArgs = count($aArgs);
for ($i = 1; $i <= $countArgs; ++$i) {
// Is it a numeric value?
if (is_numeric($aArgs[$i - 1])) {
$returnValue += $aArgs[$i - 1] / (1 + $rate) ** $i;
}
}
// Return
return $returnValue;
return Financial\CashFlow\Variable\Periodic::presentValue(...$args);
}
/**
@ -1123,6 +1046,11 @@ class Financial
*
* Calculates the number of periods required for an investment to reach a specified value.
*
* @Deprecated 1.18.0
*
* @see Financial\CashFlow\Single::periods()
* Use the periods() method in the Financial\CashFlow\Single class instead
*
* @param float $rate Interest rate per period
* @param float $pv Present Value
* @param float $fv Future Value
@ -1131,18 +1059,7 @@ class Financial
*/
public static function PDURATION($rate = 0, $pv = 0, $fv = 0)
{
$rate = Functions::flattenSingleValue($rate);
$pv = Functions::flattenSingleValue($pv);
$fv = Functions::flattenSingleValue($fv);
// Validate parameters
if (!is_numeric($rate) || !is_numeric($pv) || !is_numeric($fv)) {
return Functions::VALUE();
} elseif ($rate <= 0.0 || $pv <= 0.0 || $fv <= 0.0) {
return Functions::NAN();
}
return (log($fv) - log($pv)) / log(1 + $rate);
return Financial\CashFlow\Single::periods($rate, $pv, $fv);
}
/**
@ -1470,6 +1387,11 @@ class Financial
*
* Calculates the interest rate required for an investment to grow to a specified future value .
*
* @Deprecated 1.18.0
*
* @see Financial\CashFlow\Single::interestRate()
* Use the interestRate() method in the Financial\CashFlow\Single class instead
*
* @param float $nper The number of periods over which the investment is made
* @param float $pv Present Value
* @param float $fv Future Value
@ -1478,18 +1400,7 @@ class Financial
*/
public static function RRI($nper = 0, $pv = 0, $fv = 0)
{
$nper = Functions::flattenSingleValue($nper);
$pv = Functions::flattenSingleValue($pv);
$fv = Functions::flattenSingleValue($fv);
// Validate parameters
if (!is_numeric($nper) || !is_numeric($pv) || !is_numeric($fv)) {
return Functions::VALUE();
} elseif ($nper <= 0.0 || $pv <= 0.0 || $fv < 0.0) {
return Functions::NAN();
}
return ($fv / $pv) ** (1 / $nper) - 1;
return Financial\CashFlow\Single::interestRate($nper, $pv, $fv);
}
/**
@ -1601,85 +1512,6 @@ class Financial
return TreasuryBill::yield($settlement, $maturity, $price);
}
private static function bothNegAndPos($neg, $pos)
{
return $neg && $pos;
}
private static function xirrPart2(&$values)
{
$valCount = count($values);
$foundpos = false;
$foundneg = false;
for ($i = 0; $i < $valCount; ++$i) {
$fld = $values[$i];
if (!is_numeric($fld)) {
return Functions::VALUE();
} elseif ($fld > 0) {
$foundpos = true;
} elseif ($fld < 0) {
$foundneg = true;
}
}
if (!self::bothNegAndPos($foundneg, $foundpos)) {
return Functions::NAN();
}
return '';
}
private static function xirrPart1(&$values, &$dates)
{
if ((!is_array($values)) && (!is_array($dates))) {
return Functions::NA();
}
$values = Functions::flattenArray($values);
$dates = Functions::flattenArray($dates);
if (count($values) != count($dates)) {
return Functions::NAN();
}
$datesCount = count($dates);
for ($i = 0; $i < $datesCount; ++$i) {
try {
$dates[$i] = DateTimeExcel\Helpers::getDateValue($dates[$i]);
} catch (Exception $e) {
return $e->getMessage();
}
}
return self::xirrPart2($values);
}
private static function xirrPart3($values, $dates, $x1, $x2)
{
$f = self::xnpvOrdered($x1, $values, $dates, false);
if ($f < 0.0) {
$rtb = $x1;
$dx = $x2 - $x1;
} else {
$rtb = $x2;
$dx = $x1 - $x2;
}
$rslt = Functions::VALUE();
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
$dx *= 0.5;
$x_mid = $rtb + $dx;
$f_mid = self::xnpvOrdered($x_mid, $values, $dates, false);
if ($f_mid <= 0.0) {
$rtb = $x_mid;
}
if ((abs($f_mid) < self::FINANCIAL_PRECISION) || (abs($dx) < self::FINANCIAL_PRECISION)) {
$rslt = $x_mid;
break;
}
}
return $rslt;
}
/**
* XIRR.
*
@ -1688,6 +1520,11 @@ class Financial
* Excel Function:
* =XIRR(values,dates,guess)
*
* @Deprecated 1.18.0
*
* @see Financial\CashFlow\Variable\NonPeriodic::rate()
* Use the rate() method in the Financial\CashFlow\Variable\NonPeriodic class instead
*
* @param float[] $values A series of cash flow payments
* The series of values must contain at least one positive value & one negative value
* @param mixed[] $dates A series of payment dates
@ -1699,37 +1536,7 @@ class Financial
*/
public static function XIRR($values, $dates, $guess = 0.1)
{
$rslt = self::xirrPart1($values, $dates);
if ($rslt) {
return $rslt;
}
// create an initial range, with a root somewhere between 0 and guess
$guess = Functions::flattenSingleValue($guess);
$x1 = 0.0;
$x2 = $guess ?: 0.1;
$f1 = self::xnpvOrdered($x1, $values, $dates, false);
$f2 = self::xnpvOrdered($x2, $values, $dates, false);
$found = false;
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
if (!is_numeric($f1) || !is_numeric($f2)) {
break;
}
if (($f1 * $f2) < 0.0) {
$found = true;
break;
} elseif (abs($f1) < abs($f2)) {
$f1 = self::xnpvOrdered($x1 += 1.6 * ($x1 - $x2), $values, $dates, false);
} else {
$f2 = self::xnpvOrdered($x2 += 1.6 * ($x2 - $x1), $values, $dates, false);
}
}
if (!$found) {
return Functions::NAN();
}
return self::xirrPart3($values, $dates, $x1, $x2);
return Financial\CashFlow\Variable\NonPeriodic::rate($values, $dates, $guess);
}
/**
@ -1741,6 +1548,11 @@ class Financial
* Excel Function:
* =XNPV(rate,values,dates)
*
* @Deprecated 1.18.0
*
* @see Financial\CashFlow\Variable\NonPeriodic::presentValue()
* Use the presentValue() method in the Financial\CashFlow\Variable\NonPeriodic class instead
*
* @param float $rate the discount rate to apply to the cash flows
* @param float[] $values A series of cash flows that corresponds to a schedule of payments in dates.
* The first payment is optional and corresponds to a cost or payment that occurs at the beginning of the investment.
@ -1754,68 +1566,7 @@ class Financial
*/
public static function XNPV($rate, $values, $dates)
{
return self::xnpvOrdered($rate, $values, $dates, true);
}
private static function validateXnpv($rate, $values, $dates)
{
if (!is_numeric($rate)) {
return Functions::VALUE();
}
$valCount = count($values);
if ($valCount != count($dates)) {
return Functions::NAN();
}
if ($valCount > 1 && ((min($values) > 0) || (max($values) < 0))) {
return Functions::NAN();
}
$date0 = DateTimeExcel\Helpers::getDateValue($dates[0]);
if (is_string($date0)) {
return Functions::VALUE();
}
return '';
}
private static function xnpvOrdered($rate, $values, $dates, $ordered = true)
{
$rate = Functions::flattenSingleValue($rate);
$values = Functions::flattenArray($values);
$dates = Functions::flattenArray($dates);
$valCount = count($values);
try {
$date0 = DateTimeExcel\Helpers::getDateValue($dates[0]);
} catch (Exception $e) {
return $e->getMessage();
}
$rslt = self::validateXnpv($rate, $values, $dates);
if ($rslt) {
return $rslt;
}
$xnpv = 0.0;
for ($i = 0; $i < $valCount; ++$i) {
if (!is_numeric($values[$i])) {
return Functions::VALUE();
}
try {
$datei = DateTimeExcel\Helpers::getDateValue($dates[$i]);
} catch (Exception $e) {
return $e->getMessage();
}
if ($date0 > $datei) {
$dif = $ordered ? Functions::NAN() : -DateTimeExcel\DateDif::funcDateDif($datei, $date0, 'd');
} else {
$dif = DateTimeExcel\DateDif::funcDateDif($date0, $datei, 'd');
}
if (!is_numeric($dif)) {
return $dif;
}
$xnpv += $values[$i] / (1 + $rate) ** ($dif / 365);
}
return is_finite($xnpv) ? $xnpv : Functions::VALUE();
return Financial\CashFlow\Variable\NonPeriodic::presentValue($rate, $values, $dates);
}
/**
@ -1823,7 +1574,10 @@ class Financial
*
* Returns the annual yield of a security that pays interest at maturity.
*
* @see Use the yieldDiscounted() method in the Financial\Securities\Yields class instead
* @Deprecated 1.18.0
*
* @see Financial\Securities\Yields::yieldDiscounted()
* Use the yieldDiscounted() method in the Financial\Securities\Yields 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
@ -1853,7 +1607,8 @@ class Financial
*
* @Deprecated 1.18.0
*
* @see Use the yieldAtMaturity() method in the Financial\Securities\Yields class instead
* @see Financial\Securities\Yields::yieldAtMaturity()
* Use the yieldAtMaturity() method in the Financial\Securities\Yields 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

View File

@ -2,6 +2,110 @@
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Financial\BaseValidations;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class Single
{
use BaseValidations;
/**
* FVSCHEDULE.
*
* Returns the future value of an initial principal after applying a series of compound interest rates.
* Use FVSCHEDULE to calculate the future value of an investment with a variable or adjustable rate.
*
* Excel Function:
* FVSCHEDULE(principal,schedule)
*
* @param mixed $principal the present value
* @param float[] $schedule an array of interest rates to apply
*
* @return float|string
*/
public static function futureValue($principal, $schedule)
{
$principal = Functions::flattenSingleValue($principal);
$schedule = Functions::flattenArray($schedule);
try {
$principal = self::validateFloat($principal);
foreach ($schedule as $rate) {
$rate = self::validateFloat($rate);
$principal *= 1 + $rate;
}
} catch (Exception $e) {
return $e->getMessage();
}
return $principal;
}
/**
* PDURATION.
*
* Calculates the number of periods required for an investment to reach a specified value.
*
* @param float $rate Interest rate per period
* @param float $presentValue Present Value
* @param float $futureValue Future Value
*
* @return float|string Result, or a string containing an error
*/
public static function periods($rate = 0.0, $presentValue = 0.0, $futureValue = 0.0)
{
$rate = Functions::flattenSingleValue($rate);
$presentValue = Functions::flattenSingleValue($presentValue);
$futureValue = Functions::flattenSingleValue($futureValue);
try {
$rate = self::validateFloat($rate);
$presentValue = self::validateFloat($presentValue);
$futureValue = self::validateFloat($futureValue);
} catch (Exception $e) {
return $e->getMessage();
}
// Validate parameters
if ($rate <= 0.0 || $presentValue <= 0.0 || $futureValue <= 0.0) {
return Functions::NAN();
}
return (log($futureValue) - log($presentValue)) / log(1 + $rate);
}
/**
* RRI.
*
* Calculates the interest rate required for an investment to grow to a specified future value .
*
* @param float $periods The number of periods over which the investment is made
* @param float $presentValue Present Value
* @param float $futureValue Future Value
*
* @return float|string Result, or a string containing an error
*/
public static function interestRate($periods = 0.0, $presentValue = 0.0, $futureValue = 0.0)
{
$periods = Functions::flattenSingleValue($periods);
$presentValue = Functions::flattenSingleValue($presentValue);
$futureValue = Functions::flattenSingleValue($futureValue);
try {
$periods = self::validateFloat($periods);
$presentValue = self::validateFloat($presentValue);
$futureValue = self::validateFloat($futureValue);
} catch (Exception $e) {
return $e->getMessage();
}
// Validate parameters
if ($periods <= 0.0 || $presentValue <= 0.0 || $futureValue < 0.0) {
return Functions::NAN();
}
return ($futureValue / $presentValue) ** (1 / $periods) - 1;
}
}

View File

@ -0,0 +1,233 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Variable;
use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class NonPeriodic
{
const FINANCIAL_MAX_ITERATIONS = 128;
const FINANCIAL_PRECISION = 1.0e-08;
/**
* XIRR.
*
* Returns the internal rate of return for a schedule of cash flows that is not necessarily periodic.
*
* Excel Function:
* =XIRR(values,dates,guess)
*
* @param float[] $values A series of cash flow payments
* The series of values must contain at least one positive value & one negative value
* @param mixed[] $dates A series of payment dates
* The first payment date indicates the beginning of the schedule of payments
* All other dates must be later than this date, but they may occur in any order
* @param float $guess An optional guess at the expected answer
*
* @return float|string
*/
public static function rate($values, $dates, $guess = 0.1)
{
$rslt = self::xirrPart1($values, $dates);
if ($rslt) {
return $rslt;
}
// create an initial range, with a root somewhere between 0 and guess
$guess = Functions::flattenSingleValue($guess);
$x1 = 0.0;
$x2 = $guess ?: 0.1;
$f1 = self::xnpvOrdered($x1, $values, $dates, false);
$f2 = self::xnpvOrdered($x2, $values, $dates, false);
$found = false;
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
if (!is_numeric($f1) || !is_numeric($f2)) {
break;
}
if (($f1 * $f2) < 0.0) {
$found = true;
break;
} elseif (abs($f1) < abs($f2)) {
$f1 = self::xnpvOrdered($x1 += 1.6 * ($x1 - $x2), $values, $dates, false);
} else {
$f2 = self::xnpvOrdered($x2 += 1.6 * ($x2 - $x1), $values, $dates, false);
}
}
if (!$found) {
return Functions::NAN();
}
return self::xirrPart3($values, $dates, $x1, $x2);
}
/**
* XNPV.
*
* Returns the net present value for a schedule of cash flows that is not necessarily periodic.
* To calculate the net present value for a series of cash flows that is periodic, use the NPV function.
*
* Excel Function:
* =XNPV(rate,values,dates)
*
* @param float $rate the discount rate to apply to the cash flows
* @param float[] $values A series of cash flows that corresponds to a schedule of payments in dates.
* The first payment is optional and corresponds to a cost or payment that occurs
* at the beginning of the investment.
* 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.
* The series of values must contain at least one positive value and one negative value.
* @param mixed[] $dates A schedule of payment dates that corresponds to the cash flow payments.
* The first payment date indicates the beginning of the schedule of payments.
* All other dates must be later than this date, but they may occur in any order.
*
* @return float|string
*/
public static function presentValue($rate, $values, $dates)
{
return self::xnpvOrdered($rate, $values, $dates, true);
}
private static function bothNegAndPos($neg, $pos)
{
return $neg && $pos;
}
private static function xirrPart1(&$values, &$dates)
{
if ((!is_array($values)) && (!is_array($dates))) {
return Functions::NA();
}
$values = Functions::flattenArray($values);
$dates = Functions::flattenArray($dates);
if (count($values) != count($dates)) {
return Functions::NAN();
}
$datesCount = count($dates);
for ($i = 0; $i < $datesCount; ++$i) {
try {
$dates[$i] = DateTimeExcel\Helpers::getDateValue($dates[$i]);
} catch (Exception $e) {
return $e->getMessage();
}
}
return self::xirrPart2($values);
}
private static function xirrPart2(&$values)
{
$valCount = count($values);
$foundpos = false;
$foundneg = false;
for ($i = 0; $i < $valCount; ++$i) {
$fld = $values[$i];
if (!is_numeric($fld)) {
return Functions::VALUE();
} elseif ($fld > 0) {
$foundpos = true;
} elseif ($fld < 0) {
$foundneg = true;
}
}
if (!self::bothNegAndPos($foundneg, $foundpos)) {
return Functions::NAN();
}
return '';
}
private static function xirrPart3($values, $dates, $x1, $x2)
{
$f = self::xnpvOrdered($x1, $values, $dates, false);
if ($f < 0.0) {
$rtb = $x1;
$dx = $x2 - $x1;
} else {
$rtb = $x2;
$dx = $x1 - $x2;
}
$rslt = Functions::VALUE();
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
$dx *= 0.5;
$x_mid = $rtb + $dx;
$f_mid = self::xnpvOrdered($x_mid, $values, $dates, false);
if ($f_mid <= 0.0) {
$rtb = $x_mid;
}
if ((abs($f_mid) < self::FINANCIAL_PRECISION) || (abs($dx) < self::FINANCIAL_PRECISION)) {
$rslt = $x_mid;
break;
}
}
return $rslt;
}
private static function xnpvOrdered($rate, $values, $dates, $ordered = true)
{
$rate = Functions::flattenSingleValue($rate);
$values = Functions::flattenArray($values);
$dates = Functions::flattenArray($dates);
$valCount = count($values);
try {
$date0 = DateTimeExcel\Helpers::getDateValue($dates[0]);
} catch (Exception $e) {
return $e->getMessage();
}
$rslt = self::validateXnpv($rate, $values, $dates);
if ($rslt) {
return $rslt;
}
$xnpv = 0.0;
for ($i = 0; $i < $valCount; ++$i) {
if (!is_numeric($values[$i])) {
return Functions::VALUE();
}
try {
$datei = DateTimeExcel\Helpers::getDateValue($dates[$i]);
} catch (Exception $e) {
return $e->getMessage();
}
if ($date0 > $datei) {
$dif = $ordered ? Functions::NAN() : -DateTimeExcel\DateDif::funcDateDif($datei, $date0, 'd');
} else {
$dif = DateTimeExcel\DateDif::funcDateDif($date0, $datei, 'd');
}
if (!is_numeric($dif)) {
return $dif;
}
$xnpv += $values[$i] / (1 + $rate) ** ($dif / 365);
}
return is_finite($xnpv) ? $xnpv : Functions::VALUE();
}
private static function validateXnpv($rate, $values, $dates)
{
if (!is_numeric($rate)) {
return Functions::VALUE();
}
$valCount = count($values);
if ($valCount != count($dates)) {
return Functions::NAN();
}
if ($valCount > 1 && ((min($values) > 0) || (max($values) < 0))) {
return Functions::NAN();
}
$date0 = DateTimeExcel\Helpers::getDateValue($dates[0]);
if (is_string($date0)) {
return Functions::VALUE();
}
return '';
}
}

View File

@ -0,0 +1,160 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Variable;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class Periodic
{
const FINANCIAL_MAX_ITERATIONS = 128;
const FINANCIAL_PRECISION = 1.0e-08;
/**
* IRR.
*
* Returns the internal rate of return for a series of cash flows represented by the numbers in values.
* These cash flows do not have to be even, as they would be for an annuity. However, the cash flows must occur
* at regular intervals, such as monthly or annually. The internal rate of return is the interest rate received
* for an investment consisting of payments (negative values) and income (positive values) that occur at regular
* periods.
*
* Excel Function:
* IRR(values[,guess])
*
* @param mixed $values An array or a reference to cells that contain numbers for which you want
* to calculate the internal rate of return.
* Values must contain at least one positive value and one negative value to
* calculate the internal rate of return.
* @param mixed $guess A number that you guess is close to the result of IRR
*
* @return float|string
*/
public static function rate($values, $guess = 0.1)
{
if (!is_array($values)) {
return Functions::VALUE();
}
$values = Functions::flattenArray($values);
$guess = Functions::flattenSingleValue($guess);
// create an initial range, with a root somewhere between 0 and guess
$x1 = 0.0;
$x2 = $guess;
$f1 = self::presentValue($x1, $values);
$f2 = self::presentValue($x2, $values);
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
if (($f1 * $f2) < 0.0) {
break;
}
if (abs($f1) < abs($f2)) {
$f1 = self::presentValue($x1 += 1.6 * ($x1 - $x2), $values);
} else {
$f2 = self::presentValue($x2 += 1.6 * ($x2 - $x1), $values);
}
}
if (($f1 * $f2) > 0.0) {
return Functions::VALUE();
}
$f = self::presentValue($x1, $values);
if ($f < 0.0) {
$rtb = $x1;
$dx = $x2 - $x1;
} else {
$rtb = $x2;
$dx = $x1 - $x2;
}
for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) {
$dx *= 0.5;
$x_mid = $rtb + $dx;
$f_mid = self::presentValue($x_mid, $values);
if ($f_mid <= 0.0) {
$rtb = $x_mid;
}
if ((abs($f_mid) < self::FINANCIAL_PRECISION) || (abs($dx) < self::FINANCIAL_PRECISION)) {
return $x_mid;
}
}
return Functions::VALUE();
}
/**
* MIRR.
*
* Returns the modified internal rate of return for a series of periodic cash flows. MIRR considers both
* the cost of the investment and the interest received on reinvestment of cash.
*
* Excel Function:
* MIRR(values,finance_rate, reinvestment_rate)
*
* @param mixed $values An array or a reference to cells that contain a series of payments and
* income occurring at regular intervals.
* Payments are negative value, income is positive values.
* @param mixed $financeRate The interest rate you pay on the money used in the cash flows
* @param mixed $reinvestmentRate The interest rate you receive on the cash flows as you reinvest them
*
* @return float|string Result, or a string containing an error
*/
public static function modifiedRate($values, $financeRate, $reinvestmentRate)
{
if (!is_array($values)) {
return Functions::VALUE();
}
$values = Functions::flattenArray($values);
$financeRate = Functions::flattenSingleValue($financeRate);
$reinvestmentRate = Functions::flattenSingleValue($reinvestmentRate);
$n = count($values);
$rr = 1.0 + $reinvestmentRate;
$fr = 1.0 + $financeRate;
$npvPos = $npvNeg = 0.0;
foreach ($values as $i => $v) {
if ($v >= 0) {
$npvPos += $v / $rr ** $i;
} else {
$npvNeg += $v / $fr ** $i;
}
}
if (($npvNeg === 0.0) || ($npvPos === 0.0) || ($reinvestmentRate <= -1.0)) {
return Functions::VALUE();
}
$mirr = ((-$npvPos * $rr ** $n)
/ ($npvNeg * ($rr))) ** (1.0 / ($n - 1)) - 1.0;
return is_finite($mirr) ? $mirr : Functions::VALUE();
}
/**
* NPV.
*
* Returns the Net Present Value of a cash flow series given a discount rate.
*
* @param mixed $rate
*
* @return float
*/
public static function presentValue($rate, ...$args)
{
$returnValue = 0;
$rate = Functions::flattenSingleValue($rate);
$aArgs = Functions::flattenArray($args);
// Calculate
$countArgs = count($aArgs);
for ($i = 1; $i <= $countArgs; ++$i) {
// Is it a numeric value?
if (is_numeric($aArgs[$i - 1])) {
$returnValue += $aArgs[$i - 1] / (1 + $rate) ** $i;
}
}
return $returnValue;
}
}

View File

@ -36,4 +36,22 @@ return [
],
],
],
[
'#VALUE!',
'NaN',
[
0.089999999999999997,
0.11,
0.10000000000000001,
],
],
[
'#VALUE!',
100,
[
0.089999999999999997,
'NaN',
0.10000000000000001,
],
],
];

View File

@ -32,7 +32,7 @@ return [
15000,
18000,
],
0.10000000000000001,
0.10,
],
[
-0.13618951095869,
@ -41,7 +41,7 @@ return [
[
20,
24,
28.800000000000001,
28.8,
],
],
],
@ -52,10 +52,35 @@ return [
[
20,
24,
28.800000000000001,
34.560000000000002,
41.469999999999999,
28.8,
34.56,
41.47,
],
],
],
[
'#VALUE!',
999,
1.23,
],
[
'#VALUE!',
[
70000,
12000,
15000,
18000,
21000,
],
],
[
'#VALUE!',
[
-70000,
-12000,
-15000,
-18000,
-21000,
],
],
];

View File

@ -15,7 +15,7 @@ return [
46000,
],
],
0.10000000000000001,
0.10,
0.12,
],
[
@ -28,7 +28,7 @@ return [
21000,
],
],
0.10000000000000001,
0.10,
0.12,
],
[
@ -43,8 +43,8 @@ return [
46000,
],
],
0.10000000000000001,
0.14000000000000001,
0.10,
0.14,
],
[
0.74021752686287001,
@ -74,4 +74,28 @@ return [
5.5,
5,
],
[
'#VALUE!',
999,
1.23,
2.34,
],
[
'#VALUE!',
[0.12, 0.13, 0.125],
1.23,
2.34,
],
[
'#VALUE!',
[-0.12, -0.13, -0.125],
1.23,
2.34,
],
[
'#VALUE!',
[-0.12, 0.13, 0.125],
1.23,
-2.34,
],
];

View File

@ -5,7 +5,7 @@
return [
[
1188.4434123352,
0.10000000000000001,
0.10,
-10000,
3000,
4200,
@ -13,7 +13,7 @@ return [
],
[
41922.061554931999,
0.080000000000000002,
0.08,
8000,
9200,
10000,
@ -22,7 +22,7 @@ return [
],
[
36250.534912984003,
0.080000000000000002,
0.08,
8000,
9200,
10000,
@ -32,7 +32,7 @@ return [
],
[
12678.677633095,
0.050000000000000003,
0.05,
2000,
2400,
2900,

View File

@ -1,18 +1,6 @@
<?php
return [
[
'#VALUE!',
['ABC', 'DEF', 'GHI'],
],
[
'#NUM!',
[0.0, 0.0, 0.0],
],
[
'#NUM!',
[0.05, 10.0, -2.0],
],
[
10.33803507,
[0.04, 10000, 15000],
@ -33,4 +21,28 @@ return [
14.206699082890474,
[0.05, 50, 100],
],
[
'#VALUE!',
['NaN', 50, 100],
],
[
'#VALUE!',
[0.05, 'NaN', 100],
],
[
'#VALUE!',
[0.05, 50, 'NaN'],
],
[
'#NUM!',
[0.00, 50, 100],
],
[
'#NUM!',
[0.05, 0, 100],
],
[
'#NUM!',
[0.05, 50, 0],
],
];

View File

@ -1,18 +1,6 @@
<?php
return [
[
'#VALUE!',
['ABC', 'DEF', 'GHI'],
],
[
'#NUM!',
[0.0, 10.0, 20.0],
],
[
'#NUM!',
[0.05, 10.0, -2.0],
],
[
0.04137974399241062,
[10, 10000, 15000],
@ -25,4 +13,28 @@ return [
0.6747967875721199,
[10, 70, 12154],
],
[
'#VALUE!',
['NaN', 70, 12154],
],
[
'#VALUE!',
[10, 'NaN', 12154],
],
[
'#VALUE!',
[10, 70, 'NaN'],
],
[
'#NUM!',
[0.0, 70.0, 200.0],
],
[
'#NUM!',
[10, 0.0, 200.0],
],
[
'#NUM!',
[10, 70, -2.0],
],
];