This commit is contained in:
steven 2025-08-11 22:23:30 +02:00
commit 72a26edcff
22092 changed files with 2101903 additions and 0 deletions

View file

@ -0,0 +1,59 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
abstract class AggregateBase
{
/**
* MS Excel does not count Booleans if passed as cell values, but they are counted if passed as literals.
* OpenOffice Calc always counts Booleans.
* Gnumeric never counts Booleans.
*/
protected static function testAcceptedBoolean(mixed $arg, mixed $k): mixed
{
if (!is_bool($arg)) {
return $arg;
}
if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_GNUMERIC) {
return $arg;
}
if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) {
return (int) $arg;
}
if (!Functions::isCellValue($k)) {
return (int) $arg;
}
/*if (
(is_bool($arg)) &&
((!Functions::isCellValue($k) && (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_EXCEL)) ||
(Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE))
) {
$arg = (int) $arg;
}*/
return $arg;
}
protected static function isAcceptedCountable(mixed $arg, mixed $k, bool $countNull = false): bool
{
if ($countNull && $arg === null && !Functions::isCellValue($k) && Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_GNUMERIC) {
return true;
}
if (!is_numeric($arg)) {
return false;
}
if (!is_string($arg)) {
return true;
}
if (!Functions::isCellValue($k) && Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) {
return true;
}
if (!Functions::isCellValue($k) && Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_GNUMERIC) {
return true;
}
return false;
}
}

View file

@ -0,0 +1,259 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Averages extends AggregateBase
{
/**
* AVEDEV.
*
* Returns the average of the absolute deviations of data points from their mean.
* AVEDEV is a measure of the variability in a data set.
*
* Excel Function:
* AVEDEV(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*
* @return float|string (string if result is an error)
*/
public static function averageDeviations(mixed ...$args): string|float
{
$aArgs = Functions::flattenArrayIndexed($args);
// Return value
$returnValue = 0.0;
$aMean = self::average(...$args);
if ($aMean === ExcelError::DIV0()) {
return ExcelError::NAN();
} elseif ($aMean === ExcelError::VALUE()) {
return ExcelError::VALUE();
}
$aCount = 0;
foreach ($aArgs as $k => $arg) {
$arg = self::testAcceptedBoolean($arg, $k);
// Is it a numeric value?
// Strings containing numeric values are only counted if they are string literals (not cell values)
// and then only in MS Excel and in Open Office, not in Gnumeric
if ((is_string($arg)) && (!is_numeric($arg)) && (!Functions::isCellValue($k))) {
return ExcelError::VALUE();
}
if (self::isAcceptedCountable($arg, $k)) {
$returnValue += abs($arg - $aMean);
++$aCount;
}
}
// Return
if ($aCount === 0) {
return ExcelError::DIV0();
}
return $returnValue / $aCount;
}
/**
* AVERAGE.
*
* Returns the average (arithmetic mean) of the arguments
*
* Excel Function:
* AVERAGE(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*
* @return float|int|string (string if result is an error)
*/
public static function average(mixed ...$args): string|int|float
{
$returnValue = $aCount = 0;
// Loop through arguments
foreach (Functions::flattenArrayIndexed($args) as $k => $arg) {
$arg = self::testAcceptedBoolean($arg, $k);
// Is it a numeric value?
// Strings containing numeric values are only counted if they are string literals (not cell values)
// and then only in MS Excel and in Open Office, not in Gnumeric
if ((is_string($arg)) && (!is_numeric($arg)) && (!Functions::isCellValue($k))) {
return ExcelError::VALUE();
}
if (self::isAcceptedCountable($arg, $k)) {
$returnValue += $arg;
++$aCount;
}
}
// Return
if ($aCount > 0) {
return $returnValue / $aCount;
}
return ExcelError::DIV0();
}
/**
* AVERAGEA.
*
* Returns the average of its arguments, including numbers, text, and logical values
*
* Excel Function:
* AVERAGEA(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*
* @return float|int|string (string if result is an error)
*/
public static function averageA(mixed ...$args): string|int|float
{
$returnValue = null;
$aCount = 0;
// Loop through arguments
foreach (Functions::flattenArrayIndexed($args) as $k => $arg) {
if (is_numeric($arg)) {
// do nothing
} elseif (is_bool($arg)) {
$arg = (int) $arg;
} elseif (!Functions::isMatrixValue($k)) {
$arg = 0;
} else {
return ExcelError::VALUE();
}
$returnValue += $arg;
++$aCount;
}
if ($aCount > 0) {
return $returnValue / $aCount;
}
return ExcelError::DIV0();
}
/**
* MEDIAN.
*
* Returns the median of the given numbers. The median is the number in the middle of a set of numbers.
*
* Excel Function:
* MEDIAN(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*
* @return float|string The result, or a string containing an error
*/
public static function median(mixed ...$args): float|string
{
$aArgs = Functions::flattenArray($args);
$returnValue = ExcelError::NAN();
$aArgs = self::filterArguments($aArgs);
$valueCount = count($aArgs);
if ($valueCount > 0) {
sort($aArgs, SORT_NUMERIC);
$valueCount = $valueCount / 2;
if ($valueCount == floor($valueCount)) {
$returnValue = ($aArgs[$valueCount--] + $aArgs[$valueCount]) / 2;
} else {
$valueCount = floor($valueCount);
$returnValue = $aArgs[$valueCount];
}
}
return $returnValue;
}
/**
* MODE.
*
* Returns the most frequently occurring, or repetitive, value in an array or range of data
*
* Excel Function:
* MODE(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*
* @return float|string The result, or a string containing an error
*/
public static function mode(mixed ...$args): float|string
{
$returnValue = ExcelError::NA();
// Loop through arguments
$aArgs = Functions::flattenArray($args);
$aArgs = self::filterArguments($aArgs);
if (!empty($aArgs)) {
return self::modeCalc($aArgs);
}
return $returnValue;
}
protected static function filterArguments(array $args): array
{
return array_filter(
$args,
function ($value): bool {
// Is it a numeric value?
return is_numeric($value) && (!is_string($value));
}
);
}
/**
* Special variant of array_count_values that isn't limited to strings and integers,
* but can work with floating point numbers as values.
*/
private static function modeCalc(array $data): float|string
{
$frequencyArray = [];
$index = 0;
$maxfreq = 0;
$maxfreqkey = '';
$maxfreqdatum = '';
foreach ($data as $datum) {
$found = false;
++$index;
foreach ($frequencyArray as $key => $value) {
if ((string) $value['value'] == (string) $datum) {
++$frequencyArray[$key]['frequency'];
$freq = $frequencyArray[$key]['frequency'];
if ($freq > $maxfreq) {
$maxfreq = $freq;
$maxfreqkey = $key;
$maxfreqdatum = $datum;
} elseif ($freq == $maxfreq) {
if ($frequencyArray[$key]['index'] < $frequencyArray[$maxfreqkey]['index']) {
$maxfreqkey = $key;
$maxfreqdatum = $datum;
}
}
$found = true;
break;
}
}
if ($found === false) {
$frequencyArray[] = [
'value' => $datum,
'frequency' => 1,
'index' => $index,
];
}
}
if ($maxfreq <= 1) {
return ExcelError::NA();
}
return $maxfreqdatum;
}
}

View file

@ -0,0 +1,126 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Averages;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\MathTrig;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Averages;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Counts;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Minimum;
class Mean
{
/**
* GEOMEAN.
*
* Returns the geometric mean of an array or range of positive data. For example, you
* can use GEOMEAN to calculate average growth rate given compound interest with
* variable rates.
*
* Excel Function:
* GEOMEAN(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*/
public static function geometric(mixed ...$args): float|int|string
{
$aArgs = Functions::flattenArray($args);
$aMean = MathTrig\Operations::product($aArgs);
if (is_numeric($aMean) && ($aMean > 0)) {
$aCount = Counts::COUNT($aArgs);
if (Minimum::min($aArgs) > 0) {
return $aMean ** (1 / $aCount);
}
}
return ExcelError::NAN();
}
/**
* HARMEAN.
*
* Returns the harmonic mean of a data set. The harmonic mean is the reciprocal of the
* arithmetic mean of reciprocals.
*
* Excel Function:
* HARMEAN(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*/
public static function harmonic(mixed ...$args): string|float|int
{
// Loop through arguments
$aArgs = Functions::flattenArray($args);
if (Minimum::min($aArgs) < 0) {
return ExcelError::NAN();
}
$returnValue = 0;
$aCount = 0;
foreach ($aArgs as $arg) {
// Is it a numeric value?
if ((is_numeric($arg)) && (!is_string($arg))) {
if ($arg <= 0) {
return ExcelError::NAN();
}
$returnValue += (1 / $arg);
++$aCount;
}
}
// Return
if ($aCount > 0) {
return 1 / ($returnValue / $aCount);
}
return ExcelError::NA();
}
/**
* TRIMMEAN.
*
* Returns the mean of the interior of a data set. TRIMMEAN calculates the mean
* taken by excluding a percentage of data points from the top and bottom tails
* of a data set.
*
* Excel Function:
* TRIMEAN(value1[,value2[, ...]], $discard)
*
* @param mixed $args Data values
*/
public static function trim(mixed ...$args): float|string
{
$aArgs = Functions::flattenArray($args);
// Calculate
$percent = array_pop($aArgs);
if ((is_numeric($percent)) && (!is_string($percent))) {
if (($percent < 0) || ($percent > 1)) {
return ExcelError::NAN();
}
$mArgs = [];
foreach ($aArgs as $arg) {
// Is it a numeric value?
if ((is_numeric($arg)) && (!is_string($arg))) {
$mArgs[] = $arg;
}
}
$discard = floor(Counts::COUNT($mArgs) * $percent / 2);
sort($mArgs);
for ($i = 0; $i < $discard; ++$i) {
array_pop($mArgs);
array_shift($mArgs);
}
return Averages::average($mArgs);
}
return ExcelError::VALUE();
}
}

View file

@ -0,0 +1,293 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;
use PhpOffice\PhpSpreadsheet\Calculation\Database\DAverage;
use PhpOffice\PhpSpreadsheet\Calculation\Database\DCount;
use PhpOffice\PhpSpreadsheet\Calculation\Database\DMax;
use PhpOffice\PhpSpreadsheet\Calculation\Database\DMin;
use PhpOffice\PhpSpreadsheet\Calculation\Database\DSum;
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcException;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class Conditional
{
private const CONDITION_COLUMN_NAME = 'CONDITION';
private const VALUE_COLUMN_NAME = 'VALUE';
private const CONDITIONAL_COLUMN_NAME = 'CONDITIONAL %d';
/**
* AVERAGEIF.
*
* Returns the average value from a range of cells that contain numbers within the list of arguments
*
* Excel Function:
* AVERAGEIF(range,condition[, average_range])
*
* @param mixed $range Data values
* @param null|array|string $condition the criteria that defines which cells will be checked
* @param mixed $averageRange Data values
*/
public static function AVERAGEIF(mixed $range, null|array|string $condition, mixed $averageRange = []): null|int|float|string
{
if (!is_array($range) || !is_array($averageRange) || array_key_exists(0, $range) || array_key_exists(0, $averageRange)) {
throw new CalcException('Must specify range of cells, not any kind of literal');
}
$database = self::databaseFromRangeAndValue($range, $averageRange);
$condition = [[self::CONDITION_COLUMN_NAME, self::VALUE_COLUMN_NAME], [$condition, null]];
return DAverage::evaluate($database, self::VALUE_COLUMN_NAME, $condition);
}
/**
* AVERAGEIFS.
*
* Counts the number of cells that contain numbers within the list of arguments
*
* Excel Function:
* AVERAGEIFS(average_range, criteria_range1, criteria1, [criteria_range2, criteria2])
*
* @param mixed $args Pairs of Ranges and Criteria
*/
public static function AVERAGEIFS(mixed ...$args): null|int|float|string
{
if (empty($args)) {
return 0.0;
} elseif (count($args) === 3) {
return self::AVERAGEIF($args[1], $args[2], $args[0]);
}
foreach ($args as $arg) {
if (is_array($arg) && array_key_exists(0, $arg)) {
throw new CalcException('Must specify range of cells, not any kind of literal');
}
}
$conditions = self::buildConditionSetForValueRange(...$args);
$database = self::buildDatabaseWithValueRange(...$args);
return DAverage::evaluate($database, self::VALUE_COLUMN_NAME, $conditions);
}
/**
* COUNTIF.
*
* Counts the number of cells that contain numbers within the list of arguments
*
* Excel Function:
* COUNTIF(range,condition)
*
* @param mixed[] $range Data values
* @param null|array|string $condition the criteria that defines which cells will be counted
*/
public static function COUNTIF(array $range, null|array|string $condition): string|int
{
// Filter out any empty values that shouldn't be included in a COUNT
$range = array_filter(
Functions::flattenArray($range),
fn ($value): bool => $value !== null && $value !== ''
);
$range = array_merge([[self::CONDITION_COLUMN_NAME]], array_chunk($range, 1));
$condition = array_merge([[self::CONDITION_COLUMN_NAME]], [[$condition]]);
return DCount::evaluate($range, null, $condition, false);
}
/**
* COUNTIFS.
*
* Counts the number of cells that contain numbers within the list of arguments
*
* Excel Function:
* COUNTIFS(criteria_range1, criteria1, [criteria_range2, criteria2])
*
* @param mixed $args Pairs of Ranges and Criteria
*/
public static function COUNTIFS(mixed ...$args): int|string
{
if (empty($args)) {
return 0;
} elseif (count($args) === 2) {
return self::COUNTIF(...$args);
}
$database = self::buildDatabase(...$args);
$conditions = self::buildConditionSet(...$args);
return DCount::evaluate($database, null, $conditions, false);
}
/**
* MAXIFS.
*
* Returns the maximum value within a range of cells that contain numbers within the list of arguments
*
* Excel Function:
* MAXIFS(max_range, criteria_range1, criteria1, [criteria_range2, criteria2])
*
* @param mixed $args Pairs of Ranges and Criteria
*/
public static function MAXIFS(mixed ...$args): null|float|string
{
if (empty($args)) {
return 0.0;
}
$conditions = self::buildConditionSetForValueRange(...$args);
$database = self::buildDatabaseWithValueRange(...$args);
return DMax::evaluate($database, self::VALUE_COLUMN_NAME, $conditions, false);
}
/**
* MINIFS.
*
* Returns the minimum value within a range of cells that contain numbers within the list of arguments
*
* Excel Function:
* MINIFS(min_range, criteria_range1, criteria1, [criteria_range2, criteria2])
*
* @param mixed $args Pairs of Ranges and Criteria
*/
public static function MINIFS(mixed ...$args): null|float|string
{
if (empty($args)) {
return 0.0;
}
$conditions = self::buildConditionSetForValueRange(...$args);
$database = self::buildDatabaseWithValueRange(...$args);
return DMin::evaluate($database, self::VALUE_COLUMN_NAME, $conditions, false);
}
/**
* SUMIF.
*
* Totals the values of cells that contain numbers within the list of arguments
*
* Excel Function:
* SUMIF(range, criteria, [sum_range])
*
* @param array $range Data values
*/
public static function SUMIF(array $range, mixed $condition, array $sumRange = []): null|float|string
{
$database = self::databaseFromRangeAndValue($range, $sumRange);
$condition = [[self::CONDITION_COLUMN_NAME, self::VALUE_COLUMN_NAME], [$condition, null]];
return DSum::evaluate($database, self::VALUE_COLUMN_NAME, $condition);
}
/**
* SUMIFS.
*
* Counts the number of cells that contain numbers within the list of arguments
*
* Excel Function:
* SUMIFS(average_range, criteria_range1, criteria1, [criteria_range2, criteria2])
*
* @param mixed $args Pairs of Ranges and Criteria
*/
public static function SUMIFS(mixed ...$args): null|float|string
{
if (empty($args)) {
return 0.0;
} elseif (count($args) === 3) {
return self::SUMIF($args[1], $args[2], $args[0]);
}
$conditions = self::buildConditionSetForValueRange(...$args);
$database = self::buildDatabaseWithValueRange(...$args);
return DSum::evaluate($database, self::VALUE_COLUMN_NAME, $conditions);
}
/** @param array $args */
private static function buildConditionSet(...$args): array
{
$conditions = self::buildConditions(1, ...$args);
return array_map(null, ...$conditions);
}
/** @param array $args */
private static function buildConditionSetForValueRange(...$args): array
{
$conditions = self::buildConditions(2, ...$args);
if (count($conditions) === 1) {
return array_map(
fn ($value): array => [$value],
$conditions[0]
);
}
return array_map(null, ...$conditions);
}
/** @param array $args */
private static function buildConditions(int $startOffset, ...$args): array
{
$conditions = [];
$pairCount = 1;
$argumentCount = count($args);
for ($argument = $startOffset; $argument < $argumentCount; $argument += 2) {
$conditions[] = array_merge([sprintf(self::CONDITIONAL_COLUMN_NAME, $pairCount)], [$args[$argument]]);
++$pairCount;
}
return $conditions;
}
/** @param array $args */
private static function buildDatabase(...$args): array
{
$database = [];
return self::buildDataSet(0, $database, ...$args);
}
/** @param array $args */
private static function buildDatabaseWithValueRange(...$args): array
{
$database = [];
$database[] = array_merge(
[self::VALUE_COLUMN_NAME],
Functions::flattenArray($args[0])
);
return self::buildDataSet(1, $database, ...$args);
}
/** @param array $args */
private static function buildDataSet(int $startOffset, array $database, ...$args): array
{
$pairCount = 1;
$argumentCount = count($args);
for ($argument = $startOffset; $argument < $argumentCount; $argument += 2) {
$database[] = array_merge(
[sprintf(self::CONDITIONAL_COLUMN_NAME, $pairCount)],
Functions::flattenArray($args[$argument])
);
++$pairCount;
}
return array_map(null, ...$database);
}
private static function databaseFromRangeAndValue(array $range, array $valueRange = []): array
{
$range = Functions::flattenArray($range);
$valueRange = Functions::flattenArray($valueRange);
if (empty($valueRange)) {
$valueRange = $range;
}
$database = array_map(null, array_merge([self::CONDITION_COLUMN_NAME], $range), array_merge([self::VALUE_COLUMN_NAME], $valueRange));
return $database;
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Confidence
{
use ArrayEnabled;
/**
* CONFIDENCE.
*
* Returns the confidence interval for a population mean
*
* @param mixed $alpha As a float
* Or can be an array of values
* @param mixed $stdDev Standard Deviation as a float
* Or can be an array of values
* @param mixed $size As an integer
* Or can be an array of values
*
* @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function CONFIDENCE(mixed $alpha, mixed $stdDev, mixed $size)
{
if (is_array($alpha) || is_array($stdDev) || is_array($size)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $alpha, $stdDev, $size);
}
try {
$alpha = StatisticalValidations::validateFloat($alpha);
$stdDev = StatisticalValidations::validateFloat($stdDev);
$size = StatisticalValidations::validateInt($size);
} catch (Exception $e) {
return $e->getMessage();
}
if (($alpha <= 0) || ($alpha >= 1) || ($stdDev <= 0) || ($size < 1)) {
return ExcelError::NAN();
}
/** @var float $temp */
$temp = Distributions\StandardNormal::inverse(1 - $alpha / 2);
return Functions::scalar($temp * $stdDev / sqrt($size));
}
}

View file

@ -0,0 +1,96 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcException;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class Counts extends AggregateBase
{
/**
* COUNT.
*
* Counts the number of cells that contain numbers within the list of arguments
*
* Excel Function:
* COUNT(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*/
public static function COUNT(mixed ...$args): int
{
$returnValue = 0;
// Loop through arguments
$aArgs = Functions::flattenArrayIndexed($args);
foreach ($aArgs as $k => $arg) {
$arg = self::testAcceptedBoolean($arg, $k);
// Is it a numeric value?
// Strings containing numeric values are only counted if they are string literals (not cell values)
// and then only in MS Excel and in Open Office, not in Gnumeric
if (self::isAcceptedCountable($arg, $k, true)) {
++$returnValue;
}
}
return $returnValue;
}
/**
* COUNTA.
*
* Counts the number of cells that are not empty within the list of arguments
*
* Excel Function:
* COUNTA(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*/
public static function COUNTA(mixed ...$args): int
{
$returnValue = 0;
// Loop through arguments
$aArgs = Functions::flattenArrayIndexed($args);
foreach ($aArgs as $k => $arg) {
// Nulls are counted if literals, but not if cell values
if ($arg !== null || (!Functions::isCellValue($k))) {
++$returnValue;
}
}
return $returnValue;
}
/**
* COUNTBLANK.
*
* Counts the number of empty cells within the list of arguments
*
* Excel Function:
* COUNTBLANK(value1[,value2[, ...]])
*
* @param mixed $range Data values
*/
public static function COUNTBLANK(mixed $range): int
{
if ($range === null) {
return 1;
}
if (!is_array($range) || array_key_exists(0, $range)) {
throw new CalcException('Must specify range of cells, not any kind of literal');
}
$returnValue = 0;
// Loop through arguments
$aArgs = Functions::flattenArray($range);
foreach ($aArgs as $arg) {
// Is it a blank cell?
if (($arg === null) || ((is_string($arg)) && ($arg == ''))) {
++$returnValue;
}
}
return $returnValue;
}
}

View file

@ -0,0 +1,138 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Deviations
{
/**
* DEVSQ.
*
* Returns the sum of squares of deviations of data points from their sample mean.
*
* Excel Function:
* DEVSQ(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*/
public static function sumSquares(mixed ...$args): string|float
{
$aArgs = Functions::flattenArrayIndexed($args);
$aMean = Averages::average($aArgs);
if (!is_numeric($aMean)) {
return ExcelError::NAN();
}
// Return value
$returnValue = 0.0;
$aCount = -1;
foreach ($aArgs as $k => $arg) {
// Is it a numeric value?
if (
(is_bool($arg))
&& ((!Functions::isCellValue($k))
|| (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE))
) {
$arg = (int) $arg;
}
if ((is_numeric($arg)) && (!is_string($arg))) {
$returnValue += ($arg - $aMean) ** 2;
++$aCount;
}
}
return $aCount === 0 ? ExcelError::VALUE() : $returnValue;
}
/**
* KURT.
*
* Returns the kurtosis of a data set. Kurtosis characterizes the relative peakedness
* or flatness of a distribution compared with the normal distribution. Positive
* kurtosis indicates a relatively peaked distribution. Negative kurtosis indicates a
* relatively flat distribution.
*
* @param array ...$args Data Series
*/
public static function kurtosis(...$args): string|int|float
{
$aArgs = Functions::flattenArrayIndexed($args);
$mean = Averages::average($aArgs);
if (!is_numeric($mean)) {
return ExcelError::DIV0();
}
$stdDev = (float) StandardDeviations::STDEV($aArgs);
if ($stdDev > 0) {
$count = $summer = 0;
foreach ($aArgs as $k => $arg) {
if ((is_bool($arg)) && (!Functions::isMatrixValue($k))) {
} else {
// Is it a numeric value?
if ((is_numeric($arg)) && (!is_string($arg))) {
$summer += (($arg - $mean) / $stdDev) ** 4;
++$count;
}
}
}
if ($count > 3) {
return $summer * ($count * ($count + 1)
/ (($count - 1) * ($count - 2) * ($count - 3))) - (3 * ($count - 1) ** 2
/ (($count - 2) * ($count - 3)));
}
}
return ExcelError::DIV0();
}
/**
* SKEW.
*
* Returns the skewness of a distribution. Skewness characterizes the degree of asymmetry
* of a distribution around its mean. Positive skewness indicates a distribution with an
* asymmetric tail extending toward more positive values. Negative skewness indicates a
* distribution with an asymmetric tail extending toward more negative values.
*
* @param array ...$args Data Series
*
* @return float|int|string The result, or a string containing an error
*/
public static function skew(...$args): string|int|float
{
$aArgs = Functions::flattenArrayIndexed($args);
$mean = Averages::average($aArgs);
if (!is_numeric($mean)) {
return ExcelError::DIV0();
}
$stdDev = StandardDeviations::STDEV($aArgs);
if ($stdDev === 0.0 || is_string($stdDev)) {
return ExcelError::DIV0();
}
$count = $summer = 0;
// Loop through arguments
foreach ($aArgs as $k => $arg) {
if ((is_bool($arg)) && (!Functions::isMatrixValue($k))) {
} elseif (!is_numeric($arg)) {
return ExcelError::VALUE();
} else {
// Is it a numeric value?
if ((is_numeric($arg)) && (!is_string($arg))) {
$summer += (($arg - $mean) / $stdDev) ** 3;
++$count;
}
}
}
if ($count > 2) {
return $summer * ($count / (($count - 1) * ($count - 2)));
}
return ExcelError::DIV0();
}
}

View file

@ -0,0 +1,279 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Beta
{
use ArrayEnabled;
private const MAX_ITERATIONS = 256;
private const LOG_GAMMA_X_MAX_VALUE = 2.55e305;
private const XMININ = 2.23e-308;
/**
* BETADIST.
*
* Returns the beta distribution.
*
* @param mixed $value Float value at which you want to evaluate the distribution
* Or can be an array of values
* @param mixed $alpha Parameter to the distribution as a float
* Or can be an array of values
* @param mixed $beta Parameter to the distribution as a float
* Or can be an array of values
* @param mixed $rMin as an float
* Or can be an array of values
* @param mixed $rMax as an float
* Or can be an array of values
*
* @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function distribution(mixed $value, mixed $alpha, mixed $beta, mixed $rMin = 0.0, mixed $rMax = 1.0): array|string|float
{
if (is_array($value) || is_array($alpha) || is_array($beta) || is_array($rMin) || is_array($rMax)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $alpha, $beta, $rMin, $rMax);
}
$rMin = $rMin ?? 0.0;
$rMax = $rMax ?? 1.0;
try {
$value = DistributionValidations::validateFloat($value);
$alpha = DistributionValidations::validateFloat($alpha);
$beta = DistributionValidations::validateFloat($beta);
$rMax = DistributionValidations::validateFloat($rMax);
$rMin = DistributionValidations::validateFloat($rMin);
} catch (Exception $e) {
return $e->getMessage();
}
if ($rMin > $rMax) {
$tmp = $rMin;
$rMin = $rMax;
$rMax = $tmp;
}
if (($value < $rMin) || ($value > $rMax) || ($alpha <= 0) || ($beta <= 0) || ($rMin == $rMax)) {
return ExcelError::NAN();
}
$value -= $rMin;
$value /= ($rMax - $rMin);
return self::incompleteBeta($value, $alpha, $beta);
}
/**
* BETAINV.
*
* Returns the inverse of the Beta distribution.
*
* @param mixed $probability Float probability at which you want to evaluate the distribution
* Or can be an array of values
* @param mixed $alpha Parameter to the distribution as a float
* Or can be an array of values
* @param mixed $beta Parameter to the distribution as a float
* Or can be an array of values
* @param mixed $rMin Minimum value as a float
* Or can be an array of values
* @param mixed $rMax Maximum value as a float
* Or can be an array of values
*
* @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function inverse(mixed $probability, mixed $alpha, mixed $beta, mixed $rMin = 0.0, mixed $rMax = 1.0): array|string|float
{
if (is_array($probability) || is_array($alpha) || is_array($beta) || is_array($rMin) || is_array($rMax)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $alpha, $beta, $rMin, $rMax);
}
$rMin = $rMin ?? 0.0;
$rMax = $rMax ?? 1.0;
try {
$probability = DistributionValidations::validateProbability($probability);
$alpha = DistributionValidations::validateFloat($alpha);
$beta = DistributionValidations::validateFloat($beta);
$rMax = DistributionValidations::validateFloat($rMax);
$rMin = DistributionValidations::validateFloat($rMin);
} catch (Exception $e) {
return $e->getMessage();
}
if ($rMin > $rMax) {
$tmp = $rMin;
$rMin = $rMax;
$rMax = $tmp;
}
if (($alpha <= 0) || ($beta <= 0) || ($rMin == $rMax) || ($probability <= 0.0)) {
return ExcelError::NAN();
}
return self::calculateInverse($probability, $alpha, $beta, $rMin, $rMax);
}
private static function calculateInverse(float $probability, float $alpha, float $beta, float $rMin, float $rMax): string|float
{
$a = 0;
$b = 2;
$guess = ($a + $b) / 2;
$i = 0;
while ((($b - $a) > Functions::PRECISION) && (++$i <= self::MAX_ITERATIONS)) {
$guess = ($a + $b) / 2;
$result = self::distribution($guess, $alpha, $beta);
if (($result === $probability) || ($result === 0.0)) {
$b = $a;
} elseif ($result > $probability) {
$b = $guess;
} else {
$a = $guess;
}
}
if ($i === self::MAX_ITERATIONS) {
return ExcelError::NA();
}
return round($rMin + $guess * ($rMax - $rMin), 12);
}
/**
* Incomplete beta function.
*
* @author Jaco van Kooten
* @author Paul Meagher
*
* The computation is based on formulas from Numerical Recipes, Chapter 6.4 (W.H. Press et al, 1992).
*
* @param float $x require 0<=x<=1
* @param float $p require p>0
* @param float $q require q>0
*
* @return float 0 if x<0, p<=0, q<=0 or p+q>2.55E305 and 1 if x>1 to avoid errors and over/underflow
*/
public static function incompleteBeta(float $x, float $p, float $q): float
{
if ($x <= 0.0) {
return 0.0;
} elseif ($x >= 1.0) {
return 1.0;
} elseif (($p <= 0.0) || ($q <= 0.0) || (($p + $q) > self::LOG_GAMMA_X_MAX_VALUE)) {
return 0.0;
}
$beta_gam = exp((0 - self::logBeta($p, $q)) + $p * log($x) + $q * log(1.0 - $x));
if ($x < ($p + 1.0) / ($p + $q + 2.0)) {
return $beta_gam * self::betaFraction($x, $p, $q) / $p;
}
return 1.0 - ($beta_gam * self::betaFraction(1 - $x, $q, $p) / $q);
}
// Function cache for logBeta function
private static float $logBetaCacheP = 0.0;
private static float $logBetaCacheQ = 0.0;
private static float $logBetaCacheResult = 0.0;
/**
* The natural logarithm of the beta function.
*
* @param float $p require p>0
* @param float $q require q>0
*
* @return float 0 if p<=0, q<=0 or p+q>2.55E305 to avoid errors and over/underflow
*
* @author Jaco van Kooten
*/
private static function logBeta(float $p, float $q): float
{
if ($p != self::$logBetaCacheP || $q != self::$logBetaCacheQ) {
self::$logBetaCacheP = $p;
self::$logBetaCacheQ = $q;
if (($p <= 0.0) || ($q <= 0.0) || (($p + $q) > self::LOG_GAMMA_X_MAX_VALUE)) {
self::$logBetaCacheResult = 0.0;
} else {
self::$logBetaCacheResult = Gamma::logGamma($p) + Gamma::logGamma($q) - Gamma::logGamma($p + $q);
}
}
return self::$logBetaCacheResult;
}
/**
* Evaluates of continued fraction part of incomplete beta function.
* Based on an idea from Numerical Recipes (W.H. Press et al, 1992).
*
* @author Jaco van Kooten
*/
private static function betaFraction(float $x, float $p, float $q): float
{
$c = 1.0;
$sum_pq = $p + $q;
$p_plus = $p + 1.0;
$p_minus = $p - 1.0;
$h = 1.0 - $sum_pq * $x / $p_plus;
if (abs($h) < self::XMININ) {
$h = self::XMININ;
}
$h = 1.0 / $h;
$frac = $h;
$m = 1;
$delta = 0.0;
while ($m <= self::MAX_ITERATIONS && abs($delta - 1.0) > Functions::PRECISION) {
$m2 = 2 * $m;
// even index for d
$d = $m * ($q - $m) * $x / (($p_minus + $m2) * ($p + $m2));
$h = 1.0 + $d * $h;
if (abs($h) < self::XMININ) {
$h = self::XMININ;
}
$h = 1.0 / $h;
$c = 1.0 + $d / $c;
if (abs($c) < self::XMININ) {
$c = self::XMININ;
}
$frac *= $h * $c;
// odd index for d
$d = -($p + $m) * ($sum_pq + $m) * $x / (($p + $m2) * ($p_plus + $m2));
$h = 1.0 + $d * $h;
if (abs($h) < self::XMININ) {
$h = self::XMININ;
}
$h = 1.0 / $h;
$c = 1.0 + $d / $c;
if (abs($c) < self::XMININ) {
$c = self::XMININ;
}
$delta = $h * $c;
$frac *= $delta;
++$m;
}
return $frac;
}
/*
private static function betaValue(float $a, float $b): float
{
return (Gamma::gammaValue($a) * Gamma::gammaValue($b)) /
Gamma::gammaValue($a + $b);
}
private static function regularizedIncompleteBeta(float $value, float $a, float $b): float
{
return self::incompleteBeta($value, $a, $b) / self::betaValue($a, $b);
}
*/
}

View file

@ -0,0 +1,231 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Combinations;
class Binomial
{
use ArrayEnabled;
/**
* BINOMDIST.
*
* Returns the individual term binomial distribution probability. Use BINOMDIST in problems with
* a fixed number of tests or trials, when the outcomes of any trial are only success or failure,
* when trials are independent, and when the probability of success is constant throughout the
* experiment. For example, BINOMDIST can calculate the probability that two of the next three
* babies born are male.
*
* @param mixed $value Integer number of successes in trials
* Or can be an array of values
* @param mixed $trials Integer umber of trials
* Or can be an array of values
* @param mixed $probability Probability of success on each trial as a float
* Or can be an array of values
* @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false)
* Or can be an array of values
*
* @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function distribution(mixed $value, mixed $trials, mixed $probability, mixed $cumulative)
{
if (is_array($value) || is_array($trials) || is_array($probability) || is_array($cumulative)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $trials, $probability, $cumulative);
}
try {
$value = DistributionValidations::validateInt($value);
$trials = DistributionValidations::validateInt($trials);
$probability = DistributionValidations::validateProbability($probability);
$cumulative = DistributionValidations::validateBool($cumulative);
} catch (Exception $e) {
return $e->getMessage();
}
if (($value < 0) || ($value > $trials)) {
return ExcelError::NAN();
}
if ($cumulative) {
return self::calculateCumulativeBinomial($value, $trials, $probability);
}
/** @var float $comb */
$comb = Combinations::withoutRepetition($trials, $value);
return $comb * $probability ** $value
* (1 - $probability) ** ($trials - $value);
}
/**
* BINOM.DIST.RANGE.
*
* Returns returns the Binomial Distribution probability for the number of successes from a specified number
* of trials falling into a specified range.
*
* @param mixed $trials Integer number of trials
* Or can be an array of values
* @param mixed $probability Probability of success on each trial as a float
* Or can be an array of values
* @param mixed $successes The integer number of successes in trials
* Or can be an array of values
* @param mixed $limit Upper limit for successes in trials as null, or an integer
* If null, then this will indicate the same as the number of Successes
* Or can be an array of values
*
* @return array|float|int|string If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function range(mixed $trials, mixed $probability, mixed $successes, mixed $limit = null): array|string|float|int
{
if (is_array($trials) || is_array($probability) || is_array($successes) || is_array($limit)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $trials, $probability, $successes, $limit);
}
$limit = $limit ?? $successes;
try {
$trials = DistributionValidations::validateInt($trials);
$probability = DistributionValidations::validateProbability($probability);
$successes = DistributionValidations::validateInt($successes);
$limit = DistributionValidations::validateInt($limit);
} catch (Exception $e) {
return $e->getMessage();
}
if (($successes < 0) || ($successes > $trials)) {
return ExcelError::NAN();
}
if (($limit < 0) || ($limit > $trials) || $limit < $successes) {
return ExcelError::NAN();
}
$summer = 0;
for ($i = $successes; $i <= $limit; ++$i) {
/** @var float $comb */
$comb = Combinations::withoutRepetition($trials, $i);
$summer += $comb * $probability ** $i
* (1 - $probability) ** ($trials - $i);
}
return $summer;
}
/**
* NEGBINOMDIST.
*
* Returns the negative binomial distribution. NEGBINOMDIST returns the probability that
* there will be number_f failures before the number_s-th success, when the constant
* probability of a success is probability_s. This function is similar to the binomial
* distribution, except that the number of successes is fixed, and the number of trials is
* variable. Like the binomial, trials are assumed to be independent.
*
* @param mixed $failures Number of Failures as an integer
* Or can be an array of values
* @param mixed $successes Threshold number of Successes as an integer
* Or can be an array of values
* @param mixed $probability Probability of success on each trial as a float
* Or can be an array of values
*
* @return array|float|string The result, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*
* TODO Add support for the cumulative flag not present for NEGBINOMDIST, but introduced for NEGBINOM.DIST
* The cumulative default should be false to reflect the behaviour of NEGBINOMDIST
*/
public static function negative(mixed $failures, mixed $successes, mixed $probability): array|string|float
{
if (is_array($failures) || is_array($successes) || is_array($probability)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $failures, $successes, $probability);
}
try {
$failures = DistributionValidations::validateInt($failures);
$successes = DistributionValidations::validateInt($successes);
$probability = DistributionValidations::validateProbability($probability);
} catch (Exception $e) {
return $e->getMessage();
}
if (($failures < 0) || ($successes < 1)) {
return ExcelError::NAN();
}
if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) {
if (($failures + $successes - 1) <= 0) {
return ExcelError::NAN();
}
}
/** @var float $comb */
$comb = Combinations::withoutRepetition($failures + $successes - 1, $successes - 1);
return $comb
* ($probability ** $successes) * ((1 - $probability) ** $failures);
}
/**
* BINOM.INV.
*
* Returns the smallest value for which the cumulative binomial distribution is greater
* than or equal to a criterion value
*
* @param mixed $trials number of Bernoulli trials as an integer
* Or can be an array of values
* @param mixed $probability probability of a success on each trial as a float
* Or can be an array of values
* @param mixed $alpha criterion value as a float
* Or can be an array of values
*
* @return array|int|string If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function inverse(mixed $trials, mixed $probability, mixed $alpha): array|string|int
{
if (is_array($trials) || is_array($probability) || is_array($alpha)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $trials, $probability, $alpha);
}
try {
$trials = DistributionValidations::validateInt($trials);
$probability = DistributionValidations::validateProbability($probability);
$alpha = DistributionValidations::validateFloat($alpha);
} catch (Exception $e) {
return $e->getMessage();
}
if ($trials < 0) {
return ExcelError::NAN();
} elseif (($alpha < 0.0) || ($alpha > 1.0)) {
return ExcelError::NAN();
}
$successes = 0;
while ($successes <= $trials) {
$result = self::calculateCumulativeBinomial($successes, $trials, $probability);
if ($result >= $alpha) {
break;
}
++$successes;
}
return $successes;
}
private static function calculateCumulativeBinomial(int $value, int $trials, float $probability): float|int
{
$summer = 0;
for ($i = 0; $i <= $value; ++$i) {
/** @var float $comb */
$comb = Combinations::withoutRepetition($trials, $i);
$summer += $comb * $probability ** $i
* (1 - $probability) ** ($trials - $i);
}
return $summer;
}
}

View file

@ -0,0 +1,331 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class ChiSquared
{
use ArrayEnabled;
private const EPS = 2.22e-16;
/**
* CHIDIST.
*
* Returns the one-tailed probability of the chi-squared distribution.
*
* @param mixed $value Float value for which we want the probability
* Or can be an array of values
* @param mixed $degrees Integer degrees of freedom
* Or can be an array of values
*
* @return array|float|int|string If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function distributionRightTail(mixed $value, mixed $degrees): array|string|int|float
{
if (is_array($value) || is_array($degrees)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $degrees);
}
try {
$value = DistributionValidations::validateFloat($value);
$degrees = DistributionValidations::validateInt($degrees);
} catch (Exception $e) {
return $e->getMessage();
}
if ($degrees < 1) {
return ExcelError::NAN();
}
if ($value < 0) {
if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) {
return 1;
}
return ExcelError::NAN();
}
return 1 - (Gamma::incompleteGamma($degrees / 2, $value / 2) / Gamma::gammaValue($degrees / 2));
}
/**
* CHIDIST.
*
* Returns the one-tailed probability of the chi-squared distribution.
*
* @param mixed $value Float value for which we want the probability
* Or can be an array of values
* @param mixed $degrees Integer degrees of freedom
* Or can be an array of values
* @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false)
* Or can be an array of values
*
* @return array|float|int|string If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function distributionLeftTail(mixed $value, mixed $degrees, mixed $cumulative): array|string|int|float
{
if (is_array($value) || is_array($degrees) || is_array($cumulative)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $degrees, $cumulative);
}
try {
$value = DistributionValidations::validateFloat($value);
$degrees = DistributionValidations::validateInt($degrees);
$cumulative = DistributionValidations::validateBool($cumulative);
} catch (Exception $e) {
return $e->getMessage();
}
if ($degrees < 1) {
return ExcelError::NAN();
}
if ($value < 0) {
if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) {
return 1;
}
return ExcelError::NAN();
}
if ($cumulative === true) {
$temp = self::distributionRightTail($value, $degrees);
return 1 - (is_numeric($temp) ? $temp : 0);
}
return ($value ** (($degrees / 2) - 1) * exp(-$value / 2))
/ ((2 ** ($degrees / 2)) * Gamma::gammaValue($degrees / 2));
}
/**
* CHIINV.
*
* Returns the inverse of the right-tailed probability of the chi-squared distribution.
*
* @param mixed $probability Float probability at which you want to evaluate the distribution
* Or can be an array of values
* @param mixed $degrees Integer degrees of freedom
* Or can be an array of values
*
* @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function inverseRightTail(mixed $probability, mixed $degrees)
{
if (is_array($probability) || is_array($degrees)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $degrees);
}
try {
$probability = DistributionValidations::validateProbability($probability);
$degrees = DistributionValidations::validateInt($degrees);
} catch (Exception $e) {
return $e->getMessage();
}
if ($degrees < 1) {
return ExcelError::NAN();
}
$callback = function ($value) use ($degrees): float {
return 1 - (Gamma::incompleteGamma($degrees / 2, $value / 2)
/ Gamma::gammaValue($degrees / 2));
};
$newtonRaphson = new NewtonRaphson($callback);
return $newtonRaphson->execute($probability);
}
/**
* CHIINV.
*
* Returns the inverse of the left-tailed probability of the chi-squared distribution.
*
* @param mixed $probability Float probability at which you want to evaluate the distribution
* Or can be an array of values
* @param mixed $degrees Integer degrees of freedom
* Or can be an array of values
*
* @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function inverseLeftTail(mixed $probability, mixed $degrees): array|string|float
{
if (is_array($probability) || is_array($degrees)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $degrees);
}
try {
$probability = DistributionValidations::validateProbability($probability);
$degrees = DistributionValidations::validateInt($degrees);
} catch (Exception $e) {
return $e->getMessage();
}
if ($degrees < 1) {
return ExcelError::NAN();
}
return self::inverseLeftTailCalculation($probability, $degrees);
}
/**
* CHITEST.
*
* Uses the chi-square test to calculate the probability that the differences between two supplied data sets
* (of observed and expected frequencies), are likely to be simply due to sampling error,
* or if they are likely to be real.
*
* @param mixed $actual an array of observed frequencies
* @param mixed $expected an array of expected frequencies
*/
public static function test(mixed $actual, mixed $expected): float|string
{
$rows = count($actual);
$actual = Functions::flattenArray($actual);
$expected = Functions::flattenArray($expected);
$columns = intdiv(count($actual), $rows);
$countActuals = count($actual);
$countExpected = count($expected);
if ($countActuals !== $countExpected || $countActuals === 1) {
return ExcelError::NAN();
}
$result = 0.0;
for ($i = 0; $i < $countActuals; ++$i) {
if ($expected[$i] == 0.0) {
return ExcelError::DIV0();
} elseif ($expected[$i] < 0.0) {
return ExcelError::NAN();
}
$result += (($actual[$i] - $expected[$i]) ** 2) / $expected[$i];
}
$degrees = self::degrees($rows, $columns);
$result = Functions::scalar(self::distributionRightTail($result, $degrees));
return $result;
}
protected static function degrees(int $rows, int $columns): int
{
if ($rows === 1) {
return $columns - 1;
} elseif ($columns === 1) {
return $rows - 1;
}
return ($columns - 1) * ($rows - 1);
}
private static function inverseLeftTailCalculation(float $probability, int $degrees): float
{
// bracket the root
$min = 0;
$sd = sqrt(2.0 * $degrees);
$max = 2 * $sd;
$s = -1;
while ($s * self::pchisq($max, $degrees) > $probability * $s) {
$min = $max;
$max += 2 * $sd;
}
// Find root using bisection
$chi2 = 0.5 * ($min + $max);
while (($max - $min) > self::EPS * $chi2) {
if ($s * self::pchisq($chi2, $degrees) > $probability * $s) {
$min = $chi2;
} else {
$max = $chi2;
}
$chi2 = 0.5 * ($min + $max);
}
return $chi2;
}
private static function pchisq(float $chi2, int $degrees): float
{
return self::gammp($degrees, 0.5 * $chi2);
}
private static function gammp(int $n, float $x): float
{
if ($x < 0.5 * $n + 1) {
return self::gser($n, $x);
}
return 1 - self::gcf($n, $x);
}
// Return the incomplete gamma function P(n/2,x) evaluated by
// series representation. Algorithm from numerical recipe.
// Assume that n is a positive integer and x>0, won't check arguments.
// Relative error controlled by the eps parameter
private static function gser(int $n, float $x): float
{
/** @var float $gln */
$gln = Gamma::ln($n / 2);
$a = 0.5 * $n;
$ap = $a;
$sum = 1.0 / $a;
$del = $sum;
for ($i = 1; $i < 101; ++$i) {
++$ap;
$del = $del * $x / $ap;
$sum += $del;
if ($del < $sum * self::EPS) {
break;
}
}
return $sum * exp(-$x + $a * log($x) - $gln);
}
// Return the incomplete gamma function Q(n/2,x) evaluated by
// its continued fraction representation. Algorithm from numerical recipe.
// Assume that n is a postive integer and x>0, won't check arguments.
// Relative error controlled by the eps parameter
private static function gcf(int $n, float $x): float
{
/** @var float $gln */
$gln = Gamma::ln($n / 2);
$a = 0.5 * $n;
$b = $x + 1 - $a;
$fpmin = 1.e-300;
$c = 1 / $fpmin;
$d = 1 / $b;
$h = $d;
for ($i = 1; $i < 101; ++$i) {
$an = -$i * ($i - $a);
$b += 2;
$d = $an * $d + $b;
if (abs($d) < $fpmin) {
$d = $fpmin;
}
$c = $b + $an / $c;
if (abs($c) < $fpmin) {
$c = $fpmin;
}
$d = 1 / $d;
$del = $d * $c;
$h = $h * $del;
if (abs($del - 1) < self::EPS) {
break;
}
}
return $h * exp(-$x + $a * log($x) - $gln);
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\StatisticalValidations;
class DistributionValidations extends StatisticalValidations
{
public static function validateProbability(mixed $probability): float
{
$probability = self::validateFloat($probability);
if ($probability < 0.0 || $probability > 1.0) {
throw new Exception(ExcelError::NAN());
}
return $probability;
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Exponential
{
use ArrayEnabled;
/**
* EXPONDIST.
*
* Returns the exponential distribution. Use EXPONDIST to model the time between events,
* such as how long an automated bank teller takes to deliver cash. For example, you can
* use EXPONDIST to determine the probability that the process takes at most 1 minute.
*
* @param mixed $value Float value for which we want the probability
* Or can be an array of values
* @param mixed $lambda The parameter value as a float
* Or can be an array of values
* @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false)
* Or can be an array of values
*
* @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function distribution(mixed $value, mixed $lambda, mixed $cumulative): array|string|float
{
if (is_array($value) || is_array($lambda) || is_array($cumulative)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $lambda, $cumulative);
}
try {
$value = DistributionValidations::validateFloat($value);
$lambda = DistributionValidations::validateFloat($lambda);
$cumulative = DistributionValidations::validateBool($cumulative);
} catch (Exception $e) {
return $e->getMessage();
}
if (($value < 0) || ($lambda < 0)) {
return ExcelError::NAN();
}
if ($cumulative === true) {
return 1 - exp(0 - $value * $lambda);
}
return $lambda * exp(0 - $value * $lambda);
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class F
{
use ArrayEnabled;
/**
* F.DIST.
*
* Returns the F probability distribution.
* You can use this function to determine whether two data sets have different degrees of diversity.
* For example, you can examine the test scores of men and women entering high school, and determine
* if the variability in the females is different from that found in the males.
*
* @param mixed $value Float value for which we want the probability
* Or can be an array of values
* @param mixed $u The numerator degrees of freedom as an integer
* Or can be an array of values
* @param mixed $v The denominator degrees of freedom as an integer
* Or can be an array of values
* @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false)
* Or can be an array of values
*
* @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function distribution(mixed $value, mixed $u, mixed $v, mixed $cumulative): array|string|float
{
if (is_array($value) || is_array($u) || is_array($v) || is_array($cumulative)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $u, $v, $cumulative);
}
try {
$value = DistributionValidations::validateFloat($value);
$u = DistributionValidations::validateInt($u);
$v = DistributionValidations::validateInt($v);
$cumulative = DistributionValidations::validateBool($cumulative);
} catch (Exception $e) {
return $e->getMessage();
}
if ($value < 0 || $u < 1 || $v < 1) {
return ExcelError::NAN();
}
if ($cumulative) {
$adjustedValue = ($u * $value) / ($u * $value + $v);
return Beta::incompleteBeta($adjustedValue, $u / 2, $v / 2);
}
return (Gamma::gammaValue(($v + $u) / 2)
/ (Gamma::gammaValue($u / 2) * Gamma::gammaValue($v / 2)))
* (($u / $v) ** ($u / 2))
* (($value ** (($u - 2) / 2)) / ((1 + ($u / $v) * $value) ** (($u + $v) / 2)));
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Fisher
{
use ArrayEnabled;
/**
* FISHER.
*
* Returns the Fisher transformation at x. This transformation produces a function that
* is normally distributed rather than skewed. Use this function to perform hypothesis
* testing on the correlation coefficient.
*
* @param mixed $value Float value for which we want the probability
* Or can be an array of values
*
* @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function distribution(mixed $value): array|string|float
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
try {
DistributionValidations::validateFloat($value);
} catch (Exception $e) {
return $e->getMessage();
}
if (($value <= -1) || ($value >= 1)) {
return ExcelError::NAN();
}
return 0.5 * log((1 + $value) / (1 - $value));
}
/**
* FISHERINV.
*
* Returns the inverse of the Fisher transformation. Use this transformation when
* analyzing correlations between ranges or arrays of data. If y = FISHER(x), then
* FISHERINV(y) = x.
*
* @param mixed $probability Float probability at which you want to evaluate the distribution
* Or can be an array of values
*
* @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function inverse(mixed $probability): array|string|float
{
if (is_array($probability)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $probability);
}
try {
DistributionValidations::validateFloat($probability);
} catch (Exception $e) {
return $e->getMessage();
}
return (exp(2 * $probability) - 1) / (exp(2 * $probability) + 1);
}
}

View file

@ -0,0 +1,148 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Gamma extends GammaBase
{
use ArrayEnabled;
/**
* GAMMA.
*
* Return the gamma function value.
*
* @param mixed $value Float value for which we want the probability
* Or can be an array of values
*
* @return array|float|string The result, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function gamma(mixed $value): array|string|float
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
try {
$value = DistributionValidations::validateFloat($value);
} catch (Exception $e) {
return $e->getMessage();
}
if ((((int) $value) == ((float) $value)) && $value <= 0.0) {
return ExcelError::NAN();
}
return self::gammaValue($value);
}
/**
* GAMMADIST.
*
* Returns the gamma distribution.
*
* @param mixed $value Float Value at which you want to evaluate the distribution
* Or can be an array of values
* @param mixed $a Parameter to the distribution as a float
* Or can be an array of values
* @param mixed $b Parameter to the distribution as a float
* Or can be an array of values
* @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false)
* Or can be an array of values
*
* @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function distribution(mixed $value, mixed $a, mixed $b, mixed $cumulative)
{
if (is_array($value) || is_array($a) || is_array($b) || is_array($cumulative)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $a, $b, $cumulative);
}
try {
$value = DistributionValidations::validateFloat($value);
$a = DistributionValidations::validateFloat($a);
$b = DistributionValidations::validateFloat($b);
$cumulative = DistributionValidations::validateBool($cumulative);
} catch (Exception $e) {
return $e->getMessage();
}
if (($value < 0) || ($a <= 0) || ($b <= 0)) {
return ExcelError::NAN();
}
return self::calculateDistribution($value, $a, $b, $cumulative);
}
/**
* GAMMAINV.
*
* Returns the inverse of the Gamma distribution.
*
* @param mixed $probability Float probability at which you want to evaluate the distribution
* Or can be an array of values
* @param mixed $alpha Parameter to the distribution as a float
* Or can be an array of values
* @param mixed $beta Parameter to the distribution as a float
* Or can be an array of values
*
* @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function inverse(mixed $probability, mixed $alpha, mixed $beta)
{
if (is_array($probability) || is_array($alpha) || is_array($beta)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $alpha, $beta);
}
try {
$probability = DistributionValidations::validateProbability($probability);
$alpha = DistributionValidations::validateFloat($alpha);
$beta = DistributionValidations::validateFloat($beta);
} catch (Exception $e) {
return $e->getMessage();
}
if (($alpha <= 0.0) || ($beta <= 0.0)) {
return ExcelError::NAN();
}
return self::calculateInverse($probability, $alpha, $beta);
}
/**
* GAMMALN.
*
* Returns the natural logarithm of the gamma function.
*
* @param mixed $value Float Value at which you want to evaluate the distribution
* Or can be an array of values
*
* @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function ln(mixed $value): array|string|float
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
try {
$value = DistributionValidations::validateFloat($value);
} catch (Exception $e) {
return $e->getMessage();
}
if ($value <= 0) {
return ExcelError::NAN();
}
return log(self::gammaValue($value));
}
}

View file

@ -0,0 +1,388 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
abstract class GammaBase
{
private const LOG_GAMMA_X_MAX_VALUE = 2.55e305;
private const EPS = 2.22e-16;
private const MAX_VALUE = 1.2e308;
private const SQRT2PI = 2.5066282746310005024157652848110452530069867406099;
private const MAX_ITERATIONS = 256;
protected static function calculateDistribution(float $value, float $a, float $b, bool $cumulative): float
{
if ($cumulative) {
return self::incompleteGamma($a, $value / $b) / self::gammaValue($a);
}
return (1 / ($b ** $a * self::gammaValue($a))) * $value ** ($a - 1) * exp(0 - ($value / $b));
}
/** @return float|string */
protected static function calculateInverse(float $probability, float $alpha, float $beta)
{
$xLo = 0;
$xHi = $alpha * $beta * 5;
$dx = 1024;
$x = $xNew = 1;
$i = 0;
while ((abs($dx) > Functions::PRECISION) && (++$i <= self::MAX_ITERATIONS)) {
// Apply Newton-Raphson step
$result = self::calculateDistribution($x, $alpha, $beta, true);
if (!is_float($result)) {
return ExcelError::NA();
}
$error = $result - $probability;
if ($error == 0.0) {
$dx = 0;
} elseif ($error < 0.0) {
$xLo = $x;
} else {
$xHi = $x;
}
$pdf = self::calculateDistribution($x, $alpha, $beta, false);
// Avoid division by zero
if (!is_float($pdf)) {
return ExcelError::NA();
}
if ($pdf !== 0.0) {
$dx = $error / $pdf;
$xNew = $x - $dx;
}
// If the NR fails to converge (which for example may be the
// case if the initial guess is too rough) we apply a bisection
// step to determine a more narrow interval around the root.
if (($xNew < $xLo) || ($xNew > $xHi) || ($pdf == 0.0)) {
$xNew = ($xLo + $xHi) / 2;
$dx = $xNew - $x;
}
$x = $xNew;
}
if ($i === self::MAX_ITERATIONS) {
return ExcelError::NA();
}
return $x;
}
//
// Implementation of the incomplete Gamma function
//
public static function incompleteGamma(float $a, float $x): float
{
static $max = 32;
$summer = 0;
for ($n = 0; $n <= $max; ++$n) {
$divisor = $a;
for ($i = 1; $i <= $n; ++$i) {
$divisor *= ($a + $i);
}
$summer += ($x ** $n / $divisor);
}
return $x ** $a * exp(0 - $x) * $summer;
}
//
// Implementation of the Gamma function
//
public static function gammaValue(float $value): float
{
if ($value == 0.0) {
return 0;
}
static $p0 = 1.000000000190015;
static $p = [
1 => 76.18009172947146,
2 => -86.50532032941677,
3 => 24.01409824083091,
4 => -1.231739572450155,
5 => 1.208650973866179e-3,
6 => -5.395239384953e-6,
];
$y = $x = $value;
$tmp = $x + 5.5;
$tmp -= ($x + 0.5) * log($tmp);
$summer = $p0;
for ($j = 1; $j <= 6; ++$j) {
$summer += ($p[$j] / ++$y);
}
return exp(0 - $tmp + log(self::SQRT2PI * $summer / $x));
}
private const LG_D1 = -0.5772156649015328605195174;
private const LG_D2 = 0.4227843350984671393993777;
private const LG_D4 = 1.791759469228055000094023;
private const LG_P1 = [
4.945235359296727046734888,
201.8112620856775083915565,
2290.838373831346393026739,
11319.67205903380828685045,
28557.24635671635335736389,
38484.96228443793359990269,
26377.48787624195437963534,
7225.813979700288197698961,
];
private const LG_P2 = [
4.974607845568932035012064,
542.4138599891070494101986,
15506.93864978364947665077,
184793.2904445632425417223,
1088204.76946882876749847,
3338152.967987029735917223,
5106661.678927352456275255,
3074109.054850539556250927,
];
private const LG_P4 = [
14745.02166059939948905062,
2426813.369486704502836312,
121475557.4045093227939592,
2663432449.630976949898078,
29403789566.34553899906876,
170266573776.5398868392998,
492612579337.743088758812,
560625185622.3951465078242,
];
private const LG_Q1 = [
67.48212550303777196073036,
1113.332393857199323513008,
7738.757056935398733233834,
27639.87074403340708898585,
54993.10206226157329794414,
61611.22180066002127833352,
36351.27591501940507276287,
8785.536302431013170870835,
];
private const LG_Q2 = [
183.0328399370592604055942,
7765.049321445005871323047,
133190.3827966074194402448,
1136705.821321969608938755,
5267964.117437946917577538,
13467014.54311101692290052,
17827365.30353274213975932,
9533095.591844353613395747,
];
private const LG_Q4 = [
2690.530175870899333379843,
639388.5654300092398984238,
41355999.30241388052042842,
1120872109.61614794137657,
14886137286.78813811542398,
101680358627.2438228077304,
341747634550.7377132798597,
446315818741.9713286462081,
];
private const LG_C = [
-0.001910444077728,
8.4171387781295e-4,
-5.952379913043012e-4,
7.93650793500350248e-4,
-0.002777777777777681622553,
0.08333333333333333331554247,
0.0057083835261,
];
// Rough estimate of the fourth root of logGamma_xBig
private const LG_FRTBIG = 2.25e76;
private const PNT68 = 0.6796875;
// Function cache for logGamma
private static float $logGammaCacheResult = 0.0;
private static float $logGammaCacheX = 0.0;
/**
* logGamma function.
*
* Original author was Jaco van Kooten. Ported to PHP by Paul Meagher.
*
* The natural logarithm of the gamma function. <br />
* Based on public domain NETLIB (Fortran) code by W. J. Cody and L. Stoltz <br />
* Applied Mathematics Division <br />
* Argonne National Laboratory <br />
* Argonne, IL 60439 <br />
* <p>
* References:
* <ol>
* <li>W. J. Cody and K. E. Hillstrom, 'Chebyshev Approximations for the Natural
* Logarithm of the Gamma Function,' Math. Comp. 21, 1967, pp. 198-203.</li>
* <li>K. E. Hillstrom, ANL/AMD Program ANLC366S, DGAMMA/DLGAMA, May, 1969.</li>
* <li>Hart, Et. Al., Computer Approximations, Wiley and sons, New York, 1968.</li>
* </ol>
* </p>
* <p>
* From the original documentation:
* </p>
* <p>
* This routine calculates the LOG(GAMMA) function for a positive real argument X.
* Computation is based on an algorithm outlined in references 1 and 2.
* The program uses rational functions that theoretically approximate LOG(GAMMA)
* to at least 18 significant decimal digits. The approximation for X > 12 is from
* reference 3, while approximations for X < 12.0 are similar to those in reference
* 1, but are unpublished. The accuracy achieved depends on the arithmetic system,
* the compiler, the intrinsic functions, and proper selection of the
* machine-dependent constants.
* </p>
* <p>
* Error returns: <br />
* The program returns the value XINF for X .LE. 0.0 or when overflow would occur.
* The computation is believed to be free of underflow and overflow.
* </p>
*
* @version 1.1
*
* @author Jaco van Kooten
*
* @return float MAX_VALUE for x < 0.0 or when overflow would occur, i.e. x > 2.55E305
*/
public static function logGamma(float $x): float
{
if ($x == self::$logGammaCacheX) {
return self::$logGammaCacheResult;
}
$y = $x;
if ($y > 0.0 && $y <= self::LOG_GAMMA_X_MAX_VALUE) {
if ($y <= self::EPS) {
$res = -log($y);
} elseif ($y <= 1.5) {
$res = self::logGamma1($y);
} elseif ($y <= 4.0) {
$res = self::logGamma2($y);
} elseif ($y <= 12.0) {
$res = self::logGamma3($y);
} else {
$res = self::logGamma4($y);
}
} else {
// --------------------------
// Return for bad arguments
// --------------------------
$res = self::MAX_VALUE;
}
// ------------------------------
// Final adjustments and return
// ------------------------------
self::$logGammaCacheX = $x;
self::$logGammaCacheResult = $res;
return $res;
}
private static function logGamma1(float $y): float
{
// ---------------------
// EPS .LT. X .LE. 1.5
// ---------------------
if ($y < self::PNT68) {
$corr = -log($y);
$xm1 = $y;
} else {
$corr = 0.0;
$xm1 = $y - 1.0;
}
$xden = 1.0;
$xnum = 0.0;
if ($y <= 0.5 || $y >= self::PNT68) {
for ($i = 0; $i < 8; ++$i) {
$xnum = $xnum * $xm1 + self::LG_P1[$i];
$xden = $xden * $xm1 + self::LG_Q1[$i];
}
return $corr + $xm1 * (self::LG_D1 + $xm1 * ($xnum / $xden));
}
$xm2 = $y - 1.0;
for ($i = 0; $i < 8; ++$i) {
$xnum = $xnum * $xm2 + self::LG_P2[$i];
$xden = $xden * $xm2 + self::LG_Q2[$i];
}
return $corr + $xm2 * (self::LG_D2 + $xm2 * ($xnum / $xden));
}
private static function logGamma2(float $y): float
{
// ---------------------
// 1.5 .LT. X .LE. 4.0
// ---------------------
$xm2 = $y - 2.0;
$xden = 1.0;
$xnum = 0.0;
for ($i = 0; $i < 8; ++$i) {
$xnum = $xnum * $xm2 + self::LG_P2[$i];
$xden = $xden * $xm2 + self::LG_Q2[$i];
}
return $xm2 * (self::LG_D2 + $xm2 * ($xnum / $xden));
}
protected static function logGamma3(float $y): float
{
// ----------------------
// 4.0 .LT. X .LE. 12.0
// ----------------------
$xm4 = $y - 4.0;
$xden = -1.0;
$xnum = 0.0;
for ($i = 0; $i < 8; ++$i) {
$xnum = $xnum * $xm4 + self::LG_P4[$i];
$xden = $xden * $xm4 + self::LG_Q4[$i];
}
return self::LG_D4 + $xm4 * ($xnum / $xden);
}
protected static function logGamma4(float $y): float
{
// ---------------------------------
// Evaluate for argument .GE. 12.0
// ---------------------------------
$res = 0.0;
if ($y <= self::LG_FRTBIG) {
$res = self::LG_C[6];
$ysq = $y * $y;
for ($i = 0; $i < 6; ++$i) {
$res = $res / $ysq + self::LG_C[$i];
}
$res /= $y;
$corr = log($y);
$res = $res + log(self::SQRT2PI) - 0.5 * $corr;
$res += $y * ($corr - 1.0);
}
return $res;
}
}

View file

@ -0,0 +1,75 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Combinations;
class HyperGeometric
{
use ArrayEnabled;
/**
* HYPGEOMDIST.
*
* Returns the hypergeometric distribution. HYPGEOMDIST returns the probability of a given number of
* sample successes, given the sample size, population successes, and population size.
*
* @param mixed $sampleSuccesses Integer number of successes in the sample
* Or can be an array of values
* @param mixed $sampleNumber Integer size of the sample
* Or can be an array of values
* @param mixed $populationSuccesses Integer number of successes in the population
* Or can be an array of values
* @param mixed $populationNumber Integer population size
* Or can be an array of values
*
* @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function distribution(mixed $sampleSuccesses, mixed $sampleNumber, mixed $populationSuccesses, mixed $populationNumber): array|string|float
{
if (
is_array($sampleSuccesses) || is_array($sampleNumber)
|| is_array($populationSuccesses) || is_array($populationNumber)
) {
return self::evaluateArrayArguments(
[self::class, __FUNCTION__],
$sampleSuccesses,
$sampleNumber,
$populationSuccesses,
$populationNumber
);
}
try {
$sampleSuccesses = DistributionValidations::validateInt($sampleSuccesses);
$sampleNumber = DistributionValidations::validateInt($sampleNumber);
$populationSuccesses = DistributionValidations::validateInt($populationSuccesses);
$populationNumber = DistributionValidations::validateInt($populationNumber);
} catch (Exception $e) {
return $e->getMessage();
}
if (($sampleSuccesses < 0) || ($sampleSuccesses > $sampleNumber) || ($sampleSuccesses > $populationSuccesses)) {
return ExcelError::NAN();
}
if (($sampleNumber <= 0) || ($sampleNumber > $populationNumber)) {
return ExcelError::NAN();
}
if (($populationSuccesses <= 0) || ($populationSuccesses > $populationNumber)) {
return ExcelError::NAN();
}
$successesPopulationAndSample = (float) Combinations::withoutRepetition($populationSuccesses, $sampleSuccesses);
$numbersPopulationAndSample = (float) Combinations::withoutRepetition($populationNumber, $sampleNumber);
$adjustedPopulationAndSample = (float) Combinations::withoutRepetition(
$populationNumber - $populationSuccesses,
$sampleNumber - $sampleSuccesses
);
return $successesPopulationAndSample * $adjustedPopulationAndSample / $numbersPopulationAndSample;
}
}

View file

@ -0,0 +1,139 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class LogNormal
{
use ArrayEnabled;
/**
* LOGNORMDIST.
*
* Returns the cumulative lognormal distribution of x, where ln(x) is normally distributed
* with parameters mean and standard_dev.
*
* @param mixed $value Float value for which we want the probability
* Or can be an array of values
* @param mixed $mean Mean value as a float
* Or can be an array of values
* @param mixed $stdDev Standard Deviation as a float
* Or can be an array of values
*
* @return array|float|string The result, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function cumulative(mixed $value, mixed $mean, mixed $stdDev)
{
if (is_array($value) || is_array($mean) || is_array($stdDev)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $mean, $stdDev);
}
try {
$value = DistributionValidations::validateFloat($value);
$mean = DistributionValidations::validateFloat($mean);
$stdDev = DistributionValidations::validateFloat($stdDev);
} catch (Exception $e) {
return $e->getMessage();
}
if (($value <= 0) || ($stdDev <= 0)) {
return ExcelError::NAN();
}
return StandardNormal::cumulative((log($value) - $mean) / $stdDev);
}
/**
* LOGNORM.DIST.
*
* Returns the lognormal distribution of x, where ln(x) is normally distributed
* with parameters mean and standard_dev.
*
* @param mixed $value Float value for which we want the probability
* Or can be an array of values
* @param mixed $mean Mean value as a float
* Or can be an array of values
* @param mixed $stdDev Standard Deviation as a float
* Or can be an array of values
* @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false)
* Or can be an array of values
*
* @return array|float|string The result, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function distribution(mixed $value, mixed $mean, mixed $stdDev, mixed $cumulative = false)
{
if (is_array($value) || is_array($mean) || is_array($stdDev) || is_array($cumulative)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $mean, $stdDev, $cumulative);
}
try {
$value = DistributionValidations::validateFloat($value);
$mean = DistributionValidations::validateFloat($mean);
$stdDev = DistributionValidations::validateFloat($stdDev);
$cumulative = DistributionValidations::validateBool($cumulative);
} catch (Exception $e) {
return $e->getMessage();
}
if (($value <= 0) || ($stdDev <= 0)) {
return ExcelError::NAN();
}
if ($cumulative === true) {
return StandardNormal::distribution((log($value) - $mean) / $stdDev, true);
}
return (1 / (sqrt(2 * M_PI) * $stdDev * $value))
* exp(0 - ((log($value) - $mean) ** 2 / (2 * $stdDev ** 2)));
}
/**
* LOGINV.
*
* Returns the inverse of the lognormal cumulative distribution
*
* @param mixed $probability Float probability for which we want the value
* Or can be an array of values
* @param mixed $mean Mean Value as a float
* Or can be an array of values
* @param mixed $stdDev Standard Deviation as a float
* Or can be an array of values
*
* @return array|float|string The result, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*
* @TODO Try implementing P J Acklam's refinement algorithm for greater
* accuracy if I can get my head round the mathematics
* (as described at) http://home.online.no/~pjacklam/notes/invnorm/
*/
public static function inverse(mixed $probability, mixed $mean, mixed $stdDev): array|string|float
{
if (is_array($probability) || is_array($mean) || is_array($stdDev)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $mean, $stdDev);
}
try {
$probability = DistributionValidations::validateProbability($probability);
$mean = DistributionValidations::validateFloat($mean);
$stdDev = DistributionValidations::validateFloat($stdDev);
} catch (Exception $e) {
return $e->getMessage();
}
if ($stdDev <= 0) {
return ExcelError::NAN();
}
/** @var float $inverse */
$inverse = StandardNormal::inverse($probability);
return exp($mean + $stdDev * $inverse);
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class NewtonRaphson
{
private const MAX_ITERATIONS = 256;
/** @var callable */
protected $callback;
public function __construct(callable $callback)
{
$this->callback = $callback;
}
public function execute(float $probability): string|int|float
{
$xLo = 100;
$xHi = 0;
$dx = 1;
$x = $xNew = 1;
$i = 0;
while ((abs($dx) > Functions::PRECISION) && ($i++ < self::MAX_ITERATIONS)) {
// Apply Newton-Raphson step
$result = call_user_func($this->callback, $x);
$error = $result - $probability;
if ($error == 0.0) {
$dx = 0;
} elseif ($error < 0.0) {
$xLo = $x;
} else {
$xHi = $x;
}
// Avoid division by zero
if ($result != 0.0) {
$dx = $error / $result;
$xNew = $x - $dx;
}
// If the NR fails to converge (which for example may be the
// case if the initial guess is too rough) we apply a bisection
// step to determine a more narrow interval around the root.
if (($xNew < $xLo) || ($xNew > $xHi) || ($result == 0.0)) {
$xNew = ($xLo + $xHi) / 2;
$dx = $xNew - $x;
}
$x = $xNew;
}
if ($i == self::MAX_ITERATIONS) {
return ExcelError::NA();
}
return $x;
}
}

View file

@ -0,0 +1,180 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Engineering;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Normal
{
use ArrayEnabled;
public const SQRT2PI = 2.5066282746310005024157652848110452530069867406099;
/**
* NORMDIST.
*
* Returns the normal distribution for the specified mean and standard deviation. This
* function has a very wide range of applications in statistics, including hypothesis
* testing.
*
* @param mixed $value Float value for which we want the probability
* Or can be an array of values
* @param mixed $mean Mean value as a float
* Or can be an array of values
* @param mixed $stdDev Standard Deviation as a float
* Or can be an array of values
* @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false)
* Or can be an array of values
*
* @return array|float|string The result, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function distribution(mixed $value, mixed $mean, mixed $stdDev, mixed $cumulative): array|string|float
{
if (is_array($value) || is_array($mean) || is_array($stdDev) || is_array($cumulative)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $mean, $stdDev, $cumulative);
}
try {
$value = DistributionValidations::validateFloat($value);
$mean = DistributionValidations::validateFloat($mean);
$stdDev = DistributionValidations::validateFloat($stdDev);
$cumulative = DistributionValidations::validateBool($cumulative);
} catch (Exception $e) {
return $e->getMessage();
}
if ($stdDev < 0) {
return ExcelError::NAN();
}
if ($cumulative) {
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))));
}
/**
* NORMINV.
*
* Returns the inverse of the normal cumulative distribution for the specified mean and standard deviation.
*
* @param mixed $probability Float probability for which we want the value
* Or can be an array of values
* @param mixed $mean Mean Value as a float
* Or can be an array of values
* @param mixed $stdDev Standard Deviation as a float
* Or can be an array of values
*
* @return array|float|string The result, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function inverse(mixed $probability, mixed $mean, mixed $stdDev): array|string|float
{
if (is_array($probability) || is_array($mean) || is_array($stdDev)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $mean, $stdDev);
}
try {
$probability = DistributionValidations::validateProbability($probability);
$mean = DistributionValidations::validateFloat($mean);
$stdDev = DistributionValidations::validateFloat($stdDev);
} catch (Exception $e) {
return $e->getMessage();
}
if ($stdDev < 0) {
return ExcelError::NAN();
}
return (self::inverseNcdf($probability) * $stdDev) + $mean;
}
/*
* inverse_ncdf.php
* -------------------
* begin : Friday, January 16, 2004
* copyright : (C) 2004 Michael Nickerson
* email : nickersonm@yahoo.com
*
*/
private static function inverseNcdf(float $p): float
{
// Inverse ncdf approximation by Peter J. Acklam, implementation adapted to
// PHP by Michael Nickerson, using Dr. Thomas Ziegler's C implementation as
// a guide. http://home.online.no/~pjacklam/notes/invnorm/index.html
// I have not checked the accuracy of this implementation. Be aware that PHP
// will truncate the coeficcients to 14 digits.
// You have permission to use and distribute this function freely for
// whatever purpose you want, but please show common courtesy and give credit
// where credit is due.
// Input paramater is $p - probability - where 0 < p < 1.
// Coefficients in rational approximations
static $a = [
1 => -3.969683028665376e+01,
2 => 2.209460984245205e+02,
3 => -2.759285104469687e+02,
4 => 1.383577518672690e+02,
5 => -3.066479806614716e+01,
6 => 2.506628277459239e+00,
];
static $b = [
1 => -5.447609879822406e+01,
2 => 1.615858368580409e+02,
3 => -1.556989798598866e+02,
4 => 6.680131188771972e+01,
5 => -1.328068155288572e+01,
];
static $c = [
1 => -7.784894002430293e-03,
2 => -3.223964580411365e-01,
3 => -2.400758277161838e+00,
4 => -2.549732539343734e+00,
5 => 4.374664141464968e+00,
6 => 2.938163982698783e+00,
];
static $d = [
1 => 7.784695709041462e-03,
2 => 3.224671290700398e-01,
3 => 2.445134137142996e+00,
4 => 3.754408661907416e+00,
];
// Define lower and upper region break-points.
$p_low = 0.02425; //Use lower region approx. below this
$p_high = 1 - $p_low; //Use upper region approx. above this
if (0 < $p && $p < $p_low) {
// Rational approximation for lower region.
$q = sqrt(-2 * log($p));
return ((((($c[1] * $q + $c[2]) * $q + $c[3]) * $q + $c[4]) * $q + $c[5]) * $q + $c[6])
/ (((($d[1] * $q + $d[2]) * $q + $d[3]) * $q + $d[4]) * $q + 1);
} elseif ($p_high < $p && $p < 1) {
// Rational approximation for upper region.
$q = sqrt(-2 * log(1 - $p));
return -((((($c[1] * $q + $c[2]) * $q + $c[3]) * $q + $c[4]) * $q + $c[5]) * $q + $c[6])
/ (((($d[1] * $q + $d[2]) * $q + $d[3]) * $q + $d[4]) * $q + 1);
}
// Rational approximation for central region.
$q = $p - 0.5;
$r = $q * $q;
return ((((($a[1] * $r + $a[2]) * $r + $a[3]) * $r + $a[4]) * $r + $a[5]) * $r + $a[6]) * $q
/ ((((($b[1] * $r + $b[2]) * $r + $b[3]) * $r + $b[4]) * $r + $b[5]) * $r + 1);
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\MathTrig;
class Poisson
{
use ArrayEnabled;
/**
* POISSON.
*
* Returns the Poisson distribution. A common application of the Poisson distribution
* is predicting the number of events over a specific time, such as the number of
* cars arriving at a toll plaza in 1 minute.
*
* @param mixed $value Float value for which we want the probability
* Or can be an array of values
* @param mixed $mean Mean value as a float
* Or can be an array of values
* @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false)
* Or can be an array of values
*
* @return array|float|string The result, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function distribution(mixed $value, mixed $mean, mixed $cumulative): array|string|float
{
if (is_array($value) || is_array($mean) || is_array($cumulative)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $mean, $cumulative);
}
try {
$value = DistributionValidations::validateFloat($value);
$mean = DistributionValidations::validateFloat($mean);
$cumulative = DistributionValidations::validateBool($cumulative);
} catch (Exception $e) {
return $e->getMessage();
}
if (($value < 0) || ($mean < 0)) {
return ExcelError::NAN();
}
if ($cumulative) {
$summer = 0;
$floor = floor($value);
for ($i = 0; $i <= $floor; ++$i) {
/** @var float $fact */
$fact = MathTrig\Factorial::fact($i);
$summer += $mean ** $i / $fact;
}
return exp(0 - $mean) * $summer;
}
/** @var float $fact */
$fact = MathTrig\Factorial::fact($value);
return (exp(0 - $mean) * $mean ** $value) / $fact;
}
}

View file

@ -0,0 +1,158 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Averages;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\StandardDeviations;
class StandardNormal
{
use ArrayEnabled;
/**
* NORMSDIST.
*
* Returns the standard normal cumulative distribution function. The distribution has
* a mean of 0 (zero) and a standard deviation of one. Use this function in place of a
* table of standard normal curve areas.
*
* NOTE: We don't need to check for arrays to array-enable this function, because that is already
* handled by the logic in Normal::distribution()
* All we need to do is pass the value through as scalar or as array.
*
* @param mixed $value Float value for which we want the probability
* Or can be an array of values
*
* @return array|float|string The result, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function cumulative(mixed $value)
{
return Normal::distribution($value, 0, 1, true);
}
/**
* NORM.S.DIST.
*
* Returns the standard normal cumulative distribution function. The distribution has
* a mean of 0 (zero) and a standard deviation of one. Use this function in place of a
* table of standard normal curve areas.
*
* NOTE: We don't need to check for arrays to array-enable this function, because that is already
* handled by the logic in Normal::distribution()
* All we need to do is pass the value and cumulative through as scalar or as array.
*
* @param mixed $value Float value for which we want the probability
* Or can be an array of values
* @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false)
* Or can be an array of values
*
* @return array|float|string The result, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function distribution(mixed $value, mixed $cumulative)
{
return Normal::distribution($value, 0, 1, $cumulative);
}
/**
* NORMSINV.
*
* Returns the inverse of the standard normal cumulative distribution
*
* @param mixed $value float probability for which we want the value
* Or can be an array of values
*
* NOTE: We don't need to check for arrays to array-enable this function, because that is already
* handled by the logic in Normal::inverse()
* All we need to do is pass the value through as scalar or as array
*
* @return array|float|string The result, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function inverse(mixed $value)
{
return Normal::inverse($value, 0, 1);
}
/**
* GAUSS.
*
* Calculates the probability that a member of a standard normal population will fall between
* the mean and z standard deviations from the mean.
*
* @param mixed $value Or can be an array of values
*
* @return array|float|string The result, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function gauss(mixed $value): array|string|float
{
if (is_array($value)) {
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
}
if (!is_numeric($value)) {
return ExcelError::VALUE();
}
/** @var float $dist */
$dist = self::distribution($value, true);
return $dist - 0.5;
}
/**
* ZTEST.
*
* Returns the one-tailed P-value of a z-test.
*
* For a given hypothesized population mean, x, Z.TEST returns the probability that the sample mean would be
* greater than the average of observations in the data set (array) that is, the observed sample mean.
*
* @param mixed $dataSet The dataset should be an array of float values for the observations
* @param mixed $m0 Alpha Parameter
* Or can be an array of values
* @param mixed $sigma A null or float value for the Beta (Standard Deviation) Parameter;
* if null, we use the standard deviation of the dataset
* Or can be an array of values
*
* @return array|float|string (string if result is an error)
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function zTest(mixed $dataSet, mixed $m0, mixed $sigma = null)
{
if (is_array($m0) || is_array($sigma)) {
return self::evaluateArrayArgumentsSubsetFrom([self::class, __FUNCTION__], 1, $dataSet, $m0, $sigma);
}
$dataSet = Functions::flattenArrayIndexed($dataSet);
if (!is_numeric($m0) || ($sigma !== null && !is_numeric($sigma))) {
return ExcelError::VALUE();
}
if ($sigma === null) {
/** @var float $sigma */
$sigma = StandardDeviations::STDEV($dataSet);
}
$n = count($dataSet);
$sub1 = Averages::average($dataSet);
if (!is_numeric($sub1)) {
return $sub1;
}
$temp = self::cumulative(($sub1 - $m0) / ($sigma / sqrt($n)));
return 1 - (is_numeric($temp) ? $temp : 0);
}
}

View file

@ -0,0 +1,132 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class StudentT
{
use ArrayEnabled;
/**
* TDIST.
*
* Returns the probability of Student's T distribution.
*
* @param mixed $value Float value for the distribution
* Or can be an array of values
* @param mixed $degrees Integer value for degrees of freedom
* Or can be an array of values
* @param mixed $tails Integer value for the number of tails (1 or 2)
* Or can be an array of values
*
* @return array|float|string The result, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function distribution(mixed $value, mixed $degrees, mixed $tails)
{
if (is_array($value) || is_array($degrees) || is_array($tails)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $degrees, $tails);
}
try {
$value = DistributionValidations::validateFloat($value);
$degrees = DistributionValidations::validateInt($degrees);
$tails = DistributionValidations::validateInt($tails);
} catch (Exception $e) {
return $e->getMessage();
}
if (($value < 0) || ($degrees < 1) || ($tails < 1) || ($tails > 2)) {
return ExcelError::NAN();
}
return self::calculateDistribution($value, $degrees, $tails);
}
/**
* TINV.
*
* Returns the one-tailed probability of the chi-squared distribution.
*
* @param mixed $probability Float probability for the function
* Or can be an array of values
* @param mixed $degrees Integer value for degrees of freedom
* Or can be an array of values
*
* @return array|float|string The result, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function inverse(mixed $probability, mixed $degrees)
{
if (is_array($probability) || is_array($degrees)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $degrees);
}
try {
$probability = DistributionValidations::validateProbability($probability);
$degrees = DistributionValidations::validateInt($degrees);
} catch (Exception $e) {
return $e->getMessage();
}
if ($degrees <= 0) {
return ExcelError::NAN();
}
$callback = fn ($value) => self::distribution($value, $degrees, 2);
$newtonRaphson = new NewtonRaphson($callback);
return $newtonRaphson->execute($probability);
}
private static function calculateDistribution(float $value, int $degrees, int $tails): float
{
// tdist, which finds the probability that corresponds to a given value
// of t with k degrees of freedom. This algorithm is translated from a
// pascal function on p81 of "Statistical Computing in Pascal" by D
// Cooke, A H Craven & G M Clark (1985: Edward Arnold (Pubs.) Ltd:
// London). The above Pascal algorithm is itself a translation of the
// fortran algoritm "AS 3" by B E Cooper of the Atlas Computer
// Laboratory as reported in (among other places) "Applied Statistics
// Algorithms", editied by P Griffiths and I D Hill (1985; Ellis
// Horwood Ltd.; W. Sussex, England).
$tterm = $degrees;
$ttheta = atan2($value, sqrt($tterm));
$tc = cos($ttheta);
$ts = sin($ttheta);
if (($degrees % 2) === 1) {
$ti = 3;
$tterm = $tc;
} else {
$ti = 2;
$tterm = 1;
}
$tsum = $tterm;
while ($ti < $degrees) {
$tterm *= $tc * $tc * ($ti - 1) / $ti;
$tsum += $tterm;
$ti += 2;
}
$tsum *= $ts;
if (($degrees % 2) == 1) {
$tsum = Functions::M_2DIVPI * ($tsum + $ttheta);
}
$tValue = 0.5 * (1 + $tsum);
if ($tails == 1) {
return 1 - abs($tValue);
}
return 1 - abs((1 - $tValue) - $tValue);
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Weibull
{
use ArrayEnabled;
/**
* WEIBULL.
*
* Returns the Weibull distribution. Use this distribution in reliability
* analysis, such as calculating a device's mean time to failure.
*
* @param mixed $value Float value for the distribution
* Or can be an array of values
* @param mixed $alpha Float alpha Parameter
* Or can be an array of values
* @param mixed $beta Float beta Parameter
* Or can be an array of values
* @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false)
* Or can be an array of values
*
* @return array|float|string (string if result is an error)
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function distribution(mixed $value, mixed $alpha, mixed $beta, mixed $cumulative): array|string|float
{
if (is_array($value) || is_array($alpha) || is_array($beta) || is_array($cumulative)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $alpha, $beta, $cumulative);
}
try {
$value = DistributionValidations::validateFloat($value);
$alpha = DistributionValidations::validateFloat($alpha);
$beta = DistributionValidations::validateFloat($beta);
$cumulative = DistributionValidations::validateBool($cumulative);
} catch (Exception $e) {
return $e->getMessage();
}
if (($value < 0) || ($alpha <= 0) || ($beta <= 0)) {
return ExcelError::NAN();
}
if ($cumulative) {
return 1 - exp(0 - ($value / $beta) ** $alpha);
}
return ($alpha / $beta ** $alpha) * $value ** ($alpha - 1) * exp(0 - ($value / $beta) ** $alpha);
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;
abstract class MaxMinBase
{
protected static function datatypeAdjustmentAllowStrings(mixed $value): mixed
{
if (is_bool($value)) {
return (int) $value;
} elseif (is_string($value)) {
return 0;
}
return $value;
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
class Maximum extends MaxMinBase
{
/**
* MAX.
*
* MAX returns the value of the element of the values passed that has the highest value,
* with negative numbers considered smaller than positive numbers.
*
* Excel Function:
* MAX(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*/
public static function max(mixed ...$args): float|int|string
{
$returnValue = null;
// Loop through arguments
$aArgs = Functions::flattenArray($args);
foreach ($aArgs as $arg) {
if (ErrorValue::isError($arg)) {
$returnValue = $arg;
break;
}
// Is it a numeric value?
if ((is_numeric($arg)) && (!is_string($arg))) {
if (($returnValue === null) || ($arg > $returnValue)) {
$returnValue = $arg;
}
}
}
if ($returnValue === null) {
return 0;
}
return $returnValue;
}
/**
* MAXA.
*
* Returns the greatest value in a list of arguments, including numbers, text, and logical values
*
* Excel Function:
* MAXA(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*/
public static function maxA(mixed ...$args): float|int|string
{
$returnValue = null;
// Loop through arguments
$aArgs = Functions::flattenArray($args);
foreach ($aArgs as $arg) {
if (ErrorValue::isError($arg)) {
$returnValue = $arg;
break;
}
// Is it a numeric value?
if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) && ($arg != '')))) {
$arg = self::datatypeAdjustmentAllowStrings($arg);
if (($returnValue === null) || ($arg > $returnValue)) {
$returnValue = $arg;
}
}
}
if ($returnValue === null) {
return 0;
}
return $returnValue;
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
class Minimum extends MaxMinBase
{
/**
* MIN.
*
* MIN returns the value of the element of the values passed that has the smallest value,
* with negative numbers considered smaller than positive numbers.
*
* Excel Function:
* MIN(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*/
public static function min(mixed ...$args): float|int|string
{
$returnValue = null;
// Loop through arguments
$aArgs = Functions::flattenArray($args);
foreach ($aArgs as $arg) {
if (ErrorValue::isError($arg)) {
$returnValue = $arg;
break;
}
// Is it a numeric value?
if ((is_numeric($arg)) && (!is_string($arg))) {
if (($returnValue === null) || ($arg < $returnValue)) {
$returnValue = $arg;
}
}
}
if ($returnValue === null) {
return 0;
}
return $returnValue;
}
/**
* MINA.
*
* Returns the smallest value in a list of arguments, including numbers, text, and logical values
*
* Excel Function:
* MINA(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*/
public static function minA(mixed ...$args): float|int|string
{
$returnValue = null;
// Loop through arguments
$aArgs = Functions::flattenArray($args);
foreach ($aArgs as $arg) {
if (ErrorValue::isError($arg)) {
$returnValue = $arg;
break;
}
// Is it a numeric value?
if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) && ($arg != '')))) {
$arg = self::datatypeAdjustmentAllowStrings($arg);
if (($returnValue === null) || ($arg < $returnValue)) {
$returnValue = $arg;
}
}
}
if ($returnValue === null) {
return 0;
}
return $returnValue;
}
}

View file

@ -0,0 +1,202 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Percentiles
{
public const RANK_SORT_DESCENDING = 0;
public const RANK_SORT_ASCENDING = 1;
/**
* PERCENTILE.
*
* Returns the nth percentile of values in a range..
*
* Excel Function:
* PERCENTILE(value1[,value2[, ...]],entry)
*
* @param mixed $args Data values
*
* @return float|string The result, or a string containing an error
*/
public static function PERCENTILE(mixed ...$args)
{
$aArgs = Functions::flattenArray($args);
// Calculate
$entry = array_pop($aArgs);
try {
$entry = StatisticalValidations::validateFloat($entry);
} catch (Exception $e) {
return $e->getMessage();
}
if (($entry < 0) || ($entry > 1)) {
return ExcelError::NAN();
}
$mArgs = self::percentileFilterValues($aArgs);
$mValueCount = count($mArgs);
if ($mValueCount > 0) {
sort($mArgs);
$count = Counts::COUNT($mArgs);
$index = $entry * ($count - 1);
$iBase = floor($index);
if ($index == $iBase) {
return $mArgs[$index];
}
$iNext = $iBase + 1;
$iProportion = $index - $iBase;
return $mArgs[$iBase] + (($mArgs[$iNext] - $mArgs[$iBase]) * $iProportion);
}
return ExcelError::NAN();
}
/**
* PERCENTRANK.
*
* Returns the rank of a value in a data set as a percentage of the data set.
* Note that the returned rank is simply rounded to the appropriate significant digits,
* rather than floored (as MS Excel), so value 3 for a value set of 1, 2, 3, 4 will return
* 0.667 rather than 0.666
*
* @param mixed $valueSet An array of (float) values, or a reference to, a list of numbers
* @param mixed $value The number whose rank you want to find
* @param mixed $significance The (integer) number of significant digits for the returned percentage value
*
* @return float|string (string if result is an error)
*/
public static function PERCENTRANK(mixed $valueSet, mixed $value, mixed $significance = 3): string|float
{
$valueSet = Functions::flattenArray($valueSet);
$value = Functions::flattenSingleValue($value);
$significance = ($significance === null) ? 3 : Functions::flattenSingleValue($significance);
try {
$value = StatisticalValidations::validateFloat($value);
$significance = StatisticalValidations::validateInt($significance);
} catch (Exception $e) {
return $e->getMessage();
}
$valueSet = self::rankFilterValues($valueSet);
$valueCount = count($valueSet);
if ($valueCount == 0) {
return ExcelError::NA();
}
sort($valueSet, SORT_NUMERIC);
$valueAdjustor = $valueCount - 1;
if (($value < $valueSet[0]) || ($value > $valueSet[$valueAdjustor])) {
return ExcelError::NA();
}
$pos = array_search($value, $valueSet);
if ($pos === false) {
$pos = 0;
$testValue = $valueSet[0];
while ($testValue < $value) {
$testValue = $valueSet[++$pos];
}
--$pos;
$pos += (($value - $valueSet[$pos]) / ($testValue - $valueSet[$pos]));
}
return round(((float) $pos) / $valueAdjustor, $significance);
}
/**
* QUARTILE.
*
* Returns the quartile of a data set.
*
* Excel Function:
* QUARTILE(value1[,value2[, ...]],entry)
*
* @param mixed $args Data values
*
* @return float|string The result, or a string containing an error
*/
public static function QUARTILE(mixed ...$args)
{
$aArgs = Functions::flattenArray($args);
$entry = array_pop($aArgs);
try {
$entry = StatisticalValidations::validateFloat($entry);
} catch (Exception $e) {
return $e->getMessage();
}
$entry = floor($entry);
$entry /= 4;
if (($entry < 0) || ($entry > 1)) {
return ExcelError::NAN();
}
return self::PERCENTILE($aArgs, $entry);
}
/**
* RANK.
*
* Returns the rank of a number in a list of numbers.
*
* @param mixed $value The number whose rank you want to find
* @param mixed $valueSet An array of float values, or a reference to, a list of numbers
* @param mixed $order Order to sort the values in the value set
*
* @return float|string The result, or a string containing an error (0 = Descending, 1 = Ascending)
*/
public static function RANK(mixed $value, mixed $valueSet, mixed $order = self::RANK_SORT_DESCENDING)
{
$value = Functions::flattenSingleValue($value);
$valueSet = Functions::flattenArray($valueSet);
$order = ($order === null) ? self::RANK_SORT_DESCENDING : Functions::flattenSingleValue($order);
try {
$value = StatisticalValidations::validateFloat($value);
$order = StatisticalValidations::validateInt($order);
} catch (Exception $e) {
return $e->getMessage();
}
$valueSet = self::rankFilterValues($valueSet);
if ($order === self::RANK_SORT_DESCENDING) {
rsort($valueSet, SORT_NUMERIC);
} else {
sort($valueSet, SORT_NUMERIC);
}
$pos = array_search($value, $valueSet);
if ($pos === false) {
return ExcelError::NA();
}
return ++$pos;
}
protected static function percentileFilterValues(array $dataSet): array
{
return array_filter(
$dataSet,
fn ($value): bool => is_numeric($value) && !is_string($value)
);
}
protected static function rankFilterValues(array $dataSet): array
{
return array_filter(
$dataSet,
fn ($value): bool => is_numeric($value)
);
}
}

View file

@ -0,0 +1,99 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\MathTrig;
use PhpOffice\PhpSpreadsheet\Shared\IntOrFloat;
class Permutations
{
use ArrayEnabled;
/**
* PERMUT.
*
* Returns the number of permutations for a given number of objects that can be
* selected from number objects. A permutation is any set or subset of objects or
* events where internal order is significant. Permutations are different from
* combinations, for which the internal order is not significant. Use this function
* for lottery-style probability calculations.
*
* @param mixed $numObjs Integer number of different objects
* Or can be an array of values
* @param mixed $numInSet Integer number of objects in each permutation
* Or can be an array of values
*
* @return array|float|int|string Number of permutations, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function PERMUT(mixed $numObjs, mixed $numInSet)
{
if (is_array($numObjs) || is_array($numInSet)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $numObjs, $numInSet);
}
try {
$numObjs = StatisticalValidations::validateInt($numObjs);
$numInSet = StatisticalValidations::validateInt($numInSet);
} catch (Exception $e) {
return $e->getMessage();
}
if ($numObjs < $numInSet) {
return ExcelError::NAN();
}
$result1 = MathTrig\Factorial::fact($numObjs);
if (is_string($result1)) {
return $result1;
}
$result2 = MathTrig\Factorial::fact($numObjs - $numInSet);
if (is_string($result2)) {
return $result2;
}
// phpstan thinks result1 and result2 can be arrays; they can't.
$result = round($result1 / $result2); // @phpstan-ignore-line
return IntOrFloat::evaluate($result);
}
/**
* PERMUTATIONA.
*
* Returns the number of permutations for a given number of objects (with repetitions)
* that can be selected from the total objects.
*
* @param mixed $numObjs Integer number of different objects
* Or can be an array of values
* @param mixed $numInSet Integer number of objects in each permutation
* Or can be an array of values
*
* @return array|float|int|string Number of permutations, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function PERMUTATIONA(mixed $numObjs, mixed $numInSet)
{
if (is_array($numObjs) || is_array($numInSet)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $numObjs, $numInSet);
}
try {
$numObjs = StatisticalValidations::validateInt($numObjs);
$numInSet = StatisticalValidations::validateInt($numInSet);
} catch (Exception $e) {
return $e->getMessage();
}
if ($numObjs < 0 || $numInSet < 0) {
return ExcelError::NAN();
}
$result = $numObjs ** $numInSet;
return IntOrFloat::evaluate($result);
}
}

View file

@ -0,0 +1,97 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Size
{
/**
* LARGE.
*
* Returns the nth largest value in a data set. You can use this function to
* select a value based on its relative standing.
*
* Excel Function:
* LARGE(value1[,value2[, ...]],entry)
*
* @param mixed $args Data values
*
* @return float|string The result, or a string containing an error
*/
public static function large(mixed ...$args)
{
$aArgs = Functions::flattenArray($args);
$entry = array_pop($aArgs);
if ((is_numeric($entry)) && (!is_string($entry))) {
$entry = (int) floor($entry);
$mArgs = self::filter($aArgs);
$count = Counts::COUNT($mArgs);
--$entry;
if ($count === 0 || $entry < 0 || $entry >= $count) {
return ExcelError::NAN();
}
rsort($mArgs);
return $mArgs[$entry];
}
return ExcelError::VALUE();
}
/**
* SMALL.
*
* Returns the nth smallest value in a data set. You can use this function to
* select a value based on its relative standing.
*
* Excel Function:
* SMALL(value1[,value2[, ...]],entry)
*
* @param mixed $args Data values
*
* @return float|string The result, or a string containing an error
*/
public static function small(mixed ...$args)
{
$aArgs = Functions::flattenArray($args);
$entry = array_pop($aArgs);
if ((is_numeric($entry)) && (!is_string($entry))) {
$entry = (int) floor($entry);
$mArgs = self::filter($aArgs);
$count = Counts::COUNT($mArgs);
--$entry;
if ($count === 0 || $entry < 0 || $entry >= $count) {
return ExcelError::NAN();
}
sort($mArgs);
return $mArgs[$entry];
}
return ExcelError::VALUE();
}
/**
* @param mixed[] $args Data values
*/
protected static function filter(array $args): array
{
$mArgs = [];
foreach ($args as $arg) {
// Is it a numeric value?
if ((is_numeric($arg)) && (!is_string($arg))) {
$mArgs[] = $arg;
}
}
return $mArgs;
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;
class StandardDeviations
{
/**
* STDEV.
*
* Estimates standard deviation based on a sample. The standard deviation is a measure of how
* widely values are dispersed from the average value (the mean).
*
* Excel Function:
* STDEV(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*
* @return float|string The result, or a string containing an error
*/
public static function STDEV(mixed ...$args)
{
$result = Variances::VAR(...$args);
if (!is_numeric($result)) {
return $result;
}
return sqrt((float) $result);
}
/**
* STDEVA.
*
* Estimates standard deviation based on a sample, including numbers, text, and logical values
*
* Excel Function:
* STDEVA(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*/
public static function STDEVA(mixed ...$args): float|string
{
$result = Variances::VARA(...$args);
if (!is_numeric($result)) {
return $result;
}
return sqrt((float) $result);
}
/**
* STDEVP.
*
* Calculates standard deviation based on the entire population
*
* Excel Function:
* STDEVP(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*/
public static function STDEVP(mixed ...$args): float|string
{
$result = Variances::VARP(...$args);
if (!is_numeric($result)) {
return $result;
}
return sqrt((float) $result);
}
/**
* STDEVPA.
*
* Calculates standard deviation based on the entire population, including numbers, text, and logical values
*
* Excel Function:
* STDEVPA(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*/
public static function STDEVPA(mixed ...$args): float|string
{
$result = Variances::VARPA(...$args);
if (!is_numeric($result)) {
return $result;
}
return sqrt((float) $result);
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Standardize extends StatisticalValidations
{
use ArrayEnabled;
/**
* STANDARDIZE.
*
* Returns a normalized value from a distribution characterized by mean and standard_dev.
*
* @param array|float $value Value to normalize
* Or can be an array of values
* @param array|float $mean Mean Value
* Or can be an array of values
* @param array|float $stdDev Standard Deviation
* Or can be an array of values
*
* @return array|float|string Standardized value, or a string containing an error
* If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function execute($value, $mean, $stdDev): array|string|float
{
if (is_array($value) || is_array($mean) || is_array($stdDev)) {
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $mean, $stdDev);
}
try {
$value = self::validateFloat($value);
$mean = self::validateFloat($mean);
$stdDev = self::validateFloat($stdDev);
} catch (Exception $e) {
return $e->getMessage();
}
if ($stdDev <= 0) {
return ExcelError::NAN();
}
return ($value - $mean) / $stdDev;
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class StatisticalValidations
{
public static function validateFloat(mixed $value): float
{
if (!is_numeric($value)) {
throw new Exception(ExcelError::VALUE());
}
return (float) $value;
}
public static function validateInt(mixed $value): int
{
if (!is_numeric($value)) {
throw new Exception(ExcelError::VALUE());
}
return (int) floor((float) $value);
}
public static function validateBool(mixed $value): bool
{
if (!is_bool($value) && !is_numeric($value)) {
throw new Exception(ExcelError::VALUE());
}
return (bool) $value;
}
}

View file

@ -0,0 +1,425 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\Trend\Trend;
class Trends
{
use ArrayEnabled;
private static function filterTrendValues(array &$array1, array &$array2): void
{
foreach ($array1 as $key => $value) {
if ((is_bool($value)) || (is_string($value)) || ($value === null)) {
unset($array1[$key], $array2[$key]);
}
}
}
/**
* @param mixed $array1 should be array, but scalar is made into one
* @param mixed $array2 should be array, but scalar is made into one
*/
private static function checkTrendArrays(mixed &$array1, mixed &$array2): void
{
if (!is_array($array1)) {
$array1 = [$array1];
}
if (!is_array($array2)) {
$array2 = [$array2];
}
$array1 = Functions::flattenArray($array1);
$array2 = Functions::flattenArray($array2);
self::filterTrendValues($array1, $array2);
self::filterTrendValues($array2, $array1);
// Reset the array indexes
$array1 = array_merge($array1);
$array2 = array_merge($array2);
}
protected static function validateTrendArrays(array $yValues, array $xValues): void
{
$yValueCount = count($yValues);
$xValueCount = count($xValues);
if (($yValueCount === 0) || ($yValueCount !== $xValueCount)) {
throw new Exception(ExcelError::NA());
} elseif ($yValueCount === 1) {
throw new Exception(ExcelError::DIV0());
}
}
/**
* CORREL.
*
* Returns covariance, the average of the products of deviations for each data point pair.
*
* @param mixed $yValues array of mixed Data Series Y
* @param null|mixed $xValues array of mixed Data Series X
*/
public static function CORREL(mixed $yValues, $xValues = null): float|string
{
if (($xValues === null) || (!is_array($yValues)) || (!is_array($xValues))) {
return ExcelError::VALUE();
}
try {
self::checkTrendArrays($yValues, $xValues);
self::validateTrendArrays($yValues, $xValues);
} catch (Exception $e) {
return $e->getMessage();
}
$bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues);
return $bestFitLinear->getCorrelation();
}
/**
* COVAR.
*
* Returns covariance, the average of the products of deviations for each data point pair.
*
* @param mixed[] $yValues array of mixed Data Series Y
* @param mixed[] $xValues array of mixed Data Series X
*/
public static function COVAR(array $yValues, array $xValues): float|string
{
try {
self::checkTrendArrays($yValues, $xValues);
self::validateTrendArrays($yValues, $xValues);
} catch (Exception $e) {
return $e->getMessage();
}
$bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues);
return $bestFitLinear->getCovariance();
}
/**
* FORECAST.
*
* Calculates, or predicts, a future value by using existing values.
* The predicted value is a y-value for a given x-value.
*
* @param mixed $xValue Float value of X for which we want to find Y
* Or can be an array of values
* @param mixed[] $yValues array of mixed Data Series Y
* @param mixed[] $xValues array of mixed Data Series X
*
* @return array|bool|float|string If an array of numbers is passed as an argument, then the returned result will also be an array
* with the same dimensions
*/
public static function FORECAST(mixed $xValue, array $yValues, array $xValues)
{
if (is_array($xValue)) {
return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 1, $xValue, $yValues, $xValues);
}
try {
$xValue = StatisticalValidations::validateFloat($xValue);
self::checkTrendArrays($yValues, $xValues);
self::validateTrendArrays($yValues, $xValues);
} catch (Exception $e) {
return $e->getMessage();
}
$bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues);
return $bestFitLinear->getValueOfYForX($xValue);
}
/**
* GROWTH.
*
* Returns values along a predicted exponential Trend
*
* @param mixed[] $yValues Data Series Y
* @param mixed[] $xValues Data Series X
* @param mixed[] $newValues Values of X for which we want to find Y
* @param mixed $const A logical (boolean) value specifying whether to force the intersect to equal 0 or not
*
* @return float[]
*/
public static function GROWTH(array $yValues, array $xValues = [], array $newValues = [], mixed $const = true): array
{
$yValues = Functions::flattenArray($yValues);
$xValues = Functions::flattenArray($xValues);
$newValues = Functions::flattenArray($newValues);
$const = ($const === null) ? true : (bool) Functions::flattenSingleValue($const);
$bestFitExponential = Trend::calculate(Trend::TREND_EXPONENTIAL, $yValues, $xValues, $const);
if (empty($newValues)) {
$newValues = $bestFitExponential->getXValues();
}
$returnArray = [];
foreach ($newValues as $xValue) {
$returnArray[0][] = [$bestFitExponential->getValueOfYForX($xValue)];
}
return $returnArray; //* @phpstan-ignore-line
}
/**
* INTERCEPT.
*
* Calculates the point at which a line will intersect the y-axis by using existing x-values and y-values.
*
* @param mixed[] $yValues Data Series Y
* @param mixed[] $xValues Data Series X
*/
public static function INTERCEPT(array $yValues, array $xValues): float|string
{
try {
self::checkTrendArrays($yValues, $xValues);
self::validateTrendArrays($yValues, $xValues);
} catch (Exception $e) {
return $e->getMessage();
}
$bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues);
return $bestFitLinear->getIntersect();
}
/**
* LINEST.
*
* Calculates the statistics for a line by using the "least squares" method to calculate a straight line
* that best fits your data, and then returns an array that describes the line.
*
* @param mixed[] $yValues Data Series Y
* @param null|mixed[] $xValues Data Series X
* @param mixed $const A logical (boolean) value specifying whether to force the intersect to equal 0 or not
* @param mixed $stats A logical (boolean) value specifying whether to return additional regression statistics
*
* @return array|string The result, or a string containing an error
*/
public static function LINEST(array $yValues, ?array $xValues = null, mixed $const = true, mixed $stats = false): string|array
{
$const = ($const === null) ? true : (bool) Functions::flattenSingleValue($const);
$stats = ($stats === null) ? false : (bool) Functions::flattenSingleValue($stats);
if ($xValues === null) {
$xValues = $yValues;
}
try {
self::checkTrendArrays($yValues, $xValues);
self::validateTrendArrays($yValues, $xValues);
} catch (Exception $e) {
return $e->getMessage();
}
$bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues, $const);
if ($stats === true) {
return [
[
$bestFitLinear->getSlope(),
$bestFitLinear->getIntersect(),
],
[
$bestFitLinear->getSlopeSE(),
($const === false) ? ExcelError::NA() : $bestFitLinear->getIntersectSE(),
],
[
$bestFitLinear->getGoodnessOfFit(),
$bestFitLinear->getStdevOfResiduals(),
],
[
$bestFitLinear->getF(),
$bestFitLinear->getDFResiduals(),
],
[
$bestFitLinear->getSSRegression(),
$bestFitLinear->getSSResiduals(),
],
];
}
return [
$bestFitLinear->getSlope(),
$bestFitLinear->getIntersect(),
];
}
/**
* LOGEST.
*
* Calculates an exponential curve that best fits the X and Y data series,
* and then returns an array that describes the line.
*
* @param mixed[] $yValues Data Series Y
* @param null|mixed[] $xValues Data Series X
* @param mixed $const A logical (boolean) value specifying whether to force the intersect to equal 0 or not
* @param mixed $stats A logical (boolean) value specifying whether to return additional regression statistics
*
* @return array|string The result, or a string containing an error
*/
public static function LOGEST(array $yValues, ?array $xValues = null, mixed $const = true, mixed $stats = false): string|array
{
$const = ($const === null) ? true : (bool) Functions::flattenSingleValue($const);
$stats = ($stats === null) ? false : (bool) Functions::flattenSingleValue($stats);
if ($xValues === null) {
$xValues = $yValues;
}
try {
self::checkTrendArrays($yValues, $xValues);
self::validateTrendArrays($yValues, $xValues);
} catch (Exception $e) {
return $e->getMessage();
}
foreach ($yValues as $value) {
if ($value < 0.0) {
return ExcelError::NAN();
}
}
$bestFitExponential = Trend::calculate(Trend::TREND_EXPONENTIAL, $yValues, $xValues, $const);
if ($stats === true) {
return [
[
$bestFitExponential->getSlope(),
$bestFitExponential->getIntersect(),
],
[
$bestFitExponential->getSlopeSE(),
($const === false) ? ExcelError::NA() : $bestFitExponential->getIntersectSE(),
],
[
$bestFitExponential->getGoodnessOfFit(),
$bestFitExponential->getStdevOfResiduals(),
],
[
$bestFitExponential->getF(),
$bestFitExponential->getDFResiduals(),
],
[
$bestFitExponential->getSSRegression(),
$bestFitExponential->getSSResiduals(),
],
];
}
return [
$bestFitExponential->getSlope(),
$bestFitExponential->getIntersect(),
];
}
/**
* RSQ.
*
* Returns the square of the Pearson product moment correlation coefficient through data points
* in known_y's and known_x's.
*
* @param mixed[] $yValues Data Series Y
* @param mixed[] $xValues Data Series X
*
* @return float|string The result, or a string containing an error
*/
public static function RSQ(array $yValues, array $xValues)
{
try {
self::checkTrendArrays($yValues, $xValues);
self::validateTrendArrays($yValues, $xValues);
} catch (Exception $e) {
return $e->getMessage();
}
$bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues);
return $bestFitLinear->getGoodnessOfFit();
}
/**
* SLOPE.
*
* Returns the slope of the linear regression line through data points in known_y's and known_x's.
*
* @param mixed[] $yValues Data Series Y
* @param mixed[] $xValues Data Series X
*
* @return float|string The result, or a string containing an error
*/
public static function SLOPE(array $yValues, array $xValues)
{
try {
self::checkTrendArrays($yValues, $xValues);
self::validateTrendArrays($yValues, $xValues);
} catch (Exception $e) {
return $e->getMessage();
}
$bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues);
return $bestFitLinear->getSlope();
}
/**
* STEYX.
*
* Returns the standard error of the predicted y-value for each x in the regression.
*
* @param mixed[] $yValues Data Series Y
* @param mixed[] $xValues Data Series X
*/
public static function STEYX(array $yValues, array $xValues): float|string
{
try {
self::checkTrendArrays($yValues, $xValues);
self::validateTrendArrays($yValues, $xValues);
} catch (Exception $e) {
return $e->getMessage();
}
$bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues);
return $bestFitLinear->getStdevOfResiduals();
}
/**
* TREND.
*
* Returns values along a linear Trend
*
* @param mixed[] $yValues Data Series Y
* @param mixed[] $xValues Data Series X
* @param mixed[] $newValues Values of X for which we want to find Y
* @param mixed $const A logical (boolean) value specifying whether to force the intersect to equal 0 or not
*
* @return float[]
*/
public static function TREND(array $yValues, array $xValues = [], array $newValues = [], mixed $const = true): array
{
$yValues = Functions::flattenArray($yValues);
$xValues = Functions::flattenArray($xValues);
$newValues = Functions::flattenArray($newValues);
$const = ($const === null) ? true : (bool) Functions::flattenSingleValue($const);
$bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues, $const);
if (empty($newValues)) {
$newValues = $bestFitLinear->getXValues();
}
$returnArray = [];
foreach ($newValues as $xValue) {
$returnArray[0][] = [$bestFitLinear->getValueOfYForX($xValue)];
}
return $returnArray; //* @phpstan-ignore-line
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
abstract class VarianceBase
{
protected static function datatypeAdjustmentAllowStrings(mixed $value): mixed
{
if (is_bool($value)) {
return (int) $value;
} elseif (is_string($value)) {
return 0;
}
return $value;
}
protected static function datatypeAdjustmentBooleans(mixed $value): mixed
{
if (is_bool($value) && (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE)) {
return (int) $value;
}
return $value;
}
}

View file

@ -0,0 +1,186 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
class Variances extends VarianceBase
{
/**
* VAR.
*
* Estimates variance based on a sample.
*
* Excel Function:
* VAR(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*
* @return float|string (string if result is an error)
*/
public static function VAR(mixed ...$args): float|string
{
$returnValue = ExcelError::DIV0();
$summerA = $summerB = 0.0;
// Loop through arguments
$aArgs = Functions::flattenArray($args);
$aCount = 0;
foreach ($aArgs as $arg) {
$arg = self::datatypeAdjustmentBooleans($arg);
// Is it a numeric value?
if ((is_numeric($arg)) && (!is_string($arg))) {
$summerA += ($arg * $arg);
$summerB += $arg;
++$aCount;
}
}
if ($aCount > 1) {
$summerA *= $aCount;
$summerB *= $summerB;
return ($summerA - $summerB) / ($aCount * ($aCount - 1));
}
return $returnValue;
}
/**
* VARA.
*
* Estimates variance based on a sample, including numbers, text, and logical values
*
* Excel Function:
* VARA(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*
* @return float|string (string if result is an error)
*/
public static function VARA(mixed ...$args): string|float
{
$returnValue = ExcelError::DIV0();
$summerA = $summerB = 0.0;
// Loop through arguments
$aArgs = Functions::flattenArrayIndexed($args);
$aCount = 0;
foreach ($aArgs as $k => $arg) {
if ((is_string($arg)) && (Functions::isValue($k))) {
return ExcelError::VALUE();
} elseif ((is_string($arg)) && (!Functions::isMatrixValue($k))) {
} else {
// Is it a numeric value?
if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) && ($arg != '')))) {
$arg = self::datatypeAdjustmentAllowStrings($arg);
$summerA += ($arg * $arg);
$summerB += $arg;
++$aCount;
}
}
}
if ($aCount > 1) {
$summerA *= $aCount;
$summerB *= $summerB;
return ($summerA - $summerB) / ($aCount * ($aCount - 1));
}
return $returnValue;
}
/**
* VARP.
*
* Calculates variance based on the entire population
*
* Excel Function:
* VARP(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*
* @return float|string (string if result is an error)
*/
public static function VARP(mixed ...$args): float|string
{
// Return value
$returnValue = ExcelError::DIV0();
$summerA = $summerB = 0.0;
// Loop through arguments
$aArgs = Functions::flattenArray($args);
$aCount = 0;
foreach ($aArgs as $arg) {
$arg = self::datatypeAdjustmentBooleans($arg);
// Is it a numeric value?
if ((is_numeric($arg)) && (!is_string($arg))) {
$summerA += ($arg * $arg);
$summerB += $arg;
++$aCount;
}
}
if ($aCount > 0) {
$summerA *= $aCount;
$summerB *= $summerB;
return ($summerA - $summerB) / ($aCount * $aCount);
}
return $returnValue;
}
/**
* VARPA.
*
* Calculates variance based on the entire population, including numbers, text, and logical values
*
* Excel Function:
* VARPA(value1[,value2[, ...]])
*
* @param mixed ...$args Data values
*
* @return float|string (string if result is an error)
*/
public static function VARPA(mixed ...$args): string|float
{
$returnValue = ExcelError::DIV0();
$summerA = $summerB = 0.0;
// Loop through arguments
$aArgs = Functions::flattenArrayIndexed($args);
$aCount = 0;
foreach ($aArgs as $k => $arg) {
if ((is_string($arg)) && (Functions::isValue($k))) {
return ExcelError::VALUE();
} elseif ((is_string($arg)) && (!Functions::isMatrixValue($k))) {
} else {
// Is it a numeric value?
if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) && ($arg != '')))) {
$arg = self::datatypeAdjustmentAllowStrings($arg);
$summerA += ($arg * $arg);
$summerB += $arg;
++$aCount;
}
}
}
if ($aCount > 0) {
$summerA *= $aCount;
$summerB *= $summerB;
return ($summerA - $summerB) / ($aCount * $aCount);
}
return $returnValue;
}
}