Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Financial start refactoring cash flow functions #1986

Merged
merged 5 commits into from
Apr 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
355 changes: 55 additions & 300 deletions src/PhpSpreadsheet/Calculation/Financial.php

Large diffs are not rendered by default.

104 changes: 104 additions & 0 deletions src/PhpSpreadsheet/Calculation/Financial/CashFlow/Single.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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 '';
}
}
Loading