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:
parent
6446039f4f
commit
42761f90b7
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 '';
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -36,4 +36,22 @@ return [
|
|||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'#VALUE!',
|
||||
'NaN',
|
||||
[
|
||||
0.089999999999999997,
|
||||
0.11,
|
||||
0.10000000000000001,
|
||||
],
|
||||
],
|
||||
[
|
||||
'#VALUE!',
|
||||
100,
|
||||
[
|
||||
0.089999999999999997,
|
||||
'NaN',
|
||||
0.10000000000000001,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
],
|
||||
];
|
||||
|
|
|
|||
Loading…
Reference in New Issue