diff --git a/CHANGELOG.md b/CHANGELOG.md index b28a0c99..20d26f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/src/PhpSpreadsheet/Calculation/LookupRef.php b/src/PhpSpreadsheet/Calculation/LookupRef.php index 5bc0a435..48434300 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef.php @@ -464,9 +464,10 @@ class LookupRef * * @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) { @@ -474,9 +475,10 @@ class LookupRef $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))) { @@ -522,16 +524,54 @@ class LookupRef // 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 @@ -539,26 +579,26 @@ class LookupRef } } } 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; } } diff --git a/tests/data/Calculation/LookupRef/MATCH.php b/tests/data/Calculation/LookupRef/MATCH.php index 84644949..54ff9f1f 100644 --- a/tests/data/Calculation/LookupRef/MATCH.php +++ b/tests/data/Calculation/LookupRef/MATCH.php @@ -104,5 +104,145 @@ return [ [[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 + ], ];