Skip to content

Commit c10d86e

Browse files
author
Mark Baker
authored
Start work on array-enabling the Lookup and Reference functions (#2602)
* Start work on array-enabling the Lookup and Reference functions Requires a new method (`evaluateArrayArgumentsSubsetFrom()`) in the `ArrayEnabled` Trait to handle functions where the arguments that need special array handling are trailing rather than leading arguments
1 parent d5dc58d commit c10d86e

File tree

20 files changed

+305
-32
lines changed

20 files changed

+305
-32
lines changed

src/PhpSpreadsheet/Calculation/ArrayEnabled.php

+21
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,25 @@ protected static function evaluateArrayArgumentsSubset(callable $method, int $li
5353

5454
return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments);
5555
}
56+
57+
/**
58+
* @param mixed ...$arguments
59+
*/
60+
protected static function evaluateArrayArgumentsSubsetFrom(callable $method, int $start, ...$arguments): array
61+
{
62+
$arrayArgumentsSubset = array_combine(
63+
range($start, count($arguments) - $start),
64+
array_slice($arguments, $start)
65+
);
66+
if ($arrayArgumentsSubset === false) {
67+
return ['#VALUE!'];
68+
}
69+
70+
self::initialiseHelper($arrayArgumentsSubset);
71+
$leadingArguments = array_slice($arguments, 0, $start);
72+
$arguments = self::$arrayArgumentHelper->arguments();
73+
$arguments = array_merge($leadingArguments, $arguments);
74+
75+
return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments);
76+
}
5677
}

src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentHelper.php

+11-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66

77
class ArrayArgumentHelper
88
{
9+
/**
10+
* @var int
11+
*/
12+
protected $indexStart = 0;
13+
914
/**
1015
* @var array
1116
*/
@@ -28,6 +33,8 @@ class ArrayArgumentHelper
2833

2934
public function initialise(array $arguments): void
3035
{
36+
$keys = array_keys($arguments);
37+
$this->indexStart = (int) array_shift($keys);
3138
$this->rows = $this->rows($arguments);
3239
$this->columns = $this->columns($arguments);
3340

@@ -57,7 +64,7 @@ public function getFirstArrayArgumentNumber(): int
5764
$rowArrays = $this->filterArray($this->rows);
5865
$columnArrays = $this->filterArray($this->columns);
5966

60-
for ($index = 0; $index < $this->argumentCount; ++$index) {
67+
for ($index = $this->indexStart; $index < $this->argumentCount; ++$index) {
6168
if (isset($rowArrays[$index]) || isset($columnArrays[$index])) {
6269
return ++$index;
6370
}
@@ -76,7 +83,7 @@ public function getSingleRowVector(): ?int
7683
private function getRowVectors(): array
7784
{
7885
$rowVectors = [];
79-
for ($index = 0; $index < $this->argumentCount; ++$index) {
86+
for ($index = $this->indexStart; $index < ($this->indexStart + $this->argumentCount); ++$index) {
8087
if ($this->rows[$index] === 1 && $this->columns[$index] > 1) {
8188
$rowVectors[] = $index;
8289
}
@@ -95,7 +102,7 @@ public function getSingleColumnVector(): ?int
95102
private function getColumnVectors(): array
96103
{
97104
$columnVectors = [];
98-
for ($index = 0; $index < $this->argumentCount; ++$index) {
105+
for ($index = $this->indexStart; $index < ($this->indexStart + $this->argumentCount); ++$index) {
99106
if ($this->rows[$index] > 1 && $this->columns[$index] === 1) {
100107
$columnVectors[] = $index;
101108
}
@@ -106,7 +113,7 @@ private function getColumnVectors(): array
106113

107114
public function getMatrixPair(): array
108115
{
109-
for ($i = 0; $i < ($this->argumentCount - 1); ++$i) {
116+
for ($i = $this->indexStart; $i < ($this->indexStart + $this->argumentCount - 1); ++$i) {
110117
for ($j = $i + 1; $j < $this->argumentCount; ++$j) {
111118
if (isset($this->rows[$i], $this->rows[$j])) {
112119
return [$i, $j];

src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentProcessor.php

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public static function processArguments(
3333

3434
$singleRowVectorIndex = self::$arrayArgumentHelper->getSingleRowVector();
3535
$singleColumnVectorIndex = self::$arrayArgumentHelper->getSingleColumnVector();
36+
3637
if ($singleRowVectorIndex !== null && $singleColumnVectorIndex !== null) {
3738
// Basic logic for a single row vector and a single column vector
3839
return self::evaluateVectorPair($method, $singleRowVectorIndex, $singleColumnVectorIndex, ...$arguments);

src/PhpSpreadsheet/Calculation/Functions.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ public static function isNonText($value = null)
518518
*
519519
* @param null|mixed $value The value you want converted
520520
*
521-
* @return number N converts values listed in the following table
521+
* @return number|string N converts values listed in the following table
522522
* If value is or refers to N returns
523523
* A number That number
524524
* A date The serial number of that date

src/PhpSpreadsheet/Calculation/LookupRef.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ class LookupRef
4141
* @param bool $referenceStyle A logical value that specifies the A1 or R1C1 reference style.
4242
* TRUE or omitted CELL_ADDRESS returns an A1-style reference
4343
* FALSE CELL_ADDRESS returns an R1C1-style reference
44-
* @param string $sheetText Optional Name of worksheet to use
44+
* @param array|string $sheetText Optional Name of worksheet to use
4545
*
46-
* @return string
46+
* @return array|string
4747
*/
4848
public static function cellAddress($row, $column, $relativity = 1, $referenceStyle = true, $sheetText = '')
4949
{
@@ -277,7 +277,7 @@ public static function CHOOSE(...$chooseArgs)
277277
* @param mixed $matchType The number -1, 0, or 1. -1 means above, 0 means exact match, 1 means below.
278278
* If match_type is 1 or -1, the list has to be ordered.
279279
*
280-
* @return int|string The relative position of the found item
280+
* @return array|int|string The relative position of the found item
281281
*/
282282
public static function MATCH($lookupValue, $lookupArray, $matchType = 1)
283283
{

src/PhpSpreadsheet/Calculation/LookupRef/Address.php

+27-7
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
44

5-
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
5+
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
66
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
77
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
88

99
class Address
1010
{
11+
use ArrayEnabled;
12+
1113
public const ADDRESS_ABSOLUTE = 1;
1214
public const ADDRESS_COLUMN_RELATIVE = 2;
1315
public const ADDRESS_ROW_RELATIVE = 3;
@@ -25,26 +27,44 @@ class Address
2527
* =ADDRESS(row, column, [relativity], [referenceStyle], [sheetText])
2628
*
2729
* @param mixed $row Row number (integer) to use in the cell reference
30+
* Or can be an array of values
2831
* @param mixed $column Column number (integer) to use in the cell reference
32+
* Or can be an array of values
2933
* @param mixed $relativity Integer flag indicating the type of reference to return
3034
* 1 or omitted Absolute
3135
* 2 Absolute row; relative column
3236
* 3 Relative row; absolute column
3337
* 4 Relative
38+
* Or can be an array of values
3439
* @param mixed $referenceStyle A logical (boolean) value that specifies the A1 or R1C1 reference style.
3540
* TRUE or omitted ADDRESS returns an A1-style reference
3641
* FALSE ADDRESS returns an R1C1-style reference
42+
* Or can be an array of values
3743
* @param mixed $sheetName Optional Name of worksheet to use
44+
* Or can be an array of values
3845
*
39-
* @return string
46+
* @return array|string
47+
* If an array of values is passed as the $testValue argument, then the returned result will also be
48+
* an array with the same dimensions
4049
*/
4150
public static function cell($row, $column, $relativity = 1, $referenceStyle = true, $sheetName = '')
4251
{
43-
$row = Functions::flattenSingleValue($row);
44-
$column = Functions::flattenSingleValue($column);
45-
$relativity = ($relativity === null) ? 1 : Functions::flattenSingleValue($relativity);
46-
$referenceStyle = ($referenceStyle === null) ? true : Functions::flattenSingleValue($referenceStyle);
47-
$sheetName = Functions::flattenSingleValue($sheetName);
52+
if (
53+
is_array($row) || is_array($column) ||
54+
is_array($relativity) || is_array($referenceStyle) || is_array($sheetName)
55+
) {
56+
return self::evaluateArrayArguments(
57+
[self::class, __FUNCTION__],
58+
$row,
59+
$column,
60+
$relativity,
61+
$referenceStyle,
62+
$sheetName
63+
);
64+
}
65+
66+
$relativity = $relativity ?? 1;
67+
$referenceStyle = $referenceStyle ?? true;
4868

4969
if (($row < 1) || ($column < 1)) {
5070
return ExcelError::VALUE();

src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php

+9-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
44

5+
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
56
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
67
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
78
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
@@ -10,6 +11,8 @@
1011

1112
class ExcelMatch
1213
{
14+
use ArrayEnabled;
15+
1316
public const MATCHTYPE_SMALLEST_VALUE = -1;
1417
public const MATCHTYPE_FIRST_VALUE = 0;
1518
public const MATCHTYPE_LARGEST_VALUE = 1;
@@ -27,15 +30,16 @@ class ExcelMatch
2730
* @param mixed $matchType The number -1, 0, or 1. -1 means above, 0 means exact match, 1 means below.
2831
* If match_type is 1 or -1, the list has to be ordered.
2932
*
30-
* @return int|string The relative position of the found item
33+
* @return array|int|string The relative position of the found item
3134
*/
3235
public static function MATCH($lookupValue, $lookupArray, $matchType = self::MATCHTYPE_LARGEST_VALUE)
3336
{
37+
if (is_array($lookupValue)) {
38+
return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $matchType);
39+
}
40+
3441
$lookupArray = Functions::flattenArray($lookupArray);
35-
$lookupValue = Functions::flattenSingleValue($lookupValue);
36-
$matchType = ($matchType === null)
37-
? self::MATCHTYPE_LARGEST_VALUE
38-
: (int) Functions::flattenSingleValue($matchType);
42+
$matchType = (int) ($matchType ?? self::MATCHTYPE_LARGEST_VALUE);
3943

4044
try {
4145
// Input validation

src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php

+6-2
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
44

5-
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
5+
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
66
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
77
use PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
88

99
class Lookup
1010
{
11+
use ArrayEnabled;
12+
1113
/**
1214
* LOOKUP
1315
* The LOOKUP function searches for value either from a one-row or one-column range or from an array.
@@ -20,7 +22,9 @@ class Lookup
2022
*/
2123
public static function lookup($lookupValue, $lookupVector, $resultVector = null)
2224
{
23-
$lookupValue = Functions::flattenSingleValue($lookupValue);
25+
if (is_array($lookupValue)) {
26+
return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 1, $lookupValue, $lookupVector, $resultVector);
27+
}
2428

2529
if (!is_array($lookupVector)) {
2630
return ExcelError::NA();

src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php

+13-3
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
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

99
class Matrix
1010
{
11+
use ArrayEnabled;
12+
1113
/**
1214
* TRANSPOSE.
1315
*
@@ -46,17 +48,25 @@ public static function transpose($matrixData)
4648
* @param mixed $matrix A range of cells or an array constant
4749
* @param mixed $rowNum The row in the array or range from which to return a value.
4850
* If row_num is omitted, column_num is required.
51+
* Or can be an array of values
4952
* @param mixed $columnNum The column in the array or range from which to return a value.
5053
* If column_num is omitted, row_num is required.
54+
* Or can be an array of values
5155
*
5256
* TODO Provide support for area_num, currently not supported
5357
*
5458
* @return mixed the value of a specified cell or array of cells
59+
* If an array of values is passed as the $rowNum and/or $columnNum arguments, then the returned result
60+
* will also be an array with the same dimensions
5561
*/
5662
public static function index($matrix, $rowNum = 0, $columnNum = 0)
5763
{
58-
$rowNum = ($rowNum === null) ? 0 : Functions::flattenSingleValue($rowNum);
59-
$columnNum = ($columnNum === null) ? 0 : Functions::flattenSingleValue($columnNum);
64+
if (is_array($rowNum) || is_array($columnNum)) {
65+
return self::evaluateArrayArgumentsSubsetFrom([self::class, __FUNCTION__], 1, $matrix, $rowNum, $columnNum);
66+
}
67+
68+
$rowNum = $rowNum ?? 0;
69+
$columnNum = $columnNum ?? 0;
6070

6171
try {
6272
$rowNum = LookupRefValidations::validatePositiveInt($rowNum);

src/PhpSpreadsheet/Calculation/LookupRef/Selection.php

+9-5
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
44

5+
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
56
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
67
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
78

89
class Selection
910
{
11+
use ArrayEnabled;
12+
1013
/**
1114
* CHOOSE.
1215
*
@@ -16,18 +19,19 @@ class Selection
1619
* Excel Function:
1720
* =CHOOSE(index_num, value1, [value2], ...)
1821
*
22+
* @param mixed $chosenEntry The entry to select from the list (indexed from 1)
1923
* @param mixed ...$chooseArgs Data values
2024
*
2125
* @return mixed The selected value
2226
*/
23-
public static function choose(...$chooseArgs)
27+
public static function choose($chosenEntry, ...$chooseArgs)
2428
{
25-
$chosenEntry = Functions::flattenArray(array_shift($chooseArgs));
26-
$entryCount = count($chooseArgs) - 1;
27-
2829
if (is_array($chosenEntry)) {
29-
$chosenEntry = array_shift($chosenEntry);
30+
return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 1, $chosenEntry, ...$chooseArgs);
3031
}
32+
33+
$entryCount = count($chooseArgs) - 1;
34+
3135
if (is_numeric($chosenEntry)) {
3236
--$chosenEntry;
3337
} else {

src/PhpSpreadsheet/Cell/DataValidator.php

+3
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ private function isValueInList(Cell $cell)
6464

6565
try {
6666
$result = $calculation->calculateFormula($matchFormula, $cell->getCoordinate(), $cell);
67+
while (is_array($result)) {
68+
$result = array_pop($result);
69+
}
6770

6871
return $result !== ExcelError::NA();
6972
} catch (Exception $ex) {

src/PhpSpreadsheet/Worksheet/AutoFilter.php

+3
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,9 @@ public function showHideRows()
931931
$averageFormula = '=AVERAGE(' . $columnID . ($rangeStart[1] + 1) . ':' . $columnID . $rangeEnd[1] . ')';
932932
$spreadsheet = ($this->workSheet === null) ? null : $this->workSheet->getParent();
933933
$average = Calculation::getInstance($spreadsheet)->calculateFormula($averageFormula, null, $this->workSheet->getCell('A1'));
934+
while (is_array($average)) {
935+
$average = array_pop($average);
936+
}
934937
// Set above/below rule based on greaterThan or LessTan
935938
$operator = ($dynamicRuleType === Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE)
936939
? Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN

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

+24
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,27 @@ public function providerADDRESS(): array
2829
{
2930
return require 'tests/data/Calculation/LookupRef/ADDRESS.php';
3031
}
32+
33+
/**
34+
* @dataProvider providerAddressArray
35+
*/
36+
public function testAddressArray(array $expectedResult, string $argument1, string $argument2): void
37+
{
38+
$calculation = Calculation::getInstance();
39+
40+
$formula = "=ADDRESS({$argument1}, {$argument2}, 4)";
41+
$result = $calculation->_calculateFormulaValue($formula);
42+
self::assertEquals($expectedResult, $result);
43+
}
44+
45+
public function providerAddressArray(): array
46+
{
47+
return [
48+
'row/column vectors' => [
49+
[['A1', 'B1', 'C1'], ['A2', 'B2', 'C2'], ['A3', 'B3', 'C3']],
50+
'{1; 2; 3}',
51+
'{1, 2, 3}',
52+
],
53+
];
54+
}
3155
}

0 commit comments

Comments
 (0)