PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.4 3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 All 45 releases
tablepress / libraries / vendor / PhpSpreadsheet / ReferenceHelper.php

ReferenceHelper.php in TablePress – Tables in WordPress made easy 3.4, at libraries/vendor/PhpSpreadsheet/ReferenceHelper.php

1,412 lines 53.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace TablePress\PhpOffice\PhpSpreadsheet;
4
5 use TablePress\PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6 use TablePress\PhpOffice\PhpSpreadsheet\Cell\AddressRange;
7 use TablePress\PhpOffice\PhpSpreadsheet\Cell\Coordinate;
8 use TablePress\PhpOffice\PhpSpreadsheet\Cell\DataType;
9 use TablePress\PhpOffice\PhpSpreadsheet\Shared\StringHelper;
10 use TablePress\PhpOffice\PhpSpreadsheet\Style\Conditional;
11 use TablePress\PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter;
12 use TablePress\PhpOffice\PhpSpreadsheet\Worksheet\Table;
13 use TablePress\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
14
15 class ReferenceHelper
16 {
17 /** Constants */
18 /** Regular Expressions */
19 private const SHEETNAME_PART = '((\w*|\'[^!]*\')!)';
20 private const SHEETNAME_PART_WITH_SLASHES = '/' . self::SHEETNAME_PART . '/';
21 const REFHELPER_REGEXP_CELLREF = self::SHEETNAME_PART . '?(?<![:a-z1-9_\.\$])(\$?[a-z]{1,3}\$?\d+)(?=[^:!\d\'])';
22 const REFHELPER_REGEXP_CELLRANGE = self::SHEETNAME_PART . '?(\$?[a-z]{1,3}\$?\d+):(\$?[a-z]{1,3}\$?\d+)';
23 const REFHELPER_REGEXP_ROWRANGE = self::SHEETNAME_PART . '?(\$?\d+):(\$?\d+)';
24 const REFHELPER_REGEXP_COLRANGE = self::SHEETNAME_PART . '?(\$?[a-z]{1,3}):(\$?[a-z]{1,3})';
25
26 /**
27 * Instance of this class.
28 */
29 private static ?ReferenceHelper $instance = null;
30
31 private ?CellReferenceHelper $cellReferenceHelper = null;
32
33 /**
34 * Get an instance of this class.
35 */
36 public static function getInstance(): self
37 {
38 if (self::$instance === null) {
39 self::$instance = new self();
40 }
41
42 return self::$instance;
43 }
44
45 /**
46 * Create a new ReferenceHelper.
47 */
48 protected function __construct()
49 {
50 }
51
52 /**
53 * Compare two column addresses
54 * Intended for use as a Callback function for sorting column addresses by column.
55 *
56 * @param string $a First column to test (e.g. 'AA')
57 * @param string $b Second column to test (e.g. 'Z')
58 */
59 public static function columnSort(string $a, string $b): int
60 {
61 return strcasecmp(strlen($a) . $a, strlen($b) . $b);
62 }
63
64 /**
65 * Compare two column addresses
66 * Intended for use as a Callback function for reverse sorting column addresses by column.
67 *
68 * @param string $a First column to test (e.g. 'AA')
69 * @param string $b Second column to test (e.g. 'Z')
70 */
71 public static function columnReverseSort(string $a, string $b): int
72 {
73 return -strcasecmp(strlen($a) . $a, strlen($b) . $b);
74 }
75
76 /**
77 * Compare two cell addresses
78 * Intended for use as a Callback function for sorting cell addresses by column and row.
79 *
80 * @param string $a First cell to test (e.g. 'AA1')
81 * @param string $b Second cell to test (e.g. 'Z1')
82 */
83 public static function cellSort(string $a, string $b): int
84 {
85 sscanf($a, '%[A-Z]%d', $ac, $ar);
86 /** @var int $ar */
87 /** @var string $ac */
88 sscanf($b, '%[A-Z]%d', $bc, $br);
89 /** @var int $br */
90 /** @var string $bc */
91 if ($ar === $br) {
92 return strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc);
93 }
94
95 return ($ar < $br) ? -1 : 1;
96 }
97
98 /**
99 * Compare two cell addresses
100 * Intended for use as a Callback function for sorting cell addresses by column and row.
101 *
102 * @param string $a First cell to test (e.g. 'AA1')
103 * @param string $b Second cell to test (e.g. 'Z1')
104 */
105 public static function cellReverseSort(string $a, string $b): int
106 {
107 sscanf($a, '%[A-Z]%d', $ac, $ar);
108 /** @var int $ar */
109 /** @var string $ac */
110 sscanf($b, '%[A-Z]%d', $bc, $br);
111 /** @var int $br */
112 /** @var string $bc */
113 if ($ar === $br) {
114 return -strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc);
115 }
116
117 return ($ar < $br) ? 1 : -1;
118 }
119
120 /**
121 * Update page breaks when inserting/deleting rows/columns.
122 *
123 * @param Worksheet $worksheet The worksheet that we're editing
124 * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
125 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
126 */
127 protected function adjustPageBreaks(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
128 {
129 $aBreaks = $worksheet->getBreaks();
130 ($numberOfColumns > 0 || $numberOfRows > 0)
131 ? uksort($aBreaks, [self::class, 'cellReverseSort'])
132 : uksort($aBreaks, [self::class, 'cellSort']);
133
134 foreach ($aBreaks as $cellAddress => $value) {
135 /** @var CellReferenceHelper */
136 $cellReferenceHelper = $this->cellReferenceHelper;
137 if ($cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === true) {
138 // If we're deleting, then clear any defined breaks that are within the range
139 // of rows/columns that we're deleting
140 $worksheet->setBreak($cellAddress, Worksheet::BREAK_NONE);
141 } else {
142 // Otherwise update any affected breaks by inserting a new break at the appropriate point
143 // and removing the old affected break
144 $newReference = $this->updateCellReference($cellAddress);
145 if ($cellAddress !== $newReference) {
146 $worksheet->setBreak($newReference, $value)
147 ->setBreak($cellAddress, Worksheet::BREAK_NONE);
148 }
149 }
150 }
151 }
152
153 /**
154 * Update cell comments when inserting/deleting rows/columns.
155 *
156 * @param Worksheet $worksheet The worksheet that we're editing
157 */
158 protected function adjustComments(Worksheet $worksheet): void
159 {
160 $aComments = $worksheet->getComments();
161 $aNewComments = []; // the new array of all comments
162
163 foreach ($aComments as $cellAddress => &$value) {
164 // Any comments inside a deleted range will be ignored
165 /** @var CellReferenceHelper */
166 $cellReferenceHelper = $this->cellReferenceHelper;
167 if ($cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === false) {
168 // Otherwise build a new array of comments indexed by the adjusted cell reference
169 $newReference = $this->updateCellReference($cellAddress);
170 $aNewComments[$newReference] = $value;
171 }
172 }
173 // Replace the comments array with the new set of comments
174 $worksheet->setComments($aNewComments);
175 }
176
177 /**
178 * Update hyperlinks when inserting/deleting rows/columns.
179 *
180 * @param Worksheet $worksheet The worksheet that we're editing
181 * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
182 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
183 */
184 protected function adjustHyperlinks(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
185 {
186 $aHyperlinkCollection = $worksheet->getHyperlinkCollection();
187 ($numberOfColumns > 0 || $numberOfRows > 0)
188 ? uksort($aHyperlinkCollection, [self::class, 'cellReverseSort'])
189 : uksort($aHyperlinkCollection, [self::class, 'cellSort']);
190
191 foreach ($aHyperlinkCollection as $cellAddress => $value) {
192 $newReference = $this->updateCellReference($cellAddress);
193 /** @var CellReferenceHelper */
194 $cellReferenceHelper = $this->cellReferenceHelper;
195 if ($cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === true) {
196 $worksheet->setHyperlink($cellAddress, null);
197 } elseif ($cellAddress !== $newReference) {
198 $worksheet->setHyperlink($cellAddress, null);
199 if ($newReference) {
200 $worksheet->setHyperlink($newReference, $value);
201 }
202 }
203 }
204 }
205
206 /**
207 * Update conditional formatting styles when inserting/deleting rows/columns.
208 *
209 * @param Worksheet $worksheet The worksheet that we're editing
210 * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
211 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
212 */
213 protected function adjustConditionalFormatting(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
214 {
215 $aStyles = $worksheet->getConditionalStylesCollection();
216 ($numberOfColumns > 0 || $numberOfRows > 0)
217 ? uksort($aStyles, [self::class, 'cellReverseSort'])
218 : uksort($aStyles, [self::class, 'cellSort']);
219
220 foreach ($aStyles as $cellAddress => $cfRules) {
221 $worksheet->removeConditionalStyles($cellAddress);
222 $newReference = $this->updateCellReference($cellAddress);
223
224 foreach ($cfRules as &$cfRule) {
225 /** @var Conditional $cfRule */
226 $conditions = $cfRule->getConditions();
227 foreach ($conditions as &$condition) {
228 if (is_string($condition)) {
229 /** @var CellReferenceHelper */
230 $cellReferenceHelper = $this->cellReferenceHelper;
231 $condition = $this->updateFormulaReferences(
232 $condition,
233 $cellReferenceHelper->beforeCellAddress(),
234 $numberOfColumns,
235 $numberOfRows,
236 $worksheet->getTitle(),
237 true
238 );
239 }
240 }
241 $cfRule->setConditions($conditions);
242 }
243 $worksheet->setConditionalStyles($newReference, $cfRules);
244 }
245 }
246
247 /**
248 * Update data validations when inserting/deleting rows/columns.
249 *
250 * @param Worksheet $worksheet The worksheet that we're editing
251 * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
252 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
253 */
254 protected function adjustDataValidations(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows, string $beforeCellAddress): void
255 {
256 $aDataValidationCollection = $worksheet->getDataValidationCollection();
257 ($numberOfColumns > 0 || $numberOfRows > 0)
258 ? uksort($aDataValidationCollection, [self::class, 'cellReverseSort'])
259 : uksort($aDataValidationCollection, [self::class, 'cellSort']);
260
261 foreach ($aDataValidationCollection as $cellAddress => $dataValidation) {
262 $formula = $dataValidation->getFormula1();
263 if ($formula !== '') {
264 $dataValidation->setFormula1(
265 $this->updateFormulaReferences(
266 $formula,
267 $beforeCellAddress,
268 $numberOfColumns,
269 $numberOfRows,
270 $worksheet->getTitle(),
271 true
272 )
273 );
274 }
275 $formula = $dataValidation->getFormula2();
276 if ($formula !== '') {
277 $dataValidation->setFormula2(
278 $this->updateFormulaReferences(
279 $formula,
280 $beforeCellAddress,
281 $numberOfColumns,
282 $numberOfRows,
283 $worksheet->getTitle(),
284 true
285 )
286 );
287 }
288 $addressParts = explode(' ', $cellAddress);
289 $newReference = '';
290 $separator = '';
291 foreach ($addressParts as $addressPart) {
292 $newReference .= $separator . $this->updateCellReference($addressPart);
293 $separator = ' ';
294 }
295 if ($cellAddress !== $newReference) {
296 $worksheet->setDataValidation($newReference, $dataValidation);
297 $worksheet->setDataValidation($cellAddress, null);
298 if ($newReference) {
299 $worksheet->setDataValidation($newReference, $dataValidation);
300 }
301 }
302 }
303 }
304
305 /**
306 * Update merged cells when inserting/deleting rows/columns.
307 *
308 * @param Worksheet $worksheet The worksheet that we're editing
309 */
310 protected function adjustMergeCells(Worksheet $worksheet): void
311 {
312 $aMergeCells = $worksheet->getMergeCells();
313 $aNewMergeCells = []; // the new array of all merge cells
314 foreach ($aMergeCells as $cellAddress => &$value) {
315 $newReference = $this->updateCellReference($cellAddress);
316 if ($newReference) {
317 $aNewMergeCells[$newReference] = $newReference;
318 }
319 }
320 $worksheet->setMergeCells($aNewMergeCells); // replace the merge cells array
321 }
322
323 /**
324 * Update protected cells when inserting/deleting rows/columns.
325 *
326 * @param Worksheet $worksheet The worksheet that we're editing
327 * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
328 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
329 */
330 protected function adjustProtectedCells(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
331 {
332 $aProtectedCells = $worksheet->getProtectedCellRanges();
333 /** @var CellReferenceHelper */
334 $cellReferenceHelper = $this->cellReferenceHelper;
335 if ($numberOfRows >= 0 && $numberOfColumns >= 0) {
336 foreach ($aProtectedCells as $key2 => $value) {
337 $ranges = $value->allRanges();
338 $newKey = $separator = '';
339 foreach ($ranges as $key => $range) {
340 $oldKey = $range[0] . (array_key_exists(1, $range) ? (':' . $range[1]) : '');
341 $newKey .= $separator . $this->updateCellReference($oldKey);
342 $separator = ' ';
343 }
344 if ($key2 !== $newKey) {
345 $worksheet->unprotectCells($key2);
346 $worksheet->protectCells($newKey, $value->getPassword(), true, $value->getName(), $value->getSecurityDescriptor());
347 }
348 }
349 } else {
350 foreach ($aProtectedCells as $key2 => $value) {
351 $range = str_replace([' ', ',', "\0"], ["\0", ' ', ','], $key2);
352 $extracted = Coordinate::extractAllCellReferencesInRange($range);
353 $outArray = [];
354 foreach ($extracted as $cellAddress) {
355 if (!$cellReferenceHelper->cellAddressInDeleteRange($cellAddress)) {
356 $outArray[$this->updateCellReference($cellAddress)] = 'x';
357 }
358 }
359 $outArray2 = Coordinate::mergeRangesInCollection($outArray);
360 $newKey = implode(' ', array_keys($outArray2));
361 if ($key2 !== $newKey) {
362 $worksheet->unprotectCells($key2);
363 $worksheet->protectCells($newKey, $value->getPassword(), true, $value->getName(), $value->getSecurityDescriptor());
364 }
365 }
366 }
367 }
368
369 /**
370 * Update column dimensions when inserting/deleting rows/columns.
371 *
372 * @param Worksheet $worksheet The worksheet that we're editing
373 */
374 protected function adjustColumnDimensions(Worksheet $worksheet): void
375 {
376 $aColumnDimensions = array_reverse($worksheet->getColumnDimensions(), true);
377 if (!empty($aColumnDimensions)) {
378 foreach ($aColumnDimensions as $objColumnDimension) {
379 $newReference = $this->updateCellReference($objColumnDimension->getColumnIndex() . '1');
380 [$newReference] = Coordinate::coordinateFromString($newReference);
381 if ($objColumnDimension->getColumnIndex() !== $newReference) {
382 $objColumnDimension->setColumnIndex($newReference);
383 }
384 }
385
386 $worksheet->refreshColumnDimensions();
387 }
388 }
389
390 /**
391 * Update row dimensions when inserting/deleting rows/columns.
392 *
393 * @param Worksheet $worksheet The worksheet that we're editing
394 * @param int $beforeRow Number of the row we're inserting/deleting before
395 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
396 */
397 protected function adjustRowDimensions(Worksheet $worksheet, int $beforeRow, int $numberOfRows): void
398 {
399 $aRowDimensions = array_reverse($worksheet->getRowDimensions(), true);
400 if (!empty($aRowDimensions)) {
401 foreach ($aRowDimensions as $objRowDimension) {
402 $newReference = $this->updateCellReference('A' . $objRowDimension->getRowIndex());
403 [, $newReference] = Coordinate::coordinateFromString($newReference);
404 $newRoweference = (int) $newReference;
405 if ($objRowDimension->getRowIndex() !== $newRoweference) {
406 $objRowDimension->setRowIndex($newRoweference);
407 }
408 }
409
410 $worksheet->refreshRowDimensions();
411
412 $copyDimension = $worksheet->getRowDimension($beforeRow - 1);
413 for ($i = $beforeRow; $i <= $beforeRow - 1 + $numberOfRows; ++$i) {
414 $newDimension = $worksheet->getRowDimension($i);
415 $newDimension->setRowHeight($copyDimension->getRowHeight());
416 $newDimension->setVisible($copyDimension->getVisible());
417 $newDimension->setOutlineLevel($copyDimension->getOutlineLevel());
418 $newDimension->setCollapsed($copyDimension->getCollapsed());
419 }
420 }
421 }
422
423 /**
424 * Insert a new column or row, updating all possible related data.
425 *
426 * @param string $beforeCellAddress Insert before this cell address (e.g. 'A1')
427 * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
428 * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
429 * @param Worksheet $worksheet The worksheet that we're editing
430 */
431 public function insertNewBefore(
432 string $beforeCellAddress,
433 int $numberOfColumns,
434 int $numberOfRows,
435 Worksheet $worksheet
436 ): void {
437 $remove = ($numberOfColumns < 0 || $numberOfRows < 0);
438
439 if (
440 $this->cellReferenceHelper === null
441 || $this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows)
442 ) {
443 $this->cellReferenceHelper = new CellReferenceHelper($beforeCellAddress, $numberOfColumns, $numberOfRows);
444 }
445
446 // Get coordinate of $beforeCellAddress
447 [$beforeColumn, $beforeRow, $beforeColumnString] = Coordinate::indexesFromString($beforeCellAddress);
448
449 // Clear cells if we are removing columns or rows
450 $highestColumn = $worksheet->getHighestColumn();
451 $highestDataColumn = $worksheet->getHighestDataColumn();
452 $highestRow = $worksheet->getHighestRow();
453 $highestDataRow = $worksheet->getHighestDataRow();
454
455 // 1. Clear column strips if we are removing columns
456 if ($numberOfColumns < 0 && $beforeColumn - 2 + $numberOfColumns > 0) {
457 $this->clearColumnStrips($highestRow, $beforeColumn, $numberOfColumns, $worksheet);
458 }
459
460 // 2. Clear row strips if we are removing rows
461 if ($numberOfRows < 0 && $beforeRow - 1 + $numberOfRows > 0) {
462 $this->clearRowStrips($highestColumn, $beforeColumn, $beforeRow, $numberOfRows, $worksheet);
463 }
464
465 // Find missing coordinates. This is important when inserting or deleting column before the last column
466 $startRow = $startCol = 1;
467 $startColString = 'A';
468 if ($numberOfRows === 0) {
469 $startCol = $beforeColumn;
470 $startColString = $beforeColumnString;
471 } elseif ($numberOfColumns === 0) {
472 $startRow = $beforeRow;
473 }
474 $highColumn = Coordinate::columnIndexFromString($highestDataColumn);
475 for ($row = $startRow; $row <= $highestDataRow; ++$row) {
476 for ($col = $startCol, $colString = $startColString; $col <= $highColumn; ++$col, StringHelper::stringIncrement($colString)) {
477 $worksheet->getCell("$colString$row"); // create cell if it doesn't exist
478 }
479 }
480
481 $allCoordinates = $worksheet->getCoordinates();
482 if ($remove) {
483 // It's faster to reverse and pop than to use unshift, especially with large cell collections
484 $allCoordinates = array_reverse($allCoordinates);
485 }
486
487 // Loop through cells, bottom-up, and change cell coordinate
488 while ($coordinate = array_pop($allCoordinates)) {
489 $cell = $worksheet->getCell($coordinate);
490 $cellIndex = Coordinate::columnIndexFromString($cell->getColumn());
491
492 // Don't update cells that are being removed
493 if ($numberOfColumns < 0 && $cellIndex >= $beforeColumn + $numberOfColumns && $cellIndex < $beforeColumn) {
494 continue;
495 }
496
497 // Should the cell be updated? Move value and cellXf index from one cell to another.
498 if (($cellIndex >= $beforeColumn) && ($cell->getRow() >= $beforeRow)) {
499 // New coordinate
500 $newColumn = $cellIndex + $numberOfColumns;
501 $newRow = $cell->getRow() + $numberOfRows;
502 if ($newColumn > 0 && $newRow > 0 && $newColumn <= AddressRange::MAX_COLUMN_INT && $newRow <= AddressRange::MAX_ROW) {
503 $newCoordinate = Coordinate::stringFromColumnIndex($newColumn) . $newRow;
504 // Update cell styles
505 $worksheet->getCell($newCoordinate)
506 ->setXfIndex($cell->getXfIndex());
507
508 // Insert this cell at its new location
509 if ($cell->getDataType() === DataType::TYPE_FORMULA) {
510 // Formula should be adjusted
511 $worksheet->getCell($newCoordinate)
512 ->setValue(
513 $this->updateFormulaReferences(
514 $cell->getValueString(),
515 $beforeCellAddress,
516 $numberOfColumns,
517 $numberOfRows,
518 $worksheet->getTitle(),
519 true
520 )
521 );
522 } else {
523 // Cell value should not be adjusted
524 $worksheet->getCell($newCoordinate)
525 ->setValueExplicit($cell->getValue(), $cell->getDataType());
526 }
527 }
528
529 // Clear the original cell
530 $worksheet->getCellCollection()
531 ->delete($coordinate);
532 } else {
533 /* We don't need to update styles for rows/columns before our insertion position,
534 but we do still need to adjust any formulae in those cells */
535 if ($cell->getDataType() === DataType::TYPE_FORMULA) {
536 // Formula should be adjusted
537 $cell->setValue(
538 $this->updateFormulaReferences(
539 $cell->getValueString(),
540 $beforeCellAddress,
541 $numberOfColumns,
542 $numberOfRows,
543 $worksheet->getTitle(),
544 true
545 )
546 );
547 }
548 }
549 }
550
551 // Duplicate styles for the newly inserted cells
552 $highestColumn = $worksheet->getHighestColumn();
553 $highestRow = $worksheet->getHighestRow();
554
555 if ($numberOfColumns > 0 && $beforeColumn > 1) {
556 $this->duplicateStylesByColumn($worksheet, $beforeColumn, $beforeRow, $highestRow, $numberOfColumns);
557 }
558
559 if ($numberOfRows > 0 && $beforeRow - 1 > 0) {
560 $this->duplicateStylesByRow($worksheet, $beforeColumn, $beforeRow, $highestColumn, $numberOfRows);
561 }
562
563 // Update worksheet: column dimensions
564 $this->adjustColumnDimensions($worksheet);
565
566 // Update worksheet: row dimensions
567 $this->adjustRowDimensions($worksheet, $beforeRow, $numberOfRows);
568
569 // Update worksheet: page breaks
570 $this->adjustPageBreaks($worksheet, $numberOfColumns, $numberOfRows);
571
572 // Update worksheet: comments
573 $this->adjustComments($worksheet);
574
575 // Update worksheet: hyperlinks
576 $this->adjustHyperlinks($worksheet, $numberOfColumns, $numberOfRows);
577
578 // Update worksheet: conditional formatting styles
579 $this->adjustConditionalFormatting($worksheet, $numberOfColumns, $numberOfRows);
580
581 // Update worksheet: data validations
582 $this->adjustDataValidations($worksheet, $numberOfColumns, $numberOfRows, $beforeCellAddress);
583
584 // Update worksheet: merge cells
585 $this->adjustMergeCells($worksheet);
586
587 // Update worksheet: protected cells
588 $this->adjustProtectedCells($worksheet, $numberOfColumns, $numberOfRows);
589
590 // Update worksheet: autofilter
591 $this->adjustAutoFilter($worksheet, $beforeCellAddress, $numberOfColumns);
592
593 // Update worksheet: table
594 $this->adjustTable($worksheet, $beforeCellAddress, $numberOfColumns);
595
596 // Update worksheet: freeze pane
597 if ($worksheet->getFreezePane()) {
598 $splitCell = $worksheet->getFreezePane();
599 $topLeftCell = $worksheet->getTopLeftCell() ?? '';
600
601 $splitCell = $this->updateCellReference($splitCell);
602 $topLeftCell = $this->updateCellReference($topLeftCell);
603
604 $worksheet->freezePane($splitCell, $topLeftCell);
605 }
606
607 $this->updatePrintAreas($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows);
608
609 // Update worksheet: drawings
610 $aDrawings = $worksheet->getDrawingCollection();
611 foreach ($aDrawings as $objDrawing) {
612 $newReference = $this->updateCellReference($objDrawing->getCoordinates());
613 if ($objDrawing->getCoordinates() != $newReference) {
614 $objDrawing->setCoordinates($newReference);
615 }
616 if ($objDrawing->getCoordinates2() !== '') {
617 $newReference = $this->updateCellReference($objDrawing->getCoordinates2());
618 if ($objDrawing->getCoordinates2() != $newReference) {
619 $objDrawing->setCoordinates2($newReference);
620 }
621 }
622 }
623
624 // Update workbook: define names
625 if (count($worksheet->getParentOrThrow()->getDefinedNames()) > 0) {
626 $this->updateDefinedNames($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows);
627 }
628
629 // Garbage collect
630 $worksheet->garbageCollect();
631 }
632
633 private function updatePrintAreas(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void
634 {
635 $pageSetup = $worksheet->getPageSetup();
636 if (!$pageSetup->isPrintAreaSet()) {
637 return;
638 }
639 $printAreas = explode(',', $pageSetup->getPrintArea());
640 $newPrintAreas = [];
641 foreach ($printAreas as $printArea) {
642 $result = $this->updatePrintArea($printArea, $beforeCellAddress, $numberOfColumns, $numberOfRows);
643 if ($result !== '') {
644 $newPrintAreas[] = $result;
645 }
646 }
647 $result = implode(',', $newPrintAreas);
648 if ($result === '') {
649 $pageSetup->clearPrintArea();
650 } else {
651 $pageSetup->setPrintArea($result);
652 }
653 }
654
655 private function updatePrintArea(string $printArea, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): string
656 {
657 $coordinates = Coordinate::indexesFromString($beforeCellAddress);
658 if (preg_match('/^([A-Z]{1,3})(\d{1,7}):([A-Z]{1,3})(\d{1,7})$/i', $printArea, $matches) === 1) {
659 $firstRow = (int) $matches[2];
660 $lastRow = (int) $matches[4];
661 $firstColumnString = $matches[1];
662 $lastColumnString = $matches[3];
663 if ($numberOfRows < 0) {
664 $affectedRow = $coordinates[1] + $numberOfRows - 1;
665 $lastAffectedRow = $coordinates[1] - 1;
666 if ($affectedRow >= $firstRow && $affectedRow <= $lastRow) {
667 $newLastRow = max($affectedRow, $lastRow + $numberOfRows);
668 if ($newLastRow >= $firstRow) {
669 return $matches[1] . $matches[2] . ':' . $matches[3] . $newLastRow;
670 }
671
672 return '';
673 }
674 if ($lastAffectedRow >= $firstRow && $affectedRow <= $lastRow) {
675 $newFirstRow = $affectedRow + 1;
676 $newLastRow = $lastRow + $numberOfRows;
677 if ($newFirstRow >= 1 && $newLastRow >= $newFirstRow) {
678 return $matches[1] . $newFirstRow . ':' . $matches[3] . $newLastRow;
679 }
680
681 return '';
682 }
683 }
684 if ($numberOfColumns < 0) {
685 $firstColumnInt = Coordinate::columnIndexFromString($firstColumnString);
686 $lastColumnInt = Coordinate::columnIndexFromString($lastColumnString);
687 $affectedColumn = $coordinates[0] + $numberOfColumns - 1;
688 $lastAffectedColumn = $coordinates[0] - 1;
689 if ($affectedColumn >= $firstColumnInt && $affectedColumn <= $lastColumnInt) {
690 $newLastColumnInt = max($affectedColumn, $lastColumnInt + $numberOfColumns);
691 if ($newLastColumnInt >= $firstColumnInt) {
692 $newLastColumnString = Coordinate::stringFromColumnIndex($newLastColumnInt);
693
694 return $matches[1] . $matches[2] . ':' . $newLastColumnString . $matches[4];
695 }
696
697 return '';
698 }
699 if ($affectedColumn < $firstColumnInt && $lastAffectedColumn > $lastColumnInt) {
700 return '';
701 }
702 if ($lastAffectedColumn >= $firstColumnInt && $lastAffectedColumn <= $lastColumnInt) {
703 $newFirstColumn = $affectedColumn + 1;
704 $newLastColumn = $lastColumnInt + $numberOfColumns;
705 if ($newFirstColumn >= 1 && $newLastColumn >= $newFirstColumn) {
706 $firstString = Coordinate::stringFromColumnIndex($newFirstColumn);
707 $lastString = Coordinate::stringFromColumnIndex($newLastColumn);
708
709 return $firstString . $matches[2] . ':' . $lastString . $matches[4];
710 }
711
712 return '';
713 }
714 }
715 }
716
717 return $this->updateCellReference($printArea);
718 }
719
720 private static function matchSheetName(?string $match, string $worksheetName): bool
721 {
722 return $match === null || $match === '' || $match === "'\u{fffc}'" || $match === "'\u{fffb}'" || strcasecmp(trim($match, "'"), $worksheetName) === 0;
723 }
724
725 private static function sheetnameBeforeCells(string $match, string $worksheetName, string $cells): string
726 {
727 $toString = ($match > '') ? "$match!" : '';
728
729 return str_replace(["\u{fffc}", "'\u{fffb}'"], $worksheetName, $toString) . $cells;
730 }
731
732 /**
733 * Update references within formulas.
734 *
735 * @param string $formula Formula to update
736 * @param string $beforeCellAddress Insert before this one
737 * @param int $numberOfColumns Number of columns to insert
738 * @param int $numberOfRows Number of rows to insert
739 * @param string $worksheetName Worksheet name/title
740 *
741 * @return string Updated formula
742 */
743 public function updateFormulaReferences(
744 string $formula = '',
745 string $beforeCellAddress = 'A1',
746 int $numberOfColumns = 0,
747 int $numberOfRows = 0,
748 string $worksheetName = '',
749 bool $includeAbsoluteReferences = false,
750 bool $onlyAbsoluteReferences = false
751 ): string {
752 $callback = fn (array $matches): string => (strcasecmp(trim($matches[2], "'"), $worksheetName) === 0) ? (($matches[2][0] === "'") ? "'\u{fffc}'!" : "'\u{fffb}'!") : "'\u{fffd}'!";
753 if (
754 $this->cellReferenceHelper === null
755 || $this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows)
756 ) {
757 $this->cellReferenceHelper = new CellReferenceHelper($beforeCellAddress, $numberOfColumns, $numberOfRows);
758 }
759
760 // Update cell references in the formula
761 $formulaBlocks = explode('"', $formula);
762 $i = false;
763 foreach ($formulaBlocks as &$formulaBlock) {
764 // Ignore blocks that were enclosed in quotes (alternating entries in the $formulaBlocks array after the explode)
765 $i = $i === false;
766 if ($i) {
767 $adjustCount = 0;
768 $newCellTokens = $cellTokens = [];
769 // Search for row ranges (e.g. 'Sheet1'!3:5 or 3:5) with or without $ absolutes (e.g. $3:5)
770 $formulaBlockx = ' ' . (preg_replace_callback(self::SHEETNAME_PART_WITH_SLASHES, $callback, $formulaBlock) ?? $formulaBlock) . ' ';
771 $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_ROWRANGE . '/mui', $formulaBlockx, $matches, PREG_SET_ORDER);
772 if ($matchCount > 0) {
773 foreach ($matches as $match) {
774 $fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}:{$match[4]}");
775 $modified3 = (string) substr($this->updateCellReference('$A' . $match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences, true), 2);
776 $modified4 = (string) substr($this->updateCellReference('$A' . $match[4], $includeAbsoluteReferences, $onlyAbsoluteReferences, false), 2);
777
778 if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) {
779 if (self::matchSheetName($match[2], $worksheetName)) {
780 $toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3:$modified4");
781 // Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more
782 $column = 100000;
783 $row = 10000000 + (int) trim($match[3], '$');
784 $cellIndex = "{$column}{$row}";
785
786 $newCellTokens[$cellIndex] = preg_quote($toString, '/');
787 $cellTokens[$cellIndex] = '/(?<!\d\$\!)' . preg_quote($fromString, '/') . '(?!\d)/i';
788 ++$adjustCount;
789 }
790 }
791 }
792 }
793 // Search for column ranges (e.g. 'Sheet1'!C:E or C:E) with or without $ absolutes (e.g. $C:E)
794 $formulaBlockx = ' ' . (preg_replace_callback(self::SHEETNAME_PART_WITH_SLASHES, $callback, $formulaBlock) ?? $formulaBlock) . ' ';
795 $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_COLRANGE . '/mui', $formulaBlockx, $matches, PREG_SET_ORDER);
796 if ($matchCount > 0) {
797 foreach ($matches as $match) {
798 $fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}:{$match[4]}");
799 $modified3 = (string) substr($this->updateCellReference($match[3] . '$1', $includeAbsoluteReferences, $onlyAbsoluteReferences, true), 0, -2);
800 $modified4 = (string) substr($this->updateCellReference($match[4] . '$1', $includeAbsoluteReferences, $onlyAbsoluteReferences, false), 0, -2);
801
802 if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) {
803 if (self::matchSheetName($match[2], $worksheetName)) {
804 $toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3:$modified4");
805 // Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more
806 $column = Coordinate::columnIndexFromString(trim($match[3], '$')) + 100000;
807 $row = 10000000;
808 $cellIndex = "{$column}{$row}";
809
810 $newCellTokens[$cellIndex] = preg_quote($toString, '/');
811 $cellTokens[$cellIndex] = '/(?<![A-Z\$\!])' . preg_quote($fromString, '/') . '(?![A-Z])/i';
812 ++$adjustCount;
813 }
814 }
815 }
816 }
817 // Search for cell ranges (e.g. 'Sheet1'!A3:C5 or A3:C5) with or without $ absolutes (e.g. $A1:C$5)
818 $formulaBlockx = ' ' . (preg_replace_callback(self::SHEETNAME_PART_WITH_SLASHES, $callback, "$formulaBlock") ?? "$formulaBlock") . ' ';
819 $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLRANGE . '/mui', $formulaBlockx, $matches, PREG_SET_ORDER);
820 if ($matchCount > 0) {
821 foreach ($matches as $match) {
822 $fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}:{$match[4]}");
823 $modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences, true);
824 $modified4 = $this->updateCellReference($match[4], $includeAbsoluteReferences, $onlyAbsoluteReferences, false);
825
826 if ($match[3] . $match[4] !== $modified3 . $modified4) {
827 if (self::matchSheetName($match[2], $worksheetName)) {
828 $toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3:$modified4");
829 [$column, $row] = Coordinate::coordinateFromString($match[3]);
830 // Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more
831 $column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000;
832 $row = (int) trim($row, '$') + 10000000;
833 $cellIndex = "{$column}{$row}";
834
835 $newCellTokens[$cellIndex] = preg_quote($toString, '/');
836 $cellTokens[$cellIndex] = '/(?<![A-Z]\$\!)' . preg_quote($fromString, '/') . '(?!\d)/i';
837 ++$adjustCount;
838 }
839 }
840 }
841 }
842 // Search for cell references (e.g. 'Sheet1'!A3 or C5) with or without $ absolutes (e.g. $A1 or C$5)
843
844 $formulaBlockx = ' ' . (preg_replace_callback(self::SHEETNAME_PART_WITH_SLASHES, $callback, $formulaBlock) ?? $formulaBlock) . ' ';
845 $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLREF . '/mui', $formulaBlockx, $matches, PREG_SET_ORDER);
846
847 if ($matchCount > 0) {
848 foreach ($matches as $match) {
849 $fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}");
850
851 $modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences, null);
852 if ($match[3] !== $modified3) {
853 if (self::matchSheetName($match[2], $worksheetName)) {
854 $toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3");
855 [$column, $row] = Coordinate::coordinateFromString($match[3]);
856 $columnAdditionalIndex = $column[0] === '$' ? 1 : 0;
857 $rowAdditionalIndex = $row[0] === '$' ? 1 : 0;
858 // Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more
859 $column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000;
860 $row = (int) trim($row, '$') + 10000000;
861 $cellIndex = $row . $rowAdditionalIndex . $column . $columnAdditionalIndex;
862
863 $newCellTokens[$cellIndex] = preg_quote($toString, '/');
864 $cellTokens[$cellIndex] = '/(?<![A-Z\$\!])' . preg_quote($fromString, '/') . '(?!\d)/i';
865 ++$adjustCount;
866 }
867 }
868 }
869 }
870 if ($adjustCount > 0) {
871 if ($numberOfColumns > 0 || $numberOfRows > 0) {
872 krsort($cellTokens);
873 krsort($newCellTokens);
874 } else {
875 ksort($cellTokens);
876 ksort($newCellTokens);
877 } // Update cell references in the formula
878 $formulaBlock = str_replace('\\', '', (string) preg_replace($cellTokens, $newCellTokens, $formulaBlock));
879 }
880 }
881 }
882 unset($formulaBlock);
883
884 // Then rebuild the formula string
885 return implode('"', $formulaBlocks);
886 }
887
888 /**
889 * Update all cell references within a formula, irrespective of worksheet.
890 */
891 public function updateFormulaReferencesAnyWorksheet(string $formula = '', int $numberOfColumns = 0, int $numberOfRows = 0): string
892 {
893 $formula = $this->updateCellReferencesAllWorksheets($formula, $numberOfColumns, $numberOfRows);
894
895 if ($numberOfColumns !== 0) {
896 $formula = $this->updateColumnRangesAllWorksheets($formula, $numberOfColumns);
897 }
898
899 if ($numberOfRows !== 0) {
900 $formula = $this->updateRowRangesAllWorksheets($formula, $numberOfRows);
901 }
902
903 return $formula;
904 }
905
906 private function updateCellReferencesAllWorksheets(string $formula, int $numberOfColumns, int $numberOfRows): string
907 {
908 $splitCount = preg_match_all(
909 '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/mui',
910 $formula,
911 $splitRanges,
912 PREG_OFFSET_CAPTURE
913 );
914
915 $columnLengths = array_map('strlen', array_column($splitRanges[6], 0));
916 $rowLengths = array_map('strlen', array_column($splitRanges[7], 0));
917 $columnOffsets = array_column($splitRanges[6], 1);
918 $rowOffsets = array_column($splitRanges[7], 1);
919
920 $columns = $splitRanges[6];
921 $rows = $splitRanges[7];
922
923 while ($splitCount > 0) {
924 --$splitCount;
925 $columnLength = $columnLengths[$splitCount];
926 $rowLength = $rowLengths[$splitCount];
927 $columnOffset = $columnOffsets[$splitCount];
928 $rowOffset = $rowOffsets[$splitCount];
929 $column = $columns[$splitCount][0];
930 $row = $rows[$splitCount][0];
931
932 if ($column[0] !== '$') {
933 $column = ((Coordinate::columnIndexFromString($column) + $numberOfColumns) % AddressRange::MAX_COLUMN_INT) ?: AddressRange::MAX_COLUMN_INT;
934 $column = Coordinate::stringFromColumnIndex($column);
935 $rowOffset -= ($columnLength - strlen($column));
936 $formula = substr($formula, 0, $columnOffset) . $column . substr($formula, $columnOffset + $columnLength);
937 }
938 if (!empty($row) && $row[0] !== '$') {
939 $row = (((int) $row + $numberOfRows) % AddressRange::MAX_ROW) ?: AddressRange::MAX_ROW;
940 $formula = substr($formula, 0, $rowOffset) . $row . substr($formula, $rowOffset + $rowLength);
941 }
942 }
943
944 return $formula;
945 }
946
947 private function updateColumnRangesAllWorksheets(string $formula, int $numberOfColumns): string
948 {
949 $splitCount = preg_match_all(
950 '/' . Calculation::CALCULATION_REGEXP_COLUMNRANGE_RELATIVE . '/mui',
951 $formula,
952 $splitRanges,
953 PREG_OFFSET_CAPTURE
954 );
955
956 $fromColumnLengths = array_map('strlen', array_column($splitRanges[1], 0));
957 $fromColumnOffsets = array_column($splitRanges[1], 1);
958 $toColumnLengths = array_map('strlen', array_column($splitRanges[2], 0));
959 $toColumnOffsets = array_column($splitRanges[2], 1);
960
961 $fromColumns = $splitRanges[1];
962 $toColumns = $splitRanges[2];
963
964 while ($splitCount > 0) {
965 --$splitCount;
966 $fromColumnLength = $fromColumnLengths[$splitCount];
967 $toColumnLength = $toColumnLengths[$splitCount];
968 $fromColumnOffset = $fromColumnOffsets[$splitCount];
969 $toColumnOffset = $toColumnOffsets[$splitCount];
970 $fromColumn = $fromColumns[$splitCount][0];
971 $toColumn = $toColumns[$splitCount][0];
972
973 if (!empty($fromColumn) && $fromColumn[0] !== '$') {
974 $fromColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($fromColumn) + $numberOfColumns);
975 $formula = substr($formula, 0, $fromColumnOffset) . $fromColumn . substr($formula, $fromColumnOffset + $fromColumnLength);
976 }
977 if (!empty($toColumn) && $toColumn[0] !== '$') {
978 $toColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($toColumn) + $numberOfColumns);
979 $formula = substr($formula, 0, $toColumnOffset) . $toColumn . substr($formula, $toColumnOffset + $toColumnLength);
980 }
981 }
982
983 return $formula;
984 }
985
986 private function updateRowRangesAllWorksheets(string $formula, int $numberOfRows): string
987 {
988 $splitCount = preg_match_all(
989 '/' . Calculation::CALCULATION_REGEXP_ROWRANGE_RELATIVE . '/mui',
990 $formula,
991 $splitRanges,
992 PREG_OFFSET_CAPTURE
993 );
994
995 $fromRowLengths = array_map('strlen', array_column($splitRanges[1], 0));
996 $fromRowOffsets = array_column($splitRanges[1], 1);
997 $toRowLengths = array_map('strlen', array_column($splitRanges[2], 0));
998 $toRowOffsets = array_column($splitRanges[2], 1);
999
1000 $fromRows = $splitRanges[1];
1001 $toRows = $splitRanges[2];
1002
1003 while ($splitCount > 0) {
1004 --$splitCount;
1005 $fromRowLength = $fromRowLengths[$splitCount];
1006 $toRowLength = $toRowLengths[$splitCount];
1007 $fromRowOffset = $fromRowOffsets[$splitCount];
1008 $toRowOffset = $toRowOffsets[$splitCount];
1009 $fromRow = $fromRows[$splitCount][0];
1010 $toRow = $toRows[$splitCount][0];
1011
1012 if (!empty($fromRow) && $fromRow[0] !== '$') {
1013 $fromRow = (int) $fromRow + $numberOfRows;
1014 $formula = substr($formula, 0, $fromRowOffset) . $fromRow . substr($formula, $fromRowOffset + $fromRowLength);
1015 }
1016 if (!empty($toRow) && $toRow[0] !== '$') {
1017 $toRow = (int) $toRow + $numberOfRows;
1018 $formula = substr($formula, 0, $toRowOffset) . $toRow . substr($formula, $toRowOffset + $toRowLength);
1019 }
1020 }
1021
1022 return $formula;
1023 }
1024
1025 /**
1026 * Update cell reference.
1027 *
1028 * @param string $cellReference Cell address or range of addresses
1029 *
1030 * @return string Updated cell range
1031 */
1032 private function updateCellReference(string $cellReference = 'A1', bool $includeAbsoluteReferences = false, bool $onlyAbsoluteReferences = false, ?bool $topLeft = null)
1033 {
1034 // Is it in another worksheet? Will not have to update anything.
1035 if (str_contains($cellReference, '!')) {
1036 return $cellReference;
1037 }
1038 // Is it a range or a single cell?
1039 if (!Coordinate::coordinateIsRange($cellReference)) {
1040 // Single cell
1041 /** @var CellReferenceHelper */
1042 $cellReferenceHelper = $this->cellReferenceHelper;
1043
1044 return $cellReferenceHelper->updateCellReference($cellReference, $includeAbsoluteReferences, $onlyAbsoluteReferences, $topLeft);
1045 }
1046
1047 // Range
1048 return $this->updateCellRange($cellReference, $includeAbsoluteReferences, $onlyAbsoluteReferences);
1049 }
1050
1051 /**
1052 * Update named formulae (i.e. containing worksheet references / named ranges).
1053 *
1054 * @param Spreadsheet $spreadsheet Object to update
1055 * @param string $oldName Old name (name to replace)
1056 * @param string $newName New name
1057 */
1058 public function updateNamedFormulae(Spreadsheet $spreadsheet, string $oldName = '', string $newName = ''): void
1059 {
1060 if ($oldName == '') {
1061 return;
1062 }
1063
1064 foreach ($spreadsheet->getWorksheetIterator() as $sheet) {
1065 foreach ($sheet->getCoordinates(false) as $coordinate) {
1066 $cell = $sheet->getCell($coordinate);
1067 if ($cell->getDataType() === DataType::TYPE_FORMULA) {
1068 $formula = $cell->getValueString();
1069 if (str_contains($formula, $oldName)) {
1070 $formula = str_replace("'" . $oldName . "'!", "'" . $newName . "'!", $formula);
1071 $formula = str_replace($oldName . '!', $newName . '!', $formula);
1072 $cell->setValueExplicit($formula, DataType::TYPE_FORMULA);
1073 }
1074 }
1075 }
1076 }
1077 }
1078
1079 private function updateDefinedNames(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void
1080 {
1081 foreach ($worksheet->getParentOrThrow()->getDefinedNames() as $definedName) {
1082 if ($definedName->isFormula() === false) {
1083 $this->updateNamedRange($definedName, $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows);
1084 } else {
1085 $this->updateNamedFormula($definedName, $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows);
1086 }
1087 }
1088 }
1089
1090 private function updateNamedRange(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void
1091 {
1092 $cellAddress = $definedName->getValue();
1093 $asFormula = ($cellAddress[0] === '=');
1094 if ($definedName->getWorksheet() === $worksheet) {
1095 /**
1096 * If we delete the entire range that is referenced by a Named Range, MS Excel sets the value to #REF!
1097 * PhpSpreadsheet still only does a basic adjustment, so the Named Range will still reference Cells.
1098 * Note that this applies only when deleting columns/rows; subsequent insertion won't fix the #REF!
1099 * TODO Can we work out a method to identify Named Ranges that cease to be valid, so that we can replace
1100 * them with a #REF!
1101 */
1102 if ($asFormula === true) {
1103 $formula = $this->updateFormulaReferences($cellAddress, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true, true);
1104 $definedName->setValue($formula);
1105 } else {
1106 $definedName->setValue($this->updateCellReference(ltrim($cellAddress, '='), true));
1107 }
1108 }
1109 }
1110
1111 private function updateNamedFormula(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void
1112 {
1113 if ($definedName->getWorksheet() === $worksheet) {
1114 /**
1115 * If we delete the entire range that is referenced by a Named Formula, MS Excel sets the value to #REF!
1116 * PhpSpreadsheet still only does a basic adjustment, so the Named Formula will still reference Cells.
1117 * Note that this applies only when deleting columns/rows; subsequent insertion won't fix the #REF!
1118 * TODO Can we work out a method to identify Named Ranges that cease to be valid, so that we can replace
1119 * them with a #REF!
1120 */
1121 $formula = $definedName->getValue();
1122 $formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true);
1123 $definedName->setValue($formula);
1124 }
1125 }
1126
1127 /**
1128 * Update cell range.
1129 *
1130 * @param string $cellRange Cell range (e.g. 'B2:D4', 'B:C' or '2:3')
1131 *
1132 * @return string Updated cell range
1133 */
1134 private function updateCellRange(string $cellRange = 'A1:A1', bool $includeAbsoluteReferences = false, bool $onlyAbsoluteReferences = false): string
1135 {
1136 if (!Coordinate::coordinateIsRange($cellRange)) {
1137 throw new Exception('Only cell ranges may be passed to this method.');
1138 }
1139
1140 // Update range
1141 $range = Coordinate::splitRange($cellRange);
1142 $ic = count($range);
1143 for ($i = 0; $i < $ic; ++$i) {
1144 $jc = count($range[$i]);
1145 for ($j = 0; $j < $jc; ++$j) {
1146 /** @var CellReferenceHelper */
1147 $cellReferenceHelper = $this->cellReferenceHelper;
1148 if (ctype_alpha($range[$i][$j])) {
1149 $range[$i][$j] = Coordinate::coordinateFromString(
1150 $cellReferenceHelper->updateCellReference($range[$i][$j] . '1', $includeAbsoluteReferences, $onlyAbsoluteReferences, null)
1151 )[0];
1152 } elseif (ctype_digit($range[$i][$j])) {
1153 $range[$i][$j] = Coordinate::coordinateFromString(
1154 $cellReferenceHelper->updateCellReference('A' . $range[$i][$j], $includeAbsoluteReferences, $onlyAbsoluteReferences, null)
1155 )[1];
1156 } else {
1157 $range[$i][$j] = $cellReferenceHelper->updateCellReference($range[$i][$j], $includeAbsoluteReferences, $onlyAbsoluteReferences, null);
1158 }
1159 }
1160 }
1161
1162 // Recreate range string
1163 return Coordinate::buildRange($range);
1164 }
1165
1166 private function clearColumnStrips(int $highestRow, int $beforeColumn, int $numberOfColumns, Worksheet $worksheet): void
1167 {
1168 $startColumnId = Coordinate::stringFromColumnIndex($beforeColumn + $numberOfColumns);
1169 $endColumnId = Coordinate::stringFromColumnIndex($beforeColumn);
1170
1171 for ($row = 1; $row <= $highestRow - 1; ++$row) {
1172 for ($column = $startColumnId; $column !== $endColumnId; StringHelper::stringIncrement($column)) {
1173 $coordinate = $column . $row;
1174 $this->clearStripCell($worksheet, $coordinate);
1175 }
1176 }
1177 }
1178
1179 private function clearRowStrips(string $highestColumn, int $beforeColumn, int $beforeRow, int $numberOfRows, Worksheet $worksheet): void
1180 {
1181 $startColumnId = Coordinate::stringFromColumnIndex($beforeColumn);
1182 StringHelper::stringIncrement($highestColumn);
1183
1184 for ($column = $startColumnId; $column !== $highestColumn; StringHelper::stringIncrement($column)) {
1185 for ($row = $beforeRow + $numberOfRows; $row <= $beforeRow - 1; ++$row) {
1186 $coordinate = $column . $row;
1187 $this->clearStripCell($worksheet, $coordinate);
1188 }
1189 }
1190 }
1191
1192 private function clearStripCell(Worksheet $worksheet, string $coordinate): void
1193 {
1194 $worksheet->removeConditionalStyles($coordinate);
1195 $worksheet->setHyperlink($coordinate, null, false);
1196 $worksheet->setDataValidation($coordinate);
1197 $worksheet->removeComment($coordinate);
1198
1199 if ($worksheet->cellExists($coordinate)) {
1200 $worksheet->getCell($coordinate)->setValueExplicit(null, DataType::TYPE_NULL);
1201 $worksheet->getCell($coordinate)->setXfIndex(0);
1202 }
1203 }
1204
1205 private function adjustAutoFilter(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void
1206 {
1207 $autoFilter = $worksheet->getAutoFilter();
1208 $autoFilterRange = $autoFilter->getRange();
1209 if (!empty($autoFilterRange)) {
1210 if ($numberOfColumns !== 0) {
1211 $autoFilterColumns = $autoFilter->getColumns();
1212 if (count($autoFilterColumns) > 0) {
1213 $column = '';
1214 $row = 0;
1215 sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row);
1216 $columnIndex = Coordinate::columnIndexFromString((string) $column);
1217 [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($autoFilterRange);
1218 if ($columnIndex <= $rangeEnd[0]) {
1219 if ($numberOfColumns < 0) {
1220 $this->adjustAutoFilterDeleteRules($columnIndex, $numberOfColumns, $autoFilterColumns, $autoFilter);
1221 }
1222 $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0];
1223
1224 // Shuffle columns in autofilter range
1225 if ($numberOfColumns > 0) {
1226 $this->adjustAutoFilterInsert($startCol, $numberOfColumns, $rangeEnd[0], $autoFilter);
1227 } else {
1228 $this->adjustAutoFilterDelete($startCol, $numberOfColumns, $rangeEnd[0], $autoFilter);
1229 }
1230 }
1231 }
1232 }
1233
1234 $worksheet->setAutoFilter(
1235 $this->updateCellReference($autoFilterRange)
1236 );
1237 }
1238 }
1239
1240 /** @param mixed[] $autoFilterColumns */
1241 private function adjustAutoFilterDeleteRules(int $columnIndex, int $numberOfColumns, array $autoFilterColumns, AutoFilter $autoFilter): void
1242 {
1243 // If we're actually deleting any columns that fall within the autofilter range,
1244 // then we delete any rules for those columns
1245 $deleteColumn = $columnIndex + $numberOfColumns - 1;
1246 $deleteCount = abs($numberOfColumns);
1247
1248 for ($i = 1; $i <= $deleteCount; ++$i) {
1249 $columnName = Coordinate::stringFromColumnIndex($deleteColumn + 1);
1250 if (isset($autoFilterColumns[$columnName])) {
1251 $autoFilter->clearColumn($columnName);
1252 }
1253 ++$deleteColumn;
1254 }
1255 }
1256
1257 private function adjustAutoFilterInsert(int $startCol, int $numberOfColumns, int $rangeEnd, AutoFilter $autoFilter): void
1258 {
1259 $startColRef = $startCol;
1260 $endColRef = $rangeEnd;
1261 $toColRef = $rangeEnd + $numberOfColumns;
1262
1263 do {
1264 $autoFilter->shiftColumn(
1265 Coordinate::stringFromColumnIndex($endColRef),
1266 Coordinate::stringFromColumnIndex($toColRef)
1267 );
1268 --$endColRef;
1269 --$toColRef;
1270 } while ($startColRef <= $endColRef);
1271 }
1272
1273 private function adjustAutoFilterDelete(int $startCol, int $numberOfColumns, int $rangeEnd, AutoFilter $autoFilter): void
1274 {
1275 // For delete, we shuffle from beginning to end to avoid overwriting
1276 $startColID = Coordinate::stringFromColumnIndex($startCol);
1277 $toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns);
1278 $endColID = Coordinate::stringFromColumnIndex($rangeEnd + 1);
1279
1280 do {
1281 $autoFilter->shiftColumn($startColID, $toColID);
1282 StringHelper::stringIncrement($toColID);
1283 StringHelper::stringIncrement($startColID);
1284 } while ($startColID !== $endColID);
1285 }
1286
1287 private function adjustTable(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void
1288 {
1289 $tableCollection = $worksheet->getTableCollection();
1290
1291 foreach ($tableCollection as $table) {
1292 $tableRange = $table->getRange();
1293 if (!empty($tableRange)) {
1294 if ($numberOfColumns !== 0) {
1295 $tableColumns = $table->getColumns();
1296 if (count($tableColumns) > 0) {
1297 $column = '';
1298 $row = 0;
1299 sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row);
1300 $columnIndex = Coordinate::columnIndexFromString((string) $column);
1301 [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($tableRange);
1302 if ($columnIndex <= $rangeEnd[0]) {
1303 if ($numberOfColumns < 0) {
1304 $this->adjustTableDeleteRules($columnIndex, $numberOfColumns, $tableColumns, $table);
1305 }
1306 $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0];
1307
1308 // Shuffle columns in table range
1309 if ($numberOfColumns > 0) {
1310 $this->adjustTableInsert($startCol, $numberOfColumns, $rangeEnd[0], $table);
1311 } else {
1312 $this->adjustTableDelete($startCol, $numberOfColumns, $rangeEnd[0], $table);
1313 }
1314 }
1315 }
1316 }
1317
1318 $table->setRange($this->updateCellReference($tableRange));
1319 }
1320 }
1321 }
1322
1323 /** @param mixed[] $tableColumns */
1324 private function adjustTableDeleteRules(int $columnIndex, int $numberOfColumns, array $tableColumns, Table $table): void
1325 {
1326 // If we're actually deleting any columns that fall within the table range,
1327 // then we delete any rules for those columns
1328 $deleteColumn = $columnIndex + $numberOfColumns - 1;
1329 $deleteCount = abs($numberOfColumns);
1330
1331 for ($i = 1; $i <= $deleteCount; ++$i) {
1332 $columnName = Coordinate::stringFromColumnIndex($deleteColumn + 1);
1333 if (isset($tableColumns[$columnName])) {
1334 $table->clearColumn($columnName);
1335 }
1336 ++$deleteColumn;
1337 }
1338 }
1339
1340 private function adjustTableInsert(int $startCol, int $numberOfColumns, int $rangeEnd, Table $table): void
1341 {
1342 $startColRef = $startCol;
1343 $endColRef = $rangeEnd;
1344 $toColRef = $rangeEnd + $numberOfColumns;
1345
1346 do {
1347 $table->shiftColumn(
1348 Coordinate::stringFromColumnIndex($endColRef),
1349 Coordinate::stringFromColumnIndex($toColRef)
1350 );
1351 --$endColRef;
1352 --$toColRef;
1353 } while ($startColRef <= $endColRef);
1354 }
1355
1356 private function adjustTableDelete(int $startCol, int $numberOfColumns, int $rangeEnd, Table $table): void
1357 {
1358 // For delete, we shuffle from beginning to end to avoid overwriting
1359 $startColID = Coordinate::stringFromColumnIndex($startCol);
1360 $toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns);
1361 $endColID = Coordinate::stringFromColumnIndex($rangeEnd + 1);
1362
1363 do {
1364 $table->shiftColumn($startColID, $toColID);
1365 StringHelper::stringIncrement($toColID);
1366 StringHelper::stringIncrement($startColID);
1367 } while ($startColID !== $endColID);
1368 }
1369
1370 private function duplicateStylesByColumn(Worksheet $worksheet, int $beforeColumn, int $beforeRow, int $highestRow, int $numberOfColumns): void
1371 {
1372 $beforeColumnName = Coordinate::stringFromColumnIndex($beforeColumn - 1);
1373 for ($i = $beforeRow; $i <= $highestRow; ++$i) {
1374 // Style
1375 $coordinate = $beforeColumnName . $i;
1376 if ($worksheet->cellExists($coordinate)) {
1377 $xfIndex = $worksheet->getCell($coordinate)->getXfIndex();
1378 for ($j = $beforeColumn; $j <= $beforeColumn - 1 + $numberOfColumns; ++$j) {
1379 if (!empty($xfIndex) || $worksheet->cellExists([$j, $i])) {
1380 $worksheet->getCell([$j, $i])->setXfIndex($xfIndex);
1381 }
1382 }
1383 }
1384 }
1385 }
1386
1387 private function duplicateStylesByRow(Worksheet $worksheet, int $beforeColumn, int $beforeRow, string $highestColumn, int $numberOfRows): void
1388 {
1389 $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
1390 for ($i = $beforeColumn; $i <= $highestColumnIndex; ++$i) {
1391 // Style
1392 $coordinate = Coordinate::stringFromColumnIndex($i) . ($beforeRow - 1);
1393 if ($worksheet->cellExists($coordinate)) {
1394 $xfIndex = $worksheet->getCell($coordinate)->getXfIndex();
1395 for ($j = $beforeRow; $j <= $beforeRow - 1 + $numberOfRows; ++$j) {
1396 if (!empty($xfIndex) || $worksheet->cellExists([$i, $j])) {
1397 $worksheet->getCell(Coordinate::stringFromColumnIndex($i) . $j)->setXfIndex($xfIndex);
1398 }
1399 }
1400 }
1401 }
1402 }
1403
1404 /**
1405 * __clone implementation. Cloning should not be allowed in a Singleton!
1406 */
1407 final public function __clone()
1408 {
1409 throw new Exception('Cloning a Singleton is not allowed!');
1410 }
1411 }
1412