From 294ba58ddef29907667f535d47d2dcc2f1aa8b8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A4ntz=20Miccoli?= Date: Mon, 10 Dec 2018 16:12:51 +0100 Subject: [PATCH] Exact match in VLOOKUP now returns first match It was inconsistent with spreadsheet software before. Closes #809 --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Calculation/LookupRef.php | 61 ++++++++++++-------- tests/data/Calculation/LookupRef/HLOOKUP.php | 10 ++++ tests/data/Calculation/LookupRef/VLOOKUP.php | 21 +++++++ 4 files changed, 70 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b62c555f..6c888ae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Improve XLSX parsing speed if no readFilter is applied - [#772](https://github.com/PHPOffice/PhpSpreadsheet/issues/772) - Fix column names if read filter calls in XLSX reader skip columns - [#777](https://github.com/PHPOffice/PhpSpreadsheet/pull/777) - Fix LOOKUP function which was breaking on edge cases - [#796](https://github.com/PHPOffice/PhpSpreadsheet/issues/796) +- Fix VLOOKUP with exact matches ## [1.5.2] - 2018-11-25 diff --git a/src/PhpSpreadsheet/Calculation/LookupRef.php b/src/PhpSpreadsheet/Calculation/LookupRef.php index 92115fff..2a3c5582 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef.php @@ -421,7 +421,7 @@ class LookupRef * @param mixed $index_num Specifies which value argument is selected. * Index_num must be a number between 1 and 254, or a formula or reference to a cell containing a number * between 1 and 254. - * @param mixed $value1... Value1 is required, subsequent values are optional. + * @param mixed $value1 ... Value1 is required, subsequent values are optional. * Between 1 to 254 value arguments from which CHOOSE selects a value or an action to perform based on * index_num. The arguments can be numbers, cell references, defined names, formulas, functions, or * text. @@ -709,6 +709,7 @@ class LookupRef $rowNumber = $rowValue = false; foreach ($lookup_array as $rowKey => $rowData) { + // break if we have passed possible keys if ((is_numeric($lookup_value) && is_numeric($rowData[$firstColumn]) && ($rowData[$firstColumn] > $lookup_value)) || (!is_numeric($lookup_value) && !is_numeric($rowData[$firstColumn]) && (strtolower($rowData[$firstColumn]) > strtolower($lookup_value)))) { break; @@ -716,17 +717,25 @@ class LookupRef // remember the last key, but only if datatypes match if ((is_numeric($lookup_value) && is_numeric($rowData[$firstColumn])) || (!is_numeric($lookup_value) && !is_numeric($rowData[$firstColumn]))) { - $rowNumber = $rowKey; - $rowValue = $rowData[$firstColumn]; + if ($not_exact_match) { + $rowNumber = $rowKey; + $rowValue = $rowData[$firstColumn]; + + continue; + } elseif ((strtolower($rowData[$firstColumn]) == strtolower($lookup_value)) + // Spreadsheets software returns first exact match, + // we have sorted and we might have broken key orders + // we want the first one (by its initial index) + && (($rowNumber == false) || ($rowKey < $rowNumber)) + ) { + $rowNumber = $rowKey; + $rowValue = $rowData[$firstColumn]; + } } } if ($rowNumber !== false) { - if ((!$not_exact_match) && ($rowValue != $lookup_value)) { - // if an exact match is required, we have what we need to return an appropriate response - return Functions::NA(); - } - // otherwise return the appropriate value + // return the appropriate value return $lookup_array[$rowNumber][$returnColumn]; } @@ -764,29 +773,35 @@ class LookupRef if ((!is_array($lookup_array[$firstRow])) || ($index_number > count($lookup_array))) { return Functions::REF(); } - $columnKeys = array_keys($lookup_array[$firstRow]); + $firstkey = $f[0] - 1; $returnColumn = $firstkey + $index_number; $firstColumn = array_shift($f); - - if (!$not_exact_match) { - $firstRowH = asort($lookup_array[$firstColumn]); - } - $rowNumber = $rowValue = false; + $rowNumber = null; foreach ($lookup_array[$firstColumn] as $rowKey => $rowData) { - if ((is_numeric($lookup_value) && is_numeric($rowData) && ($rowData > $lookup_value)) || - (!is_numeric($lookup_value) && !is_numeric($rowData) && (strtolower($rowData) > strtolower($lookup_value)))) { + // break if we have passed possible keys + $bothNumeric = is_numeric($lookup_value) && is_numeric($rowData); + $bothNotNumeric = !is_numeric($lookup_value) && !is_numeric($rowData); + if (($bothNumeric && $rowData > $lookup_value) || + ($bothNotNumeric && strtolower($rowData) > strtolower($lookup_value))) { break; } - $rowNumber = $rowKey; - $rowValue = $rowData; + + // Remember the last key, but only if datatypes match (as in VLOOKUP) + if ($bothNumeric || $bothNotNumeric) { + if ($not_exact_match) { + $rowNumber = $rowKey; + + continue; + } elseif (strtolower($rowData) === strtolower($lookup_value) + && ($rowNumber === null || $rowKey < $rowNumber) + ) { + $rowNumber = $rowKey; + } + } } - if ($rowNumber !== false) { - if ((!$not_exact_match) && ($rowValue != $lookup_value)) { - // if an exact match is required, we have what we need to return an appropriate response - return Functions::NA(); - } + if ($rowNumber !== null) { // otherwise return the appropriate value return $lookup_array[$returnColumn][$rowNumber]; } diff --git a/tests/data/Calculation/LookupRef/HLOOKUP.php b/tests/data/Calculation/LookupRef/HLOOKUP.php index 8733fa78..25bcea87 100644 --- a/tests/data/Calculation/LookupRef/HLOOKUP.php +++ b/tests/data/Calculation/LookupRef/HLOOKUP.php @@ -275,4 +275,14 @@ return [ 2, true, ], + [ + 5, + 'x', + [ + ['Selection column', '0', '0', '0', '0', 'x', 'x', 'x', 'x', 'x'], + ['Value to retrieve', 1, 2, 3, 4, 5, 6, 7, 8, 9] + ], + 2, + false + ] ]; diff --git a/tests/data/Calculation/LookupRef/VLOOKUP.php b/tests/data/Calculation/LookupRef/VLOOKUP.php index 2ef64d06..1ba30a47 100644 --- a/tests/data/Calculation/LookupRef/VLOOKUP.php +++ b/tests/data/Calculation/LookupRef/VLOOKUP.php @@ -291,4 +291,25 @@ return [ 2, true, ], + [ + 5, + 'x', + [ + [ + 'Selection column', + 'Value to retrieve', + ], + ['0', 1], + ['0', 2], + ['0', 3], + ['0', 4], + ['x', 5], + ['x', 6], + ['x', 7], + ['x', 8], + ['x', 9], + ], + 2, + false + ] ];