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:
parent
40a6dee0a4
commit
25f7dcb9fd
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 @@ class Calculation
|
|||
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
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Database;
|
|||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch;
|
||||
|
||||
abstract class DatabaseAbstract
|
||||
{
|
||||
|
|
@ -82,9 +83,6 @@ abstract class DatabaseAbstract
|
|||
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 @@ abstract class DatabaseAbstract
|
|||
foreach ($criterion as $field => $value) {
|
||||
$criterionName = $criteriaNames[$field];
|
||||
if ($value !== null && $value !== '') {
|
||||
$condition = '[:' . $criterionName . ']' . Functions::ifCondition($value);
|
||||
$condition = self::buildCondition($value, $criterionName);
|
||||
$baseQuery[$key][] = $condition;
|
||||
}
|
||||
}
|
||||
|
|
@ -108,31 +106,39 @@ abstract class DatabaseAbstract
|
|||
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 buildCondition($criterion, string $criterionName): string
|
||||
{
|
||||
$ifCondition = Functions::ifCondition($criterion);
|
||||
|
||||
// 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) {
|
||||
// Substitute actual values from the database row for our [:placeholders]
|
||||
$testConditionList = $query;
|
||||
foreach ($criteriaNames as $key => $criteriaName) {
|
||||
$key = array_search($criteriaName, $fieldNames, true);
|
||||
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);
|
||||
$conditions = $query;
|
||||
foreach ($criteria as $criterion) {
|
||||
$conditions = self::processCondition($criterion, $fields, $dataValues, $conditions);
|
||||
}
|
||||
// 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) {
|
||||
unset($database[$dataRow]);
|
||||
}
|
||||
|
|
@ -140,4 +146,19 @@ abstract class DatabaseAbstract
|
|||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Internal;
|
||||
|
||||
class MakeMatrix
|
||||
{
|
||||
public static function make(...$args): array
|
||||
{
|
||||
return $args;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -94,8 +94,6 @@ class DSumTest extends TestCase
|
|||
['>2', 'North'],
|
||||
],
|
||||
],
|
||||
/*
|
||||
* We don't yet support wildcards in text search fields
|
||||
[
|
||||
710000,
|
||||
$this->database2(),
|
||||
|
|
@ -105,7 +103,6 @@ class DSumTest extends TestCase
|
|||
['3', 'C*'],
|
||||
],
|
||||
],
|
||||
*/
|
||||
[
|
||||
null,
|
||||
$this->database1(),
|
||||
|
|
|
|||
Loading…
Reference in New Issue