Skip to content

Commit 9df68f1

Browse files
rolandsusansPowerKiKi
authored andcommitted
MATCH function fix
- fix boolean search - add support for excel expressions `*?~` Fixes #1116 Closes #1122
1 parent 2166458 commit 9df68f1

File tree

3 files changed

+207
-25
lines changed

3 files changed

+207
-25
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
1313
- Add MAXIFS, MINIFS, COUNTIFS and Remove MINIF, MAXIF - [Issue #1056](https://github.com/PHPOffice/PhpSpreadsheet/issues/1056)
1414
- 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)
1515
- Improve performance of IF function calls via ranch pruning to avoid resolution of every branches [#844](https://github.com/PHPOffice/PhpSpreadsheet/pull/844)
16+
- MATCH function supports `*?~` Excel functionality, when match_type=0 - [Issue #1116](https://github.com/PHPOffice/PhpSpreadsheet/issues/1116)
1617

1718
### Fixed
1819

@@ -26,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
2627
- Cover `getSheetByName()` with tests for name with quote and spaces - [#739](https://github.com/PHPOffice/PhpSpreadsheet/issues/739)
2728
- Best effort to support invalid colspan values in HTML reader - [878](https://github.com/PHPOffice/PhpSpreadsheet/pull/878)
2829
- Fixes incorrect rows deletion [#868](https://github.com/PHPOffice/PhpSpreadsheet/issues/868)
30+
- 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)
2931

3032
## [1.8.2] - 2019-07-08
3133

src/PhpSpreadsheet/Calculation/LookupRef.php

+64-24
Original file line numberDiff line numberDiff line change
@@ -464,19 +464,21 @@ public static function CHOOSE(...$chooseArgs)
464464
*
465465
* @param mixed $lookupValue The value that you want to match in lookup_array
466466
* @param mixed $lookupArray The range of cells being searched
467-
* @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.
467+
* @param mixed $matchType The number -1, 0, or 1. -1 means above, 0 means exact match, 1 means below.
468+
* If match_type is 1 or -1, the list has to be ordered.
468469
*
469-
* @return int The relative position of the found item
470+
* @return int|string The relative position of the found item
470471
*/
471472
public static function MATCH($lookupValue, $lookupArray, $matchType = 1)
472473
{
473474
$lookupArray = Functions::flattenArray($lookupArray);
474475
$lookupValue = Functions::flattenSingleValue($lookupValue);
475476
$matchType = ($matchType === null) ? 1 : (int) Functions::flattenSingleValue($matchType);
476477

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

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

525-
if ($matchType == 0 || $matchType == 1) {
527+
if ($matchType === 0 || $matchType === 1) {
526528
foreach ($lookupArray as $i => $lookupArrayValue) {
527-
$onlyNumeric = is_numeric($lookupArrayValue) && is_numeric($lookupValue);
528-
$onlyNumericExactMatch = $onlyNumeric && $lookupArrayValue == $lookupValue;
529-
$nonOnlyNumericExactMatch = !$onlyNumeric && $lookupArrayValue === $lookupValue;
530-
$exactMatch = $onlyNumericExactMatch || $nonOnlyNumericExactMatch;
531-
if (($matchType == 0) && $exactMatch) {
532-
// exact match
533-
return $i + 1;
534-
} elseif (($matchType == 1) && ($lookupArrayValue <= $lookupValue)) {
529+
$typeMatch = gettype($lookupValue) === gettype($lookupArrayValue);
530+
$exactTypeMatch = $typeMatch && $lookupArrayValue === $lookupValue;
531+
$nonOnlyNumericExactMatch = !$typeMatch && $lookupArrayValue === $lookupValue;
532+
$exactMatch = $exactTypeMatch || $nonOnlyNumericExactMatch;
533+
534+
if ($matchType === 0) {
535+
if ($typeMatch && is_string($lookupValue) && (bool) preg_match('/([\?\*])/', $lookupValue)) {
536+
$splitString = $lookupValue;
537+
$chars = array_map(function ($i) use ($splitString) {
538+
return mb_substr($splitString, $i, 1);
539+
}, range(0, mb_strlen($splitString) - 1));
540+
541+
$length = count($chars);
542+
$pattern = '/^';
543+
for ($j = 0; $j < $length; ++$j) {
544+
if ($chars[$j] === '~') {
545+
if (isset($chars[$j + 1])) {
546+
if ($chars[$j + 1] === '*') {
547+
$pattern .= preg_quote($chars[$j + 1], '/');
548+
++$j;
549+
} elseif ($chars[$j + 1] === '?') {
550+
$pattern .= preg_quote($chars[$j + 1], '/');
551+
++$j;
552+
}
553+
} else {
554+
$pattern .= preg_quote($chars[$j], '/');
555+
}
556+
} elseif ($chars[$j] === '*') {
557+
$pattern .= '.*';
558+
} elseif ($chars[$j] === '?') {
559+
$pattern .= '.{1}';
560+
} else {
561+
$pattern .= preg_quote($chars[$j], '/');
562+
}
563+
}
564+
565+
$pattern .= '$/';
566+
if ((bool) preg_match($pattern, $lookupArrayValue)) {
567+
// exact match
568+
return $i + 1;
569+
}
570+
} elseif ($exactMatch) {
571+
// exact match
572+
return $i + 1;
573+
}
574+
} elseif (($matchType === 1) && $typeMatch && ($lookupArrayValue <= $lookupValue)) {
535575
$i = array_search($i, $keySet);
536576

537577
// The current value is the (first) match
538578
return $i + 1;
539579
}
540580
}
541581
} else {
542-
// matchType = -1
543-
544-
// "Special" case: since the array it's supposed to be ordered in descending order, the
545-
// Excel algorithm gives up immediately if the first element is smaller than the searched value
546-
if ($lookupArray[0] < $lookupValue) {
547-
return Functions::NA();
548-
}
549-
550582
$maxValueKey = null;
551583

552584
// The basic algorithm is:
553585
// Iterate and keep the highest match until the next element is smaller than the searched value.
554586
// Return immediately if perfect match is found
555587
foreach ($lookupArray as $i => $lookupArrayValue) {
556-
if ($lookupArrayValue == $lookupValue) {
588+
$typeMatch = gettype($lookupValue) === gettype($lookupArrayValue);
589+
$exactTypeMatch = $typeMatch && $lookupArrayValue === $lookupValue;
590+
$nonOnlyNumericExactMatch = !$typeMatch && $lookupArrayValue === $lookupValue;
591+
$exactMatch = $exactTypeMatch || $nonOnlyNumericExactMatch;
592+
593+
if ($exactMatch) {
557594
// Another "special" case. If a perfect match is found,
558595
// the algorithm gives up immediately
559596
return $i + 1;
560-
} elseif ($lookupArrayValue >= $lookupValue) {
597+
} elseif ($typeMatch & $lookupArrayValue >= $lookupValue) {
561598
$maxValueKey = $i + 1;
599+
} elseif ($typeMatch & $lookupArrayValue < $lookupValue) {
600+
//Excel algorithm gives up immediately if the first element is smaller than the searched value
601+
break;
562602
}
563603
}
564604

tests/data/Calculation/LookupRef/MATCH.php

+141-1
Original file line numberDiff line numberDiff line change
@@ -104,5 +104,145 @@
104104
[[0], [0], ['x'], ['x'], ['x']],
105105
0
106106
],
107-
107+
[
108+
2,
109+
'a',
110+
[false, 'a',1],
111+
-1
112+
],
113+
[
114+
'#N/A', // Expected
115+
0,
116+
['x', true, false],
117+
-1
118+
],
119+
[
120+
'#N/A', // Expected
121+
true,
122+
['a', 'b', 'c'],
123+
-1
124+
],
125+
[
126+
'#N/A', // Expected
127+
true,
128+
[0,1,2],
129+
-1
130+
],
131+
[
132+
'#N/A', // Expected
133+
true,
134+
[0,1,2],
135+
0
136+
],
137+
[
138+
'#N/A', // Expected
139+
true,
140+
[0,1,2],
141+
1
142+
],
143+
[
144+
1, // Expected
145+
true,
146+
[true,true,true],
147+
-1
148+
],
149+
[
150+
1, // Expected
151+
true,
152+
[true,true,true],
153+
0
154+
],
155+
[
156+
3, // Expected
157+
true,
158+
[true,true,true],
159+
1
160+
],
161+
// lookup stops when value < searched one
162+
[
163+
5, // Expected
164+
6,
165+
[true, false, 'a', 'z', 222222, 2, 99999999],
166+
-1
167+
],
168+
// if element of same data type met and it is < than searched one #N/A - no further processing
169+
[
170+
'#N/A', // Expected
171+
6,
172+
[true, false, 'a', 'z', 2, 888 ],
173+
-1
174+
],
175+
[
176+
'#N/A', // Expected
177+
6,
178+
['6'],
179+
-1
180+
],
181+
// expression match
182+
[
183+
2, // Expected
184+
'a?b',
185+
['a', 'abb', 'axc'],
186+
0
187+
],
188+
[
189+
1, // Expected
190+
'a*',
191+
['aAAAAAAA', 'as', 'az'],
192+
0
193+
],
194+
[
195+
3, // Expected
196+
'1*11*1',
197+
['abc', 'efh', '1a11b1'],
198+
0
199+
],
200+
[
201+
3, // Expected
202+
'1*11*1',
203+
['abc', 'efh', '1a11b1'],
204+
0
205+
],
206+
[
207+
2, // Expected
208+
'a*~*c',
209+
['aAAAAA', 'a123456*c', 'az'],
210+
0
211+
],
212+
[
213+
3, // Expected
214+
'a*123*b',
215+
['aAAAAA', 'a123456*c', 'a99999123b'],
216+
0
217+
],
218+
[
219+
1, // Expected
220+
'*',
221+
['aAAAAA', 'a111123456*c', 'qq'],
222+
0
223+
],
224+
[
225+
2, // Expected
226+
'?',
227+
['aAAAAA', 'a', 'a99999123b'],
228+
0
229+
],
230+
[
231+
'#N/A', // Expected
232+
'?',
233+
[1, 22,333],
234+
0
235+
],
236+
[
237+
3, // Expected
238+
'???',
239+
[1, 22,'aaa'],
240+
0
241+
],
242+
[
243+
3, // Expected
244+
'*',
245+
[1, 22,'aaa'],
246+
0
247+
],
108248
];

0 commit comments

Comments
 (0)