Skip to content

Commit

Permalink
Enable support for wildcard text searches in Excel Database functions (
Browse files Browse the repository at this point in the history
…#1876)

* Enable support for wildcard text searches in Excel Database functions
  • Loading branch information
Mark Baker committed Feb 23, 2021
1 parent 40a6dee commit 25f7dcb
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 32 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
11 changes: 5 additions & 6 deletions src/PhpSpreadsheet/Calculation/Calculation.php
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
67 changes: 44 additions & 23 deletions src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php
Expand Up @@ -4,6 +4,7 @@

use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch;

abstract class DatabaseAbstract
{
Expand Down Expand Up @@ -82,17 +83,14 @@ 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 = [];
foreach ($criteria as $key => $criterion) {
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;
}
}
Expand All @@ -108,36 +106,59 @@ 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 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
$result = Calculation::getInstance()->_calculateFormulaValue('=' . $conditions);

// If the row failed to meet the criteria, remove it from the database
if ($result !== true) {
unset($database[$dataRow]);
}
}

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);
}
}
11 changes: 11 additions & 0 deletions src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php
@@ -0,0 +1,11 @@
<?php

namespace PhpOffice\PhpSpreadsheet\Calculation\Internal;

class MakeMatrix
{
public static function make(...$args): array
{
return $args;
}
}
34 changes: 34 additions & 0 deletions src/PhpSpreadsheet/Calculation/Internal/WildcardMatch.php
@@ -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);
}
}
Expand Up @@ -94,8 +94,6 @@ public function providerDSum()
['>2', 'North'],
],
],
/*
* We don't yet support wildcards in text search fields
[
710000,
$this->database2(),
Expand All @@ -105,7 +103,6 @@ public function providerDSum()
['3', 'C*'],
],
],
*/
[
null,
$this->database1(),
Expand Down

0 comments on commit 25f7dcb

Please sign in to comment.