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
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\\.$#"
count: 1
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\\.$#"
count: 1
@ -845,31 +830,6 @@ parameters:
count: 1
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\\.$#"
count: 1

View File

@ -20,6 +20,11 @@ trait ArrayEnabled
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
{
$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
*/
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
*/
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
*/
protected static function evaluateArrayArgumentsSubsetFrom(callable $method, int $start, ...$arguments): array
@ -74,4 +96,27 @@ trait ArrayEnabled
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)
{
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);

View File

@ -2,14 +2,16 @@
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class HLookup extends LookupBase
{
use ArrayEnabled;
/**
* HLOOKUP
* 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)
{
$lookupValue = Functions::flattenSingleValue($lookupValue);
$indexNumber = Functions::flattenSingleValue($indexNumber);
$notExactMatch = ($notExactMatch === null) ? true : Functions::flattenSingleValue($notExactMatch);
if (is_array($lookupValue)) {
return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $indexNumber, $notExactMatch);
}
$notExactMatch = (bool) ($notExactMatch ?? true);
$lookupArray = self::convertLiteralArray($lookupArray);
try {
@ -44,7 +48,7 @@ class HLookup extends LookupBase
$firstkey = $f[0] - 1;
$returnColumn = $firstkey + $indexNumber;
$firstColumn = array_shift($f);
$firstColumn = array_shift($f) ?? 1;
$rowNumber = self::hLookupSearch($lookupValue, $lookupArray, $firstColumn, $notExactMatch);
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 $column The column to look up
* @param mixed $notExactMatch determines if you are looking for an exact match based on lookup_value
* @param int|string $column
*/
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);

View File

@ -7,7 +7,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
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
if (!is_numeric($index_number) || $index_number < 1) {
@ -25,7 +25,7 @@ abstract class LookupBase
protected static function checkMatch(
bool $bothNumeric,
bool $bothNotNumeric,
$notExactMatch,
bool $notExactMatch,
int $rowKey,
string $cellDataLower,
string $lookupLower,

View File

@ -2,13 +2,15 @@
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class VLookup extends LookupBase
{
use ArrayEnabled;
/**
* VLOOKUP
* 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)
{
$lookupValue = Functions::flattenSingleValue($lookupValue);
$indexNumber = Functions::flattenSingleValue($indexNumber);
$notExactMatch = ($notExactMatch === null) ? true : Functions::flattenSingleValue($notExactMatch);
if (is_array($lookupValue)) {
return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $indexNumber, $notExactMatch);
}
$notExactMatch = (bool) ($notExactMatch ?? true);
try {
$indexNumber = self::validateIndexLookup($lookupArray, $indexNumber);
@ -41,7 +45,7 @@ class VLookup extends LookupBase
}
$columnKeys = array_keys($lookupArray[$firstRow]);
$returnColumn = $columnKeys[--$indexNumber];
$firstColumn = array_shift($columnKeys);
$firstColumn = array_shift($columnKeys) ?? 1;
if (!$notExactMatch) {
uasort($lookupArray, ['self', 'vlookupSort']);
@ -71,7 +75,11 @@ class VLookup extends LookupBase
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);

View File

@ -2,6 +2,7 @@
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
@ -89,4 +90,40 @@ class HLookupTest extends TestCase
);
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;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
use PHPUnit\Framework\TestCase;
@ -28,4 +29,28 @@ class VLookupTest extends TestCase
{
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 $number
*/
public function testRound($expectedResult, $number = 'omitted'): void
public function testAbs($expectedResult, $number = 'omitted'): void
{
$sheet = $this->getSheet();
$this->mightHaveException($expectedResult);