diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a082ff27d2..88a983687d 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 6a89f7dae2..5adf4f09b7 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef.php @@ -148,8 +148,8 @@ public static function ROWS($cellAddress = null) * 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 @@ public static function HYPERLINK($linkURL = '', $displayName = null, ?Cell $pCel * * 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 0000000000..87ea738147 --- /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 c34dd9651b..9ba231422c 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 @@ 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 @@ public static function INDIRECT($cellAddress = null, ?Cell $pCell = null) 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 19d0d5ff8a..02c9a79e86 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php @@ -10,6 +10,21 @@ 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 @@ public static function COLUMN($cellAddress = null, ?Cell $cell = null) 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 @@ public static function COLUMN($cellAddress = null, ?Cell $cell = null) */ 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 @@ public static function COLUMNS($cellAddress = null) return $columns; } + private static function cellRow(?Cell $pCell): int + { + return ($pCell !== null) ? $pCell->getRow() : 1; + } + /** * ROW. * @@ -109,8 +137,8 @@ public static function COLUMNS($cellAddress = null) */ 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 @@ public static function ROW($cellAddress = null, ?Cell $pCell = null) 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 @@ function ($value) { */ 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 89aa32cd0f..8ca2709735 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -268,7 +268,7 @@ public function getCalculatedValue($resetLog = true) } 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 dbadd4ced5..89ee01bec0 100644 --- a/src/PhpSpreadsheet/DefinedName.php +++ b/src/PhpSpreadsheet/DefinedName.php @@ -241,9 +241,18 @@ public function isFormula(): bool /** * 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 0000000000..9374ce3200 --- /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 0000000000..8b89a6711b --- /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 0000000000..93b094493e --- /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 b11fce6800..c53dce5c0f 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); - - $result = LookupRef::INDIRECT($cellReference); + $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'); + + $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 @@ public function providerINDIRECT(): array { 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 0000000000..724349f2d0 --- /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 0000000000..ba9ea518e5 --- /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 83ffde5921..f5119ba821 100644 --- a/tests/data/Calculation/LookupRef/COLUMN.php +++ b/tests/data/Calculation/LookupRef/COLUMN.php @@ -29,4 +29,5 @@ [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 0000000000..4002c2f16d --- /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 0000000000..cc93653e80 --- /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 3ff447076d..b8519657c7 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 0000000000..d351aa6013 Binary files /dev/null and b/tests/data/Calculation/LookupRef/IndirectDefinedName.xlsx differ diff --git a/tests/data/Calculation/LookupRef/IndirectFormulaSelection.xlsx b/tests/data/Calculation/LookupRef/IndirectFormulaSelection.xlsx new file mode 100644 index 0000000000..0095a29adc Binary files /dev/null and b/tests/data/Calculation/LookupRef/IndirectFormulaSelection.xlsx differ diff --git a/tests/data/Calculation/LookupRef/ROW.php b/tests/data/Calculation/LookupRef/ROW.php index 62545c7674..d6092f7ffb 100644 --- a/tests/data/Calculation/LookupRef/ROW.php +++ b/tests/data/Calculation/LookupRef/ROW.php @@ -32,4 +32,5 @@ [[10], [11], [12]], '"WorkSheet #1"!C10:C12', ], + [1, []], ]; diff --git a/tests/data/Calculation/LookupRef/ROWSonSpreadsheet.php b/tests/data/Calculation/LookupRef/ROWSonSpreadsheet.php new file mode 100644 index 0000000000..117885bc13 --- /dev/null +++ b/tests/data/Calculation/LookupRef/ROWSonSpreadsheet.php @@ -0,0 +1,19 @@ + [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 0000000000..f2395e6b74 --- /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 +];