Initial work on implementing Array-enabled for the HLOOKUP() and VLOOKUP() functions (#2611)

* Initial work on implementing Array-enabled for the HLOOKUP() and VLOOKUP() functions

* In the MATCH() function, we should also use `evaluateArrayArgumentsIgnore()` because the lookupvalue and matchType arguments can be array arguments, but lookupArray is always a dataset matrix
This commit is contained in:
Mark Baker 2022-02-21 19:56:21 +01:00 committed by GitHub
parent db21d043fe
commit c9f948bd91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 136 additions and 58 deletions

View File

@ -765,26 +765,11 @@ parameters:
count: 1 count: 1
path: src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php path: src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php
-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\LookupBase\\:\\:checkMatch\\(\\) has parameter \\$notExactMatch with no type specified\\.$#"
count: 1
path: src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php
-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\LookupBase\\:\\:validateIndexLookup\\(\\) has no return type specified\\.$#"
count: 1
path: src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php
- -
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\LookupBase\\:\\:validateIndexLookup\\(\\) has parameter \\$index_number with no type specified\\.$#" message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\LookupBase\\:\\:validateIndexLookup\\(\\) has parameter \\$index_number with no type specified\\.$#"
count: 1 count: 1
path: src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php path: src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php
-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\LookupBase\\:\\:validateIndexLookup\\(\\) has parameter \\$lookup_array with no type specified\\.$#"
count: 1
path: src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php
- -
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\Matrix\\:\\:extractRowValue\\(\\) has no return type specified\\.$#" message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\Matrix\\:\\:extractRowValue\\(\\) has no return type specified\\.$#"
count: 1 count: 1
@ -845,31 +830,6 @@ parameters:
count: 1 count: 1
path: src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php path: src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php
-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\VLookup\\:\\:vLookupSearch\\(\\) has no return type specified\\.$#"
count: 1
path: src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php
-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\VLookup\\:\\:vLookupSearch\\(\\) has parameter \\$column with no type specified\\.$#"
count: 1
path: src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php
-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\VLookup\\:\\:vLookupSearch\\(\\) has parameter \\$lookupArray with no type specified\\.$#"
count: 1
path: src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php
-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\VLookup\\:\\:vLookupSearch\\(\\) has parameter \\$lookupValue with no type specified\\.$#"
count: 1
path: src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php
-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\VLookup\\:\\:vLookupSearch\\(\\) has parameter \\$notExactMatch with no type specified\\.$#"
count: 1
path: src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php
- -
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\VLookup\\:\\:vlookupSort\\(\\) has no return type specified\\.$#" message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\VLookup\\:\\:vlookupSort\\(\\) has no return type specified\\.$#"
count: 1 count: 1

View File

@ -20,6 +20,11 @@ trait ArrayEnabled
self::$arrayArgumentHelper->initialise($arguments); self::$arrayArgumentHelper->initialise($arguments);
} }
/**
* Handles array argument processing when the function accepts a single argument that can be an array argument.
* Example use for:
* DAYOFMONTH() or FACT().
*/
protected static function evaluateSingleArgumentArray(callable $method, array $values): array protected static function evaluateSingleArgumentArray(callable $method, array $values): array
{ {
$result = []; $result = [];
@ -31,6 +36,11 @@ trait ArrayEnabled
} }
/** /**
* Handles array argument processing when the function accepts multiple arguments,
* and any of them can be an array argument.
* Example use for:
* ROUND() or DATE().
*
* @param mixed ...$arguments * @param mixed ...$arguments
*/ */
protected static function evaluateArrayArguments(callable $method, ...$arguments): array protected static function evaluateArrayArguments(callable $method, ...$arguments): array
@ -42,6 +52,12 @@ trait ArrayEnabled
} }
/** /**
* Handles array argument processing when the function accepts multiple arguments,
* but only the first few (up to limit) can be an array arguments.
* Example use for:
* NETWORKDAYS() or CONCATENATE(), where the last argument is a matrix (or a series of values) that need
* to be treated as a such rather than as an array arguments.
*
* @param mixed ...$arguments * @param mixed ...$arguments
*/ */
protected static function evaluateArrayArgumentsSubset(callable $method, int $limit, ...$arguments): array protected static function evaluateArrayArgumentsSubset(callable $method, int $limit, ...$arguments): array
@ -55,6 +71,12 @@ trait ArrayEnabled
} }
/** /**
* Handles array argument processing when the function accepts multiple arguments,
* but only the last few (from start) can be an array arguments.
* Example use for:
* Z.TEST() or INDEX(), where the first argument 1 is a matrix that needs to be treated as a dataset
* rather than as an array argument.
*
* @param mixed ...$arguments * @param mixed ...$arguments
*/ */
protected static function evaluateArrayArgumentsSubsetFrom(callable $method, int $start, ...$arguments): array protected static function evaluateArrayArgumentsSubsetFrom(callable $method, int $start, ...$arguments): array
@ -74,4 +96,27 @@ trait ArrayEnabled
return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments); return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments);
} }
/**
* Handles array argument processing when the function accepts multiple arguments,
* and any of them can be an array argument except for the one specified by ignore.
* Example use for:
* HLOOKUP() and VLOOKUP(), where argument 1 is a matrix that needs to be treated as a database
* rather than as an array argument.
*
* @param mixed ...$arguments
*/
protected static function evaluateArrayArgumentsIgnore(callable $method, int $ignore, ...$arguments): array
{
$leadingArguments = array_slice($arguments, 0, $ignore);
$ignoreArgument = array_slice($arguments, $ignore, 1);
$trailingArguments = array_slice($arguments, $ignore + 1);
self::initialiseHelper(array_merge($leadingArguments, [[null]], $trailingArguments));
$arguments = self::$arrayArgumentHelper->arguments();
array_splice($arguments, $ignore, 1, $ignoreArgument);
return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments);
}
} }

View File

@ -35,7 +35,7 @@ class ExcelMatch
public static function MATCH($lookupValue, $lookupArray, $matchType = self::MATCHTYPE_LARGEST_VALUE) public static function MATCH($lookupValue, $lookupArray, $matchType = self::MATCHTYPE_LARGEST_VALUE)
{ {
if (is_array($lookupValue)) { if (is_array($lookupValue)) {
return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $matchType); return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $matchType);
} }
$lookupArray = Functions::flattenArray($lookupArray); $lookupArray = Functions::flattenArray($lookupArray);

View File

@ -2,14 +2,16 @@
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class HLookup extends LookupBase class HLookup extends LookupBase
{ {
use ArrayEnabled;
/** /**
* HLOOKUP * HLOOKUP
* The HLOOKUP function searches for value in the top-most row of lookup_array and returns the value * The HLOOKUP function searches for value in the top-most row of lookup_array and returns the value
@ -25,9 +27,11 @@ class HLookup extends LookupBase
*/ */
public static function lookup($lookupValue, $lookupArray, $indexNumber, $notExactMatch = true) public static function lookup($lookupValue, $lookupArray, $indexNumber, $notExactMatch = true)
{ {
$lookupValue = Functions::flattenSingleValue($lookupValue); if (is_array($lookupValue)) {
$indexNumber = Functions::flattenSingleValue($indexNumber); return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $indexNumber, $notExactMatch);
$notExactMatch = ($notExactMatch === null) ? true : Functions::flattenSingleValue($notExactMatch); }
$notExactMatch = (bool) ($notExactMatch ?? true);
$lookupArray = self::convertLiteralArray($lookupArray); $lookupArray = self::convertLiteralArray($lookupArray);
try { try {
@ -44,7 +48,7 @@ class HLookup extends LookupBase
$firstkey = $f[0] - 1; $firstkey = $f[0] - 1;
$returnColumn = $firstkey + $indexNumber; $returnColumn = $firstkey + $indexNumber;
$firstColumn = array_shift($f); $firstColumn = array_shift($f) ?? 1;
$rowNumber = self::hLookupSearch($lookupValue, $lookupArray, $firstColumn, $notExactMatch); $rowNumber = self::hLookupSearch($lookupValue, $lookupArray, $firstColumn, $notExactMatch);
if ($rowNumber !== null) { if ($rowNumber !== null) {
@ -57,10 +61,9 @@ class HLookup extends LookupBase
/** /**
* @param mixed $lookupValue The value that you want to match in lookup_array * @param mixed $lookupValue The value that you want to match in lookup_array
* @param mixed $column The column to look up * @param int|string $column
* @param mixed $notExactMatch determines if you are looking for an exact match based on lookup_value
*/ */
private static function hLookupSearch($lookupValue, array $lookupArray, $column, $notExactMatch): ?int private static function hLookupSearch($lookupValue, array $lookupArray, $column, bool $notExactMatch): ?int
{ {
$lookupLower = StringHelper::strToLower($lookupValue); $lookupLower = StringHelper::strToLower($lookupValue);

View File

@ -7,7 +7,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
abstract class LookupBase abstract class LookupBase
{ {
protected static function validateIndexLookup($lookup_array, $index_number) protected static function validateIndexLookup(array $lookup_array, $index_number): int
{ {
// index_number must be a number greater than or equal to 1 // index_number must be a number greater than or equal to 1
if (!is_numeric($index_number) || $index_number < 1) { if (!is_numeric($index_number) || $index_number < 1) {
@ -25,7 +25,7 @@ abstract class LookupBase
protected static function checkMatch( protected static function checkMatch(
bool $bothNumeric, bool $bothNumeric,
bool $bothNotNumeric, bool $bothNotNumeric,
$notExactMatch, bool $notExactMatch,
int $rowKey, int $rowKey,
string $cellDataLower, string $cellDataLower,
string $lookupLower, string $lookupLower,

View File

@ -2,13 +2,15 @@
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class VLookup extends LookupBase class VLookup extends LookupBase
{ {
use ArrayEnabled;
/** /**
* VLOOKUP * VLOOKUP
* The VLOOKUP function searches for value in the left-most column of lookup_array and returns the value * The VLOOKUP function searches for value in the left-most column of lookup_array and returns the value
@ -24,9 +26,11 @@ class VLookup extends LookupBase
*/ */
public static function lookup($lookupValue, $lookupArray, $indexNumber, $notExactMatch = true) public static function lookup($lookupValue, $lookupArray, $indexNumber, $notExactMatch = true)
{ {
$lookupValue = Functions::flattenSingleValue($lookupValue); if (is_array($lookupValue)) {
$indexNumber = Functions::flattenSingleValue($indexNumber); return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $indexNumber, $notExactMatch);
$notExactMatch = ($notExactMatch === null) ? true : Functions::flattenSingleValue($notExactMatch); }
$notExactMatch = (bool) ($notExactMatch ?? true);
try { try {
$indexNumber = self::validateIndexLookup($lookupArray, $indexNumber); $indexNumber = self::validateIndexLookup($lookupArray, $indexNumber);
@ -41,7 +45,7 @@ class VLookup extends LookupBase
} }
$columnKeys = array_keys($lookupArray[$firstRow]); $columnKeys = array_keys($lookupArray[$firstRow]);
$returnColumn = $columnKeys[--$indexNumber]; $returnColumn = $columnKeys[--$indexNumber];
$firstColumn = array_shift($columnKeys); $firstColumn = array_shift($columnKeys) ?? 1;
if (!$notExactMatch) { if (!$notExactMatch) {
uasort($lookupArray, ['self', 'vlookupSort']); uasort($lookupArray, ['self', 'vlookupSort']);
@ -71,7 +75,11 @@ class VLookup extends LookupBase
return ($aLower < $bLower) ? -1 : 1; return ($aLower < $bLower) ? -1 : 1;
} }
private static function vLookupSearch($lookupValue, $lookupArray, $column, $notExactMatch) /**
* @param mixed $lookupValue The value that you want to match in lookup_array
* @param int|string $column
*/
private static function vLookupSearch($lookupValue, array $lookupArray, $column, bool $notExactMatch): ?int
{ {
$lookupLower = StringHelper::strToLower($lookupValue); $lookupLower = StringHelper::strToLower($lookupValue);

View File

@ -2,6 +2,7 @@
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Spreadsheet;
@ -89,4 +90,40 @@ class HLookupTest extends TestCase
); );
self::assertSame($expectedResult, $result); self::assertSame($expectedResult, $result);
} }
/**
* @dataProvider providerHLookupArray
*/
public function testHLookupArray(array $expectedResult, string $values, string $database, string $index): void
{
$calculation = Calculation::getInstance();
$formula = "=HLOOKUP({$values}, {$database}, {$index}, false)";
$result = $calculation->_calculateFormulaValue($formula);
self::assertEquals($expectedResult, $result);
}
public function providerHLookupArray(): array
{
return [
'row vector #1' => [
[[4, 9]],
'{"Axles", "Bolts"}',
'{"Axles", "Bearings", "Bolts"; 4, 4, 9; 5, 7, 10; 6, 8, 11}',
'2',
],
'row vector #2' => [
[[5, 7]],
'{"Axles", "Bearings"}',
'{"Axles", "Bearings", "Bolts"; 4, 4, 9; 5, 7, 10; 6, 8, 11}',
'3',
],
'row/column vectors' => [
[[4, 9], [5, 10]],
'{"Axles", "Bolts"}',
'{"Axles", "Bearings", "Bolts"; 4, 4, 9; 5, 7, 10; 6, 8, 11}',
'{2; 3}',
],
];
}
} }

View File

@ -2,6 +2,7 @@
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@ -28,4 +29,28 @@ class VLookupTest extends TestCase
{ {
return require 'tests/data/Calculation/LookupRef/VLOOKUP.php'; return require 'tests/data/Calculation/LookupRef/VLOOKUP.php';
} }
/**
* @dataProvider providerVLookupArray
*/
public function testVLookupArray(array $expectedResult, string $values, string $database, string $index): void
{
$calculation = Calculation::getInstance();
$formula = "=VLOOKUP({$values}, {$database}, {$index}, false)";
$result = $calculation->_calculateFormulaValue($formula);
self::assertEquals($expectedResult, $result);
}
public function providerVLookupArray(): array
{
return [
'row vector' => [
[[4.19, 5.77, 4.14]],
'{"Orange", "Green", "Red"}',
'{"Red", 4.14; "Orange", 4.19; "Yellow", 5.17; "Green", 5.77; "Blue", 6.39}',
'2',
],
];
}
} }

View File

@ -12,7 +12,7 @@ class AbsTest extends AllSetupTeardown
* @param mixed $expectedResult * @param mixed $expectedResult
* @param mixed $number * @param mixed $number
*/ */
public function testRound($expectedResult, $number = 'omitted'): void public function testAbs($expectedResult, $number = 'omitted'): void
{ {
$sheet = $this->getSheet(); $sheet = $this->getSheet();
$this->mightHaveException($expectedResult); $this->mightHaveException($expectedResult);