Initial experiments using the new Database query logic with Conditional Statistical Functions (#1880)
- Refactoring of the Statistical Conditional functions (`AVERAGEIF()`, `AVERAGEIFS()`, `COUNTIF()`, `COUNTIFS()`, `MAXIFS()` and `MINIFS()` to use the new Database functions codebase. - Extended unit testing - Fix handling for null values - Fixes to wildcard text searches There's still scope for further improvements to memory usage and performance; but for now the code is stable with all unit tests passing
This commit is contained in:
parent
8dcdf58131
commit
08673b5820
|
|
@ -9,8 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org).
|
|||
|
||||
### Added
|
||||
|
||||
- Implementation of the Excel `AVERAGEIFS()` functions as part of a restructuring of Database functions and Conditional Statistical functions.
|
||||
- Support for date values and percentages in query parameters for Database functions, and the IF expressions in functions like COUNTIF() and AVERAGEIF(). [#1875](https://github.com/PHPOffice/PhpSpreadsheet/pull/1875)
|
||||
- Support for booleans, and for wildcard text search in query parameters for Database functions. [#1876](https://github.com/PHPOffice/PhpSpreadsheet/pull/1876)
|
||||
- Support for booleans, and for wildcard text search in query parameters for Database functions, and the IF expressions in functions like COUNTIF() and AVERAGEIF(). [#1876](https://github.com/PHPOffice/PhpSpreadsheet/pull/1876)
|
||||
- Implemented DataBar for conditional formatting in Xlsx, providing read/write and creation of (type, value, direction, fills, border, axis position, color settings) as DataBar options in Excel. [#1754](https://github.com/PHPOffice/PhpSpreadsheet/pull/1754)
|
||||
- Alignment for ODS Writer [#1796](https://github.com/PHPOffice/PhpSpreadsheet/issues/1796)
|
||||
- Basic implementation of the PERMUTATIONA() Statistical Function
|
||||
|
|
|
|||
|
|
@ -343,12 +343,12 @@ class Calculation
|
|||
],
|
||||
'AVERAGEIF' => [
|
||||
'category' => Category::CATEGORY_STATISTICAL,
|
||||
'functionCall' => [Statistical::class, 'AVERAGEIF'],
|
||||
'functionCall' => [Statistical\Conditional::class, 'AVERAGEIF'],
|
||||
'argumentCount' => '2,3',
|
||||
],
|
||||
'AVERAGEIFS' => [
|
||||
'category' => Category::CATEGORY_STATISTICAL,
|
||||
'functionCall' => [Functions::class, 'DUMMY'],
|
||||
'functionCall' => [Statistical\Conditional::class, 'AVERAGEIFS'],
|
||||
'argumentCount' => '3+',
|
||||
],
|
||||
'BAHTTEXT' => [
|
||||
|
|
@ -639,12 +639,12 @@ class Calculation
|
|||
],
|
||||
'COUNTIF' => [
|
||||
'category' => Category::CATEGORY_STATISTICAL,
|
||||
'functionCall' => [Statistical::class, 'COUNTIF'],
|
||||
'functionCall' => [Statistical\Conditional::class, 'COUNTIF'],
|
||||
'argumentCount' => '2',
|
||||
],
|
||||
'COUNTIFS' => [
|
||||
'category' => Category::CATEGORY_STATISTICAL,
|
||||
'functionCall' => [Statistical::class, 'COUNTIFS'],
|
||||
'functionCall' => [Statistical\Conditional::class, 'COUNTIFS'],
|
||||
'argumentCount' => '2+',
|
||||
],
|
||||
'COUPDAYBS' => [
|
||||
|
|
@ -1630,7 +1630,7 @@ class Calculation
|
|||
],
|
||||
'MAXIFS' => [
|
||||
'category' => Category::CATEGORY_STATISTICAL,
|
||||
'functionCall' => [Statistical::class, 'MAXIFS'],
|
||||
'functionCall' => [Statistical\Conditional::class, 'MAXIFS'],
|
||||
'argumentCount' => '3+',
|
||||
],
|
||||
'MDETERM' => [
|
||||
|
|
@ -1675,7 +1675,7 @@ class Calculation
|
|||
],
|
||||
'MINIFS' => [
|
||||
'category' => Category::CATEGORY_STATISTICAL,
|
||||
'functionCall' => [Statistical::class, 'MINIFS'],
|
||||
'functionCall' => [Statistical\Conditional::class, 'MINIFS'],
|
||||
'argumentCount' => '3+',
|
||||
],
|
||||
'MINUTE' => [
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class Database
|
|||
* the column label in which you specify a condition for the
|
||||
* column.
|
||||
*
|
||||
* @return float|string
|
||||
* @return null|float|string
|
||||
*/
|
||||
public static function DAVERAGE($database, $field, $criteria)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class DAverage extends DatabaseAbstract
|
|||
* the column label in which you specify a condition for the
|
||||
* column.
|
||||
*
|
||||
* @return float|string
|
||||
* @return null|float|string
|
||||
*/
|
||||
public static function evaluate($database, $field, $criteria)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class DCountA extends DatabaseAbstract
|
|||
$field = self::fieldExtract($database, $field);
|
||||
|
||||
return Statistical::COUNTA(
|
||||
self::getFilteredColumn($database, $field, $criteria)
|
||||
self::getFilteredColumn($database, $field ?? 0, $criteria)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ class DGet extends DatabaseAbstract
|
|||
return Functions::NAN();
|
||||
}
|
||||
|
||||
return $columnData[0];
|
||||
$row = array_pop($columnData);
|
||||
|
||||
return array_pop($row);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,19 +25,20 @@ abstract class DatabaseAbstract
|
|||
* represents the position of the column within the list: 1 for
|
||||
* the first column, 2 for the second column, and so on.
|
||||
*/
|
||||
protected static function fieldExtract(array $database, $field): ?string
|
||||
protected static function fieldExtract(array $database, $field): ?int
|
||||
{
|
||||
$field = strtoupper(Functions::flattenSingleValue($field));
|
||||
$fieldNames = array_map('strtoupper', array_shift($database));
|
||||
|
||||
if (is_numeric($field)) {
|
||||
$keys = array_keys($fieldNames);
|
||||
|
||||
return $keys[$field - 1];
|
||||
if ($field === '') {
|
||||
return null;
|
||||
}
|
||||
$key = array_search($field, $fieldNames);
|
||||
|
||||
return $key ?: null;
|
||||
$fieldNames = array_map('strtoupper', array_shift($database));
|
||||
if (is_numeric($field)) {
|
||||
return ((int) $field) - 1;
|
||||
}
|
||||
$key = array_search($field, array_values($fieldNames), true);
|
||||
|
||||
return ($key !== false) ? (int) $key : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -70,14 +71,19 @@ abstract class DatabaseAbstract
|
|||
return self::executeQuery($database, $query, $criteriaNames, $fieldNames);
|
||||
}
|
||||
|
||||
protected static function getFilteredColumn(array $database, $field, array $criteria): array
|
||||
protected static function getFilteredColumn(array $database, ?int $field, array $criteria): array
|
||||
{
|
||||
// reduce the database to a set of rows that match all the criteria
|
||||
$database = self::filter($database, $criteria);
|
||||
$defaultReturnColumnValue = ($field === null) ? 1 : null;
|
||||
|
||||
// extract an array of values for the requested column
|
||||
$columnData = [];
|
||||
foreach ($database as $row) {
|
||||
$columnData[] = ($field !== null) ? $row[$field] : true;
|
||||
foreach ($database as $rowKey => $row) {
|
||||
$keys = array_keys($row);
|
||||
$key = $keys[$field] ?? null;
|
||||
$columnKey = $key ?? 'A';
|
||||
$columnData[$rowKey][$columnKey] = $row[$key] ?? $defaultReturnColumnValue;
|
||||
}
|
||||
|
||||
return $columnData;
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Internal;
|
|||
class WildcardMatch
|
||||
{
|
||||
private const SEARCH_SET = [
|
||||
'/([^~])(\*)/ui',
|
||||
'/(?<!~)\*/ui',
|
||||
'/~\*/ui',
|
||||
'/([^~])(\?)/ui',
|
||||
'/(?<!~)\?/ui',
|
||||
'/~\?/ui',
|
||||
];
|
||||
|
||||
|
|
@ -20,6 +20,11 @@ class WildcardMatch
|
|||
|
||||
public static function wildcard(string $wildcard): string
|
||||
{
|
||||
// Preg Escape the wildcard, but protecting the Excel * and ? search characters
|
||||
$wildcard = str_replace(['*', '?'], [0x1A, 0x1B], $wildcard);
|
||||
$wildcard = preg_quote($wildcard);
|
||||
$wildcard = str_replace([0x1A, 0x1B], ['*', '?'], $wildcard);
|
||||
|
||||
return preg_replace(self::SEARCH_SET, self::REPLACEMENT_SET, $wildcard);
|
||||
}
|
||||
|
||||
|
|
@ -29,6 +34,6 @@ class WildcardMatch
|
|||
return true;
|
||||
}
|
||||
|
||||
return (bool) preg_match("/{$wildcard}/ui", $value);
|
||||
return (bool) preg_match("/^{$wildcard}\$/mui", $value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1281,7 +1281,7 @@ class MathTrig
|
|||
/**
|
||||
* SUMIF.
|
||||
*
|
||||
* Counts the number of cells that contain numbers within the list of arguments
|
||||
* Totals the values of cells that contain numbers within the list of arguments
|
||||
*
|
||||
* Excel Function:
|
||||
* SUMIF(value1[,value2[, ...]],condition)
|
||||
|
|
@ -1327,7 +1327,7 @@ class MathTrig
|
|||
/**
|
||||
* SUMIFS.
|
||||
*
|
||||
* Counts the number of cells that contain numbers within the list of arguments
|
||||
* Totals the values of cells that contain numbers within the list of arguments
|
||||
*
|
||||
* Excel Function:
|
||||
* SUMIFS(value1[,value2[, ...]],condition)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Conditional;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Permutations;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Trend\Trend;
|
||||
|
||||
|
|
@ -701,47 +702,20 @@ class Statistical
|
|||
* Excel Function:
|
||||
* AVERAGEIF(value1[,value2[, ...]],condition)
|
||||
*
|
||||
* @param mixed $aArgs Data values
|
||||
* @param string $condition the criteria that defines which cells will be checked
|
||||
* @param mixed[] $averageArgs Data values
|
||||
* @Deprecated 1.17.0
|
||||
*
|
||||
* @return float|string
|
||||
* @see Statistical\Conditional::AVERAGEIF()
|
||||
* Use the AVERAGEIF() method in the Statistical\Conditional class instead
|
||||
*
|
||||
* @param mixed $range Data values
|
||||
* @param string $condition the criteria that defines which cells will be checked
|
||||
* @param mixed[] $averageRange Data values
|
||||
*
|
||||
* @return null|float|string
|
||||
*/
|
||||
public static function AVERAGEIF($aArgs, $condition, $averageArgs = [])
|
||||
public static function AVERAGEIF($range, $condition, $averageRange = [])
|
||||
{
|
||||
$returnValue = 0;
|
||||
|
||||
$aArgs = Functions::flattenArray($aArgs);
|
||||
$averageArgs = Functions::flattenArray($averageArgs);
|
||||
if (empty($averageArgs)) {
|
||||
$averageArgs = $aArgs;
|
||||
}
|
||||
$condition = Functions::ifCondition($condition);
|
||||
$conditionIsNumeric = strpos($condition, '"') === false;
|
||||
|
||||
// Loop through arguments
|
||||
$aCount = 0;
|
||||
foreach ($aArgs as $key => $arg) {
|
||||
if (!is_numeric($arg)) {
|
||||
if ($conditionIsNumeric) {
|
||||
continue;
|
||||
}
|
||||
$arg = Calculation::wrapResult(strtoupper($arg));
|
||||
} elseif (!$conditionIsNumeric) {
|
||||
continue;
|
||||
}
|
||||
$testCondition = '=' . $arg . $condition;
|
||||
if (Calculation::getInstance()->_calculateFormulaValue($testCondition)) {
|
||||
$returnValue += $averageArgs[$key];
|
||||
++$aCount;
|
||||
}
|
||||
}
|
||||
|
||||
if ($aCount > 0) {
|
||||
return $returnValue / $aCount;
|
||||
}
|
||||
|
||||
return Functions::DIV0();
|
||||
return Statistical\Conditional::AVERAGEIF($range, $condition, $averageRange);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1137,38 +1111,21 @@ class Statistical
|
|||
* Counts the number of cells that contain numbers within the list of arguments
|
||||
*
|
||||
* Excel Function:
|
||||
* COUNTIF(value1[,value2[, ...]],condition)
|
||||
* COUNTIF(range,condition)
|
||||
*
|
||||
* @param mixed $aArgs Data values
|
||||
* @Deprecated 1.17.0
|
||||
*
|
||||
* @see Statistical\Conditional::COUNTIF()
|
||||
* Use the COUNTIF() method in the Statistical\Conditional class instead
|
||||
*
|
||||
* @param mixed $range Data values
|
||||
* @param string $condition the criteria that defines which cells will be counted
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function COUNTIF($aArgs, $condition)
|
||||
public static function COUNTIF($range, $condition)
|
||||
{
|
||||
$returnValue = 0;
|
||||
|
||||
$aArgs = Functions::flattenArray($aArgs);
|
||||
$condition = Functions::ifCondition($condition);
|
||||
$conditionIsNumeric = strpos($condition, '"') === false;
|
||||
// Loop through arguments
|
||||
foreach ($aArgs as $arg) {
|
||||
if (!is_numeric($arg)) {
|
||||
if ($conditionIsNumeric) {
|
||||
continue;
|
||||
}
|
||||
$arg = Calculation::wrapResult(strtoupper($arg));
|
||||
} elseif (!$conditionIsNumeric) {
|
||||
continue;
|
||||
}
|
||||
$testCondition = '=' . $arg . $condition;
|
||||
if (Calculation::getInstance()->_calculateFormulaValue($testCondition)) {
|
||||
// Is it a value within our criteria
|
||||
++$returnValue;
|
||||
}
|
||||
}
|
||||
|
||||
return $returnValue;
|
||||
return Statistical\Conditional::COUNTIF($range, $condition);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1179,66 +1136,18 @@ class Statistical
|
|||
* Excel Function:
|
||||
* COUNTIFS(criteria_range1, criteria1, [criteria_range2, criteria2]…)
|
||||
*
|
||||
* @param mixed $args Criterias
|
||||
* @Deprecated 1.17.0
|
||||
*
|
||||
* @see Statistical\Conditional::COUNTIFS()
|
||||
* Use the COUNTIFS() method in the Statistical\Conditional class instead
|
||||
*
|
||||
* @param mixed $args Pairs of Ranges and Criteria
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function COUNTIFS(...$args)
|
||||
{
|
||||
$arrayList = $args;
|
||||
|
||||
// Return value
|
||||
$returnValue = 0;
|
||||
|
||||
if (empty($arrayList)) {
|
||||
return $returnValue;
|
||||
}
|
||||
|
||||
$aArgsArray = [];
|
||||
$conditions = [];
|
||||
|
||||
while (count($arrayList) > 0) {
|
||||
$aArgsArray[] = Functions::flattenArray(array_shift($arrayList));
|
||||
$conditions[] = Functions::ifCondition(array_shift($arrayList));
|
||||
}
|
||||
|
||||
// Loop through each arg and see if arguments and conditions are true
|
||||
foreach (array_keys($aArgsArray[0]) as $index) {
|
||||
$valid = true;
|
||||
|
||||
foreach ($conditions as $cidx => $condition) {
|
||||
$conditionIsNumeric = strpos($condition, '"') === false;
|
||||
$arg = $aArgsArray[$cidx][$index];
|
||||
|
||||
// Loop through arguments
|
||||
if (!is_numeric($arg)) {
|
||||
if ($conditionIsNumeric) {
|
||||
$valid = false;
|
||||
|
||||
break; // if false found, don't need to check other conditions
|
||||
}
|
||||
$arg = Calculation::wrapResult(strtoupper($arg));
|
||||
} elseif (!$conditionIsNumeric) {
|
||||
$valid = false;
|
||||
|
||||
break; // if false found, don't need to check other conditions
|
||||
}
|
||||
$testCondition = '=' . $arg . $condition;
|
||||
if (!Calculation::getInstance()->_calculateFormulaValue($testCondition)) {
|
||||
// Is not a value within our criteria
|
||||
$valid = false;
|
||||
|
||||
break; // if false found, don't need to check other conditions
|
||||
}
|
||||
}
|
||||
|
||||
if ($valid) {
|
||||
++$returnValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Return
|
||||
return $returnValue;
|
||||
return Statistical\Conditional::COUNTIFS(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -2348,53 +2257,18 @@ class Statistical
|
|||
* Excel Function:
|
||||
* MAXIFS(max_range, criteria_range1, criteria1, [criteria_range2, criteria2], ...)
|
||||
*
|
||||
* @Deprecated 1.17.0
|
||||
*
|
||||
* @see Statistical\Conditional::MAXIFS()
|
||||
* Use the MAXIFS() method in the Statistical\Conditional class instead
|
||||
*
|
||||
* @param mixed $args Data range and criterias
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public static function MAXIFS(...$args)
|
||||
{
|
||||
$arrayList = $args;
|
||||
|
||||
// Return value
|
||||
$returnValue = null;
|
||||
|
||||
$maxArgs = Functions::flattenArray(array_shift($arrayList));
|
||||
$aArgsArray = [];
|
||||
$conditions = [];
|
||||
|
||||
while (count($arrayList) > 0) {
|
||||
$aArgsArray[] = Functions::flattenArray(array_shift($arrayList));
|
||||
$conditions[] = Functions::ifCondition(array_shift($arrayList));
|
||||
}
|
||||
|
||||
// Loop through each arg and see if arguments and conditions are true
|
||||
foreach ($maxArgs as $index => $value) {
|
||||
$valid = true;
|
||||
|
||||
foreach ($conditions as $cidx => $condition) {
|
||||
$arg = $aArgsArray[$cidx][$index];
|
||||
|
||||
// Loop through arguments
|
||||
if (!is_numeric($arg)) {
|
||||
$arg = Calculation::wrapResult(strtoupper($arg));
|
||||
}
|
||||
$testCondition = '=' . $arg . $condition;
|
||||
if (!Calculation::getInstance()->_calculateFormulaValue($testCondition)) {
|
||||
// Is not a value within our criteria
|
||||
$valid = false;
|
||||
|
||||
break; // if false found, don't need to check other conditions
|
||||
}
|
||||
}
|
||||
|
||||
if ($valid) {
|
||||
$returnValue = $returnValue === null ? $value : max($value, $returnValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Return
|
||||
return $returnValue;
|
||||
return Conditional::MAXIFS(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -2520,53 +2394,18 @@ class Statistical
|
|||
* Excel Function:
|
||||
* MINIFS(min_range, criteria_range1, criteria1, [criteria_range2, criteria2], ...)
|
||||
*
|
||||
* @Deprecated 1.17.0
|
||||
*
|
||||
* @see Statistical\Conditional::MINIFS()
|
||||
* Use the MINIFS() method in the Statistical\Conditional class instead
|
||||
*
|
||||
* @param mixed $args Data range and criterias
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public static function MINIFS(...$args)
|
||||
{
|
||||
$arrayList = $args;
|
||||
|
||||
// Return value
|
||||
$returnValue = null;
|
||||
|
||||
$minArgs = Functions::flattenArray(array_shift($arrayList));
|
||||
$aArgsArray = [];
|
||||
$conditions = [];
|
||||
|
||||
while (count($arrayList) > 0) {
|
||||
$aArgsArray[] = Functions::flattenArray(array_shift($arrayList));
|
||||
$conditions[] = Functions::ifCondition(array_shift($arrayList));
|
||||
}
|
||||
|
||||
// Loop through each arg and see if arguments and conditions are true
|
||||
foreach ($minArgs as $index => $value) {
|
||||
$valid = true;
|
||||
|
||||
foreach ($conditions as $cidx => $condition) {
|
||||
$arg = $aArgsArray[$cidx][$index];
|
||||
|
||||
// Loop through arguments
|
||||
if (!is_numeric($arg)) {
|
||||
$arg = Calculation::wrapResult(strtoupper($arg));
|
||||
}
|
||||
$testCondition = '=' . $arg . $condition;
|
||||
if (!Calculation::getInstance()->_calculateFormulaValue($testCondition)) {
|
||||
// Is not a value within our criteria
|
||||
$valid = false;
|
||||
|
||||
break; // if false found, don't need to check other conditions
|
||||
}
|
||||
}
|
||||
|
||||
if ($valid) {
|
||||
$returnValue = $returnValue === null ? $value : min($value, $returnValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Return
|
||||
return $returnValue;
|
||||
return Conditional::MINIFS(...$args);
|
||||
}
|
||||
|
||||
//
|
||||
|
|
|
|||
|
|
@ -0,0 +1,234 @@
|
|||
<?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\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 string $condition the criteria that defines which cells will be checked
|
||||
* @param mixed[] $averageRange Data values
|
||||
*
|
||||
* @return null|float|string
|
||||
*/
|
||||
public static function AVERAGEIF($range, $condition, $averageRange = [])
|
||||
{
|
||||
$range = Functions::flattenArray($range);
|
||||
$averageRange = Functions::flattenArray($averageRange);
|
||||
if (empty($averageRange)) {
|
||||
$averageRange = $range;
|
||||
}
|
||||
|
||||
$database = array_map(
|
||||
null,
|
||||
array_merge([self::CONDITION_COLUMN_NAME], $range),
|
||||
array_merge([self::VALUE_COLUMN_NAME], $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
|
||||
*
|
||||
* @return null|float|string
|
||||
*/
|
||||
public static function AVERAGEIFS(...$args)
|
||||
{
|
||||
if (empty($args)) {
|
||||
return 0.0;
|
||||
} elseif (count($args) === 3) {
|
||||
return self::AVERAGEIF($args[2], $args[1], $args[0]);
|
||||
}
|
||||
|
||||
$conditions = self::buildConditionSet(...$args);
|
||||
$database = self::buildDatabase(...$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 string $condition the criteria that defines which cells will be counted
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function COUNTIF($range, $condition)
|
||||
{
|
||||
// Filter out any empty values that shouldn't be included in a COUNT
|
||||
$range = array_filter(
|
||||
Functions::flattenArray($range),
|
||||
function ($value) {
|
||||
return $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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function COUNTIFS(...$args)
|
||||
{
|
||||
if (empty($args)) {
|
||||
return 0;
|
||||
} elseif (count($args) === 2) {
|
||||
return self::COUNTIF(...$args);
|
||||
}
|
||||
|
||||
$conditions = $database = [];
|
||||
$pairCount = 1;
|
||||
while (count($args) > 0) {
|
||||
$conditions[] = array_merge([sprintf(self::CONDITIONAL_COLUMN_NAME, $pairCount)], [array_pop($args)]);
|
||||
$database[] = array_merge(
|
||||
[sprintf(self::CONDITIONAL_COLUMN_NAME, $pairCount)],
|
||||
Functions::flattenArray(array_pop($args))
|
||||
);
|
||||
++$pairCount;
|
||||
}
|
||||
|
||||
$conditions = array_map(null, ...$conditions);
|
||||
$database = array_map(null, ...$database);
|
||||
|
||||
return DCount::evaluate($database, null, $conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @return null|float|string
|
||||
*/
|
||||
public static function MAXIFS(...$args)
|
||||
{
|
||||
if (empty($args)) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$conditions = self::buildConditionSet(...$args);
|
||||
$database = self::buildDatabase(...$args);
|
||||
|
||||
return DMax::evaluate($database, self::VALUE_COLUMN_NAME, $conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @return null|float|string
|
||||
*/
|
||||
public static function MINIFS(...$args)
|
||||
{
|
||||
if (empty($args)) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$conditions = self::buildConditionSet(...$args);
|
||||
$database = self::buildDatabase(...$args);
|
||||
|
||||
return DMin::evaluate($database, self::VALUE_COLUMN_NAME, $conditions);
|
||||
}
|
||||
|
||||
private static function buildConditionSet(...$args): array
|
||||
{
|
||||
array_shift($args);
|
||||
|
||||
$conditions = [];
|
||||
$pairCount = 1;
|
||||
while (count($args) > 0) {
|
||||
$conditions[] = array_merge([sprintf(self::CONDITIONAL_COLUMN_NAME, $pairCount)], [array_pop($args)]);
|
||||
array_pop($args);
|
||||
++$pairCount;
|
||||
}
|
||||
|
||||
if (count($conditions) === 1) {
|
||||
return array_map(
|
||||
function ($value) {
|
||||
return [$value];
|
||||
},
|
||||
$conditions[0]
|
||||
);
|
||||
}
|
||||
|
||||
return array_map(null, ...$conditions);
|
||||
}
|
||||
|
||||
private static function buildDatabase(...$args): array
|
||||
{
|
||||
$database = [];
|
||||
$database[] = array_merge(
|
||||
[self::VALUE_COLUMN_NAME],
|
||||
Functions::flattenArray(array_shift($args))
|
||||
);
|
||||
|
||||
$pairCount = 1;
|
||||
while (count($args) > 0) {
|
||||
array_pop($args);
|
||||
$database[] = array_merge(
|
||||
[sprintf(self::CONDITIONAL_COLUMN_NAME, $pairCount)],
|
||||
Functions::flattenArray(array_pop($args))
|
||||
);
|
||||
++$pairCount;
|
||||
}
|
||||
|
||||
return array_map(null, ...$database);
|
||||
}
|
||||
}
|
||||
|
|
@ -824,7 +824,7 @@ class Worksheet implements IComparable
|
|||
/**
|
||||
* Set title.
|
||||
*
|
||||
* @param string $pValue String containing the dimension of this worksheet
|
||||
* @param string $title String containing the dimension of this worksheet
|
||||
* @param bool $updateFormulaCellReferences Flag indicating whether cell references in formulae should
|
||||
* be updated to reflect the new sheet name.
|
||||
* This should be left as the default true, unless you are
|
||||
|
|
@ -835,10 +835,10 @@ class Worksheet implements IComparable
|
|||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setTitle($pValue, $updateFormulaCellReferences = true, $validate = true)
|
||||
public function setTitle($title, $updateFormulaCellReferences = true, $validate = true)
|
||||
{
|
||||
// Is this a 'rename' or not?
|
||||
if ($this->getTitle() == $pValue) {
|
||||
if ($this->getTitle() == $title) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
@ -847,37 +847,37 @@ class Worksheet implements IComparable
|
|||
|
||||
if ($validate) {
|
||||
// Syntax check
|
||||
self::checkSheetTitle($pValue);
|
||||
self::checkSheetTitle($title);
|
||||
|
||||
if ($this->parent) {
|
||||
// Is there already such sheet name?
|
||||
if ($this->parent->sheetNameExists($pValue)) {
|
||||
if ($this->parent->sheetNameExists($title)) {
|
||||
// Use name, but append with lowest possible integer
|
||||
|
||||
if (Shared\StringHelper::countCharacters($pValue) > 29) {
|
||||
$pValue = Shared\StringHelper::substring($pValue, 0, 29);
|
||||
if (Shared\StringHelper::countCharacters($title) > 29) {
|
||||
$title = Shared\StringHelper::substring($title, 0, 29);
|
||||
}
|
||||
$i = 1;
|
||||
while ($this->parent->sheetNameExists($pValue . ' ' . $i)) {
|
||||
while ($this->parent->sheetNameExists($title . ' ' . $i)) {
|
||||
++$i;
|
||||
if ($i == 10) {
|
||||
if (Shared\StringHelper::countCharacters($pValue) > 28) {
|
||||
$pValue = Shared\StringHelper::substring($pValue, 0, 28);
|
||||
if (Shared\StringHelper::countCharacters($title) > 28) {
|
||||
$title = Shared\StringHelper::substring($title, 0, 28);
|
||||
}
|
||||
} elseif ($i == 100) {
|
||||
if (Shared\StringHelper::countCharacters($pValue) > 27) {
|
||||
$pValue = Shared\StringHelper::substring($pValue, 0, 27);
|
||||
if (Shared\StringHelper::countCharacters($title) > 27) {
|
||||
$title = Shared\StringHelper::substring($title, 0, 27);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pValue .= " $i";
|
||||
$title .= " $i";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set title
|
||||
$this->title = $pValue;
|
||||
$this->title = $title;
|
||||
$this->dirty = true;
|
||||
|
||||
if ($this->parent && $this->parent->getCalculationEngine()) {
|
||||
|
|
|
|||
|
|
@ -80,10 +80,6 @@ class DCountATest extends TestCase
|
|||
['Science', 'Male'],
|
||||
],
|
||||
],
|
||||
/*
|
||||
* Null value in datacolumn behaviour for DCOUNTA... will include not include a null value in the count
|
||||
* if it is an actual cell value; but it will be included if it is a literal... this test case is
|
||||
* currently passing literals
|
||||
[
|
||||
1,
|
||||
$this->database2(),
|
||||
|
|
@ -93,7 +89,6 @@ class DCountATest extends TestCase
|
|||
['Math', 'Female'],
|
||||
],
|
||||
],
|
||||
*/
|
||||
[
|
||||
3,
|
||||
$this->database2(),
|
||||
|
|
|
|||
|
|
@ -103,6 +103,15 @@ class DSumTest extends TestCase
|
|||
['3', 'C*'],
|
||||
],
|
||||
],
|
||||
[
|
||||
705000,
|
||||
$this->database2(),
|
||||
'Sales',
|
||||
[
|
||||
['Quarter', 'Sales Rep.'],
|
||||
['3', '<>C*'],
|
||||
],
|
||||
],
|
||||
[
|
||||
null,
|
||||
$this->database1(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Statistical;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Statistical;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AverageIfsTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider providerAVERAGEIFS
|
||||
*
|
||||
* @param mixed $expectedResult
|
||||
*/
|
||||
public function testAVERAGEIFS($expectedResult, ...$args): void
|
||||
{
|
||||
$result = Statistical\Conditional::AVERAGEIFS(...$args);
|
||||
self::assertEqualsWithDelta($expectedResult, $result, 1E-12);
|
||||
}
|
||||
|
||||
public function providerAVERAGEIFS()
|
||||
{
|
||||
return require 'tests/data/Calculation/Statistical/AVERAGEIFS.php';
|
||||
}
|
||||
}
|
||||
|
|
@ -57,6 +57,9 @@ class SampleTest extends TestCase
|
|||
$result = [];
|
||||
foreach ($helper->getSamples() as $samples) {
|
||||
foreach ($samples as $sample) {
|
||||
// if (array_pop(explode('/', $sample)) !== 'DGET.php') {
|
||||
// continue;
|
||||
// }
|
||||
if (!in_array($sample, $skipped)) {
|
||||
$file = 'samples/' . $sample;
|
||||
$result[] = [$file];
|
||||
|
|
|
|||
|
|
@ -46,8 +46,39 @@ return [
|
|||
'<2013',
|
||||
],
|
||||
[
|
||||
14000,
|
||||
[7000, 14000, 'Hello World', 21000, 28000],
|
||||
200,
|
||||
[7000, 14000, 21000, 28000],
|
||||
'<23000',
|
||||
[100, 200, 300, 800],
|
||||
],
|
||||
[
|
||||
(2 + 4 + 8) / 3,
|
||||
[true, true, false, true, false],
|
||||
true,
|
||||
[2, 4, 6, 8, 10],
|
||||
],
|
||||
[
|
||||
(6 + 10) / 2,
|
||||
[true, true, false, true, false],
|
||||
'<>true',
|
||||
[2, 4, 6, 8, 10],
|
||||
],
|
||||
[
|
||||
(1 + 2 + 5 + 6) / 4,
|
||||
['North', 'South', 'East', 'West', 'North', 'South', 'East', 'West'],
|
||||
'???th',
|
||||
[1, 2, 3, 4, 5, 6, 7, 8],
|
||||
],
|
||||
[
|
||||
16733.5,
|
||||
['East', 'West', 'North', 'South (New Office)', 'Midwest'],
|
||||
'=*West',
|
||||
[45678, 23789, -4789, 0, 9678],
|
||||
],
|
||||
[
|
||||
18589,
|
||||
['East', 'West', 'North', 'South (New Office)', 'Midwest'],
|
||||
'<>*(New Office)',
|
||||
[45678, 23789, -4789, 0, 9678],
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
[
|
||||
80.5,
|
||||
[75, 94, 86, 'incomplete'],
|
||||
[75, 94, 86, 'incomplete'],
|
||||
'>70',
|
||||
[75, 94, 86, 'incomplete'],
|
||||
'<90',
|
||||
],
|
||||
[
|
||||
'#DIV/0!',
|
||||
[85, 80, 93, 75],
|
||||
[85, 80, 93, 75],
|
||||
'>95',
|
||||
],
|
||||
[
|
||||
87.5,
|
||||
[87, 88, 'incomplete', 75],
|
||||
[87, 88, 'incomplete', 75],
|
||||
'<>incomplete',
|
||||
[87, 88, 'incomplete', 75],
|
||||
'>80',
|
||||
],
|
||||
[
|
||||
174000,
|
||||
[223000, 125000, 456000, 322000, 340000, 198000, 310000, 250000, 460000, 261000, 389000, 305000],
|
||||
[1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4],
|
||||
1,
|
||||
['North', 'North', 'South', 'North', 'North', 'South', 'North', 'North', 'South', 'North', 'North', 'South'],
|
||||
'North',
|
||||
],
|
||||
[
|
||||
285500,
|
||||
[223000, 125000, 456000, 322000, 340000, 198000, 310000, 250000, 460000, 261000, 389000, 305000],
|
||||
[1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4],
|
||||
'>2',
|
||||
['Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol'],
|
||||
'Jeff',
|
||||
],
|
||||
];
|
||||
|
|
@ -23,12 +23,55 @@ return [
|
|||
],
|
||||
[
|
||||
2,
|
||||
[6, 3, 4, 'X', ''],
|
||||
[6, 3, 4, 'X', '', null],
|
||||
'<=4',
|
||||
],
|
||||
[
|
||||
2,
|
||||
[6, 3, 4, 'X', ''],
|
||||
[6, 3, 4, 31, 'X', '', null],
|
||||
'<="4"',
|
||||
],
|
||||
[
|
||||
2,
|
||||
[0, 1, 1, 2, 3, 5, 8, 0, 13, 21],
|
||||
0,
|
||||
],
|
||||
[
|
||||
3,
|
||||
[true, false, false, true, false, true, false, false],
|
||||
true,
|
||||
],
|
||||
[
|
||||
5,
|
||||
[true, false, false, true, false, true, false, false],
|
||||
'<>true',
|
||||
],
|
||||
[
|
||||
4,
|
||||
['apples', 'oranges', 'peaches', 'apples'],
|
||||
'*',
|
||||
],
|
||||
[
|
||||
3,
|
||||
['apples', 'oranges', 'peaches', 'apples'],
|
||||
'*p*s*',
|
||||
],
|
||||
[
|
||||
4,
|
||||
[
|
||||
['apples', 'oranges', 'peaches', 'apples'],
|
||||
['bananas', 'mangoes', 'grapes', 'cherries'],
|
||||
],
|
||||
'*p*e*',
|
||||
],
|
||||
[
|
||||
2,
|
||||
['apples', 'oranges', 'peaches', 'apples'],
|
||||
'?????es',
|
||||
],
|
||||
[
|
||||
2,
|
||||
['great * ratings', 'bad * ratings', 'films * wars', 'films * trek', 'music * radio'],
|
||||
'*~* ra*s',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -16,9 +16,28 @@ return [
|
|||
['C', 'B', 'A', 'B', 'B'],
|
||||
'=B',
|
||||
],
|
||||
// [
|
||||
// 2,
|
||||
// [1, 2, 3, 'B', null, '', false],
|
||||
// '<=2',
|
||||
// ],
|
||||
// [
|
||||
// 2,
|
||||
// [1, 2, 3, 'B', null, '', false],
|
||||
// '<=B',
|
||||
// ],
|
||||
[
|
||||
4,
|
||||
['Female', 'Female', 'Female', 'Male', 'Male', 'Male', 'Female', 'Female', 'Female', 'Male', 'Male', 'Male'],
|
||||
'Female',
|
||||
[0.63, 0.78, 0.39, 0.55, 0.71, 0.51, 0.78, 0.81, 0.49, 0.35, 0.69, 0.65],
|
||||
'>60%',
|
||||
],
|
||||
[
|
||||
2,
|
||||
[1, 2, 3, 'B', '', false],
|
||||
'<=2',
|
||||
['Maths', 'English', 'Science', 'Maths', 'English', 'Science', 'Maths', 'English', 'Science', 'Maths', 'English', 'Science'],
|
||||
'Science',
|
||||
[0.63, 0.78, 0.39, 0.55, 0.71, 0.51, 0.78, 0.81, 0.49, 0.35, 0.69, 0.65],
|
||||
'<50%',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -21,6 +21,20 @@ return [
|
|||
],
|
||||
'=H',
|
||||
],
|
||||
[
|
||||
2,
|
||||
[
|
||||
[1],
|
||||
[2],
|
||||
[3],
|
||||
],
|
||||
[
|
||||
['Y'],
|
||||
['Y'],
|
||||
['N'],
|
||||
],
|
||||
'=Y',
|
||||
],
|
||||
[
|
||||
2,
|
||||
[
|
||||
|
|
@ -41,4 +55,18 @@ return [
|
|||
],
|
||||
'=B',
|
||||
],
|
||||
[
|
||||
456000,
|
||||
[223000, 125000, 456000, 322000, 340000, 198000, 310000, 250000, 460000, 261000, 389000, 305000],
|
||||
[1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4],
|
||||
1,
|
||||
],
|
||||
[
|
||||
310000,
|
||||
[223000, 125000, 456000, 322000, 340000, 198000, 310000, 250000, 460000, 261000, 389000, 305000],
|
||||
[1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4],
|
||||
'>2',
|
||||
['Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol'],
|
||||
'Jeff',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -21,6 +21,20 @@ return [
|
|||
],
|
||||
'=H',
|
||||
],
|
||||
[
|
||||
1,
|
||||
[
|
||||
[1],
|
||||
[2],
|
||||
[3],
|
||||
],
|
||||
[
|
||||
['Y'],
|
||||
['Y'],
|
||||
['N'],
|
||||
],
|
||||
'=Y',
|
||||
],
|
||||
[
|
||||
2,
|
||||
[
|
||||
|
|
@ -41,4 +55,18 @@ return [
|
|||
],
|
||||
'=B',
|
||||
],
|
||||
[
|
||||
125000,
|
||||
[223000, 125000, 456000, 322000, 340000, 198000, 310000, 250000, 460000, 261000, 389000, 305000],
|
||||
[1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4],
|
||||
1,
|
||||
],
|
||||
[
|
||||
261000,
|
||||
[223000, 125000, 456000, 322000, 340000, 198000, 310000, 250000, 460000, 261000, 389000, 305000],
|
||||
[1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4],
|
||||
'>2',
|
||||
['Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol'],
|
||||
'Jeff',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
Loading…
Reference in New Issue