Skip to content

Commit a6cb80f

Browse files
authored
Merge pull request #2718 from PHPOffice/CellAddress-object
Initial work on deprecating `ByColumnAndRow` methods in Worksheet
2 parents c685888 + f672734 commit a6cb80f

File tree

14 files changed

+1628
-157
lines changed

14 files changed

+1628
-157
lines changed

.php-cs-fixer.dist.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
'braces' => true,
2222
'cast_spaces' => true,
2323
'class_attributes_separation' => ['elements' => ['method' => 'one', 'property' => 'one']], // const are often grouped with other related const
24-
'class_definition' => true,
24+
'class_definition' => false,
2525
'class_keyword_remove' => false, // ::class keyword gives us better support in IDE
2626
'combine_consecutive_issets' => true,
2727
'combine_consecutive_unsets' => true,

CHANGELOG.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org).
99

1010
### Added
1111

12-
- Implementation of the FILTER(), SORT(), SORTBY() and UNIQUE() Lookup/Reference (array) functions
12+
- 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.
13+
- Implementation of the FILTER(), SORT(), SORTBY() and UNIQUE() Lookup/Reference (array) functions.
1314
- Implementation of the ISREF() Information function.
1415
- Added support for reading "formatted" numeric values from Csv files; although default behaviour of reading these values as strings is preserved.
1516

@@ -37,6 +38,27 @@ and this project adheres to [Semantic Versioning](https://semver.org).
3738
### Deprecated
3839

3940
- 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.
41+
- 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`).
42+
43+
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.
44+
This includes the methods:
45+
- `setCellValueByColumnAndRow()` use the equivalent `setCellValue()`
46+
- `setCellValueExplicitByColumnAndRow()` use the equivalent `setCellValueExplicit()`
47+
- `getCellByColumnAndRow()` use the equivalent `getCell()`
48+
- `cellExistsByColumnAndRow()` use the equivalent `cellExists()`
49+
- `getStyleByColumnAndRow()` use the equivalent `getStyle()`
50+
- `setBreakByColumnAndRow()` use the equivalent `setBreak()`
51+
- `mergeCellsByColumnAndRow()` use the equivalent `mergeCells()`
52+
- `unmergeCellsByColumnAndRow()` use the equivalent `unmergeCells()`
53+
- `protectCellsByColumnAndRow()` use the equivalent `protectCells()`
54+
- `unprotectCellsByColumnAndRow()` use the equivalent `unprotectCells()`
55+
- `setAutoFilterByColumnAndRow()` use the equivalent `setAutoFilter()`
56+
- `freezePaneByColumnAndRow()` use the equivalent `freezePane()`
57+
- `getCommentByColumnAndRow()` use the equivalent `getComment()`
58+
- `setSelectedCellByColumnAndRow()` use the equivalent `setSelectedCells()`
59+
60+
This change provides more consistency in the methods (not every "by cell address" method has an equivalent "byColumnAndRow" method);
61+
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.
4062

4163
### Removed
4264

phpstan-baseline.neon

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4325,11 +4325,6 @@ parameters:
43254325
count: 1
43264326
path: src/PhpSpreadsheet/Worksheet/Worksheet.php
43274327

4328-
-
4329-
message: "#^Result of && is always true\\.$#"
4330-
count: 1
4331-
path: src/PhpSpreadsheet/Worksheet/Worksheet.php
4332-
43334328
-
43344329
message: "#^Right side of && is always true\\.$#"
43354330
count: 1
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheet\Cell;
4+
5+
interface AddressRange
6+
{
7+
public const MAX_ROW = 1048576;
8+
9+
public const MAX_COLUMN = 'XFD';
10+
11+
/**
12+
* @return mixed
13+
*/
14+
public function from();
15+
16+
/**
17+
* @return mixed
18+
*/
19+
public function to();
20+
21+
public function __toString(): string;
22+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheet\Cell;
4+
5+
use PhpOffice\PhpSpreadsheet\Exception;
6+
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
7+
8+
class CellAddress
9+
{
10+
/**
11+
* @var ?Worksheet
12+
*/
13+
protected $worksheet;
14+
15+
/**
16+
* @var string
17+
*/
18+
protected $cellAddress;
19+
20+
/**
21+
* @var string
22+
*/
23+
protected $columnName;
24+
25+
/**
26+
* @var int
27+
*/
28+
protected $columnId;
29+
30+
/**
31+
* @var int
32+
*/
33+
protected $rowId;
34+
35+
public function __construct(string $cellAddress, ?Worksheet $worksheet)
36+
{
37+
$this->cellAddress = str_replace('$', '', $cellAddress);
38+
[$this->columnName, $rowId] = Coordinate::coordinateFromString($cellAddress);
39+
$this->rowId = (int) $rowId;
40+
$this->columnId = Coordinate::columnIndexFromString($this->columnName);
41+
$this->worksheet = $worksheet;
42+
}
43+
44+
/**
45+
* @param mixed $columnId
46+
* @param mixed $rowId
47+
*/
48+
private static function validateColumnAndRow($columnId, $rowId): void
49+
{
50+
$array = [$columnId, $rowId];
51+
array_walk(
52+
$array,
53+
function ($value): void {
54+
if (!is_numeric($value) || $value <= 0) {
55+
throw new Exception('Row and Column Ids must be positive integer values');
56+
}
57+
}
58+
);
59+
}
60+
61+
/**
62+
* @param mixed $columnId
63+
* @param mixed $rowId
64+
*/
65+
public static function fromColumnAndRow($columnId, $rowId, ?Worksheet $worksheet = null): self
66+
{
67+
self::validateColumnAndRow($columnId, $rowId);
68+
69+
/** @phpstan-ignore-next-line */
70+
return new static(Coordinate::stringFromColumnIndex($columnId) . ((string) $rowId), $worksheet);
71+
}
72+
73+
public static function fromColumnRowArray(array $array, ?Worksheet $worksheet = null): self
74+
{
75+
[$columnId, $rowId] = $array;
76+
77+
return static::fromColumnAndRow($columnId, $rowId, $worksheet);
78+
}
79+
80+
/**
81+
* @param mixed $cellAddress
82+
*/
83+
public static function fromCellAddress($cellAddress, ?Worksheet $worksheet = null): self
84+
{
85+
/** @phpstan-ignore-next-line */
86+
return new static($cellAddress, $worksheet);
87+
}
88+
89+
/**
90+
* The returned address string will contain the worksheet name as well, if available,
91+
* (ie. if a Worksheet was provided to the constructor).
92+
* e.g. "'Mark''s Worksheet'!C5".
93+
*/
94+
public function fullCellAddress(): string
95+
{
96+
if ($this->worksheet !== null) {
97+
$title = str_replace("'", "''", $this->worksheet->getTitle());
98+
99+
return "'{$title}'!{$this->cellAddress}";
100+
}
101+
102+
return $this->cellAddress;
103+
}
104+
105+
public function worksheet(): ?Worksheet
106+
{
107+
return $this->worksheet;
108+
}
109+
110+
/**
111+
* The returned address string will contain just the column/row address,
112+
* (even if a Worksheet was provided to the constructor).
113+
* e.g. "C5".
114+
*/
115+
public function cellAddress(): string
116+
{
117+
return $this->cellAddress;
118+
}
119+
120+
public function rowId(): int
121+
{
122+
return $this->rowId;
123+
}
124+
125+
public function columnId(): int
126+
{
127+
return $this->columnId;
128+
}
129+
130+
public function columnName(): string
131+
{
132+
return $this->columnName;
133+
}
134+
135+
public function nextRow(int $offset = 1): self
136+
{
137+
$newRowId = $this->rowId + $offset;
138+
if ($newRowId < 1) {
139+
$newRowId = 1;
140+
}
141+
142+
return static::fromColumnAndRow($this->columnId, $newRowId);
143+
}
144+
145+
public function previousRow(int $offset = 1): self
146+
{
147+
return $this->nextRow(0 - $offset);
148+
}
149+
150+
public function nextColumn(int $offset = 1): self
151+
{
152+
$newColumnId = $this->columnId + $offset;
153+
if ($newColumnId < 1) {
154+
$newColumnId = 1;
155+
}
156+
157+
return static::fromColumnAndRow($newColumnId, $this->rowId);
158+
}
159+
160+
public function previousColumn(int $offset = 1): self
161+
{
162+
return $this->nextColumn(0 - $offset);
163+
}
164+
165+
/**
166+
* The returned address string will contain the worksheet name as well, if available,
167+
* (ie. if a Worksheet was provided to the constructor).
168+
* e.g. "'Mark''s Worksheet'!C5".
169+
*/
170+
public function __toString()
171+
{
172+
return $this->fullCellAddress();
173+
}
174+
}

src/PhpSpreadsheet/Cell/CellRange.php

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheet\Cell;
4+
5+
use PhpOffice\PhpSpreadsheet\Exception;
6+
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
7+
8+
class CellRange implements AddressRange
9+
{
10+
/**
11+
* @var CellAddress
12+
*/
13+
protected $from;
14+
15+
/**
16+
* @var CellAddress
17+
*/
18+
protected $to;
19+
20+
public function __construct(CellAddress $from, CellAddress $to)
21+
{
22+
$this->validateFromTo($from, $to);
23+
}
24+
25+
private function validateFromTo(CellAddress $from, CellAddress $to): void
26+
{
27+
// Identify actual top-left and bottom-right values (in case we've been given top-right and bottom-left)
28+
$firstColumn = min($from->columnId(), $to->columnId());
29+
$firstRow = min($from->rowId(), $to->rowId());
30+
$lastColumn = max($from->columnId(), $to->columnId());
31+
$lastRow = max($from->rowId(), $to->rowId());
32+
33+
$fromWorksheet = $from->worksheet();
34+
$toWorksheet = $to->worksheet();
35+
$this->validateWorksheets($fromWorksheet, $toWorksheet);
36+
37+
$this->from = $this->cellAddressWrapper($firstColumn, $firstRow, $fromWorksheet);
38+
$this->to = $this->cellAddressWrapper($lastColumn, $lastRow, $toWorksheet);
39+
}
40+
41+
private function validateWorksheets(?Worksheet $fromWorksheet, ?Worksheet $toWorksheet): void
42+
{
43+
if ($fromWorksheet !== null && $toWorksheet !== null) {
44+
// We could simply compare worksheets rather than worksheet titles; but at some point we may introduce
45+
// support for 3d ranges; and at that point we drop this check and let the validation fall through
46+
// to the check for same workbook; but unless we check on titles, this test will also detect if the
47+
// worksheets are in different spreadsheets, and the next check will never execute or throw its
48+
// own exception.
49+
if ($fromWorksheet->getTitle() !== $toWorksheet->getTitle()) {
50+
throw new Exception('3d Cell Ranges are not supported');
51+
} elseif ($fromWorksheet->getParent() !== $toWorksheet->getParent()) {
52+
throw new Exception('Worksheets must be in the same spreadsheet');
53+
}
54+
}
55+
}
56+
57+
private function cellAddressWrapper(int $column, int $row, ?Worksheet $worksheet = null): CellAddress
58+
{
59+
$cellAddress = Coordinate::stringFromColumnIndex($column) . (string) $row;
60+
61+
return new class ($cellAddress, $worksheet) extends CellAddress {
62+
public function nextRow(int $offset = 1): CellAddress
63+
{
64+
/** @var CellAddress $result */
65+
$result = parent::nextRow($offset);
66+
$this->rowId = $result->rowId;
67+
$this->cellAddress = $result->cellAddress;
68+
69+
return $this;
70+
}
71+
72+
public function previousRow(int $offset = 1): CellAddress
73+
{
74+
/** @var CellAddress $result */
75+
$result = parent::previousRow($offset);
76+
$this->rowId = $result->rowId;
77+
$this->cellAddress = $result->cellAddress;
78+
79+
return $this;
80+
}
81+
82+
public function nextColumn(int $offset = 1): CellAddress
83+
{
84+
/** @var CellAddress $result */
85+
$result = parent::nextColumn($offset);
86+
$this->columnId = $result->columnId;
87+
$this->columnName = $result->columnName;
88+
$this->cellAddress = $result->cellAddress;
89+
90+
return $this;
91+
}
92+
93+
public function previousColumn(int $offset = 1): CellAddress
94+
{
95+
/** @var CellAddress $result */
96+
$result = parent::previousColumn($offset);
97+
$this->columnId = $result->columnId;
98+
$this->columnName = $result->columnName;
99+
$this->cellAddress = $result->cellAddress;
100+
101+
return $this;
102+
}
103+
};
104+
}
105+
106+
public function from(): CellAddress
107+
{
108+
// Re-order from/to in case the cell addresses have been modified
109+
$this->validateFromTo($this->from, $this->to);
110+
111+
return $this->from;
112+
}
113+
114+
public function to(): CellAddress
115+
{
116+
// Re-order from/to in case the cell addresses have been modified
117+
$this->validateFromTo($this->from, $this->to);
118+
119+
return $this->to;
120+
}
121+
122+
public function __toString(): string
123+
{
124+
// Re-order from/to in case the cell addresses have been modified
125+
$this->validateFromTo($this->from, $this->to);
126+
127+
if ($this->from->cellAddress() === $this->to->cellAddress()) {
128+
return "{$this->from->fullCellAddress()}";
129+
}
130+
131+
$fromAddress = $this->from->fullCellAddress();
132+
$toAddress = $this->to->cellAddress();
133+
134+
return "{$fromAddress}:{$toAddress}";
135+
}
136+
}

0 commit comments

Comments
 (0)