Refactor the Excel Database functions; and rewrite the query building (#1871)

* Refactor the Excel Database functions; and rewrite the query building to fix a bug with complex multi-criteria queries that involve both AND and OR conditions
* Fix handling for empty cells and NULL values in searches
* Expand unit tests; and add TODOs for dates, percentages, and wildcard text comparisons
This commit is contained in:
Mark Baker 2021-02-22 12:46:57 +01:00 committed by GitHub
parent 1318b90330
commit 3764f30354
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 2034 additions and 256 deletions

View File

@ -769,7 +769,7 @@ class Calculation
], ],
'DAVERAGE' => [ 'DAVERAGE' => [
'category' => Category::CATEGORY_DATABASE, 'category' => Category::CATEGORY_DATABASE,
'functionCall' => [Database::class, 'DAVERAGE'], 'functionCall' => [Database\DAverage::class, 'evaluate'],
'argumentCount' => '3', 'argumentCount' => '3',
], ],
'DAY' => [ 'DAY' => [
@ -799,12 +799,12 @@ class Calculation
], ],
'DCOUNT' => [ 'DCOUNT' => [
'category' => Category::CATEGORY_DATABASE, 'category' => Category::CATEGORY_DATABASE,
'functionCall' => [Database::class, 'DCOUNT'], 'functionCall' => [Database\DCount::class, 'evaluate'],
'argumentCount' => '3', 'argumentCount' => '3',
], ],
'DCOUNTA' => [ 'DCOUNTA' => [
'category' => Category::CATEGORY_DATABASE, 'category' => Category::CATEGORY_DATABASE,
'functionCall' => [Database::class, 'DCOUNTA'], 'functionCall' => [Database\DCountA::class, 'evaluate'],
'argumentCount' => '3', 'argumentCount' => '3',
], ],
'DDB' => [ 'DDB' => [
@ -849,7 +849,7 @@ class Calculation
], ],
'DGET' => [ 'DGET' => [
'category' => Category::CATEGORY_DATABASE, 'category' => Category::CATEGORY_DATABASE,
'functionCall' => [Database::class, 'DGET'], 'functionCall' => [Database\DGet::class, 'evaluate'],
'argumentCount' => '3', 'argumentCount' => '3',
], ],
'DISC' => [ 'DISC' => [
@ -859,12 +859,12 @@ class Calculation
], ],
'DMAX' => [ 'DMAX' => [
'category' => Category::CATEGORY_DATABASE, 'category' => Category::CATEGORY_DATABASE,
'functionCall' => [Database::class, 'DMAX'], 'functionCall' => [Database\DMax::class, 'evaluate'],
'argumentCount' => '3', 'argumentCount' => '3',
], ],
'DMIN' => [ 'DMIN' => [
'category' => Category::CATEGORY_DATABASE, 'category' => Category::CATEGORY_DATABASE,
'functionCall' => [Database::class, 'DMIN'], 'functionCall' => [Database\DMin::class, 'evaluate'],
'argumentCount' => '3', 'argumentCount' => '3',
], ],
'DOLLAR' => [ 'DOLLAR' => [
@ -884,22 +884,22 @@ class Calculation
], ],
'DPRODUCT' => [ 'DPRODUCT' => [
'category' => Category::CATEGORY_DATABASE, 'category' => Category::CATEGORY_DATABASE,
'functionCall' => [Database::class, 'DPRODUCT'], 'functionCall' => [Database\DProduct::class, 'evaluate'],
'argumentCount' => '3', 'argumentCount' => '3',
], ],
'DSTDEV' => [ 'DSTDEV' => [
'category' => Category::CATEGORY_DATABASE, 'category' => Category::CATEGORY_DATABASE,
'functionCall' => [Database::class, 'DSTDEV'], 'functionCall' => [Database\DStDev::class, 'evaluate'],
'argumentCount' => '3', 'argumentCount' => '3',
], ],
'DSTDEVP' => [ 'DSTDEVP' => [
'category' => Category::CATEGORY_DATABASE, 'category' => Category::CATEGORY_DATABASE,
'functionCall' => [Database::class, 'DSTDEVP'], 'functionCall' => [Database\DStDevP::class, 'evaluate'],
'argumentCount' => '3', 'argumentCount' => '3',
], ],
'DSUM' => [ 'DSUM' => [
'category' => Category::CATEGORY_DATABASE, 'category' => Category::CATEGORY_DATABASE,
'functionCall' => [Database::class, 'DSUM'], 'functionCall' => [Database\DSum::class, 'evaluate'],
'argumentCount' => '3', 'argumentCount' => '3',
], ],
'DURATION' => [ 'DURATION' => [
@ -909,12 +909,12 @@ class Calculation
], ],
'DVAR' => [ 'DVAR' => [
'category' => Category::CATEGORY_DATABASE, 'category' => Category::CATEGORY_DATABASE,
'functionCall' => [Database::class, 'DVAR'], 'functionCall' => [Database\DVar::class, 'evaluate'],
'argumentCount' => '3', 'argumentCount' => '3',
], ],
'DVARP' => [ 'DVARP' => [
'category' => Category::CATEGORY_DATABASE, 'category' => Category::CATEGORY_DATABASE,
'functionCall' => [Database::class, 'DVARP'], 'functionCall' => [Database\DVarP::class, 'evaluate'],
'argumentCount' => '3', 'argumentCount' => '3',
], ],
'EDATE' => [ 'EDATE' => [
@ -3437,6 +3437,7 @@ class Calculation
$this->debugLog->writeDebugLog('Formula for cell ', $wsCellReference, ' is ', $formula); $this->debugLog->writeDebugLog('Formula for cell ', $wsCellReference, ' is ', $formula);
// Parse the formula onto the token stack and calculate the value // Parse the formula onto the token stack and calculate the value
$this->cyclicReferenceStack->push($wsCellReference); $this->cyclicReferenceStack->push($wsCellReference);
$cellValue = $this->processTokenStack($this->internalParseFormula($formula, $pCell), $cellID, $pCell); $cellValue = $this->processTokenStack($this->internalParseFormula($formula, $pCell), $cellID, $pCell);
$this->cyclicReferenceStack->pop(); $this->cyclicReferenceStack->pop();

View File

@ -2,126 +2,11 @@
namespace PhpOffice\PhpSpreadsheet\Calculation; namespace PhpOffice\PhpSpreadsheet\Calculation;
/**
* @deprecated 1.17.0
*/
class Database class Database
{ {
/**
* fieldExtract.
*
* Extracts the column ID to use for the data field.
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param mixed $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
*
* @return null|string
*/
private static function fieldExtract($database, $field)
{
$field = strtoupper(Functions::flattenSingleValue($field));
$fieldNames = array_map('strtoupper', array_shift($database));
if (is_numeric($field)) {
$keys = array_keys($fieldNames);
return $keys[$field - 1];
}
$key = array_search($field, $fieldNames);
return $key ?: null;
}
/**
* filter.
*
* Parses the selection criteria, extracts the database rows that match those criteria, and
* returns that subset of rows.
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return array of mixed
*/
private static function filter($database, $criteria)
{
$fieldNames = array_shift($database);
$criteriaNames = array_shift($criteria);
// Convert the criteria into a set of AND/OR conditions with [:placeholders]
$testConditions = $testValues = [];
$testConditionsCount = 0;
foreach ($criteriaNames as $key => $criteriaName) {
$testCondition = [];
$testConditionCount = 0;
foreach ($criteria as $row => $criterion) {
if ($criterion[$key] > '') {
$testCondition[] = '[:' . $criteriaName . ']' . Functions::ifCondition($criterion[$key]);
++$testConditionCount;
}
}
if ($testConditionCount > 1) {
$testConditions[] = 'OR(' . implode(',', $testCondition) . ')';
++$testConditionsCount;
} elseif ($testConditionCount == 1) {
$testConditions[] = $testCondition[0];
++$testConditionsCount;
}
}
if ($testConditionsCount > 1) {
$testConditionSet = 'AND(' . implode(',', $testConditions) . ')';
} elseif ($testConditionsCount == 1) {
$testConditionSet = $testConditions[0];
}
// Loop through each row of the database
foreach ($database as $dataRow => $dataValues) {
// Substitute actual values from the database row for our [:placeholders]
$testConditionList = $testConditionSet;
foreach ($criteriaNames as $key => $criteriaName) {
$k = array_search($criteriaName, $fieldNames);
if (isset($dataValues[$k])) {
$dataValue = $dataValues[$k];
$dataValue = (is_string($dataValue)) ? Calculation::wrapResult(strtoupper($dataValue)) : $dataValue;
$testConditionList = str_replace('[:' . $criteriaName . ']', $dataValue, $testConditionList);
}
}
// evaluate the criteria against the row data
$result = Calculation::getInstance()->_calculateFormulaValue('=' . $testConditionList);
// If the row failed to meet the criteria, remove it from the database
if (!$result) {
unset($database[$dataRow]);
}
}
return $database;
}
private static function getFilteredColumn($database, $field, $criteria)
{
// reduce the database to a set of rows that match all the criteria
$database = self::filter($database, $criteria);
// extract an array of values for the requested column
$colData = [];
foreach ($database as $row) {
$colData[] = $row[$field];
}
return $colData;
}
/** /**
* DAVERAGE. * DAVERAGE.
* *
@ -130,6 +15,10 @@ class Database
* Excel Function: * Excel Function:
* DAVERAGE(database,field,criteria) * DAVERAGE(database,field,criteria)
* *
* @Deprecated 1.17.0
*
* @see Use the evaluate() method in the Database\DAverage class instead
*
* @param mixed[] $database The range of cells that makes up the list or database. * @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related * A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The * information are records, and columns of data are fields. The
@ -149,15 +38,7 @@ class Database
*/ */
public static function DAVERAGE($database, $field, $criteria) public static function DAVERAGE($database, $field, $criteria)
{ {
$field = self::fieldExtract($database, $field); return Database\DAverage::evaluate($database, $field, $criteria);
if ($field === null) {
return null;
}
// Return
return Statistical::AVERAGE(
self::getFilteredColumn($database, $field, $criteria)
);
} }
/** /**
@ -169,14 +50,15 @@ class Database
* Excel Function: * Excel Function:
* DCOUNT(database,[field],criteria) * DCOUNT(database,[field],criteria)
* *
* Excel Function: * @Deprecated 1.17.0
* DAVERAGE(database,field,criteria) *
* @see Use the evaluate() method in the Database\DCount class instead
* *
* @param mixed[] $database The range of cells that makes up the list or database. * @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related * A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The * information are records, and columns of data are fields. The
* first row of the list contains labels for each column. * first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the * @param null|int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as * column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that * "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for * represents the position of the column within the list: 1 for
@ -194,15 +76,7 @@ class Database
*/ */
public static function DCOUNT($database, $field, $criteria) public static function DCOUNT($database, $field, $criteria)
{ {
$field = self::fieldExtract($database, $field); return Database\DCount::evaluate($database, $field, $criteria);
if ($field === null) {
return null;
}
// Return
return Statistical::COUNT(
self::getFilteredColumn($database, $field, $criteria)
);
} }
/** /**
@ -213,11 +87,15 @@ class Database
* Excel Function: * Excel Function:
* DCOUNTA(database,[field],criteria) * DCOUNTA(database,[field],criteria)
* *
* @Deprecated 1.17.0
*
* @see Use the evaluate() method in the Database\DCountA class instead
*
* @param mixed[] $database The range of cells that makes up the list or database. * @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related * A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The * information are records, and columns of data are fields. The
* first row of the list contains labels for each column. * first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the * @param null|int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as * column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that * "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for * represents the position of the column within the list: 1 for
@ -229,29 +107,10 @@ class Database
* column. * column.
* *
* @return int * @return int
*
* @TODO The field argument is optional. If field is omitted, DCOUNTA counts all records in the
* database that match the criteria.
*/ */
public static function DCOUNTA($database, $field, $criteria) public static function DCOUNTA($database, $field, $criteria)
{ {
$field = self::fieldExtract($database, $field); return Database\DCountA::evaluate($database, $field, $criteria);
if ($field === null) {
return null;
}
// reduce the database to a set of rows that match all the criteria
$database = self::filter($database, $criteria);
// extract an array of values for the requested column
$colData = [];
foreach ($database as $row) {
$colData[] = $row[$field];
}
// Return
return Statistical::COUNTA(
self::getFilteredColumn($database, $field, $criteria)
);
} }
/** /**
@ -263,6 +122,10 @@ class Database
* Excel Function: * Excel Function:
* DGET(database,field,criteria) * DGET(database,field,criteria)
* *
* @Deprecated 1.17.0
*
* @see Use the evaluate() method in the Database\DGet class instead
*
* @param mixed[] $database The range of cells that makes up the list or database. * @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related * A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The * information are records, and columns of data are fields. The
@ -282,18 +145,7 @@ class Database
*/ */
public static function DGET($database, $field, $criteria) public static function DGET($database, $field, $criteria)
{ {
$field = self::fieldExtract($database, $field); return Database\DGet::evaluate($database, $field, $criteria);
if ($field === null) {
return null;
}
// Return
$colData = self::getFilteredColumn($database, $field, $criteria);
if (count($colData) > 1) {
return Functions::NAN();
}
return $colData[0];
} }
/** /**
@ -305,6 +157,10 @@ class Database
* Excel Function: * Excel Function:
* DMAX(database,field,criteria) * DMAX(database,field,criteria)
* *
* @Deprecated 1.17.0
*
* @see Use the evaluate() method in the Database\DMax class instead
*
* @param mixed[] $database The range of cells that makes up the list or database. * @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related * A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The * information are records, and columns of data are fields. The
@ -324,15 +180,7 @@ class Database
*/ */
public static function DMAX($database, $field, $criteria) public static function DMAX($database, $field, $criteria)
{ {
$field = self::fieldExtract($database, $field); return Database\DMax::evaluate($database, $field, $criteria);
if ($field === null) {
return null;
}
// Return
return Statistical::MAX(
self::getFilteredColumn($database, $field, $criteria)
);
} }
/** /**
@ -344,6 +192,10 @@ class Database
* Excel Function: * Excel Function:
* DMIN(database,field,criteria) * DMIN(database,field,criteria)
* *
* @Deprecated 1.17.0
*
* @see Use the evaluate() method in the Database\DMin class instead
*
* @param mixed[] $database The range of cells that makes up the list or database. * @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related * A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The * information are records, and columns of data are fields. The
@ -363,15 +215,7 @@ class Database
*/ */
public static function DMIN($database, $field, $criteria) public static function DMIN($database, $field, $criteria)
{ {
$field = self::fieldExtract($database, $field); return Database\DMin::evaluate($database, $field, $criteria);
if ($field === null) {
return null;
}
// Return
return Statistical::MIN(
self::getFilteredColumn($database, $field, $criteria)
);
} }
/** /**
@ -382,6 +226,10 @@ class Database
* Excel Function: * Excel Function:
* DPRODUCT(database,field,criteria) * DPRODUCT(database,field,criteria)
* *
* @Deprecated 1.17.0
*
* @see Use the evaluate() method in the Database\DProduct class instead
*
* @param mixed[] $database The range of cells that makes up the list or database. * @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related * A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The * information are records, and columns of data are fields. The
@ -401,15 +249,7 @@ class Database
*/ */
public static function DPRODUCT($database, $field, $criteria) public static function DPRODUCT($database, $field, $criteria)
{ {
$field = self::fieldExtract($database, $field); return Database\DProduct::evaluate($database, $field, $criteria);
if ($field === null) {
return null;
}
// Return
return MathTrig::PRODUCT(
self::getFilteredColumn($database, $field, $criteria)
);
} }
/** /**
@ -421,6 +261,10 @@ class Database
* Excel Function: * Excel Function:
* DSTDEV(database,field,criteria) * DSTDEV(database,field,criteria)
* *
* @Deprecated 1.17.0
*
* @see Use the evaluate() method in the Database\DStDev class instead
*
* @param mixed[] $database The range of cells that makes up the list or database. * @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related * A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The * information are records, and columns of data are fields. The
@ -440,15 +284,7 @@ class Database
*/ */
public static function DSTDEV($database, $field, $criteria) public static function DSTDEV($database, $field, $criteria)
{ {
$field = self::fieldExtract($database, $field); return Database\DStDev::evaluate($database, $field, $criteria);
if ($field === null) {
return null;
}
// Return
return Statistical::STDEV(
self::getFilteredColumn($database, $field, $criteria)
);
} }
/** /**
@ -460,6 +296,10 @@ class Database
* Excel Function: * Excel Function:
* DSTDEVP(database,field,criteria) * DSTDEVP(database,field,criteria)
* *
* @Deprecated 1.17.0
*
* @see Use the evaluate() method in the Database\DStDevP class instead
*
* @param mixed[] $database The range of cells that makes up the list or database. * @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related * A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The * information are records, and columns of data are fields. The
@ -479,15 +319,7 @@ class Database
*/ */
public static function DSTDEVP($database, $field, $criteria) public static function DSTDEVP($database, $field, $criteria)
{ {
$field = self::fieldExtract($database, $field); return Database\DStDevP::evaluate($database, $field, $criteria);
if ($field === null) {
return null;
}
// Return
return Statistical::STDEVP(
self::getFilteredColumn($database, $field, $criteria)
);
} }
/** /**
@ -498,6 +330,10 @@ class Database
* Excel Function: * Excel Function:
* DSUM(database,field,criteria) * DSUM(database,field,criteria)
* *
* @Deprecated 1.17.0
*
* @see Use the evaluate() method in the Database\DSum class instead
*
* @param mixed[] $database The range of cells that makes up the list or database. * @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related * A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The * information are records, and columns of data are fields. The
@ -517,15 +353,7 @@ class Database
*/ */
public static function DSUM($database, $field, $criteria) public static function DSUM($database, $field, $criteria)
{ {
$field = self::fieldExtract($database, $field); return Database\DSum::evaluate($database, $field, $criteria);
if ($field === null) {
return null;
}
// Return
return MathTrig::SUM(
self::getFilteredColumn($database, $field, $criteria)
);
} }
/** /**
@ -537,6 +365,10 @@ class Database
* Excel Function: * Excel Function:
* DVAR(database,field,criteria) * DVAR(database,field,criteria)
* *
* @Deprecated 1.17.0
*
* @see Use the evaluate() method in the Database\DVar class instead
*
* @param mixed[] $database The range of cells that makes up the list or database. * @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related * A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The * information are records, and columns of data are fields. The
@ -556,15 +388,7 @@ class Database
*/ */
public static function DVAR($database, $field, $criteria) public static function DVAR($database, $field, $criteria)
{ {
$field = self::fieldExtract($database, $field); return Database\DVar::evaluate($database, $field, $criteria);
if ($field === null) {
return null;
}
// Return
return Statistical::VARFunc(
self::getFilteredColumn($database, $field, $criteria)
);
} }
/** /**
@ -576,6 +400,10 @@ class Database
* Excel Function: * Excel Function:
* DVARP(database,field,criteria) * DVARP(database,field,criteria)
* *
* @Deprecated 1.17.0
*
* @see Use the evaluate() method in the Database\DVarP class instead
*
* @param mixed[] $database The range of cells that makes up the list or database. * @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related * A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The * information are records, and columns of data are fields. The
@ -595,14 +423,6 @@ class Database
*/ */
public static function DVARP($database, $field, $criteria) public static function DVARP($database, $field, $criteria)
{ {
$field = self::fieldExtract($database, $field); return Database\DVarP::evaluate($database, $field, $criteria);
if ($field === null) {
return null;
}
// Return
return Statistical::VARP(
self::getFilteredColumn($database, $field, $criteria)
);
} }
} }

View File

@ -0,0 +1,45 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical;
class DAverage extends DatabaseAbstract
{
/**
* DAVERAGE.
*
* Averages the values in a column of a list or database that match conditions you specify.
*
* Excel Function:
* DAVERAGE(database,field,criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float|string
*/
public static function evaluate($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
return Statistical::AVERAGE(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical;
class DCount extends DatabaseAbstract
{
/**
* DCOUNT.
*
* Counts the cells that contain numbers in a column of a list or database that match conditions
* that you specify.
*
* Excel Function:
* DCOUNT(database,[field],criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param null|int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return int
*/
public static function evaluate($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
return Statistical::COUNT(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical;
class DCountA extends DatabaseAbstract
{
/**
* DCOUNTA.
*
* Counts the nonblank cells in a column of a list or database that match conditions that you specify.
*
* Excel Function:
* DCOUNTA(database,[field],criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return int
*/
public static function evaluate($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
return Statistical::COUNTA(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
class DGet extends DatabaseAbstract
{
/**
* DGET.
*
* Extracts a single value from a column of a list or database that matches conditions that you
* specify.
*
* Excel Function:
* DGET(database,field,criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return mixed
*/
public static function evaluate($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
$columnData = self::getFilteredColumn($database, $field, $criteria);
if (count($columnData) > 1) {
return Functions::NAN();
}
return $columnData[0];
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical;
class DMax extends DatabaseAbstract
{
/**
* DMAX.
*
* Returns the largest number in a column of a list or database that matches conditions you that
* specify.
*
* Excel Function:
* DMAX(database,field,criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float
*/
public static function evaluate($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
return Statistical::MAX(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical;
class DMin extends DatabaseAbstract
{
/**
* DMIN.
*
* Returns the smallest number in a column of a list or database that matches conditions you that
* specify.
*
* Excel Function:
* DMIN(database,field,criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float
*/
public static function evaluate($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
return Statistical::MIN(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\MathTrig;
class DProduct extends DatabaseAbstract
{
/**
* DPRODUCT.
*
* Multiplies the values in a column of a list or database that match conditions that you specify.
*
* Excel Function:
* DPRODUCT(database,field,criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float
*/
public static function evaluate($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
return MathTrig::PRODUCT(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical;
class DStDev extends DatabaseAbstract
{
/**
* DSTDEV.
*
* Estimates the standard deviation of a population based on a sample by using the numbers in a
* column of a list or database that match conditions that you specify.
*
* Excel Function:
* DSTDEV(database,field,criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float|string
*/
public static function evaluate($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
return Statistical::STDEV(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical;
class DStDevP extends DatabaseAbstract
{
/**
* DSTDEVP.
*
* Calculates the standard deviation of a population based on the entire population by using the
* numbers in a column of a list or database that match conditions that you specify.
*
* Excel Function:
* DSTDEVP(database,field,criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float|string
*/
public static function evaluate($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
return Statistical::STDEVP(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\MathTrig;
class DSum extends DatabaseAbstract
{
/**
* DSUM.
*
* Adds the numbers in a column of a list or database that match conditions that you specify.
*
* Excel Function:
* DSUM(database,field,criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float
*/
public static function evaluate($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
return MathTrig::SUM(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical;
class DVar extends DatabaseAbstract
{
/**
* DVAR.
*
* Estimates the variance of a population based on a sample by using the numbers in a column
* of a list or database that match conditions that you specify.
*
* Excel Function:
* DVAR(database,field,criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float|string (string if result is an error)
*/
public static function evaluate($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
return Statistical::VARFunc(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Statistical;
class DVarP extends DatabaseAbstract
{
/**
* DVARP.
*
* Calculates the variance of a population based on the entire population by using the numbers
* in a column of a list or database that match conditions that you specify.
*
* Excel Function:
* DVARP(database,field,criteria)
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param int|string $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* represents the position of the column within the list: 1 for
* the first column, 2 for the second column, and so on.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return float|string (string if result is an error)
*/
public static function evaluate($database, $field, $criteria)
{
$field = self::fieldExtract($database, $field);
if ($field === null) {
return null;
}
return Statistical::VARP(
self::getFilteredColumn($database, $field, $criteria)
);
}
}

View File

@ -0,0 +1,144 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
abstract class DatabaseAbstract
{
abstract public static function evaluate($database, $field, $criteria);
/**
* fieldExtract.
*
* Extracts the column ID to use for the data field.
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param mixed $field Indicates which column is used in the function. Enter the
* column label enclosed between double quotation marks, such as
* "Age" or "Yield," or a number (without quotation marks) that
* 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
{
$field = strtoupper(Functions::flattenSingleValue($field));
$fieldNames = array_map('strtoupper', array_shift($database));
if (is_numeric($field)) {
$keys = array_keys($fieldNames);
return $keys[$field - 1];
}
$key = array_search($field, $fieldNames);
return $key ?: null;
}
/**
* filter.
*
* Parses the selection criteria, extracts the database rows that match those criteria, and
* returns that subset of rows.
*
* @param mixed[] $database The range of cells that makes up the list or database.
* A database is a list of related data in which rows of related
* information are records, and columns of data are fields. The
* first row of the list contains labels for each column.
* @param mixed[] $criteria The range of cells that contains the conditions you specify.
* You can use any range for the criteria argument, as long as it
* includes at least one column label and at least one cell below
* the column label in which you specify a condition for the
* column.
*
* @return array of mixed
*/
protected static function filter(array $database, array $criteria): array
{
$fieldNames = array_shift($database);
$criteriaNames = array_shift($criteria);
// Convert the criteria into a set of AND/OR conditions with [:placeholders]
$query = self::buildQuery($criteriaNames, $criteria);
// Loop through each row of the database
return self::executeQuery($database, $query, $criteriaNames, $fieldNames);
}
protected static function getFilteredColumn(array $database, $field, array $criteria): array
{
// reduce the database to a set of rows that match all the criteria
$database = self::filter($database, $criteria);
// extract an array of values for the requested column
$columnData = [];
foreach ($database as $row) {
$columnData[] = ($field !== null) ? $row[$field] : true;
}
return $columnData;
}
/**
* @TODO Support for Dates (including handling for >, <=, etc)
* @TODO Suport for formatted numerics (e.g. '>12.5%' => '>0.125')
* @TODO Suport for wildcard ? and * in strings (includng escaping)
*/
private static function buildQuery(array $criteriaNames, array $criteria): string
{
$baseQuery = [];
foreach ($criteria as $key => $criterion) {
foreach ($criterion as $field => $value) {
$criterionName = $criteriaNames[$field];
if ($value !== null && $value !== '') {
$condition = '[:' . $criterionName . ']' . Functions::ifCondition($value);
$baseQuery[$key][] = $condition;
}
}
}
$rowQuery = array_map(
function ($rowValue) {
return (count($rowValue) > 1) ? 'AND(' . implode(',', $rowValue) . ')' : $rowValue[0];
},
$baseQuery
);
return (count($rowQuery) > 1) ? 'OR(' . implode(',', $rowQuery) . ')' : $rowQuery[0];
}
/**
* @param $criteriaNames
* @param $fieldNames
*/
private static function executeQuery(array $database, string $query, $criteriaNames, $fieldNames): array
{
foreach ($database as $dataRow => $dataValues) {
// Substitute actual values from the database row for our [:placeholders]
$testConditionList = $query;
foreach ($criteriaNames as $key => $criteriaName) {
$key = array_search($criteriaName, $fieldNames, true);
if (isset($dataValues[$key])) {
$dataValue = $dataValues[$key];
$dataValue = (is_string($dataValue)) ? Calculation::wrapResult(strtoupper($dataValue)) : $dataValue;
} else {
$dataValue = 'NULL';
}
$testConditionList = str_replace('[:' . $criteriaName . ']', $dataValue, $testConditionList);
}
// evaluate the criteria against the row data
$result = Calculation::getInstance()->_calculateFormulaValue('=' . $testConditionList);
// If the row failed to meet the criteria, remove it from the database
if ($result !== true) {
unset($database[$dataRow]);
}
}
return $database;
}
}

View File

@ -0,0 +1,110 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PHPUnit\Framework\TestCase;
class DAverageTest extends TestCase
{
protected function setUp(): void
{
Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL);
}
/**
* @dataProvider providerDAverage
*
* @param mixed $expectedResult
* @param mixed $database
* @param mixed $field
* @param mixed $criteria
*/
public function testDAverage($expectedResult, $database, $field, $criteria): void
{
$result = Database::DAVERAGE($database, $field, $criteria);
self::assertSame($expectedResult, $result);
}
protected function database1()
{
return [
['Tree', 'Height', 'Age', 'Yield', 'Profit'],
['Apple', 18, 20, 14, 105],
['Pear', 12, 12, 10, 96],
['Cherry', 13, 14, 9, 105],
['Apple', 14, 15, 10, 75],
['Pear', 9, 8, 8, 76.8],
['Apple', 8, 9, 6, 45],
];
}
protected function database2()
{
return [
['Quarter', 'Area', 'Sales Rep.', 'Sales'],
[1, 'North', 'Jeff', 223000],
[1, 'North', 'Chris', 125000],
[1, 'South', 'Carol', 456000],
[1, 'South', 'Tina', 289000],
[2, 'North', 'Jeff', 322000],
[2, 'North', 'Chris', 340000],
[2, 'South', 'Carol', 198000],
[2, 'South', 'Tina', 222000],
[3, 'North', 'Jeff', 310000],
[3, 'North', 'Chris', 250000],
[3, 'South', 'Carol', 460000],
[3, 'South', 'Tina', 395000],
[4, 'North', 'Jeff', 261000],
[4, 'North', 'Chris', 389000],
[4, 'South', 'Carol', 305000],
[4, 'South', 'Tina', 188000],
];
}
public function providerDAverage()
{
return [
[
12,
$this->database1(),
'Yield',
[
['Tree', 'Height'],
['=Apple', '>10'],
],
],
[
13,
$this->database1(),
3,
$this->database1(),
],
[
268333.333333333333,
$this->database2(),
'Sales',
[
['Quarter', 'Sales Rep.'],
['>1', 'Tina'],
],
],
[
372500,
$this->database2(),
'Sales',
[
['Quarter', 'Area'],
['1', 'South'],
],
],
[
null,
$this->database1(),
null,
$this->database1(),
],
];
}
}

View File

@ -0,0 +1,108 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PHPUnit\Framework\TestCase;
class DCountATest extends TestCase
{
protected function setUp(): void
{
Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL);
}
/**
* @dataProvider providerDCountA
*
* @param mixed $expectedResult
* @param mixed $database
* @param mixed $field
* @param mixed $criteria
*/
public function testDCountA($expectedResult, $database, $field, $criteria): void
{
$result = Database::DCOUNTA($database, $field, $criteria);
self::assertSame($expectedResult, $result);
}
protected function database1()
{
return [
['Tree', 'Height', 'Age', 'Yield', 'Profit'],
['Apple', 18, 20, 14, 105],
['Pear', 12, 12, 10, 96],
['Cherry', 13, 14, 9, 105],
['Apple', 14, 15, 10, 75],
['Pear', 9, 8, 8, 76.8],
['Apple', 8, 9, 6, 45],
];
}
protected function database2()
{
return [
['Name', 'Gender', 'Age', 'Subject', 'Score'],
['Amy', 'Female', 8, 'Math', 0.63],
['Amy', 'Female', 8, 'English', 0.78],
['Amy', 'Female', 8, 'Science', 0.39],
['Bill', 'Male', 8, 'Math', 0.55],
['Bill', 'Male', 8, 'English', 0.71],
['Bill', 'Male', 8, 'Science', 'awaiting'],
['Sue', 'Female', 9, 'Math', null],
['Sue', 'Female', 9, 'English', 0.52],
['Sue', 'Female', 9, 'Science', 0.48],
['Tom', 'Male', 9, 'Math', 0.78],
['Tom', 'Male', 9, 'English', 0.69],
['Tom', 'Male', 9, 'Science', 0.65],
];
}
public function providerDCountA()
{
return [
[
1,
$this->database1(),
'Profit',
[
['Tree', 'Height', 'Height'],
['=Apple', '>10', '<16'],
],
],
[
2,
$this->database2(),
'Score',
[
['Subject', 'Gender'],
['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(),
'Score',
[
['Subject', 'Gender'],
['Math', 'Female'],
],
],
*/
[
3,
$this->database2(),
'Score',
[
['Subject', 'Score'],
['English', '>0.60'],
],
],
];
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PHPUnit\Framework\TestCase;
class DCountTest extends TestCase
{
protected function setUp(): void
{
Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL);
}
/**
* @dataProvider providerDCount
*
* @param mixed $expectedResult
* @param mixed $database
* @param mixed $field
* @param mixed $criteria
*/
public function testDCount($expectedResult, $database, $field, $criteria): void
{
$result = Database::DCOUNT($database, $field, $criteria);
self::assertSame($expectedResult, $result);
}
protected function database1()
{
return [
['Tree', 'Height', 'Age', 'Yield', 'Profit'],
['Apple', 18, 20, 14, 105],
['Pear', 12, 12, 10, 96],
['Cherry', 13, 14, 9, 105],
['Apple', 14, 'N/A', 10, 75],
['Pear', 9, 8, 8, 77],
['Apple', 12, 11, 6, 45],
];
}
protected function database2()
{
return [
['Name', 'Gender', 'Age', 'Subject', 'Score'],
['Amy', 'Female', 8, 'Math', 0.63],
['Amy', 'Female', 8, 'English', 0.78],
['Amy', 'Female', 8, 'Science', 0.39],
['Bill', 'Male', 8, 'Math', 0.55],
['Bill', 'Male', 8, 'English', 0.71],
['Bill', 'Male', 8, 'Science', 'awaiting'],
['Sue', 'Female', 9, 'Math', null],
['Sue', 'Female', 9, 'English', 0.52],
['Sue', 'Female', 9, 'Science', 0.48],
['Tom', 'Male', 9, 'Math', 0.78],
['Tom', 'Male', 9, 'English', 0.69],
['Tom', 'Male', 9, 'Science', 0.65],
];
}
public function providerDCount()
{
return [
[
1,
$this->database1(),
'Age',
[
['Tree', 'Height', 'Height'],
['=Apple', '>10', '<16'],
],
],
[
1,
$this->database2(),
'Score',
[
['Subject', 'Gender'],
['Science', 'Male'],
],
],
[
1,
$this->database2(),
'Score',
[
['Subject', 'Gender'],
['Math', 'Female'],
],
],
[
3,
$this->database2(),
null,
[
['Subject', 'Score'],
['English', '>0.63'],
],
],
];
}
}

View File

@ -0,0 +1,115 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PHPUnit\Framework\TestCase;
class DGetTest extends TestCase
{
protected function setUp(): void
{
Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL);
}
/**
* @dataProvider providerDGet
*
* @param mixed $expectedResult
* @param mixed $database
* @param mixed $field
* @param mixed $criteria
*/
public function testDGet($expectedResult, $database, $field, $criteria): void
{
$result = Database::DGET($database, $field, $criteria);
self::assertSame($expectedResult, $result);
}
protected function database1()
{
return [
['Tree', 'Height', 'Age', 'Yield', 'Profit'],
['Apple', 18, 20, 14, 105],
['Pear', 12, 12, 10, 96],
['Cherry', 13, 14, 9, 105],
['Apple', 14, 15, 10, 75],
['Pear', 9, 8, 8, 77],
['Apple', 8, 9, 6, 45],
];
}
protected function database2()
{
return [
['Quarter', 'Area', 'Sales Rep.', 'Sales'],
[1, 'North', 'Jeff', 223000],
[1, 'North', 'Chris', 125000],
[1, 'South', 'Carol', 456000],
[1, 'South', 'Tina', 289000],
[2, 'North', 'Jeff', 322000],
[2, 'North', 'Chris', 340000],
[2, 'South', 'Carol', 198000],
[2, 'South', 'Tina', 222000],
[3, 'North', 'Jeff', 310000],
[3, 'North', 'Chris', 250000],
[3, 'South', 'Carol', 460000],
[3, 'South', 'Tina', 395000],
[4, 'North', 'Jeff', 261000],
[4, 'North', 'Chris', 389000],
[4, 'South', 'Carol', 305000],
[4, 'South', 'Tina', 188000],
];
}
public function providerDGet()
{
return [
[
Functions::NAN(),
$this->database1(),
'Yield',
[
['Tree'],
['=Apple'],
['=Pear'],
],
],
[
10,
$this->database1(),
'Yield',
[
['Tree', 'Height', 'Height'],
['=Apple', '>10', '<16'],
['=Pear', '>12', null],
],
],
[
188000,
$this->database2(),
'Sales',
[
['Sales Rep.', 'Quarter'],
['Tina', 4],
],
],
[
Functions::NAN(),
$this->database2(),
'Sales',
[
['Area', 'Quarter'],
['South', 4],
],
],
[
null,
$this->database1(),
null,
$this->database1(),
],
];
}
}

View File

@ -0,0 +1,105 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PHPUnit\Framework\TestCase;
class DMaxTest extends TestCase
{
protected function setUp(): void
{
Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL);
}
/**
* @dataProvider providerDMax
*
* @param mixed $expectedResult
* @param mixed $database
* @param mixed $field
* @param mixed $criteria
*/
public function testDMax($expectedResult, $database, $field, $criteria): void
{
$result = Database::DMAX($database, $field, $criteria);
self::assertSame($expectedResult, $result);
}
protected function database1()
{
return [
['Tree', 'Height', 'Age', 'Yield', 'Profit'],
['Apple', 18, 20, 14, 105],
['Pear', 12, 12, 10, 96],
['Cherry', 13, 14, 9, 105],
['Apple', 14, 15, 10, 75],
['Pear', 9, 8, 8, 77],
['Apple', 8, 9, 6, 45],
];
}
protected function database2()
{
return [
['Quarter', 'Area', 'Sales Rep.', 'Sales'],
[1, 'North', 'Jeff', 223000],
[1, 'North', 'Chris', 125000],
[1, 'South', 'Carol', 456000],
[1, 'South', 'Tina', 289000],
[2, 'North', 'Jeff', 322000],
[2, 'North', 'Chris', 340000],
[2, 'South', 'Carol', 198000],
[2, 'South', 'Tina', 222000],
[3, 'North', 'Jeff', 310000],
[3, 'North', 'Chris', 250000],
[3, 'South', 'Carol', 460000],
[3, 'South', 'Tina', 395000],
[4, 'North', 'Jeff', 261000],
[4, 'North', 'Chris', 389000],
[4, 'South', 'Carol', 305000],
[4, 'South', 'Tina', 188000],
];
}
public function providerDMax()
{
return [
[
96,
$this->database1(),
'Profit',
[
['Tree', 'Height', 'Height'],
['=Apple', '>10', '<16'],
['=Pear', null, null],
],
],
[
340000,
$this->database2(),
'Sales',
[
['Quarter', 'Area'],
[2, 'North'],
],
],
[
460000,
$this->database2(),
'Sales',
[
['Sales Rep.', 'Quarter'],
['Carol', '>1'],
],
],
[
null,
$this->database1(),
null,
$this->database1(),
],
];
}
}

View File

@ -0,0 +1,101 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PHPUnit\Framework\TestCase;
class DMinTest extends TestCase
{
protected function setUp(): void
{
Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL);
}
/**
* @dataProvider providerDMin
*
* @param mixed $expectedResult
* @param mixed $database
* @param mixed $field
* @param mixed $criteria
*/
public function testDMin($expectedResult, $database, $field, $criteria): void
{
$result = Database::DMIN($database, $field, $criteria);
self::assertSame($expectedResult, $result);
}
protected function database1()
{
return [
['Tree', 'Height', 'Age', 'Yield', 'Profit'],
['Apple', 18, 20, 14, 105],
['Pear', 12, 12, 10, 96],
['Cherry', 13, 14, 9, 105],
['Apple', 14, 15, 10, 75],
['Pear', 9, 8, 8, 77],
['Apple', 8, 9, 6, 45],
];
}
protected function database2()
{
return [
['Name', 'Gender', 'Age', 'Subject', 'Score'],
['Amy', 'Female', 8, 'Math', 0.63],
['Amy', 'Female', 8, 'English', 0.78],
['Amy', 'Female', 8, 'Science', 0.39],
['Bill', 'Male', 8, 'Math', 0.55],
['Bill', 'Male', 8, 'English', 0.71],
['Bill', 'Male', 8, 'Science', 0.51],
['Sue', 'Female', 9, 'Math', 0.39],
['Sue', 'Female', 9, 'English', 0.52],
['Sue', 'Female', 9, 'Science', 0.48],
['Tom', 'Male', 9, 'Math', 0.78],
['Tom', 'Male', 9, 'English', 0.69],
['Tom', 'Male', 9, 'Science', 0.65],
];
}
public function providerDMin()
{
return [
[
75,
$this->database1(),
'Profit',
[
['Tree', 'Height', 'Height'],
['=Apple', '>10', '<16'],
['=Pear', '>12', null],
],
],
[
0.48,
$this->database2(),
'Score',
[
['Subject', 'Age'],
['Science', '>8'],
],
],
[
0.55,
$this->database2(),
'Score',
[
['Subject', 'Gender'],
['Math', 'Male'],
],
],
[
null,
$this->database1(),
null,
$this->database1(),
],
];
}
}

View File

@ -0,0 +1,105 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\DateTime;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PHPUnit\Framework\TestCase;
class DProductTest extends TestCase
{
protected function setUp(): void
{
Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL);
}
/**
* @dataProvider providerDProduct
*
* @param mixed $expectedResult
* @param mixed $database
* @param mixed $field
* @param mixed $criteria
*/
public function testDProduct($expectedResult, $database, $field, $criteria): void
{
$result = Database::DPRODUCT($database, $field, $criteria);
self::assertSame($expectedResult, $result);
}
protected function database1()
{
return [
['Tree', 'Height', 'Age', 'Yield', 'Profit'],
['Apple', 18, 20, 14, 105],
['Pear', 12, 12, 10, 96],
['Cherry', 13, 14, 9, 105],
['Apple', 14, 15, 10, 75],
['Pear', 9, 8, 8, 77],
['Apple', 8, 9, 6, 45],
];
}
protected function database2()
{
return [
['Name', 'Date', 'Test', 'Score'],
['Gary', DateTime::getDateValue('01-Jan-2017'), 'Test1', 4],
['Gary', DateTime::getDateValue('01-Jan-2017'), 'Test2', 4],
['Gary', DateTime::getDateValue('01-Jan-2017'), 'Test3', 3],
['Gary', DateTime::getDateValue('05-Jan-2017'), 'Test1', 3],
['Gary', DateTime::getDateValue('05-Jan-2017'), 'Test2', 4],
['Gary', DateTime::getDateValue('05-Jan-2017'), 'Test3', 3],
['Kev', DateTime::getDateValue('02-Jan-2017'), 'Test1', 2],
['Kev', DateTime::getDateValue('02-Jan-2017'), 'Test2', 3],
['Kev', DateTime::getDateValue('02-Jan-2017'), 'Test3', 5],
['Kev', DateTime::getDateValue('05-Jan-2017'), 'Test1', 3],
['Kev', DateTime::getDateValue('05-Jan-2017'), 'Test2', 2],
['Kev', DateTime::getDateValue('05-Jan-2017'), 'Test3', 5],
];
}
public function providerDProduct()
{
return [
[
800,
$this->database1(),
'Yield',
[
['Tree', 'Height', 'Height'],
['=Apple', '>10', '<16'],
['=Pear', null, null],
],
],
/*
* We don't yet support date handling in the search query
[
36,
$this->database2(),
'Score',
[
['Name', 'Date'],
['Gary', '05-Jan-2017'],
],
],
[
8,
$this->database2(),
'Score',
[
['Test', 'Date'],
['Test1', '<05-Jan-2017'],
],
],
*/
[
null,
$this->database1(),
null,
$this->database1(),
],
];
}
}

View File

@ -0,0 +1,101 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PHPUnit\Framework\TestCase;
class DStDevPTest extends TestCase
{
protected function setUp(): void
{
Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL);
}
/**
* @dataProvider providerDStDevP
*
* @param mixed $expectedResult
* @param mixed $database
* @param mixed $field
* @param mixed $criteria
*/
public function testDStDevP($expectedResult, $database, $field, $criteria): void
{
$result = Database::DSTDEVP($database, $field, $criteria);
self::assertSame($expectedResult, $result);
}
protected function database1()
{
return [
['Tree', 'Height', 'Age', 'Yield', 'Profit'],
['Apple', 18, 20, 14, 105],
['Pear', 12, 12, 10, 96],
['Cherry', 13, 14, 9, 105],
['Apple', 14, 15, 10, 75],
['Pear', 9, 8, 8, 77],
['Apple', 8, 9, 6, 45],
];
}
protected function database2()
{
return [
['Name', 'Gender', 'Age', 'Subject', 'Score'],
['Amy', 'Female', 10, 'Math', 0.63],
['Amy', 'Female', 10, 'English', 0.78],
['Amy', 'Female', 10, 'Science', 0.39],
['Bill', 'Male', 8, 'Math', 0.55],
['Bill', 'Male', 8, 'English', 0.71],
['Bill', 'Male', 8, 'Science', 0.51],
['Sam', 'Male', 9, 'Math', 0.39],
['Sam', 'Male', 9, 'English', 0.52],
['Sam', 'Male', 9, 'Science', 0.48],
['Tom', 'Male', 9, 'Math', 0.78],
['Tom', 'Male', 9, 'English', 0.69],
['Tom', 'Male', 9, 'Science', 0.65],
];
}
public function providerDStDevP()
{
return [
[
2.653299832284,
$this->database1(),
'Yield',
[
['Tree'],
['=Apple'],
['=Pear'],
],
],
[
0.085244745684,
$this->database2(),
'Score',
[
['Subject', 'Gender'],
['English', 'Male'],
],
],
[
0.160623784042,
$this->database2(),
'Score',
[
['Subject', 'Age'],
['Math', '>8'],
],
],
[
null,
$this->database1(),
null,
$this->database1(),
],
];
}
}

View File

@ -0,0 +1,101 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PHPUnit\Framework\TestCase;
class DStDevTest extends TestCase
{
protected function setUp(): void
{
Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL);
}
/**
* @dataProvider providerDStDev
*
* @param mixed $expectedResult
* @param mixed $database
* @param mixed $field
* @param mixed $criteria
*/
public function testDStDev($expectedResult, $database, $field, $criteria): void
{
$result = Database::DSTDEV($database, $field, $criteria);
self::assertSame($expectedResult, $result);
}
protected function database1()
{
return [
['Tree', 'Height', 'Age', 'Yield', 'Profit'],
['Apple', 18, 20, 14, 105],
['Pear', 12, 12, 10, 96],
['Cherry', 13, 14, 9, 105],
['Apple', 14, 15, 10, 75],
['Pear', 9, 8, 8, 77],
['Apple', 8, 9, 6, 45],
];
}
protected function database2()
{
return [
['Name', 'Gender', 'Age', 'Subject', 'Score'],
['Amy', 'Female', 10, 'Math', 0.63],
['Amy', 'Female', 10, 'English', 0.78],
['Amy', 'Female', 10, 'Science', 0.39],
['Bill', 'Male', 8, 'Math', 0.55],
['Bill', 'Male', 8, 'English', 0.71],
['Bill', 'Male', 8, 'Science', 0.51],
['Sam', 'Male', 9, 'Math', 0.39],
['Sam', 'Male', 9, 'English', 0.52],
['Sam', 'Male', 9, 'Science', 0.48],
['Tom', 'Male', 9, 'Math', 0.78],
['Tom', 'Male', 9, 'English', 0.69],
['Tom', 'Male', 9, 'Science', 0.65],
];
}
public function providerDStDev()
{
return [
[
2.966479394838,
$this->database1(),
'Yield',
[
['Tree'],
['=Apple'],
['=Pear'],
],
],
[
0.104403065089,
$this->database2(),
'Score',
[
['Subject', 'Gender'],
['English', 'Male'],
],
],
[
0.196723155729,
$this->database2(),
'Score',
[
['Subject', 'Age'],
['Math', '>8'],
],
],
[
null,
$this->database1(),
null,
$this->database1(),
],
];
}
}

View File

@ -0,0 +1,117 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PHPUnit\Framework\TestCase;
class DSumTest extends TestCase
{
protected function setUp(): void
{
Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL);
}
/**
* @dataProvider providerDSum
*
* @param mixed $expectedResult
* @param mixed $database
* @param mixed $field
* @param mixed $criteria
*/
public function testDSum($expectedResult, $database, $field, $criteria): void
{
$result = Database::DSUM($database, $field, $criteria);
self::assertSame($expectedResult, $result);
}
protected function database1()
{
return [
['Tree', 'Height', 'Age', 'Yield', 'Profit'],
['Apple', 18, 20, 14, 105],
['Pear', 12, 12, 10, 96],
['Cherry', 13, 14, 9, 105],
['Apple', 14, 15, 10, 75],
['Pear', 9, 8, 8, 77],
['Apple', 8, 9, 6, 45],
];
}
protected function database2()
{
return [
['Quarter', 'Area', 'Sales Rep.', 'Sales'],
[1, 'North', 'Jeff', 223000],
[1, 'North', 'Chris', 125000],
[1, 'South', 'Carol', 456000],
[1, 'South', 'Tina', 289000],
[2, 'North', 'Jeff', 322000],
[2, 'North', 'Chris', 340000],
[2, 'South', 'Carol', 198000],
[2, 'South', 'Tina', 222000],
[3, 'North', 'Jeff', 310000],
[3, 'North', 'Chris', 250000],
[3, 'South', 'Carol', 460000],
[3, 'South', 'Tina', 395000],
[4, 'North', 'Jeff', 261000],
[4, 'North', 'Chris', 389000],
[4, 'South', 'Carol', 305000],
[4, 'South', 'Tina', 188000],
];
}
public function providerDSum()
{
return [
[
225,
$this->database1(),
'Profit',
[
['Tree'],
['=Apple'],
],
],
[
248,
$this->database1(),
'Profit',
[
['Tree', 'Height', 'Height'],
['=Apple', '>10', '<16'],
['=Pear', null, null],
],
],
[
1210000,
$this->database2(),
'Sales',
[
['Quarter', 'Area'],
['>2', 'North'],
],
],
/*
* We don't yet support woldcards in text search fields
[
710000,
$this->database2(),
'Sales',
[
['Quarter', 'Sales Rep.'],
['3', 'C*'],
],
],
*/
[
null,
$this->database1(),
null,
$this->database1(),
],
];
}
}

View File

@ -0,0 +1,101 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PHPUnit\Framework\TestCase;
class DVarPTest extends TestCase
{
protected function setUp(): void
{
Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL);
}
/**
* @dataProvider providerDVarP
*
* @param mixed $expectedResult
* @param mixed $database
* @param mixed $field
* @param mixed $criteria
*/
public function testDVarP($expectedResult, $database, $field, $criteria): void
{
$result = Database::DVARP($database, $field, $criteria);
self::assertSame($expectedResult, $result);
}
protected function database1()
{
return [
['Tree', 'Height', 'Age', 'Yield', 'Profit'],
['Apple', 18, 20, 14, 105],
['Pear', 12, 12, 10, 96],
['Cherry', 13, 14, 9, 105],
['Apple', 14, 15, 10, 75],
['Pear', 9, 8, 8, 77],
['Apple', 8, 9, 6, 45],
];
}
protected function database2()
{
return [
['Name', 'Gender', 'Age', 'Subject', 'Score'],
['Amy', 'Female', 10, 'Math', 0.63],
['Amy', 'Female', 10, 'English', 0.78],
['Amy', 'Female', 10, 'Science', 0.39],
['Bill', 'Male', 8, 'Math', 0.55],
['Bill', 'Male', 8, 'English', 0.71],
['Bill', 'Male', 8, 'Science', 0.51],
['Sam', 'Male', 9, 'Math', 0.39],
['Sam', 'Male', 9, 'English', 0.52],
['Sam', 'Male', 9, 'Science', 0.48],
['Tom', 'Male', 9, 'Math', 0.78],
['Tom', 'Male', 9, 'English', 0.69],
['Tom', 'Male', 9, 'Science', 0.65],
];
}
public function providerDVarP()
{
return [
[
7.04,
$this->database1(),
'Yield',
[
['Tree'],
['=Apple'],
['=Pear'],
],
],
[
0.025622222222,
$this->database2(),
'Score',
[
['Subject', 'Gender'],
['Math', 'Male'],
],
],
[
0.011622222222,
$this->database2(),
'Score',
[
['Subject', 'Age'],
['Science', '>8'],
],
],
[
null,
$this->database1(),
null,
$this->database1(),
],
];
}
}

View File

@ -0,0 +1,101 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PHPUnit\Framework\TestCase;
class DVarTest extends TestCase
{
protected function setUp(): void
{
Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL);
}
/**
* @dataProvider providerDVar
*
* @param mixed $expectedResult
* @param mixed $database
* @param mixed $field
* @param mixed $criteria
*/
public function testDVar($expectedResult, $database, $field, $criteria): void
{
$result = Database::DVAR($database, $field, $criteria);
self::assertSame($expectedResult, $result);
}
protected function database1()
{
return [
['Tree', 'Height', 'Age', 'Yield', 'Profit'],
['Apple', 18, 20, 14, 105],
['Pear', 12, 12, 10, 96],
['Cherry', 13, 14, 9, 105],
['Apple', 14, 15, 10, 75],
['Pear', 9, 8, 8, 77],
['Apple', 8, 9, 6, 45],
];
}
protected function database2()
{
return [
['Name', 'Gender', 'Age', 'Subject', 'Score'],
['Amy', 'Female', 10, 'Math', 0.63],
['Amy', 'Female', 10, 'English', 0.78],
['Amy', 'Female', 10, 'Science', 0.39],
['Bill', 'Male', 8, 'Math', 0.55],
['Bill', 'Male', 8, 'English', 0.71],
['Bill', 'Male', 8, 'Science', 0.51],
['Sam', 'Male', 9, 'Math', 0.39],
['Sam', 'Male', 9, 'English', 0.52],
['Sam', 'Male', 9, 'Science', 0.48],
['Tom', 'Male', 9, 'Math', 0.78],
['Tom', 'Male', 9, 'English', 0.69],
['Tom', 'Male', 9, 'Science', 0.65],
];
}
public function providerDVar()
{
return [
[
8.8,
$this->database1(),
'Yield',
[
['Tree'],
['=Apple'],
['=Pear'],
],
],
[
0.038433333333,
$this->database2(),
'Score',
[
['Subject', 'Gender'],
['Math', 'Male'],
],
],
[
0.017433333333,
$this->database2(),
'Score',
[
['Subject', 'Age'],
['Science', '>8'],
],
],
[
null,
$this->database1(),
null,
$this->database1(),
],
];
}
}