diff --git a/src/PhpSpreadsheet/Calculation/Financial.php b/src/PhpSpreadsheet/Calculation/Financial.php index 984d31bf..fde1d3da 100644 --- a/src/PhpSpreadsheet/Calculation/Financial.php +++ b/src/PhpSpreadsheet/Calculation/Financial.php @@ -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 diff --git a/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Single.php b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Single.php index 3f1c8bc6..9fecd755 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Single.php +++ b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Single.php @@ -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; + } } diff --git a/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php new file mode 100644 index 00000000..d78015e6 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php @@ -0,0 +1,233 @@ +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 ''; + } +} diff --git a/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/Periodic.php b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/Periodic.php new file mode 100644 index 00000000..c42df0c3 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/Periodic.php @@ -0,0 +1,160 @@ + 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; + } +} diff --git a/tests/data/Calculation/Financial/FVSCHEDULE.php b/tests/data/Calculation/Financial/FVSCHEDULE.php index b332912c..975fca60 100644 --- a/tests/data/Calculation/Financial/FVSCHEDULE.php +++ b/tests/data/Calculation/Financial/FVSCHEDULE.php @@ -36,4 +36,22 @@ return [ ], ], ], + [ + '#VALUE!', + 'NaN', + [ + 0.089999999999999997, + 0.11, + 0.10000000000000001, + ], + ], + [ + '#VALUE!', + 100, + [ + 0.089999999999999997, + 'NaN', + 0.10000000000000001, + ], + ], ]; diff --git a/tests/data/Calculation/Financial/IRR.php b/tests/data/Calculation/Financial/IRR.php index 142ab204..f6c24c13 100644 --- a/tests/data/Calculation/Financial/IRR.php +++ b/tests/data/Calculation/Financial/IRR.php @@ -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, + ], + ], ]; diff --git a/tests/data/Calculation/Financial/MIRR.php b/tests/data/Calculation/Financial/MIRR.php index b32edcbe..9bd6e209 100644 --- a/tests/data/Calculation/Financial/MIRR.php +++ b/tests/data/Calculation/Financial/MIRR.php @@ -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, + ], ]; diff --git a/tests/data/Calculation/Financial/NPV.php b/tests/data/Calculation/Financial/NPV.php index 27db7f4d..ac854269 100644 --- a/tests/data/Calculation/Financial/NPV.php +++ b/tests/data/Calculation/Financial/NPV.php @@ -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, diff --git a/tests/data/Calculation/Financial/PDURATION.php b/tests/data/Calculation/Financial/PDURATION.php index cfb6080f..1886f26d 100644 --- a/tests/data/Calculation/Financial/PDURATION.php +++ b/tests/data/Calculation/Financial/PDURATION.php @@ -1,18 +1,6 @@