Skip to content

Commit

Permalink
Extract all Error functions from the Engineering class into a dedicat…
Browse files Browse the repository at this point in the history
…ed Erf and ErfC classes (#1850)

* Extract all Error  functions from the Engineering class into a dedicated Erf and ErfC classes

Retain the original methods in the Engineering class as stubs for BC, but deprecate them. They will be removed for PHPSpreadsheet v2

Note that unit tests still point to the Engineering class stubs; these should be modified to use the Erf and ErfC classes directly when the stubs are removed

* Reminder that ERF is used (either directly or Indirectly) in some of the statistical functions as well
  • Loading branch information
Mark Baker authored Feb 13, 2021
1 parent 61d2e6d commit be328c3
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 88 deletions.
6 changes: 3 additions & 3 deletions src/PhpSpreadsheet/Calculation/Calculation.php
Original file line number Diff line number Diff line change
Expand Up @@ -939,17 +939,17 @@ class Calculation
],
'ERF' => [
'category' => Category::CATEGORY_ENGINEERING,
'functionCall' => [Engineering::class, 'ERF'],
'functionCall' => [Engineering\Erf::class, 'ERF'],
'argumentCount' => '1,2',
],
'ERF.PRECISE' => [
'category' => Category::CATEGORY_ENGINEERING,
'functionCall' => [Engineering::class, 'ERFPRECISE'],
'functionCall' => [Engineering\Erf::class, 'ERFPRECISE'],
'argumentCount' => '1',
],
'ERFC' => [
'category' => Category::CATEGORY_ENGINEERING,
'functionCall' => [Engineering::class, 'ERFC'],
'functionCall' => [Engineering\ErfC::class, 'ERFC'],
'argumentCount' => '1',
],
'ERFC.PRECISE' => [
Expand Down
93 changes: 9 additions & 84 deletions src/PhpSpreadsheet/Calculation/Engineering.php
Original file line number Diff line number Diff line change
Expand Up @@ -1135,34 +1135,6 @@ public static function GESTEP($number, $step = 0)
return (int) ($number >= $step);
}

//
// Private method to calculate the erf value
//
private static $twoSqrtPi = 1.128379167095512574;

public static function erfVal($x)
{
if (abs($x) > 2.2) {
return 1 - self::erfcVal($x);
}
$sum = $term = $x;
$xsqr = ($x * $x);
$j = 1;
do {
$term *= $xsqr / $j;
$sum -= $term / (2 * $j + 1);
++$j;
$term *= $xsqr / $j;
$sum += $term / (2 * $j + 1);
++$j;
if ($sum == 0.0) {
break;
}
} while (abs($term / $sum) > Functions::PRECISION);

return self::$twoSqrtPi * $sum;
}

/**
* BITAND.
*
Expand Down Expand Up @@ -1276,6 +1248,8 @@ public static function BITRSHIFT($number, $shiftAmount)
* Excel Function:
* ERF(lower[,upper])
*
* @Deprecated 2.0.0 Use the ERF() method in the Engineering\Erf class instead
*
* @param float $lower lower bound for integrating ERF
* @param float $upper upper bound for integrating ERF.
* If omitted, ERF integrates between zero and lower_limit
Expand All @@ -1284,19 +1258,7 @@ public static function BITRSHIFT($number, $shiftAmount)
*/
public static function ERF($lower, $upper = null)
{
$lower = Functions::flattenSingleValue($lower);
$upper = Functions::flattenSingleValue($upper);

if (is_numeric($lower)) {
if ($upper === null) {
return self::erfVal($lower);
}
if (is_numeric($upper)) {
return self::erfVal($upper) - self::erfVal($lower);
}
}

return Functions::VALUE();
return Engineering\Erf::ERF($lower, $upper);
}

/**
Expand All @@ -1307,48 +1269,15 @@ public static function ERF($lower, $upper = null)
* Excel Function:
* ERF.PRECISE(limit)
*
* @Deprecated 2.0.0 Use the ERFPRECISE() method in the Engineering\Erf class instead
*
* @param float $limit bound for integrating ERF
*
* @return float|string
*/
public static function ERFPRECISE($limit)
{
$limit = Functions::flattenSingleValue($limit);

return self::ERF($limit);
}

//
// Private method to calculate the erfc value
//
private static $oneSqrtPi = 0.564189583547756287;

private static function erfcVal($x)
{
if (abs($x) < 2.2) {
return 1 - self::erfVal($x);
}
if ($x < 0) {
return 2 - self::ERFC(-$x);
}
$a = $n = 1;
$b = $c = $x;
$d = ($x * $x) + 0.5;
$q1 = $q2 = $b / $d;
$t = 0;
do {
$t = $a * $n + $b * $x;
$a = $b;
$b = $t;
$t = $c * $n + $d * $x;
$c = $d;
$d = $t;
$n += 0.5;
$q1 = $q2;
$q2 = $b / $d;
} while ((abs($q1 - $q2) / $q2) > Functions::PRECISION);

return self::$oneSqrtPi * exp(-$x * $x) * $q2;
return Engineering\Erf::ERFPRECISE($limit);
}

/**
Expand All @@ -1364,19 +1293,15 @@ private static function erfcVal($x)
* Excel Function:
* ERFC(x)
*
* @Deprecated 2.0.0 Use the ERFC() method in the Engineering\ErfC class instead
*
* @param float $x The lower bound for integrating ERFC
*
* @return float|string
*/
public static function ERFC($x)
{
$x = Functions::flattenSingleValue($x);

if (is_numeric($x)) {
return self::erfcVal($x);
}

return Functions::VALUE();
return Engineering\ErfC::ERFC($x);
}

/**
Expand Down
91 changes: 91 additions & 0 deletions src/PhpSpreadsheet/Calculation/Engineering/Erf.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;

use PhpOffice\PhpSpreadsheet\Calculation\Functions;

class Erf
{
private static $twoSqrtPi = 1.128379167095512574;

/**
* ERF.
*
* Returns the error function integrated between the lower and upper bound arguments.
*
* Note: In Excel 2007 or earlier, if you input a negative value for the upper or lower bound arguments,
* the function would return a #NUM! error. However, in Excel 2010, the function algorithm was
* improved, so that it can now calculate the function for both positive and negative ranges.
* PhpSpreadsheet follows Excel 2010 behaviour, and accepts negative arguments.
*
* Excel Function:
* ERF(lower[,upper])
*
* @param float $lower lower bound for integrating ERF
* @param float $upper upper bound for integrating ERF.
* If omitted, ERF integrates between zero and lower_limit
*
* @return float|string
*/
public static function ERF($lower, $upper = null)
{
$lower = Functions::flattenSingleValue($lower);
$upper = Functions::flattenSingleValue($upper);

if (is_numeric($lower)) {
if ($upper === null) {
return self::erfValue($lower);
}
if (is_numeric($upper)) {
return self::erfValue($upper) - self::erfValue($lower);
}
}

return Functions::VALUE();
}

/**
* ERFPRECISE.
*
* Returns the error function integrated between the lower and upper bound arguments.
*
* Excel Function:
* ERF.PRECISE(limit)
*
* @param float $limit bound for integrating ERF
*
* @return float|string
*/
public static function ERFPRECISE($limit)
{
$limit = Functions::flattenSingleValue($limit);

return self::ERF($limit);
}

//
// Private method to calculate the erf value
//
public static function erfValue($value)
{
if (abs($value) > 2.2) {
return 1 - ErfC::ERFC($value);
}
$sum = $term = $value;
$xsqr = ($value * $value);
$j = 1;
do {
$term *= $xsqr / $j;
$sum -= $term / (2 * $j + 1);
++$j;
$term *= $xsqr / $j;
$sum += $term / (2 * $j + 1);
++$j;
if ($sum == 0.0) {
break;
}
} while (abs($term / $sum) > Functions::PRECISION);

return self::$twoSqrtPi * $sum;
}
}
68 changes: 68 additions & 0 deletions src/PhpSpreadsheet/Calculation/Engineering/ErfC.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering;

use PhpOffice\PhpSpreadsheet\Calculation\Functions;

class ErfC
{
/**
* ERFC.
*
* Returns the complementary ERF function integrated between x and infinity
*
* Note: In Excel 2007 or earlier, if you input a negative value for the lower bound argument,
* the function would return a #NUM! error. However, in Excel 2010, the function algorithm was
* improved, so that it can now calculate the function for both positive and negative x values.
* PhpSpreadsheet follows Excel 2010 behaviour, and accepts nagative arguments.
*
* Excel Function:
* ERFC(x)
*
* @param float $value The lower bound for integrating ERFC
*
* @return float|string
*/
public static function ERFC($value)
{
$value = Functions::flattenSingleValue($value);

if (is_numeric($value)) {
return self::erfcValue($value);
}

return Functions::VALUE();
}

//
// Private method to calculate the erfc value
//
private static $oneSqrtPi = 0.564189583547756287;

private static function erfcValue($value)
{
if (abs($value) < 2.2) {
return 1 - Erf::erfValue($value);
}
if ($value < 0) {
return 2 - self::ERFC(-$value);
}
$a = $n = 1;
$b = $c = $value;
$d = ($value * $value) + 0.5;
$q1 = $q2 = $b / $d;
do {
$t = $a * $n + $b * $value;
$a = $b;
$b = $t;
$t = $c * $n + $d * $value;
$c = $d;
$d = $t;
$n += 0.5;
$q1 = $q2;
$q2 = $b / $d;
} while ((abs($q1 - $q2) / $q2) > Functions::PRECISION);

return self::$oneSqrtPi * exp(-$value * $value) * $q2;
}
}
2 changes: 1 addition & 1 deletion src/PhpSpreadsheet/Calculation/Statistical.php
Original file line number Diff line number Diff line change
Expand Up @@ -2716,7 +2716,7 @@ public static function NORMDIST($value, $mean, $stdDev, $cumulative)
}
if ((is_numeric($cumulative)) || (is_bool($cumulative))) {
if ($cumulative) {
return 0.5 * (1 + Engineering::erfVal(($value - $mean) / ($stdDev * sqrt(2))));
return 0.5 * (1 + Engineering\Erf::erfValue(($value - $mean) / ($stdDev * sqrt(2))));
}

return (1 / (self::SQRT2PI * $stdDev)) * exp(0 - (($value - $mean) ** 2 / (2 * ($stdDev * $stdDev))));
Expand Down

0 comments on commit be328c3

Please sign in to comment.