Passed
Push — master ( a950d1...8a122f )
by Mark
14:29
created

ReferenceHelper::updateNamedFormula()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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