Skip to content

Commit c9f948b

Browse files
author
Mark Baker
authored
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
1 parent db21d04 commit c9f948b

File tree

9 files changed

+136
-58
lines changed

9 files changed

+136
-58
lines changed

phpstan-baseline.neon

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -765,26 +765,11 @@ parameters:
765765
count: 1
766766
path: src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php
767767

768-
-
769-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\LookupBase\\:\\:checkMatch\\(\\) has parameter \\$notExactMatch with no type specified\\.$#"
770-
count: 1
771-
path: src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php
772-
773-
-
774-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\LookupBase\\:\\:validateIndexLookup\\(\\) has no return type specified\\.$#"
775-
count: 1
776-
path: src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php
777-
778768
-
779769
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\LookupBase\\:\\:validateIndexLookup\\(\\) has parameter \\$index_number with no type specified\\.$#"
780770
count: 1
781771
path: src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php
782772

783-
-
784-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\LookupBase\\:\\:validateIndexLookup\\(\\) has parameter \\$lookup_array with no type specified\\.$#"
785-
count: 1
786-
path: src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php
787-
788773
-
789774
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\Matrix\\:\\:extractRowValue\\(\\) has no return type specified\\.$#"
790775
count: 1
@@ -845,31 +830,6 @@ parameters:
845830
count: 1
846831
path: src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php
847832

848-
-
849-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\VLookup\\:\\:vLookupSearch\\(\\) has no return type specified\\.$#"
850-
count: 1
851-
path: src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php
852-
853-
-
854-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\VLookup\\:\\:vLookupSearch\\(\\) has parameter \\$column with no type specified\\.$#"
855-
count: 1
856-
path: src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php
857-
858-
-
859-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\VLookup\\:\\:vLookupSearch\\(\\) has parameter \\$lookupArray with no type specified\\.$#"
860-
count: 1
861-
path: src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php
862-
863-
-
864-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\VLookup\\:\\:vLookupSearch\\(\\) has parameter \\$lookupValue with no type specified\\.$#"
865-
count: 1
866-
path: src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php
867-
868-
-
869-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\VLookup\\:\\:vLookupSearch\\(\\) has parameter \\$notExactMatch with no type specified\\.$#"
870-
count: 1
871-
path: src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php
872-
873833
-
874834
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\VLookup\\:\\:vlookupSort\\(\\) has no return type specified\\.$#"
875835
count: 1

src/PhpSpreadsheet/Calculation/ArrayEnabled.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ private static function initialiseHelper(array $arguments): void
2020
self::$arrayArgumentHelper->initialise($arguments);
2121
}
2222

23+
/**
24+
* Handles array argument processing when the function accepts a single argument that can be an array argument.
25+
* Example use for:
26+
* DAYOFMONTH() or FACT().
27+
*/
2328
protected static function evaluateSingleArgumentArray(callable $method, array $values): array
2429
{
2530
$result = [];
@@ -31,6 +36,11 @@ protected static function evaluateSingleArgumentArray(callable $method, array $v
3136
}
3237

3338
/**
39+
* Handles array argument processing when the function accepts multiple arguments,
40+
* and any of them can be an array argument.
41+
* Example use for:
42+
* ROUND() or DATE().
43+
*
3444
* @param mixed ...$arguments
3545
*/
3646
protected static function evaluateArrayArguments(callable $method, ...$arguments): array
@@ -42,6 +52,12 @@ protected static function evaluateArrayArguments(callable $method, ...$arguments
4252
}
4353

4454
/**
55+
* Handles array argument processing when the function accepts multiple arguments,
56+
* but only the first few (up to limit) can be an array arguments.
57+
* Example use for:
58+
* NETWORKDAYS() or CONCATENATE(), where the last argument is a matrix (or a series of values) that need
59+
* to be treated as a such rather than as an array arguments.
60+
*
4561
* @param mixed ...$arguments
4662
*/
4763
protected static function evaluateArrayArgumentsSubset(callable $method, int $limit, ...$arguments): array
@@ -55,6 +71,12 @@ protected static function evaluateArrayArgumentsSubset(callable $method, int $li
5571
}
5672

5773
/**
74+
* Handles array argument processing when the function accepts multiple arguments,
75+
* but only the last few (from start) can be an array arguments.
76+
* Example use for:
77+
* Z.TEST() or INDEX(), where the first argument 1 is a matrix that needs to be treated as a dataset
78+
* rather than as an array argument.
79+
*
5880
* @param mixed ...$arguments
5981
*/
6082
protected static function evaluateArrayArgumentsSubsetFrom(callable $method, int $start, ...$arguments): array
@@ -74,4 +96,27 @@ protected static function evaluateArrayArgumentsSubsetFrom(callable $method, int
7496

7597
return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments);
7698
}
99+
100+
/**
101+
* Handles array argument processing when the function accepts multiple arguments,
102+
* and any of them can be an array argument except for the one specified by ignore.
103+
* Example use for:
104+
* HLOOKUP() and VLOOKUP(), where argument 1 is a matrix that needs to be treated as a database
105+
* rather than as an array argument.
106+
*
107+
* @param mixed ...$arguments
108+
*/
109+
protected static function evaluateArrayArgumentsIgnore(callable $method, int $ignore, ...$arguments): array
110+
{
111+
$leadingArguments = array_slice($arguments, 0, $ignore);
112+
$ignoreArgument = array_slice($arguments, $ignore, 1);
113+
$trailingArguments = array_slice($arguments, $ignore + 1);
114+
115+
self::initialiseHelper(array_merge($leadingArguments, [[null]], $trailingArguments));
116+
$arguments = self::$arrayArgumentHelper->arguments();
117+
118+
array_splice($arguments, $ignore, 1, $ignoreArgument);
119+
120+
return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments);
121+
}
77122
}

src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class ExcelMatch
3535
public static function MATCH($lookupValue, $lookupArray, $matchType = self::MATCHTYPE_LARGEST_VALUE)
3636
{
3737
if (is_array($lookupValue)) {
38-
return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $matchType);
38+
return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $matchType);
3939
}
4040

4141
$lookupArray = Functions::flattenArray($lookupArray);

src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
44

5+
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
56
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
6-
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
77
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
88
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
99
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
1010

1111
class HLookup extends LookupBase
1212
{
13+
use ArrayEnabled;
14+
1315
/**
1416
* HLOOKUP
1517
* 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
2527
*/
2628
public static function lookup($lookupValue, $lookupArray, $indexNumber, $notExactMatch = true)
2729
{
28-
$lookupValue = Functions::flattenSingleValue($lookupValue);
29-
$indexNumber = Functions::flattenSingleValue($indexNumber);
30-
$notExactMatch = ($notExactMatch === null) ? true : Functions::flattenSingleValue($notExactMatch);
30+
if (is_array($lookupValue)) {
31+
return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $indexNumber, $notExactMatch);
32+
}
33+
34+
$notExactMatch = (bool) ($notExactMatch ?? true);
3135
$lookupArray = self::convertLiteralArray($lookupArray);
3236

3337
try {
@@ -44,7 +48,7 @@ public static function lookup($lookupValue, $lookupArray, $indexNumber, $notExac
4448

4549
$firstkey = $f[0] - 1;
4650
$returnColumn = $firstkey + $indexNumber;
47-
$firstColumn = array_shift($f);
51+
$firstColumn = array_shift($f) ?? 1;
4852
$rowNumber = self::hLookupSearch($lookupValue, $lookupArray, $firstColumn, $notExactMatch);
4953

5054
if ($rowNumber !== null) {
@@ -57,10 +61,9 @@ public static function lookup($lookupValue, $lookupArray, $indexNumber, $notExac
5761

5862
/**
5963
* @param mixed $lookupValue The value that you want to match in lookup_array
60-
* @param mixed $column The column to look up
61-
* @param mixed $notExactMatch determines if you are looking for an exact match based on lookup_value
64+
* @param int|string $column
6265
*/
63-
private static function hLookupSearch($lookupValue, array $lookupArray, $column, $notExactMatch): ?int
66+
private static function hLookupSearch($lookupValue, array $lookupArray, $column, bool $notExactMatch): ?int
6467
{
6568
$lookupLower = StringHelper::strToLower($lookupValue);
6669

src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
abstract class LookupBase
99
{
10-
protected static function validateIndexLookup($lookup_array, $index_number)
10+
protected static function validateIndexLookup(array $lookup_array, $index_number): int
1111
{
1212
// index_number must be a number greater than or equal to 1
1313
if (!is_numeric($index_number) || $index_number < 1) {
@@ -25,7 +25,7 @@ protected static function validateIndexLookup($lookup_array, $index_number)
2525
protected static function checkMatch(
2626
bool $bothNumeric,
2727
bool $bothNotNumeric,
28-
$notExactMatch,
28+
bool $notExactMatch,
2929
int $rowKey,
3030
string $cellDataLower,
3131
string $lookupLower,

src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
44

5+
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
56
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
6-
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
77
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
88
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
99

1010
class VLookup extends LookupBase
1111
{
12+
use ArrayEnabled;
13+
1214
/**
1315
* VLOOKUP
1416
* 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
2426
*/
2527
public static function lookup($lookupValue, $lookupArray, $indexNumber, $notExactMatch = true)
2628
{
27-
$lookupValue = Functions::flattenSingleValue($lookupValue);
28-
$indexNumber = Functions::flattenSingleValue($indexNumber);
29-
$notExactMatch = ($notExactMatch === null) ? true : Functions::flattenSingleValue($notExactMatch);
29+
if (is_array($lookupValue)) {
30+
return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $indexNumber, $notExactMatch);
31+
}
32+
33+
$notExactMatch = (bool) ($notExactMatch ?? true);
3034

3135
try {
3236
$indexNumber = self::validateIndexLookup($lookupArray, $indexNumber);
@@ -41,7 +45,7 @@ public static function lookup($lookupValue, $lookupArray, $indexNumber, $notExac
4145
}
4246
$columnKeys = array_keys($lookupArray[$firstRow]);
4347
$returnColumn = $columnKeys[--$indexNumber];
44-
$firstColumn = array_shift($columnKeys);
48+
$firstColumn = array_shift($columnKeys) ?? 1;
4549

4650
if (!$notExactMatch) {
4751
uasort($lookupArray, ['self', 'vlookupSort']);
@@ -71,7 +75,11 @@ private static function vlookupSort($a, $b)
7175
return ($aLower < $bLower) ? -1 : 1;
7276
}
7377

74-
private static function vLookupSearch($lookupValue, $lookupArray, $column, $notExactMatch)
78+
/**
79+
* @param mixed $lookupValue The value that you want to match in lookup_array
80+
* @param int|string $column
81+
*/
82+
private static function vLookupSearch($lookupValue, array $lookupArray, $column, bool $notExactMatch): ?int
7583
{
7684
$lookupLower = StringHelper::strToLower($lookupValue);
7785

tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HLookupTest.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef;
44

5+
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
56
use PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
67
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
78
use PhpOffice\PhpSpreadsheet\Spreadsheet;
@@ -89,4 +90,40 @@ public function testGrandfathered(): void
8990
);
9091
self::assertSame($expectedResult, $result);
9192
}
93+
94+
/**
95+
* @dataProvider providerHLookupArray
96+
*/
97+
public function testHLookupArray(array $expectedResult, string $values, string $database, string $index): void
98+
{
99+
$calculation = Calculation::getInstance();
100+
101+
$formula = "=HLOOKUP({$values}, {$database}, {$index}, false)";
102+
$result = $calculation->_calculateFormulaValue($formula);
103+
self::assertEquals($expectedResult, $result);
104+
}
105+
106+
public function providerHLookupArray(): array
107+
{
108+
return [
109+
'row vector #1' => [
110+
[[4, 9]],
111+
'{"Axles", "Bolts"}',
112+
'{"Axles", "Bearings", "Bolts"; 4, 4, 9; 5, 7, 10; 6, 8, 11}',
113+
'2',
114+
],
115+
'row vector #2' => [
116+
[[5, 7]],
117+
'{"Axles", "Bearings"}',
118+
'{"Axles", "Bearings", "Bolts"; 4, 4, 9; 5, 7, 10; 6, 8, 11}',
119+
'3',
120+
],
121+
'row/column vectors' => [
122+
[[4, 9], [5, 10]],
123+
'{"Axles", "Bolts"}',
124+
'{"Axles", "Bearings", "Bolts"; 4, 4, 9; 5, 7, 10; 6, 8, 11}',
125+
'{2; 3}',
126+
],
127+
];
128+
}
92129
}

tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef;
44

5+
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
56
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
67
use PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
78
use PHPUnit\Framework\TestCase;
@@ -28,4 +29,28 @@ public function providerVLOOKUP(): array
2829
{
2930
return require 'tests/data/Calculation/LookupRef/VLOOKUP.php';
3031
}
32+
33+
/**
34+
* @dataProvider providerVLookupArray
35+
*/
36+
public function testVLookupArray(array $expectedResult, string $values, string $database, string $index): void
37+
{
38+
$calculation = Calculation::getInstance();
39+
40+
$formula = "=VLOOKUP({$values}, {$database}, {$index}, false)";
41+
$result = $calculation->_calculateFormulaValue($formula);
42+
self::assertEquals($expectedResult, $result);
43+
}
44+
45+
public function providerVLookupArray(): array
46+
{
47+
return [
48+
'row vector' => [
49+
[[4.19, 5.77, 4.14]],
50+
'{"Orange", "Green", "Red"}',
51+
'{"Red", 4.14; "Orange", 4.19; "Yellow", 5.17; "Green", 5.77; "Blue", 6.39}',
52+
'2',
53+
],
54+
];
55+
}
3156
}

tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AbsTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class AbsTest extends AllSetupTeardown
1212
* @param mixed $expectedResult
1313
* @param mixed $number
1414
*/
15-
public function testRound($expectedResult, $number = 'omitted'): void
15+
public function testAbs($expectedResult, $number = 'omitted'): void
1616
{
1717
$sheet = $this->getSheet();
1818
$this->mightHaveException($expectedResult);

0 commit comments

Comments
 (0)