From 62238bc0118f442ce5157f2f050d14c234304889 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 30 Mar 2022 17:02:42 +0200 Subject: [PATCH 1/6] Initial work on deprecating `ByColumnAndRow` methods; and providing functionality to use the basic cellAddress methods to be used with a string cell address (e.g. `C5`); an array of columnId and rowId (e.g. `[3, 5]`) or a new CellAddress object. Current implementation for all methods that take a single cell reference argument: - `setCellValue()` - `setCellValueExplicit()` - `getCell()` - `cellExists()` - `setBreak()` - `freezePane()` - `getComment()` Also introducing a CellRange object to work with similar cases for methods that accept a cell range rather than simply a cell address; and RowRange/ColumnRange objects for those cases. Still need to apply to methods that accept a cell range or single cell: - `mergeCells()` - `unmergeCells()` - `protectCells()` - `unprotectCells()` - `setAutoFilter()` Then there's a few special cases that accept row and column ranges, not simply cell ranges; or a series of cell ranges. --- CHANGELOG.md | 21 ++ src/PhpSpreadsheet/Cell/CellAddress.php | 172 ++++++++++++ src/PhpSpreadsheet/Cell/CellRange.php | 78 ++++++ src/PhpSpreadsheet/Cell/ColumnRange.php | 111 ++++++++ src/PhpSpreadsheet/Cell/RowRange.php | 79 ++++++ src/PhpSpreadsheet/Worksheet/Worksheet.php | 223 +++++++++++----- .../Cell/CellAddressTest.php | 246 ++++++++++++++++++ .../Cell/CellRangeTest.php | 113 ++++++++ .../Cell/ColumnRangeTest.php | 65 +++++ .../PhpSpreadsheetTests/Cell/RowRangeTest.php | 51 ++++ 10 files changed, 1087 insertions(+), 72 deletions(-) create mode 100644 src/PhpSpreadsheet/Cell/CellAddress.php create mode 100644 src/PhpSpreadsheet/Cell/CellRange.php create mode 100644 src/PhpSpreadsheet/Cell/ColumnRange.php create mode 100644 src/PhpSpreadsheet/Cell/RowRange.php create mode 100644 tests/PhpSpreadsheetTests/Cell/CellAddressTest.php create mode 100644 tests/PhpSpreadsheetTests/Cell/CellRangeTest.php create mode 100644 tests/PhpSpreadsheetTests/Cell/ColumnRangeTest.php create mode 100644 tests/PhpSpreadsheetTests/Cell/RowRangeTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index c7ae1854..3d20a9cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,27 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Deprecated - All Excel Function implementations in `Calculation\Functions` (including the Error functions) have been moved to dedicated classes for groups of related functions. See the docblocks against all the deprecated methods for details of the new methods to call instead. At some point, these old classes will be deleted. +- Worksheet methods that reference cells "byColumnandRow". All such methods have an equivalent that references the cell by its address (e.g. '`E3'` rather than `5, 3`). + + These functions now accept either a cell address string (`'E3')` or an array with columnId and rowId (`[5, 3]`) or a new `CellAddress` object as their `cellAddress`/`coordinate` argument. + This includes the methods: + - `setCellValueByColumnAndRow()` use the equivalent `setCellValue()` + - `setCellValueExplicitByColumnAndRow()` use the equivalent `setCellValueExplicit()` + - `getCellByColumnAndRow()` use the equivalent `getCell()` + - `cellExistsByColumnAndRow()` use the equivalent `cellExists()` + - `getStyleByColumnAndRow()` use the equivalent `getStyle()` + - `setBreakByColumnAndRow()` use the equivalent `setBreak()` + - `mergeCellsByColumnAndRow()` use the equivalent `mergeCells()` + - `unmergeCellsByColumnAndRow()` use the equivalent `unmergeCells()` + - `protectCellsByColumnAndRow()` use the equivalent `protectCells()` + - `unprotectCellsByColumnAndRow()` use the equivalent `unprotectCells()` + - `setAutoFilterByColumnAndRow()` use the equivalent `setAutoFilter()` + - `freezePaneByColumnAndRow()` use the equivalent `freezePane()` + - `getCommentByColumnAndRow()` use the equivalent `getComment()` + - `setSelectedCellByColumnAndRow()` use the equivalent `setSelectedCells()` + + This change provides more consistency in the methods (not every "by cell address" method has an equivalent "byColumnAndRow" method); + and the "by cell address" methods often provide more flexibility, such as allowing a range of cells, or referencig them by passing the defined name of a named range as the argument. ### Removed diff --git a/src/PhpSpreadsheet/Cell/CellAddress.php b/src/PhpSpreadsheet/Cell/CellAddress.php new file mode 100644 index 00000000..389f09ac --- /dev/null +++ b/src/PhpSpreadsheet/Cell/CellAddress.php @@ -0,0 +1,172 @@ +cellAddress = str_replace('$', '', $cellAddress); + [$this->columnName, $rowId] = Coordinate::coordinateFromString($cellAddress); + $this->rowId = (int) $rowId; + $this->columnId = Coordinate::columnIndexFromString($this->columnName); + $this->worksheet = $worksheet; + } + + /** + * @param mixed $columnId + * @param mixed $rowId + */ + private static function validateColumnAndRow($columnId, $rowId): void + { + $array = [$columnId, $rowId]; + array_walk( + $array, + function ($value): void { + if (!is_numeric($value) || $value <= 0) { + throw new Exception('Row and Column Ids must be positive integer values'); + } + } + ); + } + + /** + * @param mixed $columnId + * @param mixed $rowId + */ + public static function fromColumnAndRow($columnId, $rowId, ?Worksheet $worksheet = null): self + { + self::validateColumnAndRow($columnId, $rowId); + + return new static(Coordinate::stringFromColumnIndex($columnId) . ((string) $rowId), $worksheet); + } + + public static function fromColumnRowArray(array $array, ?Worksheet $worksheet = null): self + { + [$columnId, $rowId] = $array; + + return static::fromColumnAndRow($columnId, $rowId, $worksheet); + } + + /** + * @param mixed $cellAddress + */ + public static function fromCellAddress($cellAddress, ?Worksheet $worksheet = null): self + { + return new static($cellAddress, $worksheet); + } + + /** + * The returned address string will contain the worksheet name as well, if available, + * (ie. if a Worksheet was provided to the constructor). + * e.g. "'Mark''s Worksheet'!C5". + */ + public function fullCellAddress(): string + { + if ($this->worksheet !== null) { + $title = str_replace("'", "''", $this->worksheet->getTitle()); + + return "'{$title}'!{$this->cellAddress}"; + } + + return $this->cellAddress; + } + + public function worksheet(): ?Worksheet + { + return $this->worksheet; + } + + /** + * The returned address string will contain just the column/row address, + * (even if a Worksheet was provided to the constructor). + * e.g. "C5". + */ + public function cellAddress(): string + { + return $this->cellAddress; + } + + public function rowId(): int + { + return $this->rowId; + } + + public function columnId(): int + { + return $this->columnId; + } + + public function columnName(): string + { + return $this->columnName; + } + + public function nextRow(int $offset = 1): self + { + $newRowId = $this->rowId + $offset; + if ($newRowId < 1) { + $newRowId = 1; + } + + return static::fromColumnAndRow($this->columnId, $newRowId); + } + + public function previousRow(int $offset = 1): self + { + return $this->nextRow(0 - $offset); + } + + public function nextColumn(int $offset = 1): self + { + $newColumnId = $this->columnId + $offset; + if ($newColumnId < 1) { + $newColumnId = 1; + } + + return static::fromColumnAndRow($newColumnId, $this->rowId); + } + + public function previousColumn(int $offset = 1): self + { + return $this->nextColumn(0 - $offset); + } + + /** + * The returned address string will contain the worksheet name as well, if available, + * (ie. if a Worksheet was provided to the constructor). + * e.g. "'Mark''s Worksheet'!C5". + */ + public function __toString() + { + return $this->fullCellAddress(); + } +} diff --git a/src/PhpSpreadsheet/Cell/CellRange.php b/src/PhpSpreadsheet/Cell/CellRange.php new file mode 100644 index 00000000..4880fa6c --- /dev/null +++ b/src/PhpSpreadsheet/Cell/CellRange.php @@ -0,0 +1,78 @@ +validateFromTo($from, $to); + } + + private function validateFromTo(CellAddress $from, CellAddress $to): void + { + // Identify actual top-left and bottom-right values (in case we've been given top-right and bottom-left) + $firstColumn = min($from->columnId(), $to->columnId()); + $firstRow = min($from->rowId(), $to->rowId()); + $lastColumn = max($from->columnId(), $to->columnId()); + $lastRow = max($from->rowId(), $to->rowId()); + + $fromWorksheet = $from->worksheet(); + $toWorksheet = $to->worksheet(); + $this->validateWorksheets($fromWorksheet, $toWorksheet); + + $this->from = CellAddress::fromColumnAndRow($firstColumn, $firstRow, $fromWorksheet); + $this->to = CellAddress::fromColumnAndRow($lastColumn, $lastRow, $toWorksheet); + } + + private function validateWorksheets(?Worksheet $fromWorksheet, ?Worksheet $toWorksheet): void + { + if ($fromWorksheet !== null && $toWorksheet !== null) { + // We could simply compare worksheets rather than worksheet titles; but at some point we may introduce + // support for 3d ranges; and at that point we drop this check and let the validation fall through + // to the check for same workbook; but unless we check on titles, this test will also detect if the + // worksheets are in different spreadsheets, and the next check will never execute or throw its + // own exception. + if ($fromWorksheet->getTitle() !== $toWorksheet->getTitle()) { + throw new Exception('3d Cell Ranges are not supported'); + } elseif ($fromWorksheet->getParent() !== $toWorksheet->getParent()) { + throw new Exception('Worksheets must be in the same spreadsheet'); + } + } + } + + public function from(): CellAddress + { + return $this->from; + } + + public function to(): CellAddress + { + return $this->to; + } + + public function __toString(): string + { + if ($this->from->cellAddress() === $this->to->cellAddress()) { + return "{$this->from->fullCellAddress()}"; + } + + $fromAddress = $this->from->fullCellAddress(); + $toAddress = $this->to->cellAddress(); + + return "{$fromAddress}:{$toAddress}"; + } +} diff --git a/src/PhpSpreadsheet/Cell/ColumnRange.php b/src/PhpSpreadsheet/Cell/ColumnRange.php new file mode 100644 index 00000000..d38caea8 --- /dev/null +++ b/src/PhpSpreadsheet/Cell/ColumnRange.php @@ -0,0 +1,111 @@ +validateFromTo( + Coordinate::columnIndexFromString($from), + Coordinate::columnIndexFromString($to ?? $from) + ); + $this->worksheet = $worksheet; + } + + public static function fromColumnIndexes(int $from, int $to, ?Worksheet $worksheet = null): self + { + return new self(Coordinate::stringFromColumnIndex($from), Coordinate::stringFromColumnIndex($to), $worksheet); + } + + /** + * @param array $array + */ + public static function fromArray(array $array, ?Worksheet $worksheet = null): self + { + array_walk( + $array, + function (&$column): void { + $column = is_numeric($column) ? Coordinate::stringFromColumnIndex((int) $column) : $column; + } + ); + /** @var string $from */ + /** @var string $to */ + [$from, $to] = $array; + + return new self($from, $to, $worksheet); + } + + private function validateFromTo(int $from, int $to): void + { + // Identify actual top and bottom values (in case we've been given bottom and top) + $this->from = min($from, $to); + $this->to = max($from, $to); + } + + public function columnCount(): int + { + return $this->to - $this->from + 1; + } + + public function from(): string + { + return Coordinate::stringFromColumnIndex($this->from); + } + + public function to(): string + { + return Coordinate::stringFromColumnIndex($this->to); + } + + public function fromIndex(): int + { + return $this->from; + } + + public function toIndex(): int + { + return $this->to; + } + + public function toCellRange(): CellRange + { + return new CellRange( + CellAddress::fromColumnAndRow($this->from, 1, $this->worksheet), + CellAddress::fromColumnAndRow($this->to, self::MAX_ROW) + ); + } + + public function __toString(): string + { + $from = $this->from(); + $to = $this->to(); + + if ($this->worksheet !== null) { + $title = str_replace("'", "''", $this->worksheet->getTitle()); + + return "'{$title}'!{$from}:{$to}"; + } + + return "{$from}:{$to}"; + } +} diff --git a/src/PhpSpreadsheet/Cell/RowRange.php b/src/PhpSpreadsheet/Cell/RowRange.php new file mode 100644 index 00000000..07190702 --- /dev/null +++ b/src/PhpSpreadsheet/Cell/RowRange.php @@ -0,0 +1,79 @@ +validateFromTo($from, $to ?? $from); + $this->worksheet = $worksheet; + } + + public static function fromArray(array $array, ?Worksheet $worksheet = null): self + { + [$from, $to] = $array; + + return new self($from, $to, $worksheet); + } + + private function validateFromTo(int $from, int $to): void + { + // Identify actual top and bottom values (in case we've been given bottom and top) + $this->from = min($from, $to); + $this->to = max($from, $to); + } + + public function from(): int + { + return $this->from; + } + + public function to(): int + { + return $this->to; + } + + public function rowCount(): int + { + return $this->to - $this->from + 1; + } + + public function toCellRange(): CellRange + { + return new CellRange( + CellAddress::fromColumnAndRow(Coordinate::columnIndexFromString('A'), $this->from, $this->worksheet), + CellAddress::fromColumnAndRow(Coordinate::columnIndexFromString(self::MAX_COLUMN), $this->to) + ); + } + + public function __toString(): string + { + if ($this->worksheet !== null) { + $title = str_replace("'", "''", $this->worksheet->getTitle()); + + return "'{$title}'!{$this->from}:{$this->to}"; + } + + return "{$this->from}:{$this->to}"; + } +} diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index d4e1e928..54fcc0de 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -5,6 +5,8 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; use ArrayObject; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Cell; +use PhpOffice\PhpSpreadsheet\Cell\CellAddress; +use PhpOffice\PhpSpreadsheet\Cell\CellRange; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Cell\DataValidation; @@ -1103,17 +1105,44 @@ class Worksheet implements IComparable return $this->cellCollection->getHighestRowAndColumn(); } + /** + * Validate a cell address. + * + * @param null|array|CellAddress|string $cellAddress Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + */ + protected function validateCellAddress($cellAddress, bool $allowNull = false): ?string + { + if (is_string($cellAddress) || ($cellAddress === null && $allowNull === true)) { + if ($cellAddress === null) { + return null; + } + [$worksheet, $address] = self::extractSheetTitle($cellAddress, true); + + return empty($worksheet) ? strtoupper($address) : $worksheet . '!' . strtoupper($address); + } + + if (is_array($cellAddress)) { + $cellAddress = CellAddress::fromColumnRowArray($cellAddress); + } + + return (string) $cellAddress; + } + /** * Set a cell value. * - * @param string $coordinate Coordinate of the cell, eg: 'A1' - * @param mixed $value Value of the cell + * @param array|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * @param mixed $value Value for the cell * * @return $this */ public function setCellValue($coordinate, $value) { - $this->getCell($coordinate)->setValue($value); + /** @var string $cellAddress */ + $cellAddress = $this->validateCellAddress($coordinate); + $this->getCell($cellAddress)->setValue($value); return $this; } @@ -1121,6 +1150,10 @@ class Worksheet implements IComparable /** * Set a cell value by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the setCellValue() method with a cell address such as 'C5' instead;, + * or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * * @param int $columnIndex Numeric column coordinate of the cell * @param int $row Numeric row coordinate of the cell * @param mixed $value Value of the cell @@ -1129,7 +1162,7 @@ class Worksheet implements IComparable */ public function setCellValueByColumnAndRow($columnIndex, $row, $value) { - $this->getCellByColumnAndRow($columnIndex, $row)->setValue($value); + $this->getCell([$columnIndex, $row])->setValue($value); return $this; } @@ -1137,7 +1170,8 @@ class Worksheet implements IComparable /** * Set a cell value. * - * @param string $coordinate Coordinate of the cell, eg: 'A1' + * @param array|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. * @param mixed $value Value of the cell * @param string $dataType Explicit data type, see DataType::TYPE_* * @@ -1145,8 +1179,9 @@ class Worksheet implements IComparable */ public function setCellValueExplicit($coordinate, $value, $dataType) { - // Set value - $this->getCell($coordinate)->setValueExplicit($value, $dataType); + /** @var string $cellAddress */ + $cellAddress = $this->validateCellAddress($coordinate); + $this->getCell($cellAddress)->setValueExplicit($value, $dataType); return $this; } @@ -1154,6 +1189,10 @@ class Worksheet implements IComparable /** * Set a cell value by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the setCellValueExplicit() method with a cell address such as 'C5' instead;, + * or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * * @param int $columnIndex Numeric column coordinate of the cell * @param int $row Numeric row coordinate of the cell * @param mixed $value Value of the cell @@ -1163,7 +1202,7 @@ class Worksheet implements IComparable */ public function setCellValueExplicitByColumnAndRow($columnIndex, $row, $value, $dataType) { - $this->getCellByColumnAndRow($columnIndex, $row)->setValueExplicit($value, $dataType); + $this->getCell([$columnIndex, $row])->setValueExplicit($value, $dataType); return $this; } @@ -1171,22 +1210,26 @@ class Worksheet implements IComparable /** * Get cell at a specific coordinate. * - * @param string $coordinate Coordinate of the cell, eg: 'A1' + * @param array|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. * * @return Cell Cell that was found or created */ - public function getCell(string $coordinate): Cell + public function getCell($coordinate): Cell { + /** @var string $cellAddress */ + $cellAddress = $this->validateCellAddress($coordinate); + // Shortcut for increased performance for the vast majority of simple cases - if ($this->cellCollection->has($coordinate)) { + if ($this->cellCollection->has($cellAddress)) { /** @var Cell $cell */ - $cell = $this->cellCollection->get($coordinate); + $cell = $this->cellCollection->get($cellAddress); return $cell; } /** @var Worksheet $sheet */ - [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($coordinate); + [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress); $cell = $sheet->cellCollection->get($finalCoordinate); return $cell ?? $sheet->createNewCell($finalCoordinate); @@ -1210,7 +1253,7 @@ class Worksheet implements IComparable $sheet = $this->parent->getSheetByName($worksheetReference[0]); $finalCoordinate = strtoupper($worksheetReference[1]); - if (!$sheet) { + if ($sheet === null) { throw new Exception('Sheet not found for name: ' . $worksheetReference[0]); } } elseif ( @@ -1221,7 +1264,7 @@ class Worksheet implements IComparable $namedRange = $this->validateNamedRange($coordinate, true); if ($namedRange !== null) { $sheet = $namedRange->getWorksheet(); - if (!$sheet) { + if ($sheet === null) { throw new Exception('Sheet not found for named range: ' . $namedRange->getName()); } @@ -1231,7 +1274,7 @@ class Worksheet implements IComparable } } - if (!$sheet || !$finalCoordinate) { + if ($sheet === null || $finalCoordinate === null) { $sheet = $this; $finalCoordinate = strtoupper($coordinate); } @@ -1265,6 +1308,10 @@ class Worksheet implements IComparable /** * Get cell at a specific coordinate by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the getCell() method with a cell address such as 'C5' instead;, + * or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * * @param int $columnIndex Numeric column coordinate of the cell * @param int $row Numeric row coordinate of the cell * @@ -1272,18 +1319,7 @@ class Worksheet implements IComparable */ public function getCellByColumnAndRow($columnIndex, $row): Cell { - $columnLetter = Coordinate::stringFromColumnIndex($columnIndex); - $coordinate = $columnLetter . $row; - - if ($this->cellCollection->has($coordinate)) { - /** @var Cell $cell */ - $cell = $this->cellCollection->get($coordinate); - - return $cell; - } - - // Create new cell object, if required - return $this->createNewCell($coordinate); + return $this->getCell([$columnIndex, $row]); } /** @@ -1328,14 +1364,15 @@ class Worksheet implements IComparable /** * Does the cell at a specific coordinate exist? * - * @param string $coordinate Coordinate of the cell eg: 'A1' - * - * @return bool + * @param array|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. */ - public function cellExists($coordinate) + public function cellExists($coordinate): bool { + /** @var string $cellAddress */ + $cellAddress = $this->validateCellAddress($coordinate); /** @var Worksheet $sheet */ - [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($coordinate); + [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress); return $sheet->cellCollection->has($finalCoordinate); } @@ -1343,14 +1380,16 @@ class Worksheet implements IComparable /** * Cell at a specific coordinate by using numeric cell coordinates exists? * + * @Deprecated 1.23.0 + * Use the cellExists() method with a cell address such as 'C5' instead;, + * or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * * @param int $columnIndex Numeric column coordinate of the cell * @param int $row Numeric row coordinate of the cell - * - * @return bool */ - public function cellExistsByColumnAndRow($columnIndex, $row) + public function cellExistsByColumnAndRow($columnIndex, $row): bool { - return $this->cellExists(Coordinate::stringFromColumnIndex($columnIndex) . $row); + return $this->cellExists([$columnIndex, $row]); } /** @@ -1548,12 +1587,15 @@ class Worksheet implements IComparable public function getStyleByColumnAndRow($columnIndex1, $row1, $columnIndex2 = null, $row2 = null) { if ($columnIndex2 !== null && $row2 !== null) { - $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; + $cellRange = new CellRange( + CellAddress::fromColumnAndRow($columnIndex1, $row1), + CellAddress::fromColumnAndRow($columnIndex2, $row2) + ); return $this->getStyle($cellRange); } - return $this->getStyle(Coordinate::stringFromColumnIndex($columnIndex1) . $row1); + return $this->getStyle(CellAddress::fromColumnAndRow($columnIndex1, $row1)); } /** @@ -1640,26 +1682,26 @@ class Worksheet implements IComparable /** * Set break on a cell. * - * @param string $coordinate Cell coordinate (e.g. A1) + * @param array|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. * @param int $break Break type (type of Worksheet::BREAK_*) * * @return $this */ public function setBreak($coordinate, $break) { - // Uppercase coordinate - $coordinate = strtoupper($coordinate); + $cellAddress = $this->validateCellAddress($coordinate); - if ($coordinate != '') { - if ($break == self::BREAK_NONE) { - if (isset($this->breaks[$coordinate])) { - unset($this->breaks[$coordinate]); - } - } else { - $this->breaks[$coordinate] = $break; + if ($cellAddress === '') { + throw new Exception('No cell coordinate specified.'); + } + + if ($break === self::BREAK_NONE) { + if (isset($this->breaks[$cellAddress])) { + unset($this->breaks[$cellAddress]); } } else { - throw new Exception('No cell coordinate specified.'); + $this->breaks[$cellAddress] = $break; } return $this; @@ -1668,6 +1710,10 @@ class Worksheet implements IComparable /** * Set break on a cell by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the setBreak() method with a cell address such as 'C5' instead;, + * or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * * @param int $columnIndex Numeric column coordinate of the cell * @param int $row Numeric row coordinate of the cell * @param int $break Break type (type of Worksheet::BREAK_*) @@ -1676,7 +1722,7 @@ class Worksheet implements IComparable */ public function setBreakByColumnAndRow($columnIndex, $row, $break) { - return $this->setBreak(Coordinate::stringFromColumnIndex($columnIndex) . $row, $break); + return $this->setBreak([$columnIndex, $row], $break); } /** @@ -1779,6 +1825,11 @@ class Worksheet implements IComparable /** * Set merge on a cell range by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the mergeCells() method with a cell address range such as 'C5:E8' instead;, + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5]), + * or a CellAddress object. + * * @param int $columnIndex1 Numeric column coordinate of the first cell * @param int $row1 Numeric row coordinate of the first cell * @param int $columnIndex2 Numeric column coordinate of the last cell @@ -1788,7 +1839,10 @@ class Worksheet implements IComparable */ public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2) { - $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; + $cellRange = new CellRange( + CellAddress::fromColumnAndRow($columnIndex1, $row1), + CellAddress::fromColumnAndRow($columnIndex2, $row2) + ); return $this->mergeCells($cellRange); } @@ -1830,7 +1884,10 @@ class Worksheet implements IComparable */ public function unmergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2) { - $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; + $cellRange = new CellRange( + CellAddress::fromColumnAndRow($columnIndex1, $row1), + CellAddress::fromColumnAndRow($columnIndex2, $row2) + ); return $this->unmergeCells($cellRange); } @@ -1896,7 +1953,10 @@ class Worksheet implements IComparable */ public function protectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $password, $alreadyHashed = false) { - $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; + $cellRange = new CellRange( + CellAddress::fromColumnAndRow($columnIndex1, $row1), + CellAddress::fromColumnAndRow($columnIndex2, $row2) + ); return $this->protectCells($cellRange, $password, $alreadyHashed); } @@ -1934,7 +1994,10 @@ class Worksheet implements IComparable */ public function unprotectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2) { - $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; + $cellRange = new CellRange( + CellAddress::fromColumnAndRow($columnIndex1, $row1), + CellAddress::fromColumnAndRow($columnIndex2, $row2) + ); return $this->unprotectCells($cellRange); } @@ -2026,23 +2089,26 @@ class Worksheet implements IComparable * - B1 will freeze the columns to the left of cell B1 (i.e column A) * - B2 will freeze the rows above and to the left of cell B2 (i.e row 1 and column A) * - * @param null|string $cell Position of the split + * @param null|array|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * Passing a null value for this argument will clear an existing freeze pane for this worksheet. * @param null|string $topLeftCell default position of the right bottom pane * * @return $this */ - public function freezePane($cell, $topLeftCell = null) + public function freezePane($coordinate, $topLeftCell = null) { - if (is_string($cell) && Coordinate::coordinateIsRange($cell)) { + $cellAddress = $this->validateCellAddress($coordinate, true); + if (is_string($cellAddress) && Coordinate::coordinateIsRange($cellAddress)) { throw new Exception('Freeze pane can not be set on a range of cells.'); } - if ($cell !== null && $topLeftCell === null) { - $coordinate = Coordinate::coordinateFromString($cell); + if ($cellAddress !== null && $topLeftCell === null) { + $coordinate = Coordinate::coordinateFromString($cellAddress); $topLeftCell = $coordinate[0] . $coordinate[1]; } - $this->freezePane = $cell; + $this->freezePane = $cellAddress; $this->topLeftCell = $topLeftCell; return $this; @@ -2058,6 +2124,10 @@ class Worksheet implements IComparable /** * Freeze Pane by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the freezePane() method with a cell address such as 'C5' instead;, + * or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * * @param int $columnIndex Numeric column coordinate of the cell * @param int $row Numeric row coordinate of the cell * @@ -2065,7 +2135,7 @@ class Worksheet implements IComparable */ public function freezePaneByColumnAndRow($columnIndex, $row) { - return $this->freezePane(Coordinate::stringFromColumnIndex($columnIndex) . $row); + return $this->freezePane([$columnIndex, $row]); } /** @@ -2427,31 +2497,32 @@ class Worksheet implements IComparable /** * Get comment for cell. * - * @param string $cellCoordinate Cell coordinate to get comment for, eg: 'A1' + * @param array|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. * * @return Comment */ public function getComment($cellCoordinate) { - // Uppercase coordinate - $cellCoordinate = strtoupper($cellCoordinate); + /** @var string $cellAddress */ + $cellAddress = $this->validateCellAddress($cellCoordinate); - if (Coordinate::coordinateIsRange($cellCoordinate)) { + if (Coordinate::coordinateIsRange($cellAddress)) { throw new Exception('Cell coordinate string can not be a range of cells.'); - } elseif (strpos($cellCoordinate, '$') !== false) { + } elseif (strpos($cellAddress, '$') !== false) { throw new Exception('Cell coordinate string must not be absolute.'); - } elseif ($cellCoordinate == '') { + } elseif ($cellAddress == '') { throw new Exception('Cell coordinate can not be zero-length string.'); } // Check if we already have a comment for this cell. - if (isset($this->comments[$cellCoordinate])) { - return $this->comments[$cellCoordinate]; + if (isset($this->comments[$cellAddress])) { + return $this->comments[$cellAddress]; } // If not, create a new comment. $newComment = new Comment(); - $this->comments[$cellCoordinate] = $newComment; + $this->comments[$cellAddress] = $newComment; return $newComment; } @@ -2459,6 +2530,10 @@ class Worksheet implements IComparable /** * Get comment for cell by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the getComment() method with a cell address such as 'C5' instead;, + * or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * * @param int $columnIndex Numeric column coordinate of the cell * @param int $row Numeric row coordinate of the cell * @@ -2466,7 +2541,7 @@ class Worksheet implements IComparable */ public function getCommentByColumnAndRow($columnIndex, $row) { - return $this->getComment(Coordinate::stringFromColumnIndex($columnIndex) . $row); + return $this->getComment([$columnIndex, $row]); } /** @@ -2574,6 +2649,10 @@ class Worksheet implements IComparable /** * Selected cell by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the setSelectedCells() method with a cell address such as 'C5' instead;, + * or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * * @param int $columnIndex Numeric column coordinate of the cell * @param int $row Numeric row coordinate of the cell * diff --git a/tests/PhpSpreadsheetTests/Cell/CellAddressTest.php b/tests/PhpSpreadsheetTests/Cell/CellAddressTest.php new file mode 100644 index 00000000..0057410a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Cell/CellAddressTest.php @@ -0,0 +1,246 @@ +cellAddress()); + self::assertSame($expectedRowId, $cellAddressObject->rowId()); + self::assertSame($expectedColumnId, $cellAddressObject->columnId()); + self::assertSame($expectedColumnName, $cellAddressObject->columnName()); + } + + public function providerCreateFromCellAddress(): array + { + return [ + ['A1', 'A', 1, 1], + ['C5', 'C', 3, 5], + ['IV256', 'IV', 256, 256], + ]; + } + + /** + * @dataProvider providerCreateFromCellAddressException + * + * @param mixed $cellAddress + */ + public function testCreateFromCellAddressException($cellAddress): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage( + $cellAddress === '' + ? 'Cell coordinate can not be zero-length string' + : "Invalid cell coordinate {$cellAddress}" + ); + + CellAddress::fromCellAddress($cellAddress); + } + + public function providerCreateFromCellAddressException(): array + { + return [ + ['INVALID'], + [''], + ['IV'], + ['12'], + [123], + ]; + } + + /** + * @dataProvider providerCreateFromColumnAndRow + */ + public function testCreateFromColumnAndRow( + int $columnId, + int $rowId, + string $expectedCellAddress, + string $expectedColumnName + ): void { + $cellAddressObject = CellAddress::fromColumnAndRow($columnId, $rowId); + + self::assertSame($expectedCellAddress, (string) $cellAddressObject); + self::assertSame($expectedCellAddress, $cellAddressObject->cellAddress()); + self::assertSame($rowId, $cellAddressObject->rowId()); + self::assertSame($columnId, $cellAddressObject->columnId()); + self::assertSame($expectedColumnName, $cellAddressObject->columnName()); + } + + /** + * @dataProvider providerCreateFromColumnRowException + * + * @param mixed $columnId + * @param mixed $rowId + */ + public function testCreateFromColumnRowException($columnId, $rowId): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Row and Column Ids must be positive integer values'); + + CellAddress::fromColumnAndRow($columnId, $rowId); + } + + public function providerCreateFromColumnAndRow(): array + { + return [ + [1, 1, 'A1', 'A'], + [3, 5, 'C5', 'C'], + [256, 256, 'IV256', 'IV'], + ]; + } + + /** + * @dataProvider providerCreateFromColumnRowArray + */ + public function testCreateFromColumnRowArray( + int $columnId, + int $rowId, + string $expectedCellAddress, + string $expectedColumnName + ): void { + $columnRowArray = [$columnId, $rowId]; + $cellAddressObject = CellAddress::fromColumnRowArray($columnRowArray); + + self::assertSame($expectedCellAddress, (string) $cellAddressObject); + self::assertSame($expectedCellAddress, $cellAddressObject->cellAddress()); + self::assertSame($rowId, $cellAddressObject->rowId()); + self::assertSame($columnId, $cellAddressObject->columnId()); + self::assertSame($expectedColumnName, $cellAddressObject->columnName()); + } + + public function providerCreateFromColumnRowArray(): array + { + return [ + [1, 1, 'A1', 'A'], + [3, 5, 'C5', 'C'], + [256, 256, 'IV256', 'IV'], + ]; + } + + /** + * @dataProvider providerCreateFromColumnRowException + * + * @param mixed $columnId + * @param mixed $rowId + */ + public function testCreateFromColumnRowArrayException($columnId, $rowId): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Row and Column Ids must be positive integer values'); + + $columnRowArray = [$columnId, $rowId]; + CellAddress::fromColumnRowArray($columnRowArray); + } + + public function providerCreateFromColumnRowException(): array + { + return [ + [-1, 1], + [3, 'A'], + ]; + } + + /** + * @dataProvider providerCreateFromCellAddressWithWorksheet + */ + public function testCreateFromCellAddressWithWorksheet( + string $cellAddress, + string $expectedCellAddress, + string $expectedColumnName, + int $expectedColumnId, + int $expectedRowId + ): void { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->setTitle("Mark's Worksheet"); + + $cellAddressObject = CellAddress::fromCellAddress($cellAddress, $worksheet); + + self::assertSame($expectedCellAddress, (string) $cellAddressObject); + self::assertSame($cellAddress, $cellAddressObject->cellAddress()); + self::assertSame($expectedRowId, $cellAddressObject->rowId()); + self::assertSame($expectedColumnId, $cellAddressObject->columnId()); + self::assertSame($expectedColumnName, $cellAddressObject->columnName()); + } + + public function providerCreateFromCellAddressWithWorksheet(): array + { + return [ + ['A1', "'Mark''s Worksheet'!A1", 'A', 1, 1], + ['C5', "'Mark''s Worksheet'!C5", 'C', 3, 5], + ['IV256', "'Mark''s Worksheet'!IV256", 'IV', 256, 256], + ]; + } + + public function testNextRow(): void + { + $cellAddress = CellAddress::fromCellAddress('C5'); + // default single row + $cellAddressC6 = $cellAddress->nextRow(); + self::assertSame('C6', (string) $cellAddressC6); + // multiple rows + $cellAddressC9 = $cellAddress->nextRow(4); + self::assertSame('C9', (string) $cellAddressC9); + // negative rows + $cellAddressC3 = $cellAddress->nextRow(-2); + self::assertSame('C3', (string) $cellAddressC3); + // negative beyond the minimum + $cellAddressC1 = $cellAddress->nextRow(-10); + self::assertSame('C1', (string) $cellAddressC1); + + // Check that the original object is still unchanged + self::assertSame('C5', (string) $cellAddress); + } + + public function testPreviousRow(): void + { + $cellAddress = CellAddress::fromCellAddress('C5'); + // default single row + $cellAddressC4 = $cellAddress->previousRow(); + self::assertSame('C4', (string) $cellAddressC4); + } + + public function testNextColumn(): void + { + $cellAddress = CellAddress::fromCellAddress('C5'); + // default single row + $cellAddressD5 = $cellAddress->nextColumn(); + self::assertSame('D5', (string) $cellAddressD5); + // multiple rows + $cellAddressG5 = $cellAddress->nextColumn(4); + self::assertSame('G5', (string) $cellAddressG5); + // negative rows + $cellAddressB5 = $cellAddress->nextColumn(-1); + self::assertSame('B5', (string) $cellAddressB5); + // negative beyond the minimum + $cellAddressA5 = $cellAddress->nextColumn(-10); + self::assertSame('A5', (string) $cellAddressA5); + + // Check that the original object is still unchanged + self::assertSame('C5', (string) $cellAddress); + } + + public function testPreviousColumn(): void + { + $cellAddress = CellAddress::fromCellAddress('C5'); + // default single row + $cellAddressC4 = $cellAddress->previousColumn(); + self::assertSame('B5', (string) $cellAddressC4); + } +} diff --git a/tests/PhpSpreadsheetTests/Cell/CellRangeTest.php b/tests/PhpSpreadsheetTests/Cell/CellRangeTest.php new file mode 100644 index 00000000..856f0e50 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Cell/CellRangeTest.php @@ -0,0 +1,113 @@ +getActiveSheet(); + $worksheet->setTitle("Mark's Worksheet"); + + $from = CellAddress::fromCellAddress('B5', $worksheet); + $to = CellAddress::fromCellAddress('E2'); + $cellRange = new CellRange($from, $to); + self::assertSame("'Mark''s Worksheet'!B2:E5", (string) $cellRange); + } + + public function testCreateCellRangeWithWorksheets(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->setTitle("Mark's Worksheet"); + + $from = CellAddress::fromCellAddress('B5', $worksheet); + $to = CellAddress::fromCellAddress('E2', $worksheet); + $cellRange = new CellRange($from, $to); + self::assertSame("'Mark''s Worksheet'!B2:E5", (string) $cellRange); + } + + public function testSingleCellRange(): void + { + $from = CellAddress::fromCellAddress('C3'); + $to = CellAddress::fromCellAddress('C3'); + $cellRange = new CellRange($from, $to); + self::assertSame('C3', (string) $cellRange); + } + + public function testSingleCellRangeWithWorksheet(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->setTitle("Mark's Worksheet"); + + $from = CellAddress::fromCellAddress('C3', $worksheet); + $to = CellAddress::fromCellAddress('C3'); + $cellRange = new CellRange($from, $to); + self::assertSame("'Mark''s Worksheet'!C3", (string) $cellRange); + } + + public function testRangeFrom(): void + { + $from = CellAddress::fromCellAddress('B5'); + $to = CellAddress::fromCellAddress('E2'); + $cellRange = new CellRange($from, $to); + self::assertSame('B2', (string) $cellRange->from()); + } + + public function testRangeTo(): void + { + $from = CellAddress::fromCellAddress('B5'); + $to = CellAddress::fromCellAddress('E2'); + $cellRange = new CellRange($from, $to); + self::assertSame('E5', (string) $cellRange->to()); + } + + public function testCreateCellRangeWithMismatchedWorksheets(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->setTitle("Mark's Worksheet"); + $secondWorksheet = new Worksheet($spreadsheet, 'A Second Worksheet'); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('3d Cell Ranges are not supported'); + + $from = CellAddress::fromCellAddress('B5', $worksheet); + $to = CellAddress::fromCellAddress('E2', $secondWorksheet); + new CellRange($from, $to); + } + + public function testCreateCellRangeWithMismatchedSpreadsheets(): void + { + $spreadsheet1 = new Spreadsheet(); + $worksheet1 = $spreadsheet1->getActiveSheet(); + $worksheet1->setTitle("Mark's Worksheet"); + $spreadsheet2 = new Spreadsheet(); + $worksheet2 = $spreadsheet2->getActiveSheet(); + $worksheet2->setTitle("Mark's Worksheet"); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Worksheets must be in the same spreadsheet'); + + $from = CellAddress::fromCellAddress('B5', $worksheet1); + $to = CellAddress::fromCellAddress('E2', $worksheet2); + new CellRange($from, $to); + } +} diff --git a/tests/PhpSpreadsheetTests/Cell/ColumnRangeTest.php b/tests/PhpSpreadsheetTests/Cell/ColumnRangeTest.php new file mode 100644 index 00000000..6fa18d89 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Cell/ColumnRangeTest.php @@ -0,0 +1,65 @@ +from()); + self::assertSame('E', $columnRange->to()); + self::assertSame(3, $columnRange->fromIndex()); + self::assertSame(5, $columnRange->toIndex()); + self::assertSame('C:E', (string) $columnRange); + self::assertSame(3, $columnRange->columnCount()); + self::assertSame('C1:E1048576', (string) $columnRange->toCellRange()); + } + + public function testCreateSingleColumnRange(): void + { + $columnRange = new ColumnRange('E'); + self::assertSame('E', $columnRange->from()); + self::assertSame('E', $columnRange->to()); + self::assertSame('E:E', (string) $columnRange); + self::assertSame(1, $columnRange->columnCount()); + self::assertSame('E1:E1048576', (string) $columnRange->toCellRange()); + } + + public function testCreateColumnRangeWithWorksheet(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->setTitle("Mark's Worksheet"); + + $columnRange = new ColumnRange('C', 'E', $worksheet); + self::assertSame('C', $columnRange->from()); + self::assertSame('E', $columnRange->to()); + self::assertSame("'Mark''s Worksheet'!C:E", (string) $columnRange); + self::assertSame("'Mark''s Worksheet'!C1:E1048576", (string) $columnRange->toCellRange()); + } + + public function testCreateColumnRangeFromArray(): void + { + $columnRange = ColumnRange::fromArray(['C', 'E']); + self::assertSame('C', $columnRange->from()); + self::assertSame('E', $columnRange->to()); + self::assertSame('C:E', (string) $columnRange); + self::assertSame(3, $columnRange->columnCount()); + self::assertSame('C1:E1048576', (string) $columnRange->toCellRange()); + } + + public function testCreateColumnRangeFromIndexes(): void + { + $columnRange = ColumnRange::fromColumnIndexes(3, 5); + self::assertSame('C', $columnRange->from()); + self::assertSame('E', $columnRange->to()); + self::assertSame('C:E', (string) $columnRange); + self::assertSame(3, $columnRange->columnCount()); + self::assertSame('C1:E1048576', (string) $columnRange->toCellRange()); + } +} diff --git a/tests/PhpSpreadsheetTests/Cell/RowRangeTest.php b/tests/PhpSpreadsheetTests/Cell/RowRangeTest.php new file mode 100644 index 00000000..41d4992c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Cell/RowRangeTest.php @@ -0,0 +1,51 @@ +from()); + self::assertSame(5, $rowRange->to()); + self::assertSame('3:5', (string) $rowRange); + self::assertSame(3, $rowRange->rowCount()); + self::assertSame('A3:XFD5', (string) $rowRange->toCellRange()); + } + + public function testCreateSingleRowRange(): void + { + $rowRange = new RowRange(3); + self::assertSame(3, $rowRange->from()); + self::assertSame(3, $rowRange->to()); + self::assertSame('3:3', (string) $rowRange); + self::assertSame(1, $rowRange->rowCount()); + } + + public function testCreateRowRangeWithWorksheet(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->setTitle("Mark's Worksheet"); + + $rowRange = new RowRange(3, 5, $worksheet); + self::assertSame(3, $rowRange->from()); + self::assertSame(5, $rowRange->to()); + self::assertSame("'Mark''s Worksheet'!3:5", (string) $rowRange); + } + + public function testCreateRowRangeFromArray(): void + { + $rowRange = RowRange::fromArray([3, 5]); + self::assertSame(3, $rowRange->from()); + self::assertSame(5, $rowRange->to()); + self::assertSame('3:5', (string) $rowRange); + self::assertSame(3, $rowRange->rowCount()); + self::assertSame('A3:XFD5', (string) $rowRange->toCellRange()); + } +} From 1849737abc77675e2ce66c51eadaad0db31ac88c Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 1 Apr 2022 11:26:47 +0200 Subject: [PATCH 2/6] Add functionality to shift RowRange and ColumnRange --- src/PhpSpreadsheet/Cell/CellAddress.php | 2 ++ src/PhpSpreadsheet/Cell/ColumnRange.php | 16 +++++++++++++ src/PhpSpreadsheet/Cell/RowRange.php | 16 +++++++++++++ .../Cell/ColumnRangeTest.php | 24 +++++++++++++++++++ .../PhpSpreadsheetTests/Cell/RowRangeTest.php | 24 +++++++++++++++++++ 5 files changed, 82 insertions(+) diff --git a/src/PhpSpreadsheet/Cell/CellAddress.php b/src/PhpSpreadsheet/Cell/CellAddress.php index 389f09ac..412cff56 100644 --- a/src/PhpSpreadsheet/Cell/CellAddress.php +++ b/src/PhpSpreadsheet/Cell/CellAddress.php @@ -66,6 +66,7 @@ class CellAddress { self::validateColumnAndRow($columnId, $rowId); + /** @phpstan-ignore-next-line */ return new static(Coordinate::stringFromColumnIndex($columnId) . ((string) $rowId), $worksheet); } @@ -81,6 +82,7 @@ class CellAddress */ public static function fromCellAddress($cellAddress, ?Worksheet $worksheet = null): self { + /** @phpstan-ignore-next-line */ return new static($cellAddress, $worksheet); } diff --git a/src/PhpSpreadsheet/Cell/ColumnRange.php b/src/PhpSpreadsheet/Cell/ColumnRange.php index d38caea8..dbd91bb6 100644 --- a/src/PhpSpreadsheet/Cell/ColumnRange.php +++ b/src/PhpSpreadsheet/Cell/ColumnRange.php @@ -67,6 +67,22 @@ class ColumnRange return $this->to - $this->from + 1; } + public function shiftDown(int $offset = 1): self + { + $newFrom = $this->from + $offset; + $newFrom = ($newFrom < 1) ? 1 : $newFrom; + + $newTo = $this->to + $offset; + $newTo = ($newTo < 1) ? 1 : $newTo; + + return self::fromColumnIndexes($newFrom, $newTo, $this->worksheet); + } + + public function shiftUp(int $offset = 1): self + { + return $this->shiftDown(0 - $offset); + } + public function from(): string { return Coordinate::stringFromColumnIndex($this->from); diff --git a/src/PhpSpreadsheet/Cell/RowRange.php b/src/PhpSpreadsheet/Cell/RowRange.php index 07190702..bef97111 100644 --- a/src/PhpSpreadsheet/Cell/RowRange.php +++ b/src/PhpSpreadsheet/Cell/RowRange.php @@ -58,6 +58,22 @@ class RowRange return $this->to - $this->from + 1; } + public function shiftRight(int $offset = 1): self + { + $newFrom = $this->from + $offset; + $newFrom = ($newFrom < 1) ? 1 : $newFrom; + + $newTo = $this->to + $offset; + $newTo = ($newTo < 1) ? 1 : $newTo; + + return new self($newFrom, $newTo, $this->worksheet); + } + + public function shiftLeft(int $offset = 1): self + { + return $this->shiftRight(0 - $offset); + } + public function toCellRange(): CellRange { return new CellRange( diff --git a/tests/PhpSpreadsheetTests/Cell/ColumnRangeTest.php b/tests/PhpSpreadsheetTests/Cell/ColumnRangeTest.php index 6fa18d89..14455701 100644 --- a/tests/PhpSpreadsheetTests/Cell/ColumnRangeTest.php +++ b/tests/PhpSpreadsheetTests/Cell/ColumnRangeTest.php @@ -62,4 +62,28 @@ class ColumnRangeTest extends TestCase self::assertSame(3, $columnRange->columnCount()); self::assertSame('C1:E1048576', (string) $columnRange->toCellRange()); } + + public function testColumnRangeNext(): void + { + $columnRange = new ColumnRange('C', 'E'); + $columnRangeNext = $columnRange->shiftDown(3); + + self::assertSame('F', $columnRangeNext->from()); + self::assertSame('H', $columnRangeNext->to()); + + // Check that original Column Range isn't changed + self::assertSame('C:E', (string) $columnRange); + } + + public function testColumnRangePrevious(): void + { + $columnRange = new ColumnRange('C', 'E'); + $columnRangeNext = $columnRange->shiftUp(); + + self::assertSame('B', $columnRangeNext->from()); + self::assertSame('D', $columnRangeNext->to()); + + // Check that original Column Range isn't changed + self::assertSame('C:E', (string) $columnRange); + } } diff --git a/tests/PhpSpreadsheetTests/Cell/RowRangeTest.php b/tests/PhpSpreadsheetTests/Cell/RowRangeTest.php index 41d4992c..32605cf0 100644 --- a/tests/PhpSpreadsheetTests/Cell/RowRangeTest.php +++ b/tests/PhpSpreadsheetTests/Cell/RowRangeTest.php @@ -48,4 +48,28 @@ class RowRangeTest extends TestCase self::assertSame(3, $rowRange->rowCount()); self::assertSame('A3:XFD5', (string) $rowRange->toCellRange()); } + + public function testRowRangeNext(): void + { + $rowRange = new RowRange(3, 5); + $rowRangeNext = $rowRange->shiftRight(3); + + self::assertSame(6, $rowRangeNext->from()); + self::assertSame(8, $rowRangeNext->to()); + + // Check that original Row Range isn't changed + self::assertSame('3:5', (string) $rowRange); + } + + public function testRowRangePrevious(): void + { + $rowRange = new RowRange(3, 5); + $rowRangeNext = $rowRange->shiftLeft(); + + self::assertSame(2, $rowRangeNext->from()); + self::assertSame(4, $rowRangeNext->to()); + + // Check that original Row Range isn't changed + self::assertSame('3:5', (string) $rowRange); + } } From 6b4ffda5ae3b394b5ac0bf8a9197ed25ffc35759 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 1 Apr 2022 12:23:32 +0200 Subject: [PATCH 3/6] Add functionality to adjust CellRange by "modifying" the from/to CellAddress Value objects --- .php-cs-fixer.dist.php | 2 +- CHANGELOG.md | 3 +- src/PhpSpreadsheet/Cell/AddressRange.php | 22 +++++++ src/PhpSpreadsheet/Cell/CellAddress.php | 2 +- src/PhpSpreadsheet/Cell/CellRange.php | 64 ++++++++++++++++++- src/PhpSpreadsheet/Cell/ColumnRange.php | 6 +- src/PhpSpreadsheet/Cell/RowRange.php | 6 +- .../Cell/CellRangeTest.php | 40 ++++++++++++ 8 files changed, 131 insertions(+), 14 deletions(-) create mode 100644 src/PhpSpreadsheet/Cell/AddressRange.php diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 8a8886c2..ca2feb42 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -21,7 +21,7 @@ $config 'braces' => true, 'cast_spaces' => true, 'class_attributes_separation' => ['elements' => ['method' => 'one', 'property' => 'one']], // const are often grouped with other related const - 'class_definition' => true, + 'class_definition' => false, 'class_keyword_remove' => false, // ::class keyword gives us better support in IDE 'combine_consecutive_issets' => true, 'combine_consecutive_unsets' => true, diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d20a9cc..35ed928e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added -- Implementation of the FILTER(), SORT(), SORTBY() and UNIQUE() Lookup/Reference (array) functions +- Introduced CellAddress, CellRange, RowRange and ColumnRange value objects that can be used as an alternative to a string value (e.g. `'C5'`, `'B2:D4'`, `'2:2'` or `'B:C'`) in appropriate contexts. +- Implementation of the FILTER(), SORT(), SORTBY() and UNIQUE() Lookup/Reference (array) functions. - Implementation of the ISREF() Information function. - Added support for reading "formatted" numeric values from Csv files; although default behaviour of reading these values as strings is preserved. diff --git a/src/PhpSpreadsheet/Cell/AddressRange.php b/src/PhpSpreadsheet/Cell/AddressRange.php new file mode 100644 index 00000000..54752317 --- /dev/null +++ b/src/PhpSpreadsheet/Cell/AddressRange.php @@ -0,0 +1,22 @@ +cellAddress = str_replace('$', '', $cellAddress); [$this->columnName, $rowId] = Coordinate::coordinateFromString($cellAddress); diff --git a/src/PhpSpreadsheet/Cell/CellRange.php b/src/PhpSpreadsheet/Cell/CellRange.php index 4880fa6c..908a0d07 100644 --- a/src/PhpSpreadsheet/Cell/CellRange.php +++ b/src/PhpSpreadsheet/Cell/CellRange.php @@ -5,7 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Cell; use PhpOffice\PhpSpreadsheet\Exception; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; -class CellRange +class CellRange implements AddressRange { /** * @var CellAddress @@ -34,8 +34,8 @@ class CellRange $toWorksheet = $to->worksheet(); $this->validateWorksheets($fromWorksheet, $toWorksheet); - $this->from = CellAddress::fromColumnAndRow($firstColumn, $firstRow, $fromWorksheet); - $this->to = CellAddress::fromColumnAndRow($lastColumn, $lastRow, $toWorksheet); + $this->from = $this->cellAddressWrapper($firstColumn, $firstRow, $fromWorksheet); + $this->to = $this->cellAddressWrapper($lastColumn, $lastRow, $toWorksheet); } private function validateWorksheets(?Worksheet $fromWorksheet, ?Worksheet $toWorksheet): void @@ -54,18 +54,76 @@ class CellRange } } + private function cellAddressWrapper(int $column, int $row, ?Worksheet $worksheet = null): CellAddress + { + $cellAddress = Coordinate::stringFromColumnIndex($column) . (string) $row; + + return new class ($cellAddress, $worksheet) extends CellAddress { + public function nextRow(int $offset = 1): CellAddress + { + /** @var CellAddress $result */ + $result = parent::nextRow($offset); + $this->rowId = $result->rowId; + $this->cellAddress = $result->cellAddress; + + return $this; + } + + public function previousRow(int $offset = 1): CellAddress + { + /** @var CellAddress $result */ + $result = parent::previousRow($offset); + $this->rowId = $result->rowId; + $this->cellAddress = $result->cellAddress; + + return $this; + } + + public function nextColumn(int $offset = 1): CellAddress + { + /** @var CellAddress $result */ + $result = parent::nextColumn($offset); + $this->columnId = $result->columnId; + $this->columnName = $result->columnName; + $this->cellAddress = $result->cellAddress; + + return $this; + } + + public function previousColumn(int $offset = 1): CellAddress + { + /** @var CellAddress $result */ + $result = parent::previousColumn($offset); + $this->columnId = $result->columnId; + $this->columnName = $result->columnName; + $this->cellAddress = $result->cellAddress; + + return $this; + } + }; + } + public function from(): CellAddress { + // Re-order from/to in case the cell addresses have been modified + $this->validateFromTo($this->from, $this->to); + return $this->from; } public function to(): CellAddress { + // Re-order from/to in case the cell addresses have been modified + $this->validateFromTo($this->from, $this->to); + return $this->to; } public function __toString(): string { + // Re-order from/to in case the cell addresses have been modified + $this->validateFromTo($this->from, $this->to); + if ($this->from->cellAddress() === $this->to->cellAddress()) { return "{$this->from->fullCellAddress()}"; } diff --git a/src/PhpSpreadsheet/Cell/ColumnRange.php b/src/PhpSpreadsheet/Cell/ColumnRange.php index dbd91bb6..1e521a13 100644 --- a/src/PhpSpreadsheet/Cell/ColumnRange.php +++ b/src/PhpSpreadsheet/Cell/ColumnRange.php @@ -4,10 +4,8 @@ namespace PhpOffice\PhpSpreadsheet\Cell; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; -class ColumnRange +class ColumnRange implements AddressRange { - private const MAX_ROW = 1048576; - /** * @var ?Worksheet */ @@ -107,7 +105,7 @@ class ColumnRange { return new CellRange( CellAddress::fromColumnAndRow($this->from, 1, $this->worksheet), - CellAddress::fromColumnAndRow($this->to, self::MAX_ROW) + CellAddress::fromColumnAndRow($this->to, AddressRange::MAX_ROW) ); } diff --git a/src/PhpSpreadsheet/Cell/RowRange.php b/src/PhpSpreadsheet/Cell/RowRange.php index bef97111..38e6c141 100644 --- a/src/PhpSpreadsheet/Cell/RowRange.php +++ b/src/PhpSpreadsheet/Cell/RowRange.php @@ -4,10 +4,8 @@ namespace PhpOffice\PhpSpreadsheet\Cell; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; -class RowRange +class RowRange implements AddressRange { - private const MAX_COLUMN = 'XFD'; - /** * @var ?Worksheet */ @@ -78,7 +76,7 @@ class RowRange { return new CellRange( CellAddress::fromColumnAndRow(Coordinate::columnIndexFromString('A'), $this->from, $this->worksheet), - CellAddress::fromColumnAndRow(Coordinate::columnIndexFromString(self::MAX_COLUMN), $this->to) + CellAddress::fromColumnAndRow(Coordinate::columnIndexFromString(AddressRange::MAX_COLUMN), $this->to) ); } diff --git a/tests/PhpSpreadsheetTests/Cell/CellRangeTest.php b/tests/PhpSpreadsheetTests/Cell/CellRangeTest.php index 856f0e50..1557a698 100644 --- a/tests/PhpSpreadsheetTests/Cell/CellRangeTest.php +++ b/tests/PhpSpreadsheetTests/Cell/CellRangeTest.php @@ -110,4 +110,44 @@ class CellRangeTest extends TestCase $to = CellAddress::fromCellAddress('E2', $worksheet2); new CellRange($from, $to); } + + public function testShiftRangeTo(): void + { + $from = CellAddress::fromCellAddress('B5'); + $to = CellAddress::fromCellAddress('E2'); + $cellRange = new CellRange($from, $to); + self::assertSame('B2:E5', (string) $cellRange); + + $cellRange->to() + ->nextColumn(2) + ->nextRow(2); + + self::assertSame('B2', (string) $cellRange->from()); + self::assertSame('G7', (string) $cellRange->to()); + self::assertSame('B2:G7', (string) $cellRange); + + $cellRange->to() + ->previousColumn() + ->previousRow(); + + self::assertSame('B2', (string) $cellRange->from()); + self::assertSame('F6', (string) $cellRange->to()); + self::assertSame('B2:F6', (string) $cellRange); + } + + public function testShiftRangeFrom(): void + { + $from = CellAddress::fromCellAddress('B5'); + $to = CellAddress::fromCellAddress('E2'); + $cellRange = new CellRange($from, $to); + self::assertSame('B2:E5', (string) $cellRange); + + $cellRange->from() + ->nextColumn(5) + ->nextRow(5); + + self::assertSame('E5', (string) $cellRange->from()); + self::assertSame('G7', (string) $cellRange->to()); + self::assertSame('E5:G7', (string) $cellRange); + } } From 3ae5a3fae397973c5d81c1250a8fd2d5896653eb Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 1 Apr 2022 16:55:58 +0200 Subject: [PATCH 4/6] Additional unit tests for BColumnAndRow methods, to verify that they still work as expected --- phpstan-baseline.neon | 5 - src/PhpSpreadsheet/Worksheet/Worksheet.php | 211 ++++++++++++------ .../Worksheet/ByColumnAndRowTest.php | 188 ++++++++++++++++ 3 files changed, 335 insertions(+), 69 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Worksheet/ByColumnAndRowTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 9ecc0f01..278efd69 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -4345,11 +4345,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Worksheet/Worksheet.php - - - message: "#^Result of && is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/Worksheet.php - - message: "#^Right side of && is always true\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 54fcc0de..2f8f6972 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -4,6 +4,8 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; use ArrayObject; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Cell\AddressRange; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\CellAddress; use PhpOffice\PhpSpreadsheet\Cell\CellRange; @@ -1118,6 +1120,9 @@ class Worksheet implements IComparable return null; } [$worksheet, $address] = self::extractSheetTitle($cellAddress, true); +// if (!empty($worksheet) && $worksheet !== $this->getTitle()) { +// throw new Exception('Reference is not for this worksheet'); +// } return empty($worksheet) ? strtoupper($address) : $worksheet . '!' . strtoupper($address); } @@ -1129,6 +1134,50 @@ class Worksheet implements IComparable return (string) $cellAddress; } + /** + * Validate a cell range. + * + * @param AddressRange|array|CellAddress|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; + * or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]), + * or as a CellAddress or AddressRange object. + */ + protected function validateCellOrCellRange($cellRange): string + { + if (is_object($cellRange) && $cellRange instanceof CellAddress) { + $cellRange = new CellRange($cellRange, $cellRange); + } + + return $this->validateCellRange($cellRange); + } + + /** + * Validate a cell range. + * + * @param AddressRange|array|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; + * or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]), + * or as an AddressRange object. + */ + protected function validateCellRange($cellRange): string + { + if (is_string($cellRange)) { + [$worksheet, $addressRange] = self::extractSheetTitle($cellRange, true); + + // Convert 'A:C' to 'A1:C1048576' + $addressRange = self::pregReplace('/^([A-Z]+):([A-Z]+)$/', '${1}1:${2}1048576', $addressRange); + // Convert '1:3' to 'A1:XFD3' + $addressRange = self::pregReplace('/^(\\d+):(\\d+)$/', 'A${1}:XFD${2}', $addressRange); + + return empty($worksheet) ? strtoupper($addressRange) : $worksheet . '!' . strtoupper($addressRange); + } + + if (is_array($cellRange)) { + [$from, $to] = array_chunk($cellRange, 2); + $cellRange = new CellRange(CellAddress::fromColumnRowArray($from), CellAddress::fromColumnRowArray($to)); + } + + return (string) $cellRange; + } + /** * Set a cell value. * @@ -1455,10 +1504,15 @@ class Worksheet implements IComparable /** * Get style for cell. * - * @param string $cellCoordinate Cell coordinate (or range) to get style for, eg: 'A1' + * @param AddressRange|array|CellAddress|string $cellCoordinate + * A simple string containing a cell address like 'A1' or a cell range like 'A1:E10' + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or a CellAddress or AddressRange object. */ public function getStyle($cellCoordinate): Style { + $cellCoordinate = $this->validateCellRange($cellCoordinate); + // set this sheet as active $this->parent->setActiveSheetIndex($this->parent->getIndex($this)); @@ -1468,6 +1522,35 @@ class Worksheet implements IComparable return $this->parent->getCellXfSupervisor(); } + /** + * Get style for cell by using numeric cell coordinates. + * + * @Deprecated 1.23.0 + * Use the getStyle() method with a cell address range such as 'C5:F8' instead;, + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange object. + * + * @param int $columnIndex1 Numeric column coordinate of the cell + * @param int $row1 Numeric row coordinate of the cell + * @param null|int $columnIndex2 Numeric column coordinate of the range cell + * @param null|int $row2 Numeric row coordinate of the range cell + * + * @return Style + */ + public function getStyleByColumnAndRow($columnIndex1, $row1, $columnIndex2 = null, $row2 = null) + { + if ($columnIndex2 !== null && $row2 !== null) { + $cellRange = new CellRange( + CellAddress::fromColumnAndRow($columnIndex1, $row1), + CellAddress::fromColumnAndRow($columnIndex2, $row2) + ); + + return $this->getStyle($cellRange); + } + + return $this->getStyle(CellAddress::fromColumnAndRow($columnIndex1, $row1)); + } + /** * Get conditional styles for a cell. * @@ -1522,7 +1605,7 @@ class Worksheet implements IComparable { $coordinate = strtoupper($coordinate); if (strpos($coordinate, ':') !== false) { - return isset($this->conditionalStylesCollection[strtoupper($coordinate)]); + return isset($this->conditionalStylesCollection[$coordinate]); } $cell = $this->getCell($coordinate); @@ -1574,30 +1657,6 @@ class Worksheet implements IComparable return $this; } - /** - * Get style for cell by using numeric cell coordinates. - * - * @param int $columnIndex1 Numeric column coordinate of the cell - * @param int $row1 Numeric row coordinate of the cell - * @param null|int $columnIndex2 Numeric column coordinate of the range cell - * @param null|int $row2 Numeric row coordinate of the range cell - * - * @return Style - */ - public function getStyleByColumnAndRow($columnIndex1, $row1, $columnIndex2 = null, $row2 = null) - { - if ($columnIndex2 !== null && $row2 !== null) { - $cellRange = new CellRange( - CellAddress::fromColumnAndRow($columnIndex1, $row1), - CellAddress::fromColumnAndRow($columnIndex2, $row2) - ); - - return $this->getStyle($cellRange); - } - - return $this->getStyle(CellAddress::fromColumnAndRow($columnIndex1, $row1)); - } - /** * Duplicate cell style to a range of cells. * @@ -1690,11 +1749,7 @@ class Worksheet implements IComparable */ public function setBreak($coordinate, $break) { - $cellAddress = $this->validateCellAddress($coordinate); - - if ($cellAddress === '') { - throw new Exception('No cell coordinate specified.'); - } + $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate) ?? ''); if ($break === self::BREAK_NONE) { if (isset($this->breaks[$cellAddress])) { @@ -1738,18 +1793,15 @@ class Worksheet implements IComparable /** * Set merge on a cell range. * - * @param string $range Cell range (e.g. A1:E1) + * @param AddressRange|array|string $range A simple string containing a Cell range like 'A1:E10' + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange. * * @return $this */ public function mergeCells($range) { - // Uppercase coordinate - $range = strtoupper($range); - // Convert 'A:C' to 'A1:C1048576' - $range = self::pregReplace('/^([A-Z]+):([A-Z]+)$/', '${1}1:${2}1048576', $range); - // Convert '1:3' to 'A1:XFD3' - $range = self::pregReplace('/^(\\d+):(\\d+)$/', 'A${1}:XFD${2}', $range); + $range = Functions::trimSheetFromCellReference($this->validateCellRange($range)); if (preg_match('/^([A-Z]+)(\\d+):([A-Z]+)(\\d+)$/', $range, $matches) === 1) { $this->mergeCells[$range] = $range; @@ -1763,7 +1815,7 @@ class Worksheet implements IComparable $numberColumns = $lastColumnIndex - $firstColumnIndex; // create upper left cell if it does not already exist - $upperLeft = "$firstColumn$firstRow"; + $upperLeft = "{$firstColumn}{$firstRow}"; if (!$this->cellExists($upperLeft)) { $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL); } @@ -1826,9 +1878,9 @@ class Worksheet implements IComparable * Set merge on a cell range by using numeric cell coordinates. * * @Deprecated 1.23.0 - * Use the mergeCells() method with a cell address range such as 'C5:E8' instead;, - * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5]), - * or a CellAddress object. + * Use the mergeCells() method with a cell address range such as 'C5:F8' instead;, + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange object. * * @param int $columnIndex1 Numeric column coordinate of the first cell * @param int $row1 Numeric row coordinate of the first cell @@ -1850,14 +1902,15 @@ class Worksheet implements IComparable /** * Remove merge on a cell range. * - * @param string $range Cell range (e.g. A1:E1) + * @param AddressRange|array|string $range A simple string containing a Cell range like 'A1:E10' + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange. * * @return $this */ public function unmergeCells($range) { - // Uppercase coordinate - $range = strtoupper($range); + $range = Functions::trimSheetFromCellReference($this->validateCellRange($range)); if (strpos($range, ':') !== false) { if (isset($this->mergeCells[$range])) { @@ -1875,6 +1928,11 @@ class Worksheet implements IComparable /** * Remove merge on a cell range by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the unmergeCells() method with a cell address range such as 'C5:F8' instead;, + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange object. + * * @param int $columnIndex1 Numeric column coordinate of the first cell * @param int $row1 Numeric row coordinate of the first cell * @param int $columnIndex2 Numeric column coordinate of the last cell @@ -1918,9 +1976,11 @@ class Worksheet implements IComparable } /** - * Set protection on a cell range. + * Set protection on a cell or cell range. * - * @param string $range Cell (e.g. A1) or cell range (e.g. A1:E1) + * @param AddressRange|array|CellAddress|string $range A simple string containing a Cell range like 'A1:E10' + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or a CellAddress or AddressRange object. * @param string $password Password to unlock the protection * @param bool $alreadyHashed If the password has already been hashed, set this to true * @@ -1928,8 +1988,7 @@ class Worksheet implements IComparable */ public function protectCells($range, $password, $alreadyHashed = false) { - // Uppercase coordinate - $range = strtoupper($range); + $range = Functions::trimSheetFromCellReference($this->validateCellRange($range)); if (!$alreadyHashed) { $password = Shared\PasswordHasher::hashPassword($password); @@ -1942,6 +2001,11 @@ class Worksheet implements IComparable /** * Set protection on a cell range by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the protectCells() method with a cell address range such as 'C5:F8' instead;, + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange object. + * * @param int $columnIndex1 Numeric column coordinate of the first cell * @param int $row1 Numeric row coordinate of the first cell * @param int $columnIndex2 Numeric column coordinate of the last cell @@ -1962,16 +2026,17 @@ class Worksheet implements IComparable } /** - * Remove protection on a cell range. + * Remove protection on a cell or cell range. * - * @param string $range Cell (e.g. A1) or cell range (e.g. A1:E1) + * @param AddressRange|array|CellAddress|string $range A simple string containing a Cell range like 'A1:E10' + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or a CellAddress or AddressRange object. * * @return $this */ public function unprotectCells($range) { - // Uppercase coordinate - $range = strtoupper($range); + $range = Functions::trimSheetFromCellReference($this->validateCellRange($range)); if (isset($this->protectedCells[$range])) { unset($this->protectedCells[$range]); @@ -1985,6 +2050,11 @@ class Worksheet implements IComparable /** * Remove protection on a cell range by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the protectCells() method with a cell address range such as 'C5:F8' instead;, + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange object. + * * @param int $columnIndex1 Numeric column coordinate of the first cell * @param int $row1 Numeric row coordinate of the first cell * @param int $columnIndex2 Numeric column coordinate of the last cell @@ -2025,17 +2095,21 @@ class Worksheet implements IComparable /** * Set AutoFilter. * - * @param AutoFilter|string $autoFilterOrRange + * @param AddressRange|array|AutoFilter|string $autoFilterOrRange * A simple string containing a Cell range like 'A1:E10' is permitted for backward compatibility + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange. * * @return $this */ public function setAutoFilter($autoFilterOrRange) { - if (is_string($autoFilterOrRange)) { - $this->autoFilter->setRange($autoFilterOrRange); - } elseif (is_object($autoFilterOrRange) && ($autoFilterOrRange instanceof AutoFilter)) { + if (is_object($autoFilterOrRange) && ($autoFilterOrRange instanceof AutoFilter)) { $this->autoFilter = $autoFilterOrRange; + } else { + $cellRange = Functions::trimSheetFromCellReference($this->validateCellRange($autoFilterOrRange)); + + $this->autoFilter->setRange($cellRange); } return $this; @@ -2044,6 +2118,11 @@ class Worksheet implements IComparable /** * Set Autofilter Range by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the setAutoFilter() method with a cell address range such as 'C5:F8' instead;, + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange object or AutoFilter object. + * * @param int $columnIndex1 Numeric column coordinate of the first cell * @param int $row1 Numeric row coordinate of the first cell * @param int $columnIndex2 Numeric column coordinate of the second cell @@ -2053,11 +2132,12 @@ class Worksheet implements IComparable */ public function setAutoFilterByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2) { - return $this->setAutoFilter( - Coordinate::stringFromColumnIndex($columnIndex1) . $row1 - . ':' . - Coordinate::stringFromColumnIndex($columnIndex2) . $row2 + $cellRange = new CellRange( + CellAddress::fromColumnAndRow($columnIndex1, $row1), + CellAddress::fromColumnAndRow($columnIndex2, $row2) ); + + return $this->setAutoFilter($cellRange); } /** @@ -2090,9 +2170,11 @@ class Worksheet implements IComparable * - B2 will freeze the rows above and to the left of cell B2 (i.e row 1 and column A) * * @param null|array|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5'; - * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. - * Passing a null value for this argument will clear an existing freeze pane for this worksheet. - * @param null|string $topLeftCell default position of the right bottom pane + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * Passing a null value for this argument will clear any existing freeze pane for this worksheet. + * @param null|array|CellAddress|string $topLeftCell default position of the right bottom pane + * Coordinate of the cell as a string, eg: 'C5'; or as an array of [$columnIndex, $row] (e.g. [3, 5]), + * or a CellAddress object. * * @return $this */ @@ -2102,6 +2184,7 @@ class Worksheet implements IComparable if (is_string($cellAddress) && Coordinate::coordinateIsRange($cellAddress)) { throw new Exception('Freeze pane can not be set on a range of cells.'); } + $topLeftCell = $this->validateCellAddress($topLeftCell, true); if ($cellAddress !== null && $topLeftCell === null) { $coordinate = Coordinate::coordinateFromString($cellAddress); diff --git a/tests/PhpSpreadsheetTests/Worksheet/ByColumnAndRowTest.php b/tests/PhpSpreadsheetTests/Worksheet/ByColumnAndRowTest.php new file mode 100644 index 00000000..d38be252 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/ByColumnAndRowTest.php @@ -0,0 +1,188 @@ +getActiveSheet(); + + $sheet->setCellValueByColumnAndRow(2, 2, 2); + self::assertSame(2, $sheet->getCell('B2')->getValue()); + } + + public function testSetCellValueExplicitByColumnAndRow(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $sheet->setCellValueExplicitByColumnAndRow(2, 2, '="PHP Rules"', DataType::TYPE_STRING); + self::assertSame('="PHP Rules"', $sheet->getCell('B2')->getValue()); + self::assertSame(DataType::TYPE_STRING, $sheet->getCell('B2')->getDataType()); + } + + public function testCellExistsByColumnAndRow(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $cellExists = $sheet->cellExistsByColumnAndRow(2, 2); + self::assertFalse($cellExists); + + $sheet->setCellValue('B2', 2); + + $cellExists = $sheet->cellExistsByColumnAndRow(2, 2); + self::assertTrue($cellExists); + } + + public function testGetCellByColumnAndRow(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $sheet->setCellValue('B2', 2); + $cell = $sheet->getCellByColumnAndRow(2, 2); + self::assertSame('B2', $cell->getCoordinate()); + self::assertSame(2, $cell->getValue()); + } + + public function testGetStyleByColumnAndRow(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $data = [['A', 'B'], ['C', 'D']]; + $sheet->fromArray($data, null, 'B2', true); + $sheet->getStyle('B2:C3')->getFont()->setBold(true); + + $rangeStyle = $sheet->getStyleByColumnAndRow(2, 2, 3, 3); + self::assertTrue($rangeStyle->getFont()->getBold()); + + $cellStyle = $sheet->getStyleByColumnAndRow(2, 2); + self::assertTrue($cellStyle->getFont()->getBold()); + } + + public function testSetBreakByColumnAndRow(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $sheet->setCellValue('B2', 2); + $sheet->setBreakByColumnAndRow(2, 2, Worksheet::BREAK_COLUMN); + + $breaks = $sheet->getBreaks(); + self::assertArrayHasKey('B2', $breaks); + self::assertSame(Worksheet::BREAK_COLUMN, $breaks['B2']); + } + + public function testMergeCellsByColumnAndRow(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $data = [['A', 'B'], ['C', 'D']]; + $sheet->fromArray($data, null, 'B2', true); + + $sheet->mergeCellsByColumnAndRow(2, 2, 3, 3); + $mergeRanges = $sheet->getMergeCells(); + self::assertArrayHasKey('B2:C3', $mergeRanges); + } + + public function testUnergeCellsByColumnAndRow(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $data = [['A', 'B'], ['C', 'D']]; + $sheet->fromArray($data, null, 'B2', true); + + $sheet->mergeCells('B2:C3'); + $mergeRanges = $sheet->getMergeCells(); + self::assertArrayHasKey('B2:C3', $mergeRanges); + + $sheet->unmergeCellsByColumnAndRow(2, 2, 3, 3); + $mergeRanges = $sheet->getMergeCells(); + self::assertEmpty($mergeRanges); + } + + public function testProtectCellsByColumnAndRow(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $data = [['A', 'B'], ['C', 'D']]; + $sheet->fromArray($data, null, 'B2', true); + + $sheet->protectCellsByColumnAndRow(2, 2, 3, 3, 'secret', false); + $protectedRanges = $sheet->getProtectedCells(); + self::assertArrayHasKey('B2:C3', $protectedRanges); + } + + public function testUnprotectCellsByColumnAndRow(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $data = [['A', 'B'], ['C', 'D']]; + $sheet->fromArray($data, null, 'B2', true); + + $sheet->protectCells('B2:C3', 'secret', false); + $protectedRanges = $sheet->getProtectedCells(); + self::assertArrayHasKey('B2:C3', $protectedRanges); + + $sheet->unprotectCellsByColumnAndRow(2, 2, 3, 3); + $protectedRanges = $sheet->getProtectedCells(); + self::assertEmpty($protectedRanges); + } + + public function testSetAutoFilterByColumnAndRow(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $data = [['A', 'B'], ['C', 'D']]; + $sheet->fromArray($data, null, 'B2', true); + + $sheet->setAutoFilterByColumnAndRow(2, 2, 3, 3); + $autoFilter = $sheet->getAutoFilter(); + self::assertInstanceOf(AutoFilter::class, $autoFilter); + self::assertSame('B2:C3', $autoFilter->getRange()); + } + + public function testFreezePaneByColumnAndRow(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $data = [['A', 'B'], ['C', 'D']]; + $sheet->fromArray($data, null, 'B2', true); + + $sheet->freezePaneByColumnAndRow(2, 2); + $freezePane = $sheet->getFreezePane(); + self::assertSame('B2', $freezePane); + } + + public function testGetCommentByColumnAndRow(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $sheet->setCellValue('B2', 2); + $spreadsheet->getActiveSheet() + ->getComment('B2') + ->getText()->createTextRun('My Test Comment'); + + $comment = $sheet->getCommentByColumnAndRow(2, 2); + self::assertInstanceOf(Comment::class, $comment); + self::assertSame('My Test Comment', $comment->getText()->getPlainText()); + } +} From 83aaf32161649a04ca2c23578fe72273da27f507 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 3 Apr 2022 21:49:25 +0200 Subject: [PATCH 5/6] Handle selected cells --- src/PhpSpreadsheet/Worksheet/Worksheet.php | 129 ++++++++++----------- 1 file changed, 60 insertions(+), 69 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 2f8f6972..a1bfbfdb 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -1113,12 +1113,9 @@ class Worksheet implements IComparable * @param null|array|CellAddress|string $cellAddress Coordinate of the cell as a string, eg: 'C5'; * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. */ - protected function validateCellAddress($cellAddress, bool $allowNull = false): ?string + protected function validateCellAddress($cellAddress): string { - if (is_string($cellAddress) || ($cellAddress === null && $allowNull === true)) { - if ($cellAddress === null) { - return null; - } + if (is_string($cellAddress)) { [$worksheet, $address] = self::extractSheetTitle($cellAddress, true); // if (!empty($worksheet) && $worksheet !== $this->getTitle()) { // throw new Exception('Reference is not for this worksheet'); @@ -1134,16 +1131,38 @@ class Worksheet implements IComparable return (string) $cellAddress; } + private function tryDefinedName(string $coordinate): string + { + // Uppercase coordinate + $coordinate = strtoupper($coordinate); + // Eliminate leading equal sign + $coordinate = self::pregReplace('/^=/', '', $coordinate); + $defined = $this->parent->getDefinedName($coordinate, $this); + if ($defined !== null) { + if ($defined->getWorksheet() === $this && !$defined->isFormula()) { + $coordinate = self::pregReplace('/^=/', '', $defined->getValue()); + } + } + + return $coordinate; + } + /** - * Validate a cell range. + * Validate a cell address or cell range. * - * @param AddressRange|array|CellAddress|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; + * @param AddressRange|array|CellAddress|int|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; * or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]), * or as a CellAddress or AddressRange object. */ protected function validateCellOrCellRange($cellRange): string { - if (is_object($cellRange) && $cellRange instanceof CellAddress) { + if (is_string($cellRange) || is_numeric($cellRange)) { + $cellRange = (string) $cellRange; + // Convert a single column reference like 'A' to 'A:A' + $cellRange = self::pregReplace('/^([A-Z]+)$/', '${1}:${1}', $cellRange); + // Convert a single row reference like '1' to '1:1' + $cellRange = self::pregReplace('/^(\d+)$/', '${1}:${1}', $cellRange); + } elseif (is_object($cellRange) && $cellRange instanceof CellAddress) { $cellRange = new CellRange($cellRange, $cellRange); } @@ -1162,9 +1181,9 @@ class Worksheet implements IComparable if (is_string($cellRange)) { [$worksheet, $addressRange] = self::extractSheetTitle($cellRange, true); - // Convert 'A:C' to 'A1:C1048576' + // Convert Column ranges like 'A:C' to 'A1:C1048576' $addressRange = self::pregReplace('/^([A-Z]+):([A-Z]+)$/', '${1}1:${2}1048576', $addressRange); - // Convert '1:3' to 'A1:XFD3' + // Convert Row ranges like '1:3' to 'A1:XFD3' $addressRange = self::pregReplace('/^(\\d+):(\\d+)$/', 'A${1}:XFD${2}', $addressRange); return empty($worksheet) ? strtoupper($addressRange) : $worksheet . '!' . strtoupper($addressRange); @@ -1189,8 +1208,7 @@ class Worksheet implements IComparable */ public function setCellValue($coordinate, $value) { - /** @var string $cellAddress */ - $cellAddress = $this->validateCellAddress($coordinate); + $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate)); $this->getCell($cellAddress)->setValue($value); return $this; @@ -1211,7 +1229,7 @@ class Worksheet implements IComparable */ public function setCellValueByColumnAndRow($columnIndex, $row, $value) { - $this->getCell([$columnIndex, $row])->setValue($value); + $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row)->setValue($value); return $this; } @@ -1228,8 +1246,7 @@ class Worksheet implements IComparable */ public function setCellValueExplicit($coordinate, $value, $dataType) { - /** @var string $cellAddress */ - $cellAddress = $this->validateCellAddress($coordinate); + $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate)); $this->getCell($cellAddress)->setValueExplicit($value, $dataType); return $this; @@ -1251,7 +1268,7 @@ class Worksheet implements IComparable */ public function setCellValueExplicitByColumnAndRow($columnIndex, $row, $value, $dataType) { - $this->getCell([$columnIndex, $row])->setValueExplicit($value, $dataType); + $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row)->setValueExplicit($value, $dataType); return $this; } @@ -1266,8 +1283,7 @@ class Worksheet implements IComparable */ public function getCell($coordinate): Cell { - /** @var string $cellAddress */ - $cellAddress = $this->validateCellAddress($coordinate); + $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate)); // Shortcut for increased performance for the vast majority of simple cases if ($this->cellCollection->has($cellAddress)) { @@ -1368,7 +1384,7 @@ class Worksheet implements IComparable */ public function getCellByColumnAndRow($columnIndex, $row): Cell { - return $this->getCell([$columnIndex, $row]); + return $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row); } /** @@ -1418,7 +1434,6 @@ class Worksheet implements IComparable */ public function cellExists($coordinate): bool { - /** @var string $cellAddress */ $cellAddress = $this->validateCellAddress($coordinate); /** @var Worksheet $sheet */ [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress); @@ -1438,7 +1453,7 @@ class Worksheet implements IComparable */ public function cellExistsByColumnAndRow($columnIndex, $row): bool { - return $this->cellExists([$columnIndex, $row]); + return $this->cellExists(Coordinate::stringFromColumnIndex($columnIndex) . $row); } /** @@ -1504,14 +1519,14 @@ class Worksheet implements IComparable /** * Get style for cell. * - * @param AddressRange|array|CellAddress|string $cellCoordinate + * @param AddressRange|array|CellAddress|int|string $cellCoordinate * A simple string containing a cell address like 'A1' or a cell range like 'A1:E10' * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or a CellAddress or AddressRange object. */ public function getStyle($cellCoordinate): Style { - $cellCoordinate = $this->validateCellRange($cellCoordinate); + $cellCoordinate = $this->validateCellOrCellRange($cellCoordinate); // set this sheet as active $this->parent->setActiveSheetIndex($this->parent->getIndex($this)); @@ -1749,7 +1764,7 @@ class Worksheet implements IComparable */ public function setBreak($coordinate, $break) { - $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate) ?? ''); + $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate)); if ($break === self::BREAK_NONE) { if (isset($this->breaks[$cellAddress])) { @@ -1777,7 +1792,7 @@ class Worksheet implements IComparable */ public function setBreakByColumnAndRow($columnIndex, $row, $break) { - return $this->setBreak([$columnIndex, $row], $break); + return $this->setBreak(Coordinate::stringFromColumnIndex($columnIndex) . $row, $break); } /** @@ -1978,7 +1993,7 @@ class Worksheet implements IComparable /** * Set protection on a cell or cell range. * - * @param AddressRange|array|CellAddress|string $range A simple string containing a Cell range like 'A1:E10' + * @param AddressRange|array|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10' * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or a CellAddress or AddressRange object. * @param string $password Password to unlock the protection @@ -1988,7 +2003,7 @@ class Worksheet implements IComparable */ public function protectCells($range, $password, $alreadyHashed = false) { - $range = Functions::trimSheetFromCellReference($this->validateCellRange($range)); + $range = Functions::trimSheetFromCellReference($this->validateCellOrCellRange($range)); if (!$alreadyHashed) { $password = Shared\PasswordHasher::hashPassword($password); @@ -2028,7 +2043,7 @@ class Worksheet implements IComparable /** * Remove protection on a cell or cell range. * - * @param AddressRange|array|CellAddress|string $range A simple string containing a Cell range like 'A1:E10' + * @param AddressRange|array|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10' * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or a CellAddress or AddressRange object. * @@ -2036,7 +2051,7 @@ class Worksheet implements IComparable */ public function unprotectCells($range) { - $range = Functions::trimSheetFromCellReference($this->validateCellRange($range)); + $range = Functions::trimSheetFromCellReference($this->validateCellOrCellRange($range)); if (isset($this->protectedCells[$range])) { unset($this->protectedCells[$range]); @@ -2180,11 +2195,15 @@ class Worksheet implements IComparable */ public function freezePane($coordinate, $topLeftCell = null) { - $cellAddress = $this->validateCellAddress($coordinate, true); - if (is_string($cellAddress) && Coordinate::coordinateIsRange($cellAddress)) { + $cellAddress = ($coordinate !== null) + ? Functions::trimSheetFromCellReference($this->validateCellAddress($coordinate)) + : null; + if ($cellAddress !== null && Coordinate::coordinateIsRange($cellAddress)) { throw new Exception('Freeze pane can not be set on a range of cells.'); } - $topLeftCell = $this->validateCellAddress($topLeftCell, true); + $topLeftCell = ($topLeftCell !== null) + ? Functions::trimSheetFromCellReference($this->validateCellAddress($topLeftCell)) + : null; if ($cellAddress !== null && $topLeftCell === null) { $coordinate = Coordinate::coordinateFromString($cellAddress); @@ -2218,7 +2237,7 @@ class Worksheet implements IComparable */ public function freezePaneByColumnAndRow($columnIndex, $row) { - return $this->freezePane([$columnIndex, $row]); + return $this->freezePane(Coordinate::stringFromColumnIndex($columnIndex) . $row); } /** @@ -2587,8 +2606,7 @@ class Worksheet implements IComparable */ public function getComment($cellCoordinate) { - /** @var string $cellAddress */ - $cellAddress = $this->validateCellAddress($cellCoordinate); + $cellAddress = Functions::trimSheetFromCellReference($this->validateCellAddress($cellCoordinate)); if (Coordinate::coordinateIsRange($cellAddress)) { throw new Exception('Cell coordinate string can not be a range of cells.'); @@ -2624,7 +2642,7 @@ class Worksheet implements IComparable */ public function getCommentByColumnAndRow($columnIndex, $row) { - return $this->getComment([$columnIndex, $row]); + return $this->getComment(Coordinate::stringFromColumnIndex($columnIndex) . $row); } /** @@ -2675,48 +2693,21 @@ class Worksheet implements IComparable return self::ensureString(preg_replace($pattern, $replacement, $subject)); } - private function tryDefinedName(string $coordinate): string - { - // Uppercase coordinate - $coordinate = strtoupper($coordinate); - // Eliminate leading equal sign - $coordinate = self::pregReplace('/^=/', '', $coordinate); - $defined = $this->parent->getDefinedName($coordinate, $this); - if ($defined !== null) { - if ($defined->getWorksheet() === $this && !$defined->isFormula()) { - $coordinate = self::pregReplace('/^=/', '', $defined->getValue()); - } - } - - return $coordinate; - } - /** * Select a range of cells. * - * @param string $coordinate Cell range, examples: 'A1', 'B2:G5', 'A:C', '3:6' + * @param AddressRange|array|CellAddress|int|string $coordinate A simple string containing a Cell range like 'A1:E10' + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or a CellAddress or AddressRange object. * * @return $this */ public function setSelectedCells($coordinate) { - $originalCoordinate = $coordinate; - $coordinate = $this->tryDefinedName($coordinate); - - // Convert 'A' to 'A:A' - $coordinate = self::pregReplace('/^([A-Z]+)$/', '${1}:${1}', $coordinate); - - // Convert '1' to '1:1' - $coordinate = self::pregReplace('/^(\d+)$/', '${1}:${1}', $coordinate); - - // Convert 'A:C' to 'A1:C1048576' - $coordinate = self::pregReplace('/^([A-Z]+):([A-Z]+)$/', '${1}1:${2}1048576', $coordinate); - - // Convert '1:3' to 'A1:XFD3' - $coordinate = self::pregReplace('/^(\d+):(\d+)$/', 'A${1}:XFD${2}', $coordinate); - if (preg_match('/^\\$?[A-Z]{1,3}\\$?\d{1,7}(:\\$?[A-Z]{1,3}\\$?\d{1,7})?$/', $coordinate) !== 1) { - throw new Exception("Invalid setSelectedCells $originalCoordinate $coordinate"); + if (is_string($coordinate)) { + $coordinate = $this->tryDefinedName($coordinate); } + $coordinate = $this->validateCellOrCellRange($coordinate); if (Coordinate::coordinateIsRange($coordinate)) { [$first] = Coordinate::splitRange($coordinate); From f67273425842adf88fcc8df0fdac5c52d8680e93 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 12 Apr 2022 08:34:03 +0200 Subject: [PATCH 6/6] Typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35ed928e..3af4e647 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,7 +58,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - `setSelectedCellByColumnAndRow()` use the equivalent `setSelectedCells()` This change provides more consistency in the methods (not every "by cell address" method has an equivalent "byColumnAndRow" method); - and the "by cell address" methods often provide more flexibility, such as allowing a range of cells, or referencig them by passing the defined name of a named range as the argument. + and the "by cell address" methods often provide more flexibility, such as allowing a range of cells, or referencing them by passing the defined name of a named range as the argument. ### Removed