diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 6540c84913..9c087daa51 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -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 diff --git a/src/PhpSpreadsheet/Calculation/ArrayEnabled.php b/src/PhpSpreadsheet/Calculation/ArrayEnabled.php index 2553c9fd79..ad61eb7d4a 100644 --- a/src/PhpSpreadsheet/Calculation/ArrayEnabled.php +++ b/src/PhpSpreadsheet/Calculation/ArrayEnabled.php @@ -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 = []; @@ -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 @@ -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 @@ -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 @@ -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); + } } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php b/src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php index 989602833a..2f691beeff 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php @@ -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); diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php index 69060e1df8..ae16e56312 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php @@ -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 @@ 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) { @@ -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); diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php b/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php index b26278919f..6a0933d6fe 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php @@ -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) { @@ -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, diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php index bbbaeda164..26f42eba54 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php @@ -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 @@ 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']); @@ -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); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HLookupTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HLookupTest.php index 7dc5b29c58..0ce3513b5e 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HLookupTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HLookupTest.php @@ -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 @@ 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}', + ], + ]; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php index 35e1e06923..4e05ea8a5c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php @@ -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 @@ 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', + ], + ]; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AbsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AbsTest.php index fda464eddb..23e0ae35cc 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AbsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AbsTest.php @@ -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);