From c79a9a8e21f5b762b9a1d717c2195e1f29fe751c Mon Sep 17 00:00:00 2001 From: oleibman Date: Tue, 20 Apr 2021 13:16:21 -0700 Subject: [PATCH] Improved Support for INDIRECT, ROW, and COLUMN Functions (#2004) * Improved Support for INDIRECT, ROW, and COLUMN Functions This should address issues #1913 and #1993. INDIRECT had heretofore not supported an optional parameter intended to support addresses in R1C1 format which was introduced with Excel 2010. It also had not supported the use of defined names as an argument. This PR is a replacement for #1995, which is currently in draft status and which I will close in a day or two. The ROW and COLUMN functions also should support defined names. I have added that, and test cases, with the latest push. ROWS and COLUMNS already supported it correctly, but there had been no test cases. Because ROW and COLUMN can return arrays, and PhpSpreadsheet does not support dynamic arrays, I left the existing direct-call tests unchanged to demonstrate those capabilities. The unit tests for INDIRECT had used mocking, and were sorely lacking (tested only error conditions). They have been replaced with normal, and hopefully adequate, tests. This includes testing globally defined names, as well as locally defined names, both in and out of scope. The test case in 1913 was too complicated for me to add as a unit test. The main impediments to it are now removed, and its complex situation will, I hope, be corrected with this fix. INDIRECT can also support a reference of the form Sheetname!localName when localName on its own would be out of scope. That functionality is added. It is also added, in theory, for ROW and COLUMN, however such a construction is rejected by the Calculation engine before passing control to ROW or COLUMN. It might be possible to change the engine to allow this, and I may want to look into that later, but it seems much too risky, and not nearly useful enough, to attempt to address that as part of this change. Several unusual test cases (leading equals sign, not-quite-as-expected name definition in file, complex indirection involving concatenation and a dropdown list) were suggested by @MarkBaker and are included in this request. --- phpstan-baseline.neon | 15 -- src/PhpSpreadsheet/Calculation/LookupRef.php | 10 +- .../Calculation/LookupRef/Helpers.php | 74 +++++++++ .../Calculation/LookupRef/Indirect.php | 83 ++++++---- .../LookupRef/RowColumnInformation.php | 58 +++++-- src/PhpSpreadsheet/Cell/Cell.php | 2 +- src/PhpSpreadsheet/DefinedName.php | 13 +- .../Functions/LookupRef/AllSetupTeardown.php | 75 +++++++++ .../LookupRef/ColumnOnSpreadsheetTest.php | 57 +++++++ .../LookupRef/ColumnsOnSpreadsheetTest.php | 60 +++++++ .../Functions/LookupRef/IndirectTest.php | 148 ++++++++++++++---- .../LookupRef/RowOnSpreadsheetTest.php | 60 +++++++ .../LookupRef/RowsOnSpreadsheetTest.php | 60 +++++++ tests/data/Calculation/LookupRef/COLUMN.php | 1 + .../LookupRef/COLUMNSonSpreadsheet.php | 19 +++ .../LookupRef/COLUMNonSpreadsheet.php | 17 ++ tests/data/Calculation/LookupRef/INDIRECT.php | 45 ++++-- .../LookupRef/IndirectDefinedName.xlsx | Bin 0 -> 9116 bytes .../LookupRef/IndirectFormulaSelection.xlsx | Bin 0 -> 9275 bytes tests/data/Calculation/LookupRef/ROW.php | 1 + .../LookupRef/ROWSonSpreadsheet.php | 19 +++ .../LookupRef/ROWonSpreadsheet.php | 18 +++ 22 files changed, 723 insertions(+), 112 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AllSetupTeardown.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnOnSpreadsheetTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnsOnSpreadsheetTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowOnSpreadsheetTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowsOnSpreadsheetTest.php create mode 100644 tests/data/Calculation/LookupRef/COLUMNSonSpreadsheet.php create mode 100644 tests/data/Calculation/LookupRef/COLUMNonSpreadsheet.php create mode 100644 tests/data/Calculation/LookupRef/IndirectDefinedName.xlsx create mode 100644 tests/data/Calculation/LookupRef/IndirectFormulaSelection.xlsx create mode 100644 tests/data/Calculation/LookupRef/ROWSonSpreadsheet.php create mode 100644 tests/data/Calculation/LookupRef/ROWonSpreadsheet.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a082ff27..88a98368 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1090,16 +1090,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\Indirect\\:\\:extractRequiredCells\\(\\) has no return typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\Indirect\\:\\:extractWorksheet\\(\\) has parameter \\$cellAddress with no typehint specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\LookupRef\\\\Lookup\\:\\:verifyResultVector\\(\\) has no return typehint specified\\.$#" count: 1 @@ -1185,11 +1175,6 @@ parameters: count: 3 path: src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php - - - message: "#^Cannot cast array\\|string to string\\.$#" - count: 2 - path: src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php - - message: "#^Parameter \\#1 \\$low of function range expects float\\|int\\|string, string\\|null given\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Calculation/LookupRef.php b/src/PhpSpreadsheet/Calculation/LookupRef.php index 6a89f7da..5adf4f09 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef.php @@ -148,8 +148,8 @@ class LookupRef * Excel Function: * =HYPERLINK(linkURL,displayName) * - * @param mixed $linkURL URL Value to check, is also the value returned when no error - * @param mixed $displayName String Value to return when testValue is an error condition + * @param mixed $linkURL Expect string. Value to check, is also the value returned when no error + * @param mixed $displayName Expect string. Value to return when testValue is an error condition * @param Cell $pCell The cell to set the hyperlink in * * @return mixed The value of $displayName (or $linkURL if $displayName was blank) @@ -188,16 +188,16 @@ class LookupRef * * NOTE - INDIRECT() does not yet support the optional a1 parameter introduced in Excel 2010 * - * @param null|array|string $cellAddress $cellAddress The cell address of the current cell (containing this formula) + * @param array|string $cellAddress $cellAddress The cell address of the current cell (containing this formula) * @param Cell $pCell The current cell (containing this formula) * * @return array|string An array containing a cell or range of cells, or a string on error * * @TODO Support for the optional a1 parameter introduced in Excel 2010 */ - public static function INDIRECT($cellAddress = null, ?Cell $pCell = null) + public static function INDIRECT($cellAddress, Cell $pCell) { - return Indirect::INDIRECT($cellAddress, $pCell); + return Indirect::INDIRECT($cellAddress, true, $pCell); } /** diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php b/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php new file mode 100644 index 00000000..87ea7381 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php @@ -0,0 +1,74 @@ +getWorkSheet(); + $sheetTitle = ($workSheet === null) ? '' : $workSheet->getTitle(); + $value = preg_replace('/^=/', '', $namedRange->getValue()); + self::adjustSheetTitle($sheetTitle, $value); + $cellAddress1 = $sheetTitle . $value; + $cellAddress = $cellAddress1; + $a1 = self::CELLADDRESS_USE_A1; + } + if (strpos($cellAddress, ':') !== false) { + [$cellAddress1, $cellAddress2] = explode(':', $cellAddress); + } + $cellAddress = self::convertR1C1($cellAddress1, $cellAddress2, $a1); + + return [$cellAddress1, $cellAddress2, $cellAddress]; + } + + public static function extractWorksheet(string $cellAddress, Cell $pCell): array + { + $sheetName = ''; + if (strpos($cellAddress, '!') !== false) { + [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); + $sheetName = trim($sheetName, "'"); + } + + $pSheet = ($sheetName !== '') + ? $pCell->getWorksheet()->getParent()->getSheetByName($sheetName) + : $pCell->getWorksheet(); + + return [$cellAddress, $pSheet, $sheetName]; + } +} diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php b/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php index c34dd965..9ba23142 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; +use Exception; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Cell\Cell; @@ -9,6 +10,39 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Indirect { + /** + * Determine whether cell address is in A1 (true) or R1C1 (false) format. + * + * @param mixed $a1fmt Expect bool Helpers::CELLADDRESS_USE_A1 or CELLADDRESS_USE_R1C1, but can be provided as numeric which is cast to bool + */ + private static function a1Format($a1fmt): bool + { + $a1fmt = Functions::flattenSingleValue($a1fmt); + if ($a1fmt === null) { + return Helpers::CELLADDRESS_USE_A1; + } + if (is_string($a1fmt)) { + throw new Exception(Functions::VALUE()); + } + + return (bool) $a1fmt; + } + + /** + * Convert cellAddress to string, verify not null string. + * + * @param array|string $cellAddress + */ + private static function validateAddress($cellAddress): string + { + $cellAddress = Functions::flattenSingleValue($cellAddress); + if (!is_string($cellAddress) || !$cellAddress) { + throw new Exception(Functions::REF()); + } + + return $cellAddress; + } + /** * INDIRECT. * @@ -16,31 +50,26 @@ class Indirect * References are immediately evaluated to display their contents. * * Excel Function: - * =INDIRECT(cellAddress) + * =INDIRECT(cellAddress, bool) where the bool argument is optional * - * NOTE - INDIRECT() does not yet support the optional a1 parameter introduced in Excel 2010 - * - * @param null|array|string $cellAddress $cellAddress The cell address of the current cell (containing this formula) - * @param null|Cell $pCell The current cell (containing this formula) + * @param array|string $cellAddress $cellAddress The cell address of the current cell (containing this formula) + * @param mixed $a1fmt Expect bool Helpers::CELLADDRESS_USE_A1 or CELLADDRESS_USE_R1C1, but can be provided as numeric which is cast to bool + * @param Cell $pCell The current cell (containing this formula) * * @return array|string An array containing a cell or range of cells, or a string on error - * - * @TODO Support for the optional a1 parameter introduced in Excel 2010 */ - public static function INDIRECT($cellAddress = null, ?Cell $pCell = null) + public static function INDIRECT($cellAddress, $a1fmt, Cell $pCell) { - $cellAddress = Functions::flattenSingleValue($cellAddress); - if ($cellAddress === null || $cellAddress === '' || !is_object($pCell)) { - return Functions::REF(); + try { + $a1 = self::a1Format($a1fmt); + $cellAddress = self::validateAddress($cellAddress); + } catch (Exception $e) { + return $e->getMessage(); } - [$cellAddress, $pSheet] = self::extractWorksheet($cellAddress, $pCell); + [$cellAddress, $pSheet, $sheetName] = Helpers::extractWorksheet($cellAddress, $pCell); - $cellAddress1 = $cellAddress; - $cellAddress2 = null; - if (strpos($cellAddress, ':') !== false) { - [$cellAddress1, $cellAddress2] = explode(':', $cellAddress); - } + [$cellAddress1, $cellAddress2, $cellAddress] = Helpers::extractCellAddresses($cellAddress, $a1, $pCell->getWorkSheet(), $sheetName); if ( (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress1, $matches)) || @@ -52,24 +81,14 @@ class Indirect return self::extractRequiredCells($pSheet, $cellAddress); } + /** + * Extract range values. + * + * @return mixed Array of values in range if range contains more than one element. Otherwise, a single value is returned. + */ private static function extractRequiredCells(?Worksheet $pSheet, string $cellAddress) { return Calculation::getInstance($pSheet !== null ? $pSheet->getParent() : null) ->extractCellRange($cellAddress, $pSheet, false); } - - private static function extractWorksheet($cellAddress, Cell $pCell): array - { - $sheetName = ''; - if (strpos($cellAddress, '!') !== false) { - [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); - $sheetName = trim($sheetName, "'"); - } - - $pSheet = ($sheetName !== '') - ? $pCell->getWorksheet()->getParent()->getSheetByName($sheetName) - : $pCell->getWorksheet(); - - return [$cellAddress, $pSheet]; - } } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php b/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php index 19d0d5ff..02c9a79e 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php @@ -10,6 +10,21 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class RowColumnInformation { + /** + * Test if cellAddress is null or whitespace string. + * + * @param null|array|string $cellAddress A reference to a range of cells + */ + private static function cellAddressNullOrWhitespace($cellAddress): bool + { + return $cellAddress === null || (!is_array($cellAddress) && trim($cellAddress) === ''); + } + + private static function cellColumn(?Cell $pCell): int + { + return ($pCell !== null) ? (int) Coordinate::columnIndexFromString($pCell->getColumn()) : 1; + } + /** * COLUMN. * @@ -27,10 +42,10 @@ class RowColumnInformation * * @return int|int[] */ - public static function COLUMN($cellAddress = null, ?Cell $cell = null) + public static function COLUMN($cellAddress = null, ?Cell $pCell = null) { - if ($cellAddress === null || (!is_array($cellAddress) && trim($cellAddress) === '')) { - return ($cell !== null) ? (int) Coordinate::columnIndexFromString($cell->getColumn()) : 1; + if (self::cellAddressNullOrWhitespace($cellAddress)) { + return self::cellColumn($pCell); } if (is_array($cellAddress)) { @@ -39,9 +54,16 @@ class RowColumnInformation return (int) Coordinate::columnIndexFromString($columnKey); } + + return self::cellColumn($pCell); } - [, $cellAddress] = Worksheet::extractSheetTitle((string) $cellAddress, true); + $cellAddress = $cellAddress ?? ''; + if ($pCell != null) { + [,, $sheetName] = Helpers::extractWorksheet($cellAddress, $pCell); + [,, $cellAddress] = Helpers::extractCellAddresses($cellAddress, true, $pCell->getWorksheet(), $sheetName); + } + [, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); if (strpos($cellAddress, ':') !== false) { [$startAddress, $endAddress] = explode(':', $cellAddress); $startAddress = preg_replace('/[^a-z]/i', '', $startAddress); @@ -73,9 +95,10 @@ class RowColumnInformation */ public static function COLUMNS($cellAddress = null) { - if ($cellAddress === null || (is_string($cellAddress) && trim($cellAddress) === '')) { + if (self::cellAddressNullOrWhitespace($cellAddress)) { return 1; - } elseif (!is_array($cellAddress)) { + } + if (!is_array($cellAddress)) { return Functions::VALUE(); } @@ -90,6 +113,11 @@ class RowColumnInformation return $columns; } + private static function cellRow(?Cell $pCell): int + { + return ($pCell !== null) ? $pCell->getRow() : 1; + } + /** * ROW. * @@ -109,8 +137,8 @@ class RowColumnInformation */ public static function ROW($cellAddress = null, ?Cell $pCell = null) { - if ($cellAddress === null || (!is_array($cellAddress) && trim($cellAddress) === '')) { - return ($pCell !== null) ? $pCell->getRow() : 1; + if (self::cellAddressNullOrWhitespace($cellAddress)) { + return self::cellRow($pCell); } if (is_array($cellAddress)) { @@ -119,9 +147,16 @@ class RowColumnInformation return (int) preg_replace('/\D/', '', $rowKey); } } + + return self::cellRow($pCell); } - [, $cellAddress] = Worksheet::extractSheetTitle((string) $cellAddress, true); + $cellAddress = $cellAddress ?? ''; + if ($pCell !== null) { + [,, $sheetName] = Helpers::extractWorksheet($cellAddress, $pCell); + [,, $cellAddress] = Helpers::extractCellAddresses($cellAddress, true, $pCell->getWorksheet(), $sheetName); + } + [, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); if (strpos($cellAddress, ':') !== false) { [$startAddress, $endAddress] = explode(':', $cellAddress); $startAddress = preg_replace('/\D/', '', $startAddress); @@ -154,9 +189,10 @@ class RowColumnInformation */ public static function ROWS($cellAddress = null) { - if ($cellAddress === null || (is_string($cellAddress) && trim($cellAddress) === '')) { + if (self::cellAddressNullOrWhitespace($cellAddress)) { return 1; - } elseif (!is_array($cellAddress)) { + } + if (!is_array($cellAddress)) { return Functions::VALUE(); } diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index 89aa32cd..8ca27097 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -268,7 +268,7 @@ class Cell } catch (Exception $ex) { if (($ex->getMessage() === 'Unable to access External Workbook') && ($this->calculatedValue !== null)) { return $this->calculatedValue; // Fallback for calculations referencing external files. - } elseif (strpos($ex->getMessage(), 'undefined name') !== false) { + } elseif (preg_match('/[Uu]ndefined (name|offset: 2|array key 2)/', $ex->getMessage()) === 1) { return \PhpOffice\PhpSpreadsheet\Calculation\Functions::NAME(); } diff --git a/src/PhpSpreadsheet/DefinedName.php b/src/PhpSpreadsheet/DefinedName.php index dbadd4ce..89ee01be 100644 --- a/src/PhpSpreadsheet/DefinedName.php +++ b/src/PhpSpreadsheet/DefinedName.php @@ -241,9 +241,18 @@ abstract class DefinedName /** * Resolve a named range to a regular cell range or formula. */ - public static function resolveName(string $pDefinedName, Worksheet $pSheet): ?self + public static function resolveName(string $pDefinedName, Worksheet $pSheet, string $sheetName = ''): ?self { - return $pSheet->getParent()->getDefinedName($pDefinedName, $pSheet); + if ($sheetName === '') { + $pSheet2 = $pSheet; + } else { + $pSheet2 = $pSheet->getParent()->getSheetByName($sheetName); + if ($pSheet2 === null) { + return null; + } + } + + return $pSheet->getParent()->getDefinedName($pDefinedName, $pSheet2); } /** diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AllSetupTeardown.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AllSetupTeardown.php new file mode 100644 index 00000000..9374ce32 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AllSetupTeardown.php @@ -0,0 +1,75 @@ +compatibilityMode = Functions::getCompatibilityMode(); + $this->spreadsheet = new Spreadsheet(); + $this->sheet = $this->spreadsheet->getActiveSheet(); + } + + protected function tearDown(): void + { + Functions::setCompatibilityMode($this->compatibilityMode); + $this->spreadsheet->disconnectWorksheets(); + } + + protected static function setOpenOffice(): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + } + + protected static function setGnumeric(): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_GNUMERIC); + } + + /** + * @param mixed $expectedResult + */ + protected function mightHaveException($expectedResult): void + { + if ($expectedResult === 'exception') { + $this->expectException(CalcException::class); + } + } + + /** + * @param mixed $value + */ + protected function setCell(string $cell, $value): void + { + if ($value !== null) { + if (is_string($value) && is_numeric($value)) { + $this->sheet->getCell($cell)->setValueExplicit($value, DataType::TYPE_STRING); + } else { + $this->sheet->getCell($cell)->setValue($value); + } + } + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnOnSpreadsheetTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnOnSpreadsheetTest.php new file mode 100644 index 00000000..8b89a671 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnOnSpreadsheetTest.php @@ -0,0 +1,57 @@ +mightHaveException($expectedResult); + $sheet = $this->sheet; + $this->spreadsheet->addNamedRange(new NamedRange('namedrangex', $sheet, '$E$2:$E$6')); + $this->spreadsheet->addNamedRange(new NamedRange('namedrangey', $sheet, '$F$2:$H$2')); + $this->spreadsheet->addNamedRange(new NamedRange('namedrange3', $sheet, '$F$4:$H$4')); + + $sheet1 = $this->spreadsheet->createSheet(); + $sheet1->setTitle('OtherSheet'); + + if ($cellReference === 'omitted') { + $sheet->getCell('B3')->setValue('=COLUMN()'); + } else { + $sheet->getCell('B3')->setValue("=COLUMN($cellReference)"); + } + + $result = $sheet->getCell('B3')->getCalculatedValue(); + self::assertSame($expectedResult, $result); + } + + public function providerCOLUMNonSpreadsheet(): array + { + return require 'tests/data/Calculation/LookupRef/COLUMNonSpreadsheet.php'; + } + + public function testCOLUMNLocalDefinedName(): void + { + $sheet = $this->sheet; + + $sheet1 = $this->spreadsheet->createSheet(); + $sheet1->setTitle('OtherSheet'); + $this->spreadsheet->addNamedRange(new NamedRange('newnr', $sheet1, '$F$5:$H$5', true)); // defined locally, only usable on sheet1 + + $sheet1->getCell('B3')->setValue('=COLUMN(newnr)'); + $result = $sheet1->getCell('B3')->getCalculatedValue(); + self::assertSame(6, $result); + + $sheet->getCell('B3')->setValue('=COLUMN(newnr)'); + $result = $sheet->getCell('B3')->getCalculatedValue(); + self::assertSame('#NAME?', $result); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnsOnSpreadsheetTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnsOnSpreadsheetTest.php new file mode 100644 index 00000000..93b09449 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnsOnSpreadsheetTest.php @@ -0,0 +1,60 @@ +mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->setTitle('ThisSheet'); + $this->spreadsheet->addNamedRange(new NamedRange('namedrangex', $sheet, '$E$2:$E$6')); + $this->spreadsheet->addNamedRange(new NamedRange('namedrangey', $sheet, '$F$2:$H$2')); + $this->spreadsheet->addNamedRange(new NamedRange('namedrange3', $sheet, '$F$4:$H$4')); + $this->spreadsheet->addNamedRange(new NamedRange('namedrange5', $sheet, '$F$5:$I$5', true)); + + $sheet1 = $this->spreadsheet->createSheet(); + $sheet1->setTitle('OtherSheet'); + $this->spreadsheet->addNamedRange(new NamedRange('localname', $sheet1, '$F$6:$H$6', true)); + + if ($cellReference === 'omitted') { + $sheet->getCell('B3')->setValue('=COLUMNS()'); + } else { + $sheet->getCell('B3')->setValue("=COLUMNS($cellReference)"); + } + + $result = $sheet->getCell('B3')->getCalculatedValue(); + self::assertSame($expectedResult, $result); + } + + public function providerCOLUMNSonSpreadsheet(): array + { + return require 'tests/data/Calculation/LookupRef/COLUMNSonSpreadsheet.php'; + } + + public function testCOLUMNSLocalDefinedName(): void + { + $sheet = $this->sheet; + + $sheet1 = $this->spreadsheet->createSheet(); + $sheet1->setTitle('OtherSheet'); + $this->spreadsheet->addNamedRange(new NamedRange('newnr', $sheet1, '$F$5:$H$5', true)); // defined locally, only usable on sheet1 + + $sheet1->getCell('B3')->setValue('=COLUMNS(newnr)'); + $result = $sheet1->getCell('B3')->getCalculatedValue(); + self::assertSame(3, $result); + + $sheet->getCell('B3')->setValue('=COLUMNS(newnr)'); + $result = $sheet->getCell('B3')->getCalculatedValue(); + self::assertSame('#NAME?', $result); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php index b11fce68..c53dce5c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php @@ -2,49 +2,55 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; -use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; -use PhpOffice\PhpSpreadsheet\Cell\Cell; -use PHPUnit\Framework\TestCase; +use PhpOffice\PhpSpreadsheet\NamedRange; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx as ReaderXlsx; -class IndirectTest extends TestCase +class IndirectTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerINDIRECT * * @param mixed $expectedResult - * @param null|mixed $cellReference + * @param mixed $cellReference + * @param mixed $a1 */ - public function testINDIRECT($expectedResult, $cellReference = null): void + public function testINDIRECT($expectedResult, $cellReference = 'omitted', $a1 = 'omitted'): void { -// $calculation = $this->getMockBuilder(Calculation::class) -// ->setMethods(['getInstance', 'extractCellRange']) -// ->disableOriginalConstructor() -// ->getMock(); -// $calculation->method('getInstance') -// ->willReturn($calculation); -// $calculation->method('extractCellRange') -// ->willReturn([]); -// -// $worksheet = $this->getMockBuilder(Cell::class) -// ->setMethods(['getParent']) -// ->disableOriginalConstructor() -// ->getMock(); -// -// $cell = $this->getMockBuilder(Cell::class) -// ->setMethods(['getWorksheet']) -// ->disableOriginalConstructor() -// ->getMock(); -// $cell->method('getWorksheet') -// ->willReturn($worksheet); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->getCell('A1')->setValue(100); + $sheet->getCell('A2')->setValue(200); + $sheet->getCell('A3')->setValue(300); + $sheet->getCell('A4')->setValue(400); + $sheet->getCell('A5')->setValue(500); + $sheet->setTitle('ThisSheet'); - $result = LookupRef::INDIRECT($cellReference); + $sheet1 = $this->spreadsheet->createSheet(); + $sheet1->getCell('A1')->setValue(10); + $sheet1->getCell('A2')->setValue(20); + $sheet1->getCell('A3')->setValue(30); + $sheet1->getCell('A4')->setValue(40); + $sheet1->getCell('A5')->setValue(50); + $sheet1->getCell('B1')->setValue(1); + $sheet1->getCell('B2')->setValue(2); + $sheet1->getCell('B3')->setValue(3); + $sheet1->getCell('B4')->setValue(4); + $sheet1->getCell('B5')->setValue(5); + $sheet1->setTitle('OtherSheet'); + $this->spreadsheet->addNamedRange(new NamedRange('newnr', $sheet1, '$A$2:$A$4')); + $this->spreadsheet->addNamedRange(new NamedRange('localname', $sheet1, '$B$2:$B$4', true)); + + $this->setCell('B1', $cellReference); + $this->setCell('B2', $a1); + if ($cellReference === 'omitted') { + $sheet->getCell('B3')->setValue('=SUM(INDIRECT())'); + } elseif ($a1 === 'omitted') { + $sheet->getCell('B3')->setValue('=SUM(INDIRECT(B1))'); + } else { + $sheet->getCell('B3')->setValue('=SUM(INDIRECT(B1, B2))'); + } + + $result = $sheet->getCell('B3')->getCalculatedValue(); self::assertSame($expectedResult, $result); } @@ -52,4 +58,78 @@ class IndirectTest extends TestCase { return require 'tests/data/Calculation/LookupRef/INDIRECT.php'; } + + public function testINDIRECTEurUsd(): void + { + $sheet = $this->sheet; + $sheet->getCell('A1')->setValue('EUR'); + $sheet->getCell('A2')->setValue('USD'); + $sheet->getCell('B1')->setValue(360); + $sheet->getCell('B2')->setValue(300); + + $this->spreadsheet->addNamedRange(new NamedRange('EUR', $sheet, '$B$1')); + $this->spreadsheet->addNamedRange(new NamedRange('USD', $sheet, '$B$2')); + + $this->setCell('E1', '=INDIRECT("USD")'); + + $result = $sheet->getCell('E1')->getCalculatedValue(); + self::assertSame(300, $result); + } + + public function testINDIRECTLeadingEquals(): void + { + $sheet = $this->sheet; + $sheet->getCell('A1')->setValue('EUR'); + $sheet->getCell('A2')->setValue('USD'); + $sheet->getCell('B1')->setValue(360); + $sheet->getCell('B2')->setValue(300); + + $this->spreadsheet->addNamedRange(new NamedRange('EUR', $sheet, '=$B$1')); + $this->spreadsheet->addNamedRange(new NamedRange('USD', $sheet, '=$B$2')); + + $this->setCell('E1', '=INDIRECT("USD")'); + + $result = $sheet->getCell('E1')->getCalculatedValue(); + self::assertSame(300, $result); + } + + public function testIndirectFile1(): void + { + $reader = new ReaderXlsx(); + $file = 'tests/data/Calculation/LookupRef/IndirectDefinedName.xlsx'; + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + $result = $sheet->getCell('A5')->getCalculatedValue(); + self::assertSame(80, $result); + $value = $sheet->getCell('A5')->getValue(); + self::assertSame('=INDIRECT("CURRENCY_EUR")', $value); + } + + public function testIndirectFile2(): void + { + $reader = new ReaderXlsx(); + $file = 'tests/data/Calculation/LookupRef/IndirectFormulaSelection.xlsx'; + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + $result = $sheet->getCell('A5')->getCalculatedValue(); + self::assertSame(100, $result); + $value = $sheet->getCell('A5')->getValue(); + self::assertSame('=CURRENCY_SELECTOR', $value); + $formula = $spreadsheet->getNamedFormula('CURRENCY_SELECTOR'); + if ($formula === null) { + self::fail('Expected named formula was not defined'); + } else { + self::assertSame('INDIRECT("CURRENCY_"&Sheet1!$D$1)', $formula->getFormula()); + } + } + + public function testDeprecatedCall(): void + { + $sheet = $this->sheet; + $sheet->getCell('A1')->setValue('A2'); + $sheet->getCell('A2')->setValue('This is it'); + $result = \PhpOffice\PhpSpreadsheet\Calculation\LookupRef::INDIRECT('A2', $sheet->getCell('A1')); + $result = \PhpOffice\PhpSpreadsheet\Calculation\Functions::flattenSingleValue($result); + self::assertSame('This is it', $result); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowOnSpreadsheetTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowOnSpreadsheetTest.php new file mode 100644 index 00000000..724349f2 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowOnSpreadsheetTest.php @@ -0,0 +1,60 @@ +mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->setTitle('ThisSheet'); + $this->spreadsheet->addNamedRange(new NamedRange('namedrangex', $sheet, '$E$2:$E$6')); + $this->spreadsheet->addNamedRange(new NamedRange('namedrangey', $sheet, '$F$2:$H$2')); + $this->spreadsheet->addNamedRange(new NamedRange('namedrange3', $sheet, '$F$4:$H$4')); + $this->spreadsheet->addNamedRange(new NamedRange('namedrange5', $sheet, '$F$5:$H$5', true)); + + $sheet1 = $this->spreadsheet->createSheet(); + $sheet1->setTitle('OtherSheet'); + $this->spreadsheet->addNamedRange(new NamedRange('localname', $sheet1, '$F$6:$H$6', true)); + + if ($cellReference === 'omitted') { + $sheet->getCell('B3')->setValue('=ROW()'); + } else { + $sheet->getCell('B3')->setValue("=ROW($cellReference)"); + } + + $result = $sheet->getCell('B3')->getCalculatedValue(); + self::assertSame($expectedResult, $result); + } + + public function providerROWOnSpreadsheet(): array + { + return require 'tests/data/Calculation/LookupRef/ROWonSpreadsheet.php'; + } + + public function testINDIRECTLocalDefinedName(): void + { + $sheet = $this->sheet; + + $sheet1 = $this->spreadsheet->createSheet(); + $sheet1->setTitle('OtherSheet'); + $this->spreadsheet->addNamedRange(new NamedRange('newnr', $sheet1, '$F$5:$H$5', true)); // defined locally, only usable on sheet1 + + $sheet1->getCell('B3')->setValue('=ROW(newnr)'); + $result = $sheet1->getCell('B3')->getCalculatedValue(); + self::assertSame(5, $result); + + $sheet->getCell('B3')->setValue('=ROW(newnr)'); + $result = $sheet->getCell('B3')->getCalculatedValue(); + self::assertSame('#NAME?', $result); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowsOnSpreadsheetTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowsOnSpreadsheetTest.php new file mode 100644 index 00000000..ba9ea518 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowsOnSpreadsheetTest.php @@ -0,0 +1,60 @@ +mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->setTitle('ThisSheet'); + $this->spreadsheet->addNamedRange(new NamedRange('namedrangex', $sheet, '$E$2:$E$6')); + $this->spreadsheet->addNamedRange(new NamedRange('namedrangey', $sheet, '$F$2:$H$2')); + $this->spreadsheet->addNamedRange(new NamedRange('namedrange3', $sheet, '$F$4:$H$4')); + $this->spreadsheet->addNamedRange(new NamedRange('namedrange5', $sheet, '$F$5:$H$5', true)); + + $sheet1 = $this->spreadsheet->createSheet(); + $sheet1->setTitle('OtherSheet'); + $this->spreadsheet->addNamedRange(new NamedRange('localname', $sheet1, '$F$6:$H$6', true)); + + if ($cellReference === 'omitted') { + $sheet->getCell('B3')->setValue('=ROWS()'); + } else { + $sheet->getCell('B3')->setValue("=ROWS($cellReference)"); + } + + $result = $sheet->getCell('B3')->getCalculatedValue(); + self::assertSame($expectedResult, $result); + } + + public function providerROWSOnSpreadsheet(): array + { + return require 'tests/data/Calculation/LookupRef/ROWSonSpreadsheet.php'; + } + + public function testRowsLocalDefinedName(): void + { + $sheet = $this->sheet; + + $sheet1 = $this->spreadsheet->createSheet(); + $sheet1->setTitle('OtherSheet'); + $this->spreadsheet->addNamedRange(new NamedRange('newnr', $sheet1, '$F$5:$H$10', true)); // defined locally, only usable on sheet1 + + $sheet1->getCell('B3')->setValue('=ROWS(newnr)'); + $result = $sheet1->getCell('B3')->getCalculatedValue(); + self::assertSame(6, $result); + + $sheet->getCell('B3')->setValue('=ROWS(newnr)'); + $result = $sheet->getCell('B3')->getCalculatedValue(); + self::assertSame('#NAME?', $result); + } +} diff --git a/tests/data/Calculation/LookupRef/COLUMN.php b/tests/data/Calculation/LookupRef/COLUMN.php index 83ffde59..f5119ba8 100644 --- a/tests/data/Calculation/LookupRef/COLUMN.php +++ b/tests/data/Calculation/LookupRef/COLUMN.php @@ -29,4 +29,5 @@ return [ [2, 3, 4], '"WorkSheet #1"!B2:D2', ], + [1, []], ]; diff --git a/tests/data/Calculation/LookupRef/COLUMNSonSpreadsheet.php b/tests/data/Calculation/LookupRef/COLUMNSonSpreadsheet.php new file mode 100644 index 00000000..4002c2f1 --- /dev/null +++ b/tests/data/Calculation/LookupRef/COLUMNSonSpreadsheet.php @@ -0,0 +1,19 @@ + [1, 'namedrangex'], + 'global $f$2:$h$2' => [3, 'namedrangey'], + 'global $f$4:$h$4' => [3, 'namedrange3'], + 'local in scope $f$5:$i$5' => [4, 'namedrange5'], + 'local out of scope' => ['#NAME?', 'localname'], + 'non-existent sheet' => [10, 'UnknownSheet!B2:K6'], + 'not enough arguments' => ['exception', 'omitted'], + 'other existing sheet' => [6, 'OtherSheet!B1:G1'], + 'qualified in scope $f$5:$i$5' => [4, 'ThisSheet!namedrange5'], + 'single cell absolute' => [1, '$C$15'], + 'single cell relative' => [1, 'C7'], + 'unknown name' => ['#NAME?', 'namedrange2'], + 'unknown name as first part of range' => ['#NAME?', 'Invalid:A2'], + 'unknown name as second part of range' => ['#NAME?', 'A2:Invalid'], + //'qualified out of scope $f$6:$h$6' => [3, 'OtherSheet!localname'], // needs investigation +]; diff --git a/tests/data/Calculation/LookupRef/COLUMNonSpreadsheet.php b/tests/data/Calculation/LookupRef/COLUMNonSpreadsheet.php new file mode 100644 index 00000000..cc93653e --- /dev/null +++ b/tests/data/Calculation/LookupRef/COLUMNonSpreadsheet.php @@ -0,0 +1,17 @@ + [2, 'omitted'], + 'global name $E$2:$E$6' => [5, 'namedrangex'], + 'global name $F$2:$H$2' => [6, 'namedrange3'], + 'global name $F$4:$H$4' => [6, 'namedrangey'], + 'out of scope name' => ['#NAME?', 'localname'], + 'qualified cell existing sheet' => [1, 'OtherSheet!A1'], + 'qualified cell non-existent sheet' => [1, 'UnknownSheet!A1'], + 'single cell absolute' => [3, '$C$15'], + 'single cell relative' => [3, 'C7'], + 'unknown name' => ['#NAME?', 'namedrange2'], + 'unknown name as first part of range' => ['#NAME?', 'Invalid:A2'], + 'unknown name as second part of range' => ['#NAME?', 'A2:Invalid'], + //'qualified name' => [6, 'OtherSheet!localname'], // Never reaches function +]; diff --git a/tests/data/Calculation/LookupRef/INDIRECT.php b/tests/data/Calculation/LookupRef/INDIRECT.php index 3ff44707..b8519657 100644 --- a/tests/data/Calculation/LookupRef/INDIRECT.php +++ b/tests/data/Calculation/LookupRef/INDIRECT.php @@ -1,16 +1,37 @@ [600, '$A$1:$A$3'], + 'cell range on different sheet' => [30, 'OtherSheet!A1:A2'], + 'cell range on different sheet absolute' => [60, 'OtherSheet!$A$1:$A$3'], + 'cell range relative' => [500, 'A2:A3'], + 'global name qualified with correct sheet' => [90, 'OtherSheet!newnr'], + 'global name qualified with incorrect sheet' => [90, 'ThisSheet!newnr'], + 'global name qualified with non-existent sheet' => ['#REF!', 'UnknownSheet!newnr'], + 'invalid address' => ['#REF!', 'InvalidCellAddress'], + 'invalid address as first part of range' => ['#REF!', 'Invalid:A2'], + 'invalid address as second part of range' => ['#REF!', 'A2:Invalid'], + 'local name out of scope' => ['#REF!', 'localname'], + 'named range' => [90, 'newnr'], + 'named range a1 arg ignored' => [90, 'newnr', false], + 'named range case-insensitive' => [90, 'newNr'], + 'null address' => ['#REF!', null], + 'omit a1 argument' => [900, 'A2:A4'], + 'qualified name correct sheet even out of scope' => [9, 'OtherSheet!localname'], + 'qualified name incorrect sheet' => ['#REF!', 'ThisSheet!localname'], + 'qualified name non-existent sheet' => ['#REF!', 'OtherSheetx!localname'], + 'r1c1 cell on different sheet' => [40, 'OtherSheet!R4C1', false], + 'r1c1 format a1 as string not permitted' => ['#VALUE!', 'R2C1', '0'], + 'r1c1 format a1 is bool' => [200, 'R2C1', false], + 'r1c1 format a1 is int' => [200, 'R2C1', 0], + 'r1c1 range' => [600, 'R1C1:R3C1', false], + 'r1c1 range on different sheet' => [90, 'OtherSheet!R4C1:R5C1', false], + 'single cell absolute' => [200, '$A$2'], + 'single cell relative' => [100, 'A1'], + 'single cell on different sheet absolute' => [30, 'OtherSheet!$A$3'], + 'single cell on different sheet relative' => [10, 'OtherSheet!A1'], + 'supply a1 argument as bool' => [900, 'A2:A4', true], + 'supply a1 argument as int' => [900, 'A2:A4', 1], + 'supply a1 argument as float' => [900, 'A2:A4', 7.3], + 'supply a1 argument as string not permitted' => ['#VALUE!', 'A2:A4', '1'], ]; diff --git a/tests/data/Calculation/LookupRef/IndirectDefinedName.xlsx b/tests/data/Calculation/LookupRef/IndirectDefinedName.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..d351aa60137d91e0919d18ab6371db6d7120769e GIT binary patch literal 9116 zcmeHtg;yNe_B9&ZHMqM&a0wnDxD(u=fyRO)xVuAe39i8Aa( zFf=GHjzc#NhmHvFnNOxDm!zzH{4TtiM%>$I^1fC` zqOXTCwPQ`VkkjvwxnBB1KTIF5$8-(2%QJJ>V<>#xc_;?rb-L#vpq_gBoM712I4O#u ze-4_8ad~w=7+dhXfhGR2olc2(MR3_-iWN++=VxdLrN3#kPMwYN2l$`2Un1LRj5TxH>y*aDRGuH`|W?i}93G0-?BNja3Q_h7ipT+Upj zNXxp@xwJ+zlr|RS$PKPiN==9~+c zYsY%@U{**UP@&+$yx|y=?x)Va^wz26IvdV#@8Ln{E*&@>^b02~S^=l4e~RRLs8mcm zG&qSNKtP~@pNyLgt1HkEU z$nsiDj!=-+*2DpCE&P%9YSmN{lxC^Ei*nUwe|mZfA1B{ab%pF^Y-keC@9&E^U!8fM0WUAL|z2Fb8p@Q%O z4c$Bub zyIjhU8Cqcb{Tx2KJoAUZGeSMQJC5JwJw8#;mX@E8V`4Wk_} zqz(d^8>xho>ynL=qu>s8^Ikq+eEZx_gF!XlDxo$}<{R~J|9AAH(XiG<^kt4iH-y5g zjvCLY)WaltvE#J{x@Q*w;M1R+t5 zT`ZBV1)lnq^cu@RP@7=M`` z>Mw{XFmOhJmvNH|PmOS9M2dCzAvLy6TIO0NyTBrMcCk&RHU71@Mok!tVEF4+@$~N! z7C^uYitn$6)#V`uR;;0M^b`vRC4U^TG-am7liW=0oO1HjU$Hz-o@bppt~%N`ZeOam z0Ik}qmMkE)ICadGlB_3%qv7kNhqUXHEXt=j)nJ0(V8LY}pACrezHA&gljc6p@QtLP zt;cdr?vW|VFA&!5%(7hi%I1I6pj5?F6vHW619#)6uppSnj4hc(xYn%5?fi(2=JPo| z4x7(3KpFo7s^g~f8mbHtwOz^nXWG)GEbmI&yQF$^HJ5P}HJ*us*O7bXt_^%AJly>5 zGa`OgWgBf(PcyxYr){Qi`U*hQ7JR+*v2SQCACpFo?Ns%!p7+Z0YjUK@$pLBIgNk5v z{!_nka0!PnU}cMdHB0~j4++-q?|JU8D*jK-g9O*3;A{W8x3ail>mD{_$?L!;|A{sS z4o{^Z2lEM)WZN_^ANvH}x5jYqJhx5&D*S^TF?QALZ@R8YLYqO}uH`pWqy!c?rZR|r zWiDnW0N9h>;Ze$v4of+k3~E?t*!i6~&1OyO*v&%2RN;^!V}GdbC15`CqEaxqgU|wR zar#M&$sS-KSJ+tIF}?t8a_kkE$P{+4imymQ16f#3^tWd~bBQ&Vkig?a>^2fE$LV+< ztb}hHjmGIa+E|P&v-{E1E21&6_Q^t@(nVf~_h9uG<81M22Q(RANtu*p zA#=O<@zo`K|cMjCd?}+>>hsJW<)J(udQYd&agAcyqj~w7+ zX=di^#QNKb{bw>rPf!M?v7z^`(jEzDc`cG)Ld&GSz$k%HVMZ%0D9flV9AuX^#$l_; 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 literal 0 HcmV?d00001 diff --git a/tests/data/Calculation/LookupRef/IndirectFormulaSelection.xlsx b/tests/data/Calculation/LookupRef/IndirectFormulaSelection.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..0095a29adc76c9440a4026048e2c640c3bb0698b GIT binary patch literal 9275 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 [5, 'namedrangex'], + 'global $F$2:$H$2' => [1, 'namedrangey'], + 'global $F$4:$H$4' => [1, 'namedrange3'], + 'local in scope $F$5:$H$5' => [1, 'namedrange5'], + 'local out of scope' => ['#NAME?', 'localname'], + 'non-existent sheet' => [5, 'UnknownSheet!B2:K6'], + 'not enough arguments' => ['exception', 'omitted'], + 'other existing sheet' => [6, 'OtherSheet!A1:H6'], + 'qualified in scope $F$5:$H$5' => [1, 'ThisSheet!namedrange5'], + 'single cell absolute' => [1, '$C$7'], + 'single cell relative' => [1, 'C7'], + 'unknown name' => ['#NAME?', 'InvalidCellAddress'], + 'unknown name as first part of range' => ['#NAME?', 'Invalid:A2'], + 'unknown name as second part of range' => ['#NAME?', 'A2:Invalid'], + //'qualified out of scope $F$6:$H$6' => [1, 'OtherSheet!localname'], // needs investigation +]; diff --git a/tests/data/Calculation/LookupRef/ROWonSpreadsheet.php b/tests/data/Calculation/LookupRef/ROWonSpreadsheet.php new file mode 100644 index 00000000..f2395e6b --- /dev/null +++ b/tests/data/Calculation/LookupRef/ROWonSpreadsheet.php @@ -0,0 +1,18 @@ + [3, 'omitted'], + 'global name $E$2:$E$6' => [2, 'namedrangex'], + 'global name $F$2:$H$2' => [2, 'namedrangey'], + 'global name $F$4:$H$4' => [4, 'namedrange3'], + 'local name $F$5:$H$5' => [5, 'namedrange5'], + 'out of scope name' => ['#NAME?', 'localname'], + 'qualified cell existing sheet' => [1, 'OtherSheet!A1'], + 'qualified cell non-existent sheet' => [1, 'UnknownSheet!A1'], + 'single cell absolute' => [7, '$C$7'], + 'single cell relative' => [7, 'C7'], + 'unknown name' => ['#NAME?', 'namedrange2'], + 'unknown name as first part of range' => ['#NAME?', 'InvalidCell:A2'], + 'unknown name as second part of range' => ['#NAME?', 'A2:InvalidCell'], + //'qualified name' => [6, 'OtherSheet!localname'], // Never reaches function +];