Enable support for wildcard text searches in Excel Database functions (#1876)

* Enable support for wildcard text searches in Excel Database functions
This commit is contained in:
Mark Baker 2021-02-23 19:26:29 +01:00 committed by GitHub
parent 40a6dee0a4
commit 25f7dcb9fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 96 additions and 33 deletions

View File

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Added ### 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 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) - 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

@ -2663,12 +2663,16 @@ class Calculation
private static $controlFunctions = [ private static $controlFunctions = [
'MKMATRIX' => [ 'MKMATRIX' => [
'argumentCount' => '*', 'argumentCount' => '*',
'functionCall' => [__CLASS__, 'mkMatrix'], 'functionCall' => [Internal\MakeMatrix::class, 'make'],
], ],
'NAME.ERROR' => [ 'NAME.ERROR' => [
'argumentCount' => '*', 'argumentCount' => '*',
'functionCall' => [Functions::class, 'NAME'], 'functionCall' => [Functions::class, 'NAME'],
], ],
'WILDCARDMATCH' => [
'argumentCount' => '2',
'functionCall' => [Internal\WildcardMatch::class, 'compare'],
],
]; ];
public function __construct(?Spreadsheet $spreadsheet = null) public function __construct(?Spreadsheet $spreadsheet = null)
@ -3742,11 +3746,6 @@ class Calculation
return $formula; return $formula;
} }
private static function mkMatrix(...$args)
{
return $args;
}
// Binary Operators // Binary Operators
// These operators always work on two values // These operators always work on two values
// Array key is the operator, the value indicates whether this is a left or right associative operator // Array key is the operator, the value indicates whether this is a left or right associative operator

View File

@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch;
abstract class DatabaseAbstract abstract class DatabaseAbstract
{ {
@ -82,9 +83,6 @@ abstract class DatabaseAbstract
return $columnData; return $columnData;
} }
/**
* @TODO Suport for wildcard ? and * in strings (includng escaping)
*/
private static function buildQuery(array $criteriaNames, array $criteria): string private static function buildQuery(array $criteriaNames, array $criteria): string
{ {
$baseQuery = []; $baseQuery = [];
@ -92,7 +90,7 @@ abstract class DatabaseAbstract
foreach ($criterion as $field => $value) { foreach ($criterion as $field => $value) {
$criterionName = $criteriaNames[$field]; $criterionName = $criteriaNames[$field];
if ($value !== null && $value !== '') { if ($value !== null && $value !== '') {
$condition = '[:' . $criterionName . ']' . Functions::ifCondition($value); $condition = self::buildCondition($value, $criterionName);
$baseQuery[$key][] = $condition; $baseQuery[$key][] = $condition;
} }
} }
@ -108,31 +106,39 @@ abstract class DatabaseAbstract
return (count($rowQuery) > 1) ? 'OR(' . implode(',', $rowQuery) . ')' : $rowQuery[0]; return (count($rowQuery) > 1) ? 'OR(' . implode(',', $rowQuery) . ')' : $rowQuery[0];
} }
/** private static function buildCondition($criterion, string $criterionName): string
* @param $criteriaNames {
* @param $fieldNames $ifCondition = Functions::ifCondition($criterion);
*/
private static function executeQuery(array $database, string $query, $criteriaNames, $fieldNames): array // Check for wildcard characters used in the condition
$result = preg_match('/(?<operator>[^"]*)(?<operand>".*[*?].*")/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 $criteria, array $fields): array
{ {
foreach ($database as $dataRow => $dataValues) { foreach ($database as $dataRow => $dataValues) {
// Substitute actual values from the database row for our [:placeholders] // Substitute actual values from the database row for our [:placeholders]
$testConditionList = $query; $conditions = $query;
foreach ($criteriaNames as $key => $criteriaName) { foreach ($criteria as $criterion) {
$key = array_search($criteriaName, $fieldNames, true); $conditions = self::processCondition($criterion, $fields, $dataValues, $conditions);
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);
} }
// 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
// evaluate the criteria against the row data
$result = Calculation::getInstance()->_calculateFormulaValue('=' . $conditions);
// If the row failed to meet the criteria, remove it from the database
if ($result !== true) { if ($result !== true) {
unset($database[$dataRow]); unset($database[$dataRow]);
} }
@ -140,4 +146,19 @@ abstract class DatabaseAbstract
return $database; 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);
}
} }

View File

@ -0,0 +1,11 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Internal;
class MakeMatrix
{
public static function make(...$args): array
{
return $args;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Calculation\Internal;
class WildcardMatch
{
private const SEARCH_SET = [
'/([^~])(\*)/ui',
'/~\*/ui',
'/([^~])(\?)/ui',
'/~\?/ui',
];
private const REPLACEMENT_SET = [
'${1}.*',
'\*',
'${1}.',
'\?',
];
public static function wildcard(string $wildcard): string
{
return preg_replace(self::SEARCH_SET, self::REPLACEMENT_SET, $wildcard);
}
public static function compare($value, string $wildcard): bool
{
if ($value === '') {
return true;
}
return (bool) preg_match("/{$wildcard}/ui", $value);
}
}

View File

@ -94,8 +94,6 @@ class DSumTest extends TestCase
['>2', 'North'], ['>2', 'North'],
], ],
], ],
/*
* We don't yet support wildcards in text search fields
[ [
710000, 710000,
$this->database2(), $this->database2(),
@ -105,7 +103,6 @@ class DSumTest extends TestCase
['3', 'C*'], ['3', 'C*'],
], ],
], ],
*/
[ [
null, null,
$this->database1(), $this->database1(),