PluginProbe
wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin / 6.5.1.7
wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin v6.5.1.7
6.5.1.7 6.5.1.6 6.5.1.5 6.5.1.4 6.5.1.3 6.5.1.2 6.5.1.1 6.5.0.9 6.5.0.8 6.5.0.7 6.5.0.6 trunk 3.4.2.40 3.4.2.41 3.4.2.42 3.4.2.43 3.4.2.44 3.4.2.45 3.4.2.46 3.4.2.47 3.4.2.48 3.4.2.49 3.4.2.50 6.3.2 6.3.3.1 All 47 releases
wpdatatables / lib / phpoffice / phpspreadsheet / src / PhpSpreadsheet / ReferenceHelper.php

ReferenceHelper.php in wpDataTables – WordPress Data Table, Dynamic Tables & Table Charts Plugin 6.5.1.7, at lib/phpoffice/phpspreadsheet/src/PhpSpreadsheet/ReferenceHelper.php

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