Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 0 additions & 40 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions src/PhpSpreadsheet/Calculation/ArrayEnabled.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ private static function initialiseHelper(array $arguments): void
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 = [];
Expand All @@ -31,6 +36,11 @@ protected static function evaluateSingleArgumentArray(callable $method, array $v
}

/**
* 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
Expand All @@ -42,6 +52,12 @@ protected static function evaluateArrayArguments(callable $method, ...$arguments
}

/**
* 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
Expand All @@ -55,6 +71,12 @@ protected static function evaluateArrayArgumentsSubset(callable $method, int $li
}

/**
* 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
Expand All @@ -74,4 +96,27 @@ protected static function evaluateArrayArgumentsSubsetFrom(callable $method, int

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);
}
}
2 changes: 1 addition & 1 deletion src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
19 changes: 11 additions & 8 deletions src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -44,7 +48,7 @@ public static function lookup($lookupValue, $lookupArray, $indexNumber, $notExac

$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) {
Expand All @@ -57,10 +61,9 @@ public static function lookup($lookupValue, $lookupArray, $indexNumber, $notExac

/**
* @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);

Expand Down
4 changes: 2 additions & 2 deletions src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

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) {
Expand All @@ -25,7 +25,7 @@ protected static function validateIndexLookup($lookup_array, $index_number)
protected static function checkMatch(
bool $bothNumeric,
bool $bothNotNumeric,
$notExactMatch,
bool $notExactMatch,
int $rowKey,
string $cellDataLower,
string $lookupLower,
Expand Down
20 changes: 14 additions & 6 deletions src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -41,7 +45,7 @@ public static function lookup($lookupValue, $lookupArray, $indexNumber, $notExac
}
$columnKeys = array_keys($lookupArray[$firstRow]);
$returnColumn = $columnKeys[--$indexNumber];
$firstColumn = array_shift($columnKeys);
$firstColumn = array_shift($columnKeys) ?? 1;

if (!$notExactMatch) {
uasort($lookupArray, ['self', 'vlookupSort']);
Expand Down Expand Up @@ -71,7 +75,11 @@ private static function vlookupSort($a, $b)
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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -89,4 +90,40 @@ public function testGrandfathered(): void
);
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}',
],
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,4 +29,28 @@ public function providerVLOOKUP(): array
{
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',
],
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down