From 3dd890bcc8dbe51354089beb4671841d34274e54 Mon Sep 17 00:00:00 2001 From: Mark Kimsal Date: Mon, 14 Jan 2019 10:25:21 -0500 Subject: [PATCH 1/2] implement RATE algorithm from ODFF --- src/PhpSpreadsheet/Calculation/Financial.php | 68 +++++++++++-------- .../Calculation/FinancialTest.php | 2 +- tests/data/Calculation/Financial/RATE.php | 8 ++- 3 files changed, 48 insertions(+), 30 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Financial.php b/src/PhpSpreadsheet/Calculation/Financial.php index 3cb6d40a52..80d36e4f64 100644 --- a/src/PhpSpreadsheet/Calculation/Financial.php +++ b/src/PhpSpreadsheet/Calculation/Financial.php @@ -1873,43 +1873,55 @@ public static function PV($rate = 0, $nper = 0, $pmt = 0, $fv = 0, $type = 0) */ public static function RATE($nper, $pmt, $pv, $fv = 0.0, $type = 0, $guess = 0.1) { - $nper = (int) Functions::flattenSingleValue($nper); + $nper = Functions::flattenSingleValue($nper); $pmt = Functions::flattenSingleValue($pmt); $pv = Functions::flattenSingleValue($pv); $fv = ($fv === null) ? 0.0 : Functions::flattenSingleValue($fv); $type = ($type === null) ? 0 : (int) Functions::flattenSingleValue($type); $guess = ($guess === null) ? 0.1 : Functions::flattenSingleValue($guess); + $maxIter = 0; - $rate = $guess; - if (abs($rate) < self::FINANCIAL_PRECISION) { - $y = $pv * (1 + $nper * $rate) + $pmt * (1 + $rate * $type) * $nper + $fv; - } else { - $f = exp($nper * log(1 + $rate)); - $y = $pv * $f + $pmt * (1 / $rate + $type) * ($f - 1) + $fv; - } - $y0 = $pv + $pmt * $nper + $fv; - $y1 = $pv * $f + $pmt * (1 / $rate + $type) * ($f - 1) + $fv; - - // find root by secant method - $i = $x0 = 0.0; - $x1 = $rate; - while ((abs($y0 - $y1) > self::FINANCIAL_PRECISION) && ($i < self::FINANCIAL_MAX_ITERATIONS)) { - $rate = ($y1 * $x0 - $y0 * $x1) / ($y1 - $y0); - $x0 = $x1; - $x1 = $rate; - if (($nper * abs($pmt)) > ($pv - $fv)) { - $x1 = abs($x1); - } - if (abs($rate) < self::FINANCIAL_PRECISION) { - $y = $pv * (1 + $nper * $rate) + $pmt * (1 + $rate * $type) * $nper + $fv; + $epsilon = self::FINANCIAL_PRECISION; + $rate = $guess; + $found = false; + $geoSeries = $geoSeriesPrime = 0.0; + $term = $termPrime = 0.0; + $rateNew = 0.0; + $isInteger = false; + + if ($nper === (int)round($nper)) { + $isInteger = true; + } + + while (!$found && ++$maxIter <= self::FINANCIAL_MAX_ITERATIONS) { + if ($isInteger) { + $powNMinus1 = pow(1+$rate, $nper-1); + $powN = $powNMinus1 * (1+$rate); } else { - $f = exp($nper * log(1 + $rate)); - $y = $pv * $f + $pmt * (1 / $rate + $type) * ($f - 1) + $fv; + $powNMinus1 = pow(1+$rate, $nper); + $powN = pow(1+$rate, $nper); } - $y0 = $y1; - $y1 = $y; - ++$i; + if ($rate == 0) { + $geoSeries = $nper; + $geoSeriesPrime = $nper * (($nper-1)/2.0); + } else { + $geoSeries = ($powN-1)/$rate; + $geoSeriesPrime = $nper * $powNMinus1 / $rate - ($geoSeries/$rate); + } + $term = $fv + $pv * $powN + $pmt * $geoSeries; + $termPrime = $pv * $nper * $powNMinus1 + $pmt * $geoSeriesPrime; + if (abs($term) < $epsilon) { + $found = true; + } else { + if ($termPrime == 0) { //will catch root which is at an extreme + $rateNew = $rate + 1.1 * $epsilon; + } else { + $rateNew = $rate - $term / $termPrime; + } + } + $found = abs($rateNew - $rate) < $epsilon; + $rate = $rateNew; } return $rate; diff --git a/tests/PhpSpreadsheetTests/Calculation/FinancialTest.php b/tests/PhpSpreadsheetTests/Calculation/FinancialTest.php index d9103896cb..345a9e32f8 100644 --- a/tests/PhpSpreadsheetTests/Calculation/FinancialTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/FinancialTest.php @@ -518,7 +518,7 @@ public function providerPV() */ public function testRATE($expectedResult, ...$args) { - $this->markTestIncomplete('TODO: This test should be fixed'); +// $this->markTestIncomplete('TODO: This test should be fixed'); $result = Financial::RATE(...$args); self::assertEquals($expectedResult, $result, null, 1E-8); diff --git a/tests/data/Calculation/Financial/RATE.php b/tests/data/Calculation/Financial/RATE.php index 20f5397afc..cc183566a3 100644 --- a/tests/data/Calculation/Financial/RATE.php +++ b/tests/data/Calculation/Financial/RATE.php @@ -28,7 +28,13 @@ 5000, ], [ - 0.016550119066711999, + 0.017929869399484, + 24.99, + -250, + 5000 + ], + [ + 0.01513084390231, 24, -250, 5000, From c4e50b9259e518571f01bd3b83d645d80abca329 Mon Sep 17 00:00:00 2001 From: Mark Kimsal Date: Mon, 14 Jan 2019 10:47:32 -0500 Subject: [PATCH 2/2] style fix --- src/PhpSpreadsheet/Calculation/Financial.php | 28 ++++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Financial.php b/src/PhpSpreadsheet/Calculation/Financial.php index 80d36e4f64..401e9f1bfb 100644 --- a/src/PhpSpreadsheet/Calculation/Financial.php +++ b/src/PhpSpreadsheet/Calculation/Financial.php @@ -1881,33 +1881,33 @@ public static function RATE($nper, $pmt, $pv, $fv = 0.0, $type = 0, $guess = 0.1 $guess = ($guess === null) ? 0.1 : Functions::flattenSingleValue($guess); $maxIter = 0; - $epsilon = self::FINANCIAL_PRECISION; - $rate = $guess; - $found = false; + $epsilon = self::FINANCIAL_PRECISION; + $rate = $guess; + $found = false; $geoSeries = $geoSeriesPrime = 0.0; - $term = $termPrime = 0.0; - $rateNew = 0.0; + $term = $termPrime = 0.0; + $rateNew = 0.0; $isInteger = false; - if ($nper === (int)round($nper)) { + if ($nper === (int) round($nper)) { $isInteger = true; } while (!$found && ++$maxIter <= self::FINANCIAL_MAX_ITERATIONS) { if ($isInteger) { - $powNMinus1 = pow(1+$rate, $nper-1); - $powN = $powNMinus1 * (1+$rate); + $powNMinus1 = pow(1 + $rate, $nper - 1); + $powN = $powNMinus1 * (1 + $rate); } else { - $powNMinus1 = pow(1+$rate, $nper); - $powN = pow(1+$rate, $nper); + $powNMinus1 = pow(1 + $rate, $nper); + $powN = pow(1 + $rate, $nper); } if ($rate == 0) { $geoSeries = $nper; - $geoSeriesPrime = $nper * (($nper-1)/2.0); + $geoSeriesPrime = $nper * (($nper - 1) / 2.0); } else { - $geoSeries = ($powN-1)/$rate; - $geoSeriesPrime = $nper * $powNMinus1 / $rate - ($geoSeries/$rate); + $geoSeries = ($powN - 1) / $rate; + $geoSeriesPrime = $nper * $powNMinus1 / $rate - ($geoSeries / $rate); } $term = $fv + $pv * $powN + $pmt * $geoSeries; $termPrime = $pv * $nper * $powNMinus1 + $pmt * $geoSeriesPrime; @@ -1920,7 +1920,7 @@ public static function RATE($nper, $pmt, $pv, $fv = 0.0, $type = 0, $guess = 0.1 $rateNew = $rate - $term / $termPrime; } } - $found = abs($rateNew - $rate) < $epsilon; + $found = abs($rateNew - $rate) < $epsilon; $rate = $rateNew; }