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
|
### 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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'],
|
['>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(),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue