From dd9352909016b50b8867dca59f236526edb28558 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 23 Feb 2021 18:00:31 +0100 Subject: [PATCH 1/4] Enable support for wildcard text searches in Excel Database functions --- CHANGELOG.md | 1 + .../Calculation/Calculation.php | 11 ++--- .../Calculation/Database/DatabaseAbstract.php | 46 +++++++++++++------ .../Calculation/Internal/MakeMatrix.php | 11 +++++ .../Calculation/Internal/WildcardMatch.php | 34 ++++++++++++++ .../Functions/Database/DSumTest.php | 3 -- 6 files changed, 82 insertions(+), 24 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php create mode 100644 src/PhpSpreadsheet/Calculation/Internal/WildcardMatch.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c64984403..f8baacc01e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added - 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) - Implemented DataBar for conditional formatting in Xlsx, providing read/write and creation of (type, value, direction, fills, border, axis position, color settings) as DataBar options in Excel. [#1754](https://github.com/PHPOffice/PhpSpreadsheet/pull/1754) - Alignment for ODS Writer [#1796](https://github.com/PHPOffice/PhpSpreadsheet/issues/1796) - Basic implementation of the PERMUTATIONA() Statistical Function diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 87bf44a516..ba6299572f 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -2663,12 +2663,16 @@ class Calculation private static $controlFunctions = [ 'MKMATRIX' => [ 'argumentCount' => '*', - 'functionCall' => [__CLASS__, 'mkMatrix'], + 'functionCall' => [Internal\MakeMatrix::class, 'make'], ], 'NAME.ERROR' => [ 'argumentCount' => '*', 'functionCall' => [Functions::class, 'NAME'], ], + 'WILDCARDMATCH' => [ + 'argumentCount' => '2', + 'functionCall' => [Internal\WildcardMatch::class, 'compare'], + ], ]; public function __construct(?Spreadsheet $spreadsheet = null) @@ -3742,11 +3746,6 @@ private function convertMatrixReferences($formula) return $formula; } - private static function mkMatrix(...$args) - { - return $args; - } - // Binary Operators // These operators always work on two values // Array key is the operator, the value indicates whether this is a left or right associative operator diff --git a/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php b/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php index ae2c3fd72f..6562cf53e0 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php +++ b/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php @@ -4,6 +4,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch; abstract class DatabaseAbstract { @@ -82,9 +83,6 @@ protected static function getFilteredColumn(array $database, $field, array $crit return $columnData; } - /** - * @TODO Suport for wildcard ? and * in strings (includng escaping) - */ private static function buildQuery(array $criteriaNames, array $criteria): string { $baseQuery = []; @@ -92,7 +90,7 @@ private static function buildQuery(array $criteriaNames, array $criteria): strin foreach ($criterion as $field => $value) { $criterionName = $criteriaNames[$field]; if ($value !== null && $value !== '') { - $condition = '[:' . $criterionName . ']' . Functions::ifCondition($value); + $condition = self::evaluateCondition($value, $criterionName); $baseQuery[$key][] = $condition; } } @@ -108,29 +106,47 @@ function ($rowValue) { return (count($rowQuery) > 1) ? 'OR(' . implode(',', $rowQuery) . ')' : $rowQuery[0]; } - /** - * @param $criteriaNames - * @param $fieldNames - */ - private static function executeQuery(array $database, string $query, $criteriaNames, $fieldNames): array + private static function evaluateCondition($value, $criterionName): string + { + $ifCondition = Functions::ifCondition($value); + + // Check for wildcard characters used in the condition + $result = preg_match('/(?[^"]*)(?".*[*?].*")/ui', $ifCondition, $matches); + if ($result !== 1) { + return "[:{$criterionName}]{$ifCondition}"; + } + + $trueFalse = ($matches['operator'] !== '<>'); + $wildcard = WildcardMatch::wildcard($matches['operand']); + $condition = "WILDCARDMATCH([:{$criterionName}],{$wildcard})"; + if ($trueFalse === false) { + $condition = "NOT({$condition})"; + } + + return $condition; + } + + private static function executeQuery(array $database, string $query, array $criteriaNames, array $fieldNames): array { foreach ($database as $dataRow => $dataValues) { // Substitute actual values from the database row for our [:placeholders] - $testConditionList = $query; - foreach ($criteriaNames as $key => $criteriaName) { + $testConditions = $query; + foreach ($criteriaNames as $criteriaName) { $key = array_search($criteriaName, $fieldNames, true); + + $dataValue = 'NULL'; if (is_bool($dataValues[$key])) { $dataValue = ($dataValues[$key]) ? 'TRUE' : 'FALSE'; } elseif ($dataValues[$key] !== null) { $dataValue = $dataValues[$key]; $dataValue = (is_string($dataValue)) ? Calculation::wrapResult(strtoupper($dataValue)) : $dataValue; - } else { - $dataValue = 'NULL'; } - $testConditionList = str_replace('[:' . $criteriaName . ']', $dataValue, $testConditionList); + + $testConditions = str_replace('[:' . $criteriaName . ']', $dataValue, $testConditions); } + // evaluate the criteria against the row data - $result = Calculation::getInstance()->_calculateFormulaValue('=' . $testConditionList); + $result = Calculation::getInstance()->_calculateFormulaValue('=' . $testConditions); // If the row failed to meet the criteria, remove it from the database if ($result !== true) { diff --git a/src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php b/src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php new file mode 100644 index 0000000000..38a651f3ca --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php @@ -0,0 +1,11 @@ +2', 'North'], ], ], - /* - * We don't yet support wildcards in text search fields [ 710000, $this->database2(), @@ -105,7 +103,6 @@ public function providerDSum() ['3', 'C*'], ], ], - */ [ null, $this->database1(), From 0d2522459fce408ff270a5b59330244fbf2e9269 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 23 Feb 2021 18:07:24 +0100 Subject: [PATCH 2/4] MatriX make() method should be public --- src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php b/src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php index 38a651f3ca..8e0ec6d8df 100644 --- a/src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php +++ b/src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php @@ -4,7 +4,7 @@ class MakeMatrix { - private static function make(...$args) + public static function make(...$args) { return $args; } From 449dde322c1e3fd29f91e4a56253e093e33266fc Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 23 Feb 2021 18:53:45 +0100 Subject: [PATCH 3/4] Minor refactoring tweaks --- .../Calculation/Database/DatabaseAbstract.php | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php b/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php index 6562cf53e0..a08f1251fc 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php +++ b/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php @@ -90,7 +90,7 @@ private static function buildQuery(array $criteriaNames, array $criteria): strin foreach ($criterion as $field => $value) { $criterionName = $criteriaNames[$field]; if ($value !== null && $value !== '') { - $condition = self::evaluateCondition($value, $criterionName); + $condition = self::buildCondition($value, $criterionName); $baseQuery[$key][] = $condition; } } @@ -106,9 +106,9 @@ function ($rowValue) { return (count($rowQuery) > 1) ? 'OR(' . implode(',', $rowQuery) . ')' : $rowQuery[0]; } - private static function evaluateCondition($value, $criterionName): string + private static function buildCondition($criterion, string $criterionName): string { - $ifCondition = Functions::ifCondition($value); + $ifCondition = Functions::ifCondition($criterion); // Check for wildcard characters used in the condition $result = preg_match('/(?[^"]*)(?".*[*?].*")/ui', $ifCondition, $matches); @@ -126,29 +126,19 @@ private static function evaluateCondition($value, $criterionName): string return $condition; } - private static function executeQuery(array $database, string $query, array $criteriaNames, array $fieldNames): array + private static function executeQuery(array $database, string $query, array $criteria, array $fields): array { foreach ($database as $dataRow => $dataValues) { // Substitute actual values from the database row for our [:placeholders] - $testConditions = $query; - foreach ($criteriaNames as $criteriaName) { - $key = array_search($criteriaName, $fieldNames, true); - - $dataValue = 'NULL'; - if (is_bool($dataValues[$key])) { - $dataValue = ($dataValues[$key]) ? 'TRUE' : 'FALSE'; - } elseif ($dataValues[$key] !== null) { - $dataValue = $dataValues[$key]; - $dataValue = (is_string($dataValue)) ? Calculation::wrapResult(strtoupper($dataValue)) : $dataValue; - } - - $testConditions = str_replace('[:' . $criteriaName . ']', $dataValue, $testConditions); + $conditions = $query; + foreach ($criteria as $criterion) { + $conditions = self::processCondition($criterion, $fields, $dataValues, $conditions); } // evaluate the criteria against the row data - $result = Calculation::getInstance()->_calculateFormulaValue('=' . $testConditions); - // If the row failed to meet the criteria, remove it from the database + $result = Calculation::getInstance()->_calculateFormulaValue('=' . $conditions); + // If the row failed to meet the criteria, remove it from the database if ($result !== true) { unset($database[$dataRow]); } @@ -156,4 +146,19 @@ private static function executeQuery(array $database, string $query, array $crit return $database; } + + private static function processCondition(string $criterion, array $fields, array $dataValues, string $conditions) + { + $key = array_search($criterion, $fields, true); + + $dataValue = 'NULL'; + if (is_bool($dataValues[$key])) { + $dataValue = ($dataValues[$key]) ? 'TRUE' : 'FALSE'; + } elseif ($dataValues[$key] !== null) { + $dataValue = $dataValues[$key]; + $dataValue = (is_string($dataValue)) ? Calculation::wrapResult(strtoupper($dataValue)) : $dataValue; + } + + return str_replace('[:' . $criterion . ']', $dataValue, $conditions); + } } From 5fd46b441ef3e892f86de7481b052ea638e914b3 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 23 Feb 2021 19:14:30 +0100 Subject: [PATCH 4/4] Typehints --- src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php | 2 +- src/PhpSpreadsheet/Calculation/Internal/WildcardMatch.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php b/src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php index 8e0ec6d8df..8b53464fc4 100644 --- a/src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php +++ b/src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php @@ -4,7 +4,7 @@ class MakeMatrix { - public static function make(...$args) + public static function make(...$args): array { return $args; } diff --git a/src/PhpSpreadsheet/Calculation/Internal/WildcardMatch.php b/src/PhpSpreadsheet/Calculation/Internal/WildcardMatch.php index 4219bb0865..5b4fe5b14d 100644 --- a/src/PhpSpreadsheet/Calculation/Internal/WildcardMatch.php +++ b/src/PhpSpreadsheet/Calculation/Internal/WildcardMatch.php @@ -18,17 +18,17 @@ class WildcardMatch '\?', ]; - public static function wildcard($wildcard) + public static function wildcard(string $wildcard): string { return preg_replace(self::SEARCH_SET, self::REPLACEMENT_SET, $wildcard); } - public static function compare($value, $wildcard) + public static function compare($value, string $wildcard): bool { if ($value === '') { return true; } - return preg_match("/{$wildcard}/ui", $value); + return (bool) preg_match("/{$wildcard}/ui", $value); } }