Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MATCH function fix #1122

Merged
merged 7 commits into from
Aug 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
- Add MAXIFS, MINIFS, COUNTIFS and Remove MINIF, MAXIF - [Issue #1056](https://github.com/PHPOffice/PhpSpreadsheet/issues/1056)
- HLookup needs an ordered list even if range_lookup is set to false [Issue #1055](https://github.com/PHPOffice/PhpSpreadsheet/issues/1055) and [PR #1076](https://github.com/PHPOffice/PhpSpreadsheet/pull/1076)
- Improve performance of IF function calls via ranch pruning to avoid resolution of every branches [#844](https://github.com/PHPOffice/PhpSpreadsheet/pull/844)
- MATCH function supports `*?~` Excel functionality, when match_type=0 - [Issue #1116](https://github.com/PHPOffice/PhpSpreadsheet/issues/1116)

### Fixed

Expand All @@ -26,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
- Cover `getSheetByName()` with tests for name with quote and spaces - [#739](https://github.com/PHPOffice/PhpSpreadsheet/issues/739)
- Best effort to support invalid colspan values in HTML reader - [878](https://github.com/PHPOffice/PhpSpreadsheet/pull/878)
- Fixes incorrect rows deletion [#868](https://github.com/PHPOffice/PhpSpreadsheet/issues/868)
- MATCH function fix (value search by type, stop search when match_type=-1 and unordered element encountered) - [Issue #1116](https://github.com/PHPOffice/PhpSpreadsheet/issues/1116)

## [1.8.2] - 2019-07-08

Expand Down
88 changes: 64 additions & 24 deletions src/PhpSpreadsheet/Calculation/LookupRef.php
Original file line number Diff line number Diff line change
Expand Up @@ -464,19 +464,21 @@ public static function CHOOSE(...$chooseArgs)
*
* @param mixed $lookupValue The value that you want to match in lookup_array
* @param mixed $lookupArray The range of cells being searched
* @param mixed $matchType The number -1, 0, or 1. -1 means above, 0 means exact match, 1 means below. If match_type is 1 or -1, the list has to be ordered.
* @param mixed $matchType The number -1, 0, or 1. -1 means above, 0 means exact match, 1 means below.
* If match_type is 1 or -1, the list has to be ordered.
*
* @return int The relative position of the found item
* @return int|string The relative position of the found item
*/
public static function MATCH($lookupValue, $lookupArray, $matchType = 1)
{
$lookupArray = Functions::flattenArray($lookupArray);
$lookupValue = Functions::flattenSingleValue($lookupValue);
$matchType = ($matchType === null) ? 1 : (int) Functions::flattenSingleValue($matchType);

$initialLookupValue = $lookupValue;
// MATCH is not case sensitive
$lookupValue = StringHelper::strToLower($lookupValue);
// MATCH is not case sensitive, so we convert lookup value to be lower cased in case it's string type.
if (is_string($lookupValue)) {
$lookupValue = StringHelper::strToLower($lookupValue);
}

// Lookup_value type has to be number, text, or logical values
if ((!is_numeric($lookupValue)) && (!is_string($lookupValue)) && (!is_bool($lookupValue))) {
Expand Down Expand Up @@ -522,43 +524,81 @@ public static function MATCH($lookupValue, $lookupArray, $matchType = 1)
// find the match
// **

if ($matchType == 0 || $matchType == 1) {
if ($matchType === 0 || $matchType === 1) {
foreach ($lookupArray as $i => $lookupArrayValue) {
$onlyNumeric = is_numeric($lookupArrayValue) && is_numeric($lookupValue);
$onlyNumericExactMatch = $onlyNumeric && $lookupArrayValue == $lookupValue;
$nonOnlyNumericExactMatch = !$onlyNumeric && $lookupArrayValue === $lookupValue;
$exactMatch = $onlyNumericExactMatch || $nonOnlyNumericExactMatch;
if (($matchType == 0) && $exactMatch) {
// exact match
return $i + 1;
} elseif (($matchType == 1) && ($lookupArrayValue <= $lookupValue)) {
$typeMatch = gettype($lookupValue) === gettype($lookupArrayValue);
$exactTypeMatch = $typeMatch && $lookupArrayValue === $lookupValue;
$nonOnlyNumericExactMatch = !$typeMatch && $lookupArrayValue === $lookupValue;
$exactMatch = $exactTypeMatch || $nonOnlyNumericExactMatch;

if ($matchType === 0) {
if ($typeMatch && is_string($lookupValue) && (bool) preg_match('/([\?\*])/', $lookupValue)) {
$splitString = $lookupValue;
$chars = array_map(function ($i) use ($splitString) {
return mb_substr($splitString, $i, 1);
}, range(0, mb_strlen($splitString) - 1));

$length = count($chars);
$pattern = '/^';
for ($j = 0; $j < $length; ++$j) {
if ($chars[$j] === '~') {
if (isset($chars[$j + 1])) {
if ($chars[$j + 1] === '*') {
$pattern .= preg_quote($chars[$j + 1], '/');
++$j;
} elseif ($chars[$j + 1] === '?') {
$pattern .= preg_quote($chars[$j + 1], '/');
++$j;
}
} else {
$pattern .= preg_quote($chars[$j], '/');
}
} elseif ($chars[$j] === '*') {
$pattern .= '.*';
} elseif ($chars[$j] === '?') {
$pattern .= '.{1}';
} else {
$pattern .= preg_quote($chars[$j], '/');
}
}

$pattern .= '$/';
if ((bool) preg_match($pattern, $lookupArrayValue)) {
// exact match
return $i + 1;
}
} elseif ($exactMatch) {
// exact match
return $i + 1;
}
} elseif (($matchType === 1) && $typeMatch && ($lookupArrayValue <= $lookupValue)) {
$i = array_search($i, $keySet);

// The current value is the (first) match
return $i + 1;
}
}
} else {
// matchType = -1

// "Special" case: since the array it's supposed to be ordered in descending order, the
// Excel algorithm gives up immediately if the first element is smaller than the searched value
if ($lookupArray[0] < $lookupValue) {
return Functions::NA();
}

$maxValueKey = null;

// The basic algorithm is:
// Iterate and keep the highest match until the next element is smaller than the searched value.
// Return immediately if perfect match is found
foreach ($lookupArray as $i => $lookupArrayValue) {
if ($lookupArrayValue == $lookupValue) {
$typeMatch = gettype($lookupValue) === gettype($lookupArrayValue);
$exactTypeMatch = $typeMatch && $lookupArrayValue === $lookupValue;
$nonOnlyNumericExactMatch = !$typeMatch && $lookupArrayValue === $lookupValue;
$exactMatch = $exactTypeMatch || $nonOnlyNumericExactMatch;

if ($exactMatch) {
// Another "special" case. If a perfect match is found,
// the algorithm gives up immediately
return $i + 1;
} elseif ($lookupArrayValue >= $lookupValue) {
} elseif ($typeMatch & $lookupArrayValue >= $lookupValue) {
$maxValueKey = $i + 1;
} elseif ($typeMatch & $lookupArrayValue < $lookupValue) {
//Excel algorithm gives up immediately if the first element is smaller than the searched value
break;
}
}

Expand Down
142 changes: 141 additions & 1 deletion tests/data/Calculation/LookupRef/MATCH.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,145 @@
[[0], [0], ['x'], ['x'], ['x']],
0
],

[
2,
'a',
[false, 'a',1],
-1
],
[
'#N/A', // Expected
0,
['x', true, false],
-1
],
[
'#N/A', // Expected
true,
['a', 'b', 'c'],
-1
],
[
'#N/A', // Expected
true,
[0,1,2],
-1
],
[
'#N/A', // Expected
true,
[0,1,2],
0
],
[
'#N/A', // Expected
true,
[0,1,2],
1
],
[
1, // Expected
true,
[true,true,true],
-1
],
[
1, // Expected
true,
[true,true,true],
0
],
[
3, // Expected
true,
[true,true,true],
1
],
// lookup stops when value < searched one
[
5, // Expected
6,
[true, false, 'a', 'z', 222222, 2, 99999999],
-1
],
// if element of same data type met and it is < than searched one #N/A - no further processing
[
'#N/A', // Expected
6,
[true, false, 'a', 'z', 2, 888 ],
-1
],
[
'#N/A', // Expected
6,
['6'],
-1
],
// expression match
[
2, // Expected
'a?b',
['a', 'abb', 'axc'],
0
],
[
1, // Expected
'a*',
['aAAAAAAA', 'as', 'az'],
0
],
[
3, // Expected
'1*11*1',
['abc', 'efh', '1a11b1'],
0
],
[
3, // Expected
'1*11*1',
['abc', 'efh', '1a11b1'],
0
],
[
2, // Expected
'a*~*c',
['aAAAAA', 'a123456*c', 'az'],
0
],
[
3, // Expected
'a*123*b',
['aAAAAA', 'a123456*c', 'a99999123b'],
0
],
[
1, // Expected
'*',
['aAAAAA', 'a111123456*c', 'qq'],
0
],
[
2, // Expected
'?',
['aAAAAA', 'a', 'a99999123b'],
0
],
[
'#N/A', // Expected
'?',
[1, 22,333],
0
],
[
3, // Expected
'???',
[1, 22,'aaa'],
0
],
[
3, // Expected
'*',
[1, 22,'aaa'],
0
],
];