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:
Mark Baker 2021-02-27 18:26:12 +01:00 committed by GitHub
parent 8dcdf58131
commit 08673b5820
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 570 additions and 254 deletions

View File

@ -9,8 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Added ### 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 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) - 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) - Alignment for ODS Writer [#1796](https://github.com/PHPOffice/PhpSpreadsheet/issues/1796)
- Basic implementation of the PERMUTATIONA() Statistical Function - Basic implementation of the PERMUTATIONA() Statistical Function

View File

@ -343,12 +343,12 @@ class Calculation
], ],
'AVERAGEIF' => [ 'AVERAGEIF' => [
'category' => Category::CATEGORY_STATISTICAL, 'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'AVERAGEIF'], 'functionCall' => [Statistical\Conditional::class, 'AVERAGEIF'],
'argumentCount' => '2,3', 'argumentCount' => '2,3',
], ],
'AVERAGEIFS' => [ 'AVERAGEIFS' => [
'category' => Category::CATEGORY_STATISTICAL, 'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Functions::class, 'DUMMY'], 'functionCall' => [Statistical\Conditional::class, 'AVERAGEIFS'],
'argumentCount' => '3+', 'argumentCount' => '3+',
], ],
'BAHTTEXT' => [ 'BAHTTEXT' => [
@ -639,12 +639,12 @@ class Calculation
], ],
'COUNTIF' => [ 'COUNTIF' => [
'category' => Category::CATEGORY_STATISTICAL, 'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'COUNTIF'], 'functionCall' => [Statistical\Conditional::class, 'COUNTIF'],
'argumentCount' => '2', 'argumentCount' => '2',
], ],
'COUNTIFS' => [ 'COUNTIFS' => [
'category' => Category::CATEGORY_STATISTICAL, 'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'COUNTIFS'], 'functionCall' => [Statistical\Conditional::class, 'COUNTIFS'],
'argumentCount' => '2+', 'argumentCount' => '2+',
], ],
'COUPDAYBS' => [ 'COUPDAYBS' => [
@ -1630,7 +1630,7 @@ class Calculation
], ],
'MAXIFS' => [ 'MAXIFS' => [
'category' => Category::CATEGORY_STATISTICAL, 'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'MAXIFS'], 'functionCall' => [Statistical\Conditional::class, 'MAXIFS'],
'argumentCount' => '3+', 'argumentCount' => '3+',
], ],
'MDETERM' => [ 'MDETERM' => [
@ -1675,7 +1675,7 @@ class Calculation
], ],
'MINIFS' => [ 'MINIFS' => [
'category' => Category::CATEGORY_STATISTICAL, 'category' => Category::CATEGORY_STATISTICAL,
'functionCall' => [Statistical::class, 'MINIFS'], 'functionCall' => [Statistical\Conditional::class, 'MINIFS'],
'argumentCount' => '3+', 'argumentCount' => '3+',
], ],
'MINUTE' => [ 'MINUTE' => [

View File

@ -34,7 +34,7 @@ class Database
* the column label in which you specify a condition for the * the column label in which you specify a condition for the
* column. * column.
* *
* @return float|string * @return null|float|string
*/ */
public static function DAVERAGE($database, $field, $criteria) public static function DAVERAGE($database, $field, $criteria)
{ {

View File

@ -29,7 +29,7 @@ class DAverage extends DatabaseAbstract
* the column label in which you specify a condition for the * the column label in which you specify a condition for the
* column. * column.
* *
* @return float|string * @return null|float|string
*/ */
public static function evaluate($database, $field, $criteria) public static function evaluate($database, $field, $criteria)
{ {

View File

@ -36,7 +36,7 @@ class DCountA extends DatabaseAbstract
$field = self::fieldExtract($database, $field); $field = self::fieldExtract($database, $field);
return Statistical::COUNTA( return Statistical::COUNTA(
self::getFilteredColumn($database, $field, $criteria) self::getFilteredColumn($database, $field ?? 0, $criteria)
); );
} }
} }

View File

@ -44,6 +44,8 @@ class DGet extends DatabaseAbstract
return Functions::NAN(); return Functions::NAN();
} }
return $columnData[0]; $row = array_pop($columnData);
return array_pop($row);
} }
} }

View File

@ -25,19 +25,20 @@ abstract class DatabaseAbstract
* represents the position of the column within the list: 1 for * represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on. * 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)); $field = strtoupper(Functions::flattenSingleValue($field));
$fieldNames = array_map('strtoupper', array_shift($database)); if ($field === '') {
return null;
if (is_numeric($field)) {
$keys = array_keys($fieldNames);
return $keys[$field - 1];
} }
$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); 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 // reduce the database to a set of rows that match all the criteria
$database = self::filter($database, $criteria); $database = self::filter($database, $criteria);
$defaultReturnColumnValue = ($field === null) ? 1 : null;
// extract an array of values for the requested column // extract an array of values for the requested column
$columnData = []; $columnData = [];
foreach ($database as $row) { foreach ($database as $rowKey => $row) {
$columnData[] = ($field !== null) ? $row[$field] : true; $keys = array_keys($row);
$key = $keys[$field] ?? null;
$columnKey = $key ?? 'A';
$columnData[$rowKey][$columnKey] = $row[$key] ?? $defaultReturnColumnValue;
} }
return $columnData; return $columnData;

View File

@ -5,9 +5,9 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Internal;
class WildcardMatch class WildcardMatch
{ {
private const SEARCH_SET = [ private const SEARCH_SET = [
'/([^~])(\*)/ui', '/(?<!~)\*/ui',
'/~\*/ui', '/~\*/ui',
'/([^~])(\?)/ui', '/(?<!~)\?/ui',
'/~\?/ui', '/~\?/ui',
]; ];
@ -20,6 +20,11 @@ class WildcardMatch
public static function wildcard(string $wildcard): string 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); return preg_replace(self::SEARCH_SET, self::REPLACEMENT_SET, $wildcard);
} }
@ -29,6 +34,6 @@ class WildcardMatch
return true; return true;
} }
return (bool) preg_match("/{$wildcard}/ui", $value); return (bool) preg_match("/^{$wildcard}\$/mui", $value);
} }
} }

View File

@ -1281,7 +1281,7 @@ class MathTrig
/** /**
* SUMIF. * 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: * Excel Function:
* SUMIF(value1[,value2[, ...]],condition) * SUMIF(value1[,value2[, ...]],condition)
@ -1327,7 +1327,7 @@ class MathTrig
/** /**
* SUMIFS. * 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: * Excel Function:
* SUMIFS(value1[,value2[, ...]],condition) * SUMIFS(value1[,value2[, ...]],condition)

View File

@ -2,6 +2,7 @@
namespace PhpOffice\PhpSpreadsheet\Calculation; namespace PhpOffice\PhpSpreadsheet\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Conditional;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Permutations; use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Permutations;
use PhpOffice\PhpSpreadsheet\Shared\Trend\Trend; use PhpOffice\PhpSpreadsheet\Shared\Trend\Trend;
@ -701,47 +702,20 @@ class Statistical
* Excel Function: * Excel Function:
* AVERAGEIF(value1[,value2[, ...]],condition) * AVERAGEIF(value1[,value2[, ...]],condition)
* *
* @param mixed $aArgs Data values * @Deprecated 1.17.0
* @param string $condition the criteria that defines which cells will be checked
* @param mixed[] $averageArgs Data values
* *
* @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; return Statistical\Conditional::AVERAGEIF($range, $condition, $averageRange);
$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();
} }
/** /**
@ -1137,38 +1111,21 @@ class Statistical
* Counts the number of cells that contain numbers within the list of arguments * Counts the number of cells that contain numbers within the list of arguments
* *
* Excel Function: * 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 * @param string $condition the criteria that defines which cells will be counted
* *
* @return int * @return int
*/ */
public static function COUNTIF($aArgs, $condition) public static function COUNTIF($range, $condition)
{ {
$returnValue = 0; return Statistical\Conditional::COUNTIF($range, $condition);
$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;
} }
/** /**
@ -1179,66 +1136,18 @@ class Statistical
* Excel Function: * Excel Function:
* COUNTIFS(criteria_range1, criteria1, [criteria_range2, criteria2]) * 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 * @return int
*/ */
public static function COUNTIFS(...$args) public static function COUNTIFS(...$args)
{ {
$arrayList = $args; return Statistical\Conditional::COUNTIFS(...$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;
} }
/** /**
@ -2348,53 +2257,18 @@ class Statistical
* Excel Function: * Excel Function:
* MAXIFS(max_range, criteria_range1, criteria1, [criteria_range2, criteria2], ...) * 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 * @param mixed $args Data range and criterias
* *
* @return float * @return float
*/ */
public static function MAXIFS(...$args) public static function MAXIFS(...$args)
{ {
$arrayList = $args; return Conditional::MAXIFS(...$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;
} }
/** /**
@ -2520,53 +2394,18 @@ class Statistical
* Excel Function: * Excel Function:
* MINIFS(min_range, criteria_range1, criteria1, [criteria_range2, criteria2], ...) * 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 * @param mixed $args Data range and criterias
* *
* @return float * @return float
*/ */
public static function MINIFS(...$args) public static function MINIFS(...$args)
{ {
$arrayList = $args; return Conditional::MINIFS(...$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;
} }
// //

View File

@ -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);
}
}

View File

@ -824,7 +824,7 @@ class Worksheet implements IComparable
/** /**
* Set title. * 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 * @param bool $updateFormulaCellReferences Flag indicating whether cell references in formulae should
* be updated to reflect the new sheet name. * be updated to reflect the new sheet name.
* This should be left as the default true, unless you are * This should be left as the default true, unless you are
@ -835,10 +835,10 @@ class Worksheet implements IComparable
* *
* @return $this * @return $this
*/ */
public function setTitle($pValue, $updateFormulaCellReferences = true, $validate = true) public function setTitle($title, $updateFormulaCellReferences = true, $validate = true)
{ {
// Is this a 'rename' or not? // Is this a 'rename' or not?
if ($this->getTitle() == $pValue) { if ($this->getTitle() == $title) {
return $this; return $this;
} }
@ -847,37 +847,37 @@ class Worksheet implements IComparable
if ($validate) { if ($validate) {
// Syntax check // Syntax check
self::checkSheetTitle($pValue); self::checkSheetTitle($title);
if ($this->parent) { if ($this->parent) {
// Is there already such sheet name? // Is there already such sheet name?
if ($this->parent->sheetNameExists($pValue)) { if ($this->parent->sheetNameExists($title)) {
// Use name, but append with lowest possible integer // Use name, but append with lowest possible integer
if (Shared\StringHelper::countCharacters($pValue) > 29) { if (Shared\StringHelper::countCharacters($title) > 29) {
$pValue = Shared\StringHelper::substring($pValue, 0, 29); $title = Shared\StringHelper::substring($title, 0, 29);
} }
$i = 1; $i = 1;
while ($this->parent->sheetNameExists($pValue . ' ' . $i)) { while ($this->parent->sheetNameExists($title . ' ' . $i)) {
++$i; ++$i;
if ($i == 10) { if ($i == 10) {
if (Shared\StringHelper::countCharacters($pValue) > 28) { if (Shared\StringHelper::countCharacters($title) > 28) {
$pValue = Shared\StringHelper::substring($pValue, 0, 28); $title = Shared\StringHelper::substring($title, 0, 28);
} }
} elseif ($i == 100) { } elseif ($i == 100) {
if (Shared\StringHelper::countCharacters($pValue) > 27) { if (Shared\StringHelper::countCharacters($title) > 27) {
$pValue = Shared\StringHelper::substring($pValue, 0, 27); $title = Shared\StringHelper::substring($title, 0, 27);
} }
} }
} }
$pValue .= " $i"; $title .= " $i";
} }
} }
} }
// Set title // Set title
$this->title = $pValue; $this->title = $title;
$this->dirty = true; $this->dirty = true;
if ($this->parent && $this->parent->getCalculationEngine()) { if ($this->parent && $this->parent->getCalculationEngine()) {

View File

@ -80,10 +80,6 @@ class DCountATest extends TestCase
['Science', 'Male'], ['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, 1,
$this->database2(), $this->database2(),
@ -93,7 +89,6 @@ class DCountATest extends TestCase
['Math', 'Female'], ['Math', 'Female'],
], ],
], ],
*/
[ [
3, 3,
$this->database2(), $this->database2(),

View File

@ -103,6 +103,15 @@ class DSumTest extends TestCase
['3', 'C*'], ['3', 'C*'],
], ],
], ],
[
705000,
$this->database2(),
'Sales',
[
['Quarter', 'Sales Rep.'],
['3', '<>C*'],
],
],
[ [
null, null,
$this->database1(), $this->database1(),

View File

@ -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';
}
}

View File

@ -57,6 +57,9 @@ class SampleTest extends TestCase
$result = []; $result = [];
foreach ($helper->getSamples() as $samples) { foreach ($helper->getSamples() as $samples) {
foreach ($samples as $sample) { foreach ($samples as $sample) {
// if (array_pop(explode('/', $sample)) !== 'DGET.php') {
// continue;
// }
if (!in_array($sample, $skipped)) { if (!in_array($sample, $skipped)) {
$file = 'samples/' . $sample; $file = 'samples/' . $sample;
$result[] = [$file]; $result[] = [$file];

View File

@ -46,8 +46,39 @@ return [
'<2013', '<2013',
], ],
[ [
14000, 200,
[7000, 14000, 'Hello World', 21000, 28000], [7000, 14000, 21000, 28000],
'<23000', '<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],
], ],
]; ];

View File

@ -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',
],
];

View File

@ -23,12 +23,55 @@ return [
], ],
[ [
2, 2,
[6, 3, 4, 'X', ''], [6, 3, 4, 'X', '', null],
'<=4', '<=4',
], ],
[ [
2, 2,
[6, 3, 4, 'X', ''], [6, 3, 4, 31, 'X', '', null],
'<="4"', '<="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',
],
]; ];

View File

@ -16,9 +16,28 @@ return [
['C', 'B', 'A', 'B', 'B'], ['C', 'B', 'A', 'B', '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, 2,
[1, 2, 3, 'B', '', false], ['Maths', 'English', 'Science', 'Maths', 'English', 'Science', 'Maths', 'English', 'Science', 'Maths', 'English', 'Science'],
'<=2', '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%',
], ],
]; ];

View File

@ -21,6 +21,20 @@ return [
], ],
'=H', '=H',
], ],
[
2,
[
[1],
[2],
[3],
],
[
['Y'],
['Y'],
['N'],
],
'=Y',
],
[ [
2, 2,
[ [
@ -41,4 +55,18 @@ return [
], ],
'=B', '=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',
],
]; ];

View File

@ -21,6 +21,20 @@ return [
], ],
'=H', '=H',
], ],
[
1,
[
[1],
[2],
[3],
],
[
['Y'],
['Y'],
['N'],
],
'=Y',
],
[ [
2, 2,
[ [
@ -41,4 +55,18 @@ return [
], ],
'=B', '=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',
],
]; ];