Failed Conditions
Pull Request — master (#3876)
by Abdul Malik
22:45 queued 13:31
created

ReferenceHelper::updateFormulaReferences()   D

Complexity

Conditions 38
Paths 100

Size

Total Lines 145
Code Lines 92

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 91
CRAP Score 38

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 92
c 1
b 0
f 0
dl 0
loc 145
ccs 91
cts 91
cp 1
rs 4.1666
cc 38
nc 100
nop 7
crap 38

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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