• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPOffice / PhpSpreadsheet / 17250634970

26 Aug 2025 09:14PM UTC coverage: 95.114% (+0.01%) from 95.102%
17250634970

Pull #4596

github

web-flow
Merge 87147c5cc into b99dc608a
Pull Request #4596: WIP Some Additional Support for Intersection and Union

26 of 26 new or added lines in 2 files covered. (100.0%)

39 existing lines in 3 files now uncovered.

40178 of 42242 relevant lines covered (95.11%)

347.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.42
/src/PhpSpreadsheet/ReferenceHelper.php
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
    private const SHEETNAME_PART = '((\w*|\'[^!]*\')!)';
19
    private const SHEETNAME_PART_WITH_SLASHES = '/' . self::SHEETNAME_PART . '/';
20
    const REFHELPER_REGEXP_CELLREF = self::SHEETNAME_PART . '?(?<![:a-z1-9_\.\$])(\$?[a-z]{1,3}\$?\d+)(?=[^:!\d\'])';
21
    const REFHELPER_REGEXP_CELLRANGE = self::SHEETNAME_PART . '?(\$?[a-z]{1,3}\$?\d+):(\$?[a-z]{1,3}\$?\d+)';
22
    const REFHELPER_REGEXP_ROWRANGE = self::SHEETNAME_PART . '?(\$?\d+):(\$?\d+)';
23
    const REFHELPER_REGEXP_COLRANGE = self::SHEETNAME_PART . '?(\$?[a-z]{1,3}):(\$?[a-z]{1,3})';
24

25
    /**
26
     * Instance of this class.
27
     */
28
    private static ?ReferenceHelper $instance = null;
29

30
    private ?CellReferenceHelper $cellReferenceHelper = null;
31

32
    /**
33
     * Get an instance of this class.
34
     */
35
    public static function getInstance(): self
36
    {
37
        if (self::$instance === null) {
1,744✔
38
            self::$instance = new self();
87✔
39
        }
40

41
        return self::$instance;
1,744✔
42
    }
43

44
    /**
45
     * Create a new ReferenceHelper.
46
     */
47
    protected function __construct()
48
    {
49
    }
87✔
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
    public static function columnSort(string $a, string $b): int
59
    {
60
        return strcasecmp(strlen($a) . $a, strlen($b) . $b);
1✔
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
    public static function columnReverseSort(string $a, string $b): int
71
    {
72
        return -strcasecmp(strlen($a) . $a, strlen($b) . $b);
1✔
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
    public static function cellSort(string $a, string $b): int
83
    {
84
        sscanf($a, '%[A-Z]%d', $ac, $ar);
20✔
85
        /** @var int $ar */
86
        /** @var string $ac */
87
        sscanf($b, '%[A-Z]%d', $bc, $br);
20✔
88
        /** @var int $br */
89
        /** @var string $bc */
90
        if ($ar === $br) {
20✔
91
            return strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc);
1✔
92
        }
93

94
        return ($ar < $br) ? -1 : 1;
20✔
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
    public static function cellReverseSort(string $a, string $b): int
105
    {
106
        sscanf($a, '%[A-Z]%d', $ac, $ar);
23✔
107
        /** @var int $ar */
108
        /** @var string $ac */
109
        sscanf($b, '%[A-Z]%d', $bc, $br);
23✔
110
        /** @var int $br */
111
        /** @var string $bc */
112
        if ($ar === $br) {
23✔
113
            return -strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc);
2✔
114
        }
115

116
        return ($ar < $br) ? 1 : -1;
22✔
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
    protected function adjustPageBreaks(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
127
    {
128
        $aBreaks = $worksheet->getBreaks();
122✔
129
        ($numberOfColumns > 0 || $numberOfRows > 0)
122✔
130
            ? uksort($aBreaks, [self::class, 'cellReverseSort'])
68✔
131
            : uksort($aBreaks, [self::class, 'cellSort']);
75✔
132

133
        foreach ($aBreaks as $cellAddress => $value) {
122✔
134
            /** @var CellReferenceHelper */
135
            $cellReferenceHelper = $this->cellReferenceHelper;
4✔
136
            if ($cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === true) {
4✔
137
                //    If we're deleting, then clear any defined breaks that are within the range
138
                //        of rows/columns that we're deleting
139
                $worksheet->setBreak($cellAddress, Worksheet::BREAK_NONE);
1✔
140
            } else {
141
                //    Otherwise update any affected breaks by inserting a new break at the appropriate point
142
                //        and removing the old affected break
143
                $newReference = $this->updateCellReference($cellAddress);
4✔
144
                if ($cellAddress !== $newReference) {
4✔
145
                    $worksheet->setBreak($newReference, $value)
4✔
146
                        ->setBreak($cellAddress, Worksheet::BREAK_NONE);
4✔
147
                }
148
            }
149
        }
150
    }
151

152
    /**
153
     * Update cell comments when inserting/deleting rows/columns.
154
     *
155
     * @param Worksheet $worksheet The worksheet that we're editing
156
     */
157
    protected function adjustComments(Worksheet $worksheet): void
158
    {
159
        $aComments = $worksheet->getComments();
122✔
160
        $aNewComments = []; // the new array of all comments
122✔
161

162
        foreach ($aComments as $cellAddress => &$value) {
122✔
163
            // Any comments inside a deleted range will be ignored
164
            /** @var CellReferenceHelper */
165
            $cellReferenceHelper = $this->cellReferenceHelper;
21✔
166
            if ($cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === false) {
21✔
167
                // Otherwise build a new array of comments indexed by the adjusted cell reference
168
                $newReference = $this->updateCellReference($cellAddress);
21✔
169
                $aNewComments[$newReference] = $value;
21✔
170
            }
171
        }
172
        //    Replace the comments array with the new set of comments
173
        $worksheet->setComments($aNewComments);
122✔
174
    }
175

176
    /**
177
     * Update hyperlinks when inserting/deleting rows/columns.
178
     *
179
     * @param Worksheet $worksheet The worksheet that we're editing
180
     * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
181
     * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
182
     */
183
    protected function adjustHyperlinks(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
184
    {
185
        $aHyperlinkCollection = $worksheet->getHyperlinkCollection();
122✔
186
        ($numberOfColumns > 0 || $numberOfRows > 0)
122✔
187
            ? uksort($aHyperlinkCollection, [self::class, 'cellReverseSort'])
68✔
188
            : uksort($aHyperlinkCollection, [self::class, 'cellSort']);
75✔
189

190
        foreach ($aHyperlinkCollection as $cellAddress => $value) {
122✔
191
            $newReference = $this->updateCellReference($cellAddress);
20✔
192
            /** @var CellReferenceHelper */
193
            $cellReferenceHelper = $this->cellReferenceHelper;
20✔
194
            if ($cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === true) {
20✔
195
                $worksheet->setHyperlink($cellAddress, null);
×
196
            } elseif ($cellAddress !== $newReference) {
20✔
197
                $worksheet->setHyperlink($cellAddress, null);
20✔
198
                if ($newReference) {
20✔
199
                    $worksheet->setHyperlink($newReference, $value);
20✔
200
                }
201
            }
202
        }
203
    }
204

205
    /**
206
     * Update conditional formatting styles when inserting/deleting rows/columns.
207
     *
208
     * @param Worksheet $worksheet The worksheet that we're editing
209
     * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
210
     * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
211
     */
212
    protected function adjustConditionalFormatting(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
213
    {
214
        $aStyles = $worksheet->getConditionalStylesCollection();
122✔
215
        ($numberOfColumns > 0 || $numberOfRows > 0)
122✔
216
            ? uksort($aStyles, [self::class, 'cellReverseSort'])
68✔
217
            : uksort($aStyles, [self::class, 'cellSort']);
75✔
218

219
        foreach ($aStyles as $cellAddress => $cfRules) {
122✔
220
            $worksheet->removeConditionalStyles($cellAddress);
4✔
221
            $newReference = $this->updateCellReference($cellAddress);
4✔
222

223
            foreach ($cfRules as &$cfRule) {
4✔
224
                /** @var Conditional $cfRule */
225
                $conditions = $cfRule->getConditions();
4✔
226
                foreach ($conditions as &$condition) {
4✔
227
                    if (is_string($condition)) {
4✔
228
                        /** @var CellReferenceHelper */
229
                        $cellReferenceHelper = $this->cellReferenceHelper;
4✔
230
                        $condition = $this->updateFormulaReferences(
4✔
231
                            $condition,
4✔
232
                            $cellReferenceHelper->beforeCellAddress(),
4✔
233
                            $numberOfColumns,
4✔
234
                            $numberOfRows,
4✔
235
                            $worksheet->getTitle(),
4✔
236
                            true
4✔
237
                        );
4✔
238
                    }
239
                }
240
                $cfRule->setConditions($conditions);
4✔
241
            }
242
            $worksheet->setConditionalStyles($newReference, $cfRules);
4✔
243
        }
244
    }
245

246
    /**
247
     * Update data validations when inserting/deleting rows/columns.
248
     *
249
     * @param Worksheet $worksheet The worksheet that we're editing
250
     * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
251
     * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
252
     */
253
    protected function adjustDataValidations(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows, string $beforeCellAddress): void
254
    {
255
        $aDataValidationCollection = $worksheet->getDataValidationCollection();
122✔
256
        ($numberOfColumns > 0 || $numberOfRows > 0)
122✔
257
            ? uksort($aDataValidationCollection, [self::class, 'cellReverseSort'])
68✔
258
            : uksort($aDataValidationCollection, [self::class, 'cellSort']);
75✔
259

260
        foreach ($aDataValidationCollection as $cellAddress => $dataValidation) {
122✔
261
            $formula = $dataValidation->getFormula1();
7✔
262
            if ($formula !== '') {
7✔
263
                $dataValidation->setFormula1(
7✔
264
                    $this->updateFormulaReferences(
7✔
265
                        $formula,
7✔
266
                        $beforeCellAddress,
7✔
267
                        $numberOfColumns,
7✔
268
                        $numberOfRows,
7✔
269
                        $worksheet->getTitle(),
7✔
270
                        true
7✔
271
                    )
7✔
272
                );
7✔
273
            }
274
            $formula = $dataValidation->getFormula2();
7✔
275
            if ($formula !== '') {
7✔
276
                $dataValidation->setFormula2(
1✔
277
                    $this->updateFormulaReferences(
1✔
278
                        $formula,
1✔
279
                        $beforeCellAddress,
1✔
280
                        $numberOfColumns,
1✔
281
                        $numberOfRows,
1✔
282
                        $worksheet->getTitle(),
1✔
283
                        true
1✔
284
                    )
1✔
285
                );
1✔
286
            }
287
            $addressParts = explode(' ', $cellAddress);
7✔
288
            $newReference = '';
7✔
289
            $separator = '';
7✔
290
            foreach ($addressParts as $addressPart) {
7✔
291
                $newReference .= $separator . $this->updateCellReference($addressPart);
7✔
292
                $separator = ' ';
7✔
293
            }
294
            if ($cellAddress !== $newReference) {
7✔
295
                $worksheet->setDataValidation($newReference, $dataValidation);
7✔
296
                $worksheet->setDataValidation($cellAddress, null);
7✔
297
                if ($newReference) {
7✔
298
                    $worksheet->setDataValidation($newReference, $dataValidation);
7✔
299
                }
300
            }
301
        }
302
    }
303

304
    /**
305
     * Update merged cells when inserting/deleting rows/columns.
306
     *
307
     * @param Worksheet $worksheet The worksheet that we're editing
308
     */
309
    protected function adjustMergeCells(Worksheet $worksheet): void
310
    {
311
        $aMergeCells = $worksheet->getMergeCells();
122✔
312
        $aNewMergeCells = []; // the new array of all merge cells
122✔
313
        foreach ($aMergeCells as $cellAddress => &$value) {
122✔
314
            $newReference = $this->updateCellReference($cellAddress);
24✔
315
            if ($newReference) {
24✔
316
                $aNewMergeCells[$newReference] = $newReference;
24✔
317
            }
318
        }
319
        $worksheet->setMergeCells($aNewMergeCells); // replace the merge cells array
122✔
320
    }
321

322
    /**
323
     * Update protected cells when inserting/deleting rows/columns.
324
     *
325
     * @param Worksheet $worksheet The worksheet that we're editing
326
     * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
327
     * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
328
     */
329
    protected function adjustProtectedCells(Worksheet $worksheet, int $numberOfColumns, int $numberOfRows): void
330
    {
331
        $aProtectedCells = $worksheet->getProtectedCellRanges();
122✔
332
        ($numberOfColumns > 0 || $numberOfRows > 0)
122✔
333
            ? uksort($aProtectedCells, [self::class, 'cellReverseSort'])
68✔
334
            : uksort($aProtectedCells, [self::class, 'cellSort']);
75✔
335
        foreach ($aProtectedCells as $cellAddress => $protectedRange) {
122✔
336
            $newReference = $this->updateCellReference($cellAddress);
17✔
337
            if ($cellAddress !== $newReference) {
17✔
338
                $worksheet->unprotectCells($cellAddress);
17✔
339
                if ($newReference) {
17✔
340
                    $worksheet->protectCells($newReference, $protectedRange->getPassword(), true);
17✔
341
                }
342
            }
343
        }
344
    }
345

346
    /**
347
     * Update column dimensions when inserting/deleting rows/columns.
348
     *
349
     * @param Worksheet $worksheet The worksheet that we're editing
350
     */
351
    protected function adjustColumnDimensions(Worksheet $worksheet): void
352
    {
353
        $aColumnDimensions = array_reverse($worksheet->getColumnDimensions(), true);
122✔
354
        if (!empty($aColumnDimensions)) {
122✔
355
            foreach ($aColumnDimensions as $objColumnDimension) {
25✔
356
                $newReference = $this->updateCellReference($objColumnDimension->getColumnIndex() . '1');
25✔
357
                [$newReference] = Coordinate::coordinateFromString($newReference);
25✔
358
                if ($objColumnDimension->getColumnIndex() !== $newReference) {
25✔
359
                    $objColumnDimension->setColumnIndex($newReference);
19✔
360
                }
361
            }
362

363
            $worksheet->refreshColumnDimensions();
25✔
364
        }
365
    }
366

367
    /**
368
     * Update row dimensions when inserting/deleting rows/columns.
369
     *
370
     * @param Worksheet $worksheet The worksheet that we're editing
371
     * @param int $beforeRow Number of the row we're inserting/deleting before
372
     * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
373
     */
374
    protected function adjustRowDimensions(Worksheet $worksheet, int $beforeRow, int $numberOfRows): void
375
    {
376
        $aRowDimensions = array_reverse($worksheet->getRowDimensions(), true);
122✔
377
        if (!empty($aRowDimensions)) {
122✔
378
            foreach ($aRowDimensions as $objRowDimension) {
7✔
379
                $newReference = $this->updateCellReference('A' . $objRowDimension->getRowIndex());
7✔
380
                [, $newReference] = Coordinate::coordinateFromString($newReference);
7✔
381
                $newRoweference = (int) $newReference;
7✔
382
                if ($objRowDimension->getRowIndex() !== $newRoweference) {
7✔
383
                    $objRowDimension->setRowIndex($newRoweference);
7✔
384
                }
385
            }
386

387
            $worksheet->refreshRowDimensions();
7✔
388

389
            $copyDimension = $worksheet->getRowDimension($beforeRow - 1);
7✔
390
            for ($i = $beforeRow; $i <= $beforeRow - 1 + $numberOfRows; ++$i) {
7✔
391
                $newDimension = $worksheet->getRowDimension($i);
5✔
392
                $newDimension->setRowHeight($copyDimension->getRowHeight());
5✔
393
                $newDimension->setVisible($copyDimension->getVisible());
5✔
394
                $newDimension->setOutlineLevel($copyDimension->getOutlineLevel());
5✔
395
                $newDimension->setCollapsed($copyDimension->getCollapsed());
5✔
396
            }
397
        }
398
    }
399

400
    /**
401
     * Insert a new column or row, updating all possible related data.
402
     *
403
     * @param string $beforeCellAddress Insert before this cell address (e.g. 'A1')
404
     * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
405
     * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
406
     * @param Worksheet $worksheet The worksheet that we're editing
407
     */
408
    public function insertNewBefore(
409
        string $beforeCellAddress,
410
        int $numberOfColumns,
411
        int $numberOfRows,
412
        Worksheet $worksheet
413
    ): void {
414
        $remove = ($numberOfColumns < 0 || $numberOfRows < 0);
122✔
415

416
        if (
417
            $this->cellReferenceHelper === null
122✔
418
            || $this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows)
122✔
419
        ) {
420
            $this->cellReferenceHelper = new CellReferenceHelper($beforeCellAddress, $numberOfColumns, $numberOfRows);
110✔
421
        }
422

423
        // Get coordinate of $beforeCellAddress
424
        [$beforeColumn, $beforeRow, $beforeColumnString] = Coordinate::indexesFromString($beforeCellAddress);
122✔
425

426
        // Clear cells if we are removing columns or rows
427
        $highestColumn = $worksheet->getHighestColumn();
122✔
428
        $highestDataColumn = $worksheet->getHighestDataColumn();
122✔
429
        $highestRow = $worksheet->getHighestRow();
122✔
430
        $highestDataRow = $worksheet->getHighestDataRow();
122✔
431

432
        // 1. Clear column strips if we are removing columns
433
        if ($numberOfColumns < 0 && $beforeColumn - 2 + $numberOfColumns > 0) {
122✔
434
            $this->clearColumnStrips($highestRow, $beforeColumn, $numberOfColumns, $worksheet);
30✔
435
        }
436

437
        // 2. Clear row strips if we are removing rows
438
        if ($numberOfRows < 0 && $beforeRow - 1 + $numberOfRows > 0) {
122✔
439
            $this->clearRowStrips($highestColumn, $beforeColumn, $beforeRow, $numberOfRows, $worksheet);
45✔
440
        }
441

442
        // Find missing coordinates. This is important when inserting or deleting column before the last column
443
        $startRow = $startCol = 1;
122✔
444
        $startColString = 'A';
122✔
445
        if ($numberOfRows === 0) {
122✔
446
            $startCol = $beforeColumn;
72✔
447
            $startColString = $beforeColumnString;
72✔
448
        } elseif ($numberOfColumns === 0) {
71✔
449
            $startRow = $beforeRow;
71✔
450
        }
451
        $highColumn = Coordinate::columnIndexFromString($highestDataColumn);
122✔
452
        for ($row = $startRow; $row <= $highestDataRow; ++$row) {
122✔
453
            for ($col = $startCol, $colString = $startColString; $col <= $highColumn; ++$col, ++$colString) {
101✔
454
                /** @var string $colString */
455
                $worksheet->getCell("$colString$row"); // create cell if it doesn't exist
85✔
456
            }
457
        }
458

459
        $allCoordinates = $worksheet->getCoordinates();
122✔
460
        if ($remove) {
122✔
461
            // It's faster to reverse and pop than to use unshift, especially with large cell collections
462
            $allCoordinates = array_reverse($allCoordinates);
75✔
463
        }
464

465
        // Loop through cells, bottom-up, and change cell coordinate
466
        while ($coordinate = array_pop($allCoordinates)) {
122✔
467
            $cell = $worksheet->getCell($coordinate);
97✔
468
            $cellIndex = Coordinate::columnIndexFromString($cell->getColumn());
97✔
469

470
            // Don't update cells that are being removed
471
            if ($numberOfColumns < 0 && $cellIndex >= $beforeColumn + $numberOfColumns && $cellIndex < $beforeColumn) {
97✔
472
                continue;
30✔
473
            }
474

475
            // New coordinate
476
            $newCoordinate = Coordinate::stringFromColumnIndex($cellIndex + $numberOfColumns) . ($cell->getRow() + $numberOfRows);
97✔
477

478
            // Should the cell be updated? Move value and cellXf index from one cell to another.
479
            if (($cellIndex >= $beforeColumn) && ($cell->getRow() >= $beforeRow)) {
97✔
480
                // Update cell styles
481
                $worksheet->getCell($newCoordinate)->setXfIndex($cell->getXfIndex());
85✔
482

483
                // Insert this cell at its new location
484
                if ($cell->getDataType() === DataType::TYPE_FORMULA) {
85✔
485
                    // Formula should be adjusted
486
                    $worksheet->getCell($newCoordinate)
38✔
487
                        ->setValue($this->updateFormulaReferences($cell->getValueString(), $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true));
38✔
488
                } else {
489
                    // Cell value should not be adjusted
490
                    $worksheet->getCell($newCoordinate)->setValueExplicit($cell->getValue(), $cell->getDataType());
85✔
491
                }
492

493
                // Clear the original cell
494
                $worksheet->getCellCollection()->delete($coordinate);
85✔
495
            } else {
496
                /*    We don't need to update styles for rows/columns before our insertion position,
497
                        but we do still need to adjust any formulae in those cells                    */
498
                if ($cell->getDataType() === DataType::TYPE_FORMULA) {
69✔
499
                    // Formula should be adjusted
500
                    $cell->setValue($this->updateFormulaReferences($cell->getValueString(), $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true));
22✔
501
                }
502
            }
503
        }
504

505
        // Duplicate styles for the newly inserted cells
506
        $highestColumn = $worksheet->getHighestColumn();
122✔
507
        $highestRow = $worksheet->getHighestRow();
122✔
508

509
        if ($numberOfColumns > 0 && $beforeColumn > 1) {
122✔
510
            $this->duplicateStylesByColumn($worksheet, $beforeColumn, $beforeRow, $highestRow, $numberOfColumns);
31✔
511
        }
512

513
        if ($numberOfRows > 0 && $beforeRow - 1 > 0) {
122✔
514
            $this->duplicateStylesByRow($worksheet, $beforeColumn, $beforeRow, $highestColumn, $numberOfRows);
38✔
515
        }
516

517
        // Update worksheet: column dimensions
518
        $this->adjustColumnDimensions($worksheet);
122✔
519

520
        // Update worksheet: row dimensions
521
        $this->adjustRowDimensions($worksheet, $beforeRow, $numberOfRows);
122✔
522

523
        //    Update worksheet: page breaks
524
        $this->adjustPageBreaks($worksheet, $numberOfColumns, $numberOfRows);
122✔
525

526
        //    Update worksheet: comments
527
        $this->adjustComments($worksheet);
122✔
528

529
        // Update worksheet: hyperlinks
530
        $this->adjustHyperlinks($worksheet, $numberOfColumns, $numberOfRows);
122✔
531

532
        // Update worksheet: conditional formatting styles
533
        $this->adjustConditionalFormatting($worksheet, $numberOfColumns, $numberOfRows);
122✔
534

535
        // Update worksheet: data validations
536
        $this->adjustDataValidations($worksheet, $numberOfColumns, $numberOfRows, $beforeCellAddress);
122✔
537

538
        // Update worksheet: merge cells
539
        $this->adjustMergeCells($worksheet);
122✔
540

541
        // Update worksheet: protected cells
542
        $this->adjustProtectedCells($worksheet, $numberOfColumns, $numberOfRows);
122✔
543

544
        // Update worksheet: autofilter
545
        $this->adjustAutoFilter($worksheet, $beforeCellAddress, $numberOfColumns);
122✔
546

547
        // Update worksheet: table
548
        $this->adjustTable($worksheet, $beforeCellAddress, $numberOfColumns);
122✔
549

550
        // Update worksheet: freeze pane
551
        if ($worksheet->getFreezePane()) {
122✔
552
            $splitCell = $worksheet->getFreezePane();
1✔
553
            $topLeftCell = $worksheet->getTopLeftCell() ?? '';
1✔
554

555
            $splitCell = $this->updateCellReference($splitCell);
1✔
556
            $topLeftCell = $this->updateCellReference($topLeftCell);
1✔
557

558
            $worksheet->freezePane($splitCell, $topLeftCell);
1✔
559
        }
560

561
        $this->updatePrintAreas($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows);
122✔
562

563
        // Update worksheet: drawings
564
        $aDrawings = $worksheet->getDrawingCollection();
122✔
565
        foreach ($aDrawings as $objDrawing) {
122✔
566
            $newReference = $this->updateCellReference($objDrawing->getCoordinates());
19✔
567
            if ($objDrawing->getCoordinates() != $newReference) {
19✔
568
                $objDrawing->setCoordinates($newReference);
19✔
569
            }
570
            if ($objDrawing->getCoordinates2() !== '') {
19✔
571
                $newReference = $this->updateCellReference($objDrawing->getCoordinates2());
1✔
572
                if ($objDrawing->getCoordinates2() != $newReference) {
1✔
573
                    $objDrawing->setCoordinates2($newReference);
1✔
574
                }
575
            }
576
        }
577

578
        // Update workbook: define names
579
        if (count($worksheet->getParentOrThrow()->getDefinedNames()) > 0) {
122✔
580
            $this->updateDefinedNames($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows);
7✔
581
        }
582

583
        // Garbage collect
584
        $worksheet->garbageCollect();
122✔
585
    }
586

587
    private function updatePrintAreas(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void
588
    {
589
        $pageSetup = $worksheet->getPageSetup();
122✔
590
        if (!$pageSetup->isPrintAreaSet()) {
122✔
591
            return;
96✔
592
        }
593
        $printAreas = explode(',', $pageSetup->getPrintArea());
26✔
594
        $newPrintAreas = [];
26✔
595
        foreach ($printAreas as $printArea) {
26✔
596
            $result = $this->updatePrintArea($printArea, $beforeCellAddress, $numberOfColumns, $numberOfRows);
26✔
597
            if ($result !== '') {
26✔
598
                $newPrintAreas[] = $result;
22✔
599
            }
600
        }
601
        $result = implode(',', $newPrintAreas);
26✔
602
        if ($result === '') {
26✔
603
            $pageSetup->clearPrintArea();
4✔
604
        } else {
605
            $pageSetup->setPrintArea($result);
22✔
606
        }
607
    }
608

609
    private function updatePrintArea(string $printArea, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): string
610
    {
611
        $coordinates = Coordinate::indexesFromString($beforeCellAddress);
26✔
612
        if (preg_match('/^([A-Z]{1,3})(\d{1,7}):([A-Z]{1,3})(\d{1,7})$/i', $printArea, $matches) === 1) {
26✔
613
            $firstRow = (int) $matches[2];
26✔
614
            $lastRow = (int) $matches[4];
26✔
615
            $firstColumnString = $matches[1];
26✔
616
            $lastColumnString = $matches[3];
26✔
617
            if ($numberOfRows < 0) {
26✔
618
                $affectedRow = $coordinates[1] + $numberOfRows - 1;
8✔
619
                $lastAffectedRow = $coordinates[1] - 1;
8✔
620
                if ($affectedRow >= $firstRow && $affectedRow <= $lastRow) {
8✔
621
                    $newLastRow = max($affectedRow, $lastRow + $numberOfRows);
3✔
622
                    if ($newLastRow >= $firstRow) {
3✔
623
                        return $matches[1] . $matches[2] . ':' . $matches[3] . $newLastRow;
3✔
624
                    }
625

UNCOV
626
                    return '';
×
627
                }
628
                if ($lastAffectedRow >= $firstRow && $affectedRow <= $lastRow) {
5✔
629
                    $newFirstRow = $affectedRow + 1;
3✔
630
                    $newLastRow = $lastRow + $numberOfRows;
3✔
631
                    if ($newFirstRow >= 1 && $newLastRow >= $newFirstRow) {
3✔
632
                        return $matches[1] . $newFirstRow . ':' . $matches[3] . $newLastRow;
1✔
633
                    }
634

635
                    return '';
2✔
636
                }
637
            }
638
            if ($numberOfColumns < 0) {
20✔
639
                $firstColumnInt = Coordinate::columnIndexFromString($firstColumnString);
8✔
640
                $lastColumnInt = Coordinate::columnIndexFromString($lastColumnString);
8✔
641
                $affectedColumn = $coordinates[0] + $numberOfColumns - 1;
8✔
642
                $lastAffectedColumn = $coordinates[0] - 1;
8✔
643
                if ($affectedColumn >= $firstColumnInt && $affectedColumn <= $lastColumnInt) {
8✔
644
                    $newLastColumnInt = max($affectedColumn, $lastColumnInt + $numberOfColumns);
3✔
645
                    if ($newLastColumnInt >= $firstColumnInt) {
3✔
646
                        $newLastColumnString = Coordinate::stringFromColumnIndex($newLastColumnInt);
3✔
647

648
                        return $matches[1] . $matches[2] . ':' . $newLastColumnString . $matches[4];
3✔
649
                    }
650

UNCOV
651
                    return '';
×
652
                }
653
                if ($affectedColumn < $firstColumnInt && $lastAffectedColumn > $lastColumnInt) {
5✔
654
                    return '';
1✔
655
                }
656
                if ($lastAffectedColumn >= $firstColumnInt && $lastAffectedColumn <= $lastColumnInt) {
4✔
657
                    $newFirstColumn = $affectedColumn + 1;
2✔
658
                    $newLastColumn = $lastColumnInt + $numberOfColumns;
2✔
659
                    if ($newFirstColumn >= 1 && $newLastColumn >= $newFirstColumn) {
2✔
660
                        $firstString = Coordinate::stringFromColumnIndex($newFirstColumn);
1✔
661
                        $lastString = Coordinate::stringFromColumnIndex($newLastColumn);
1✔
662

663
                        return $firstString . $matches[2] . ':' . $lastString . $matches[4];
1✔
664
                    }
665

666
                    return '';
1✔
667
                }
668
            }
669
        }
670

671
        return $this->updateCellReference($printArea);
14✔
672
    }
673

674
    private static function matchSheetName(?string $match, string $worksheetName): bool
675
    {
676
        return $match === null || $match === '' || $match === "'\u{fffc}'" || $match === "'\u{fffb}'" || strcasecmp(trim($match, "'"), $worksheetName) === 0;
274✔
677
    }
678

679
    private static function sheetnameBeforeCells(string $match, string $worksheetName, string $cells): string
680
    {
681
        $toString = ($match > '') ? "$match!" : '';
283✔
682

683
        return str_replace(["\u{fffc}", "'\u{fffb}'"], $worksheetName, $toString) . $cells;
283✔
684
    }
685

686
    /**
687
     * Update references within formulas.
688
     *
689
     * @param string $formula Formula to update
690
     * @param string $beforeCellAddress Insert before this one
691
     * @param int $numberOfColumns Number of columns to insert
692
     * @param int $numberOfRows Number of rows to insert
693
     * @param string $worksheetName Worksheet name/title
694
     *
695
     * @return string Updated formula
696
     */
697
    public function updateFormulaReferences(
698
        string $formula = '',
699
        string $beforeCellAddress = 'A1',
700
        int $numberOfColumns = 0,
701
        int $numberOfRows = 0,
702
        string $worksheetName = '',
703
        bool $includeAbsoluteReferences = false,
704
        bool $onlyAbsoluteReferences = false
705
    ): string {
706
        $callback = fn (array $matches): string => (strcasecmp(trim($matches[2], "'"), $worksheetName) === 0) ? (($matches[2][0] === "'") ? "'\u{fffc}'!" : "'\u{fffb}'!") : "'\u{fffd}'!";
297✔
707
        if (
708
            $this->cellReferenceHelper === null
297✔
709
            || $this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows)
297✔
710
        ) {
711
            $this->cellReferenceHelper = new CellReferenceHelper($beforeCellAddress, $numberOfColumns, $numberOfRows);
234✔
712
        }
713

714
        //    Update cell references in the formula
715
        $formulaBlocks = explode('"', $formula);
297✔
716
        $i = false;
297✔
717
        foreach ($formulaBlocks as &$formulaBlock) {
297✔
718
            //    Ignore blocks that were enclosed in quotes (alternating entries in the $formulaBlocks array after the explode)
719
            $i = $i === false;
297✔
720
            if ($i) {
297✔
721
                $adjustCount = 0;
297✔
722
                $newCellTokens = $cellTokens = [];
297✔
723
                //    Search for row ranges (e.g. 'Sheet1'!3:5 or 3:5) with or without $ absolutes (e.g. $3:5)
724
                $formulaBlockx = ' ' . (preg_replace_callback(self::SHEETNAME_PART_WITH_SLASHES, $callback, $formulaBlock) ?? $formulaBlock) . ' ';
297✔
725
                $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_ROWRANGE . '/mui', $formulaBlockx, $matches, PREG_SET_ORDER);
297✔
726
                if ($matchCount > 0) {
297✔
727
                    foreach ($matches as $match) {
3✔
728
                        $fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}:{$match[4]}");
3✔
729
                        $modified3 = substr($this->updateCellReference('$A' . $match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences, true), 2);
3✔
730
                        $modified4 = substr($this->updateCellReference('$A' . $match[4], $includeAbsoluteReferences, $onlyAbsoluteReferences, false), 2);
3✔
731

732
                        if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) {
3✔
733
                            if (self::matchSheetName($match[2], $worksheetName)) {
3✔
734
                                $toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3:$modified4");
3✔
735
                                //    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
736
                                $column = 100000;
3✔
737
                                $row = 10000000 + (int) trim($match[3], '$');
3✔
738
                                $cellIndex = "{$column}{$row}";
3✔
739

740
                                $newCellTokens[$cellIndex] = preg_quote($toString, '/');
3✔
741
                                $cellTokens[$cellIndex] = '/(?<!\d\$\!)' . preg_quote($fromString, '/') . '(?!\d)/i';
3✔
742
                                ++$adjustCount;
3✔
743
                            }
744
                        }
745
                    }
746
                }
747
                //    Search for column ranges (e.g. 'Sheet1'!C:E or C:E) with or without $ absolutes (e.g. $C:E)
748
                $formulaBlockx = ' ' . (preg_replace_callback(self::SHEETNAME_PART_WITH_SLASHES, $callback, $formulaBlock) ?? $formulaBlock) . ' ';
297✔
749
                $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_COLRANGE . '/mui', $formulaBlockx, $matches, PREG_SET_ORDER);
297✔
750
                if ($matchCount > 0) {
297✔
751
                    foreach ($matches as $match) {
3✔
752
                        $fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}:{$match[4]}");
3✔
753
                        $modified3 = substr($this->updateCellReference($match[3] . '$1', $includeAbsoluteReferences, $onlyAbsoluteReferences, true), 0, -2);
3✔
754
                        $modified4 = substr($this->updateCellReference($match[4] . '$1', $includeAbsoluteReferences, $onlyAbsoluteReferences, false), 0, -2);
3✔
755

756
                        if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) {
3✔
757
                            if (self::matchSheetName($match[2], $worksheetName)) {
3✔
758
                                $toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3:$modified4");
3✔
759
                                //    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
760
                                $column = Coordinate::columnIndexFromString(trim($match[3], '$')) + 100000;
3✔
761
                                $row = 10000000;
3✔
762
                                $cellIndex = "{$column}{$row}";
3✔
763

764
                                $newCellTokens[$cellIndex] = preg_quote($toString, '/');
3✔
765
                                $cellTokens[$cellIndex] = '/(?<![A-Z\$\!])' . preg_quote($fromString, '/') . '(?![A-Z])/i';
3✔
766
                                ++$adjustCount;
3✔
767
                            }
768
                        }
769
                    }
770
                }
771
                //    Search for cell ranges (e.g. 'Sheet1'!A3:C5 or A3:C5) with or without $ absolutes (e.g. $A1:C$5)
772
                $formulaBlockx = ' ' . (preg_replace_callback(self::SHEETNAME_PART_WITH_SLASHES, $callback, "$formulaBlock") ?? "$formulaBlock") . ' ';
297✔
773
                $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLRANGE . '/mui', $formulaBlockx, $matches, PREG_SET_ORDER);
297✔
774
                if ($matchCount > 0) {
297✔
775
                    foreach ($matches as $match) {
50✔
776
                        $fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}:{$match[4]}");
50✔
777
                        $modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences, true);
50✔
778
                        $modified4 = $this->updateCellReference($match[4], $includeAbsoluteReferences, $onlyAbsoluteReferences, false);
50✔
779

780
                        if ($match[3] . $match[4] !== $modified3 . $modified4) {
50✔
781
                            if (self::matchSheetName($match[2], $worksheetName)) {
43✔
782
                                $toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3:$modified4");
41✔
783
                                [$column, $row] = Coordinate::coordinateFromString($match[3]);
41✔
784
                                //    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
785
                                $column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000;
41✔
786
                                $row = (int) trim($row, '$') + 10000000;
41✔
787
                                $cellIndex = "{$column}{$row}";
41✔
788

789
                                $newCellTokens[$cellIndex] = preg_quote($toString, '/');
41✔
790
                                $cellTokens[$cellIndex] = '/(?<![A-Z]\$\!)' . preg_quote($fromString, '/') . '(?!\d)/i';
41✔
791
                                ++$adjustCount;
41✔
792
                            }
793
                        }
794
                    }
795
                }
796
                //    Search for cell references (e.g. 'Sheet1'!A3 or C5) with or without $ absolutes (e.g. $A1 or C$5)
797

798
                $formulaBlockx = ' ' . (preg_replace_callback(self::SHEETNAME_PART_WITH_SLASHES, $callback, $formulaBlock) ?? $formulaBlock) . ' ';
297✔
799
                $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLREF . '/mui', $formulaBlockx, $matches, PREG_SET_ORDER);
297✔
800

801
                if ($matchCount > 0) {
297✔
802
                    foreach ($matches as $match) {
257✔
803
                        $fromString = self::sheetnameBeforeCells($match[2], $worksheetName, "{$match[3]}");
257✔
804

805
                        $modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences, $onlyAbsoluteReferences, null);
257✔
806
                        if ($match[3] !== $modified3) {
257✔
807
                            if (self::matchSheetName($match[2], $worksheetName)) {
255✔
808
                                $toString = self::sheetnameBeforeCells($match[2], $worksheetName, "$modified3");
255✔
809
                                [$column, $row] = Coordinate::coordinateFromString($match[3]);
255✔
810
                                $columnAdditionalIndex = $column[0] === '$' ? 1 : 0;
255✔
811
                                $rowAdditionalIndex = $row[0] === '$' ? 1 : 0;
255✔
812
                                //    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
813
                                $column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000;
255✔
814
                                $row = (int) trim($row, '$') + 10000000;
255✔
815
                                $cellIndex = $row . $rowAdditionalIndex . $column . $columnAdditionalIndex;
255✔
816

817
                                $newCellTokens[$cellIndex] = preg_quote($toString, '/');
255✔
818
                                $cellTokens[$cellIndex] = '/(?<![A-Z\$\!])' . preg_quote($fromString, '/') . '(?!\d)/i';
255✔
819
                                ++$adjustCount;
255✔
820
                            }
821
                        }
822
                    }
823
                }
824
                if ($adjustCount > 0) {
297✔
825
                    if ($numberOfColumns > 0 || $numberOfRows > 0) {
274✔
826
                        krsort($cellTokens);
265✔
827
                        krsort($newCellTokens);
265✔
828
                    } else {
829
                        ksort($cellTokens);
31✔
830
                        ksort($newCellTokens);
31✔
831
                    }   //  Update cell references in the formula
832
                    $formulaBlock = str_replace('\\', '', (string) preg_replace($cellTokens, $newCellTokens, $formulaBlock));
274✔
833
                }
834
            }
835
        }
836
        unset($formulaBlock);
297✔
837

838
        //    Then rebuild the formula string
839
        return implode('"', $formulaBlocks);
297✔
840
    }
841

842
    /**
843
     * Update all cell references within a formula, irrespective of worksheet.
844
     */
845
    public function updateFormulaReferencesAnyWorksheet(string $formula = '', int $numberOfColumns = 0, int $numberOfRows = 0): string
846
    {
847
        $formula = $this->updateCellReferencesAllWorksheets($formula, $numberOfColumns, $numberOfRows);
151✔
848

849
        if ($numberOfColumns !== 0) {
151✔
850
            $formula = $this->updateColumnRangesAllWorksheets($formula, $numberOfColumns);
142✔
851
        }
852

853
        if ($numberOfRows !== 0) {
151✔
854
            $formula = $this->updateRowRangesAllWorksheets($formula, $numberOfRows);
119✔
855
        }
856

857
        return $formula;
151✔
858
    }
859

860
    private function updateCellReferencesAllWorksheets(string $formula, int $numberOfColumns, int $numberOfRows): string
861
    {
862
        $splitCount = preg_match_all(
151✔
863
            '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/mui',
151✔
864
            $formula,
151✔
865
            $splitRanges,
151✔
866
            PREG_OFFSET_CAPTURE
151✔
867
        );
151✔
868

869
        $columnLengths = array_map('strlen', array_column($splitRanges[6], 0));
151✔
870
        $rowLengths = array_map('strlen', array_column($splitRanges[7], 0));
151✔
871
        $columnOffsets = array_column($splitRanges[6], 1);
151✔
872
        $rowOffsets = array_column($splitRanges[7], 1);
151✔
873

874
        $columns = $splitRanges[6];
151✔
875
        $rows = $splitRanges[7];
151✔
876

877
        while ($splitCount > 0) {
151✔
878
            --$splitCount;
145✔
879
            $columnLength = $columnLengths[$splitCount];
145✔
880
            $rowLength = $rowLengths[$splitCount];
145✔
881
            $columnOffset = $columnOffsets[$splitCount];
145✔
882
            $rowOffset = $rowOffsets[$splitCount];
145✔
883
            $column = $columns[$splitCount][0];
145✔
884
            $row = $rows[$splitCount][0];
145✔
885

886
            if ($column[0] !== '$') {
145✔
887
                $column = ((Coordinate::columnIndexFromString($column) + $numberOfColumns) % AddressRange::MAX_COLUMN_INT) ?: AddressRange::MAX_COLUMN_INT;
22✔
888
                $column = Coordinate::stringFromColumnIndex($column);
22✔
889
                $rowOffset -= ($columnLength - strlen($column));
22✔
890
                $formula = substr($formula, 0, $columnOffset) . $column . substr($formula, $columnOffset + $columnLength);
22✔
891
            }
892
            if (!empty($row) && $row[0] !== '$') {
145✔
893
                $row = (((int) $row + $numberOfRows) % AddressRange::MAX_ROW) ?: AddressRange::MAX_ROW;
42✔
894
                $formula = substr($formula, 0, $rowOffset) . $row . substr($formula, $rowOffset + $rowLength);
42✔
895
            }
896
        }
897

898
        return $formula;
151✔
899
    }
900

901
    private function updateColumnRangesAllWorksheets(string $formula, int $numberOfColumns): string
902
    {
903
        $splitCount = preg_match_all(
142✔
904
            '/' . Calculation::CALCULATION_REGEXP_COLUMNRANGE_RELATIVE . '/mui',
142✔
905
            $formula,
142✔
906
            $splitRanges,
142✔
907
            PREG_OFFSET_CAPTURE
142✔
908
        );
142✔
909

910
        $fromColumnLengths = array_map('strlen', array_column($splitRanges[1], 0));
142✔
911
        $fromColumnOffsets = array_column($splitRanges[1], 1);
142✔
912
        $toColumnLengths = array_map('strlen', array_column($splitRanges[2], 0));
142✔
913
        $toColumnOffsets = array_column($splitRanges[2], 1);
142✔
914

915
        $fromColumns = $splitRanges[1];
142✔
916
        $toColumns = $splitRanges[2];
142✔
917

918
        while ($splitCount > 0) {
142✔
919
            --$splitCount;
3✔
920
            $fromColumnLength = $fromColumnLengths[$splitCount];
3✔
921
            $toColumnLength = $toColumnLengths[$splitCount];
3✔
922
            $fromColumnOffset = $fromColumnOffsets[$splitCount];
3✔
923
            $toColumnOffset = $toColumnOffsets[$splitCount];
3✔
924
            $fromColumn = $fromColumns[$splitCount][0];
3✔
925
            $toColumn = $toColumns[$splitCount][0];
3✔
926

927
            if (!empty($fromColumn) && $fromColumn[0] !== '$') {
3✔
928
                $fromColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($fromColumn) + $numberOfColumns);
2✔
929
                $formula = substr($formula, 0, $fromColumnOffset) . $fromColumn . substr($formula, $fromColumnOffset + $fromColumnLength);
2✔
930
            }
931
            if (!empty($toColumn) && $toColumn[0] !== '$') {
3✔
932
                $toColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($toColumn) + $numberOfColumns);
2✔
933
                $formula = substr($formula, 0, $toColumnOffset) . $toColumn . substr($formula, $toColumnOffset + $toColumnLength);
2✔
934
            }
935
        }
936

937
        return $formula;
142✔
938
    }
939

940
    private function updateRowRangesAllWorksheets(string $formula, int $numberOfRows): string
941
    {
942
        $splitCount = preg_match_all(
119✔
943
            '/' . Calculation::CALCULATION_REGEXP_ROWRANGE_RELATIVE . '/mui',
119✔
944
            $formula,
119✔
945
            $splitRanges,
119✔
946
            PREG_OFFSET_CAPTURE
119✔
947
        );
119✔
948

949
        $fromRowLengths = array_map('strlen', array_column($splitRanges[1], 0));
119✔
950
        $fromRowOffsets = array_column($splitRanges[1], 1);
119✔
951
        $toRowLengths = array_map('strlen', array_column($splitRanges[2], 0));
119✔
952
        $toRowOffsets = array_column($splitRanges[2], 1);
119✔
953

954
        $fromRows = $splitRanges[1];
119✔
955
        $toRows = $splitRanges[2];
119✔
956

957
        while ($splitCount > 0) {
119✔
958
            --$splitCount;
3✔
959
            $fromRowLength = $fromRowLengths[$splitCount];
3✔
960
            $toRowLength = $toRowLengths[$splitCount];
3✔
961
            $fromRowOffset = $fromRowOffsets[$splitCount];
3✔
962
            $toRowOffset = $toRowOffsets[$splitCount];
3✔
963
            $fromRow = $fromRows[$splitCount][0];
3✔
964
            $toRow = $toRows[$splitCount][0];
3✔
965

966
            if (!empty($fromRow) && $fromRow[0] !== '$') {
3✔
967
                $fromRow = (int) $fromRow + $numberOfRows;
2✔
968
                $formula = substr($formula, 0, $fromRowOffset) . $fromRow . substr($formula, $fromRowOffset + $fromRowLength);
2✔
969
            }
970
            if (!empty($toRow) && $toRow[0] !== '$') {
3✔
971
                $toRow = (int) $toRow + $numberOfRows;
2✔
972
                $formula = substr($formula, 0, $toRowOffset) . $toRow . substr($formula, $toRowOffset + $toRowLength);
2✔
973
            }
974
        }
975

976
        return $formula;
119✔
977
    }
978

979
    /**
980
     * Update cell reference.
981
     *
982
     * @param string $cellReference Cell address or range of addresses
983
     *
984
     * @return string Updated cell range
985
     */
986
    private function updateCellReference(string $cellReference = 'A1', bool $includeAbsoluteReferences = false, bool $onlyAbsoluteReferences = false, ?bool $topLeft = null)
987
    {
988
        // Is it in another worksheet? Will not have to update anything.
989
        if (str_contains($cellReference, '!')) {
321✔
990
            return $cellReference;
1✔
991
        }
992
        // Is it a range or a single cell?
993
        if (!Coordinate::coordinateIsRange($cellReference)) {
321✔
994
            // Single cell
995
            /** @var CellReferenceHelper */
996
            $cellReferenceHelper = $this->cellReferenceHelper;
297✔
997

998
            return $cellReferenceHelper->updateCellReference($cellReference, $includeAbsoluteReferences, $onlyAbsoluteReferences, $topLeft);
297✔
999
        }
1000

1001
        // Range
1002
        return $this->updateCellRange($cellReference, $includeAbsoluteReferences, $onlyAbsoluteReferences);
49✔
1003
    }
1004

1005
    /**
1006
     * Update named formulae (i.e. containing worksheet references / named ranges).
1007
     *
1008
     * @param Spreadsheet $spreadsheet Object to update
1009
     * @param string $oldName Old name (name to replace)
1010
     * @param string $newName New name
1011
     */
1012
    public function updateNamedFormulae(Spreadsheet $spreadsheet, string $oldName = '', string $newName = ''): void
1013
    {
1014
        if ($oldName == '') {
813✔
1015
            return;
1✔
1016
        }
1017

1018
        foreach ($spreadsheet->getWorksheetIterator() as $sheet) {
813✔
1019
            foreach ($sheet->getCoordinates(false) as $coordinate) {
813✔
1020
                $cell = $sheet->getCell($coordinate);
135✔
1021
                if ($cell->getDataType() === DataType::TYPE_FORMULA) {
135✔
1022
                    $formula = $cell->getValueString();
68✔
1023
                    if (str_contains($formula, $oldName)) {
68✔
1024
                        $formula = str_replace("'" . $oldName . "'!", "'" . $newName . "'!", $formula);
1✔
1025
                        $formula = str_replace($oldName . '!', $newName . '!', $formula);
1✔
1026
                        $cell->setValueExplicit($formula, DataType::TYPE_FORMULA);
1✔
1027
                    }
1028
                }
1029
            }
1030
        }
1031
    }
1032

1033
    private function updateDefinedNames(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void
1034
    {
1035
        foreach ($worksheet->getParentOrThrow()->getDefinedNames() as $definedName) {
7✔
1036
            if ($definedName->isFormula() === false) {
7✔
1037
                $this->updateNamedRange($definedName, $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows);
7✔
1038
            } else {
1039
                $this->updateNamedFormula($definedName, $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows);
4✔
1040
            }
1041
        }
1042
    }
1043

1044
    private function updateNamedRange(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void
1045
    {
1046
        $cellAddress = $definedName->getValue();
7✔
1047
        $asFormula = ($cellAddress[0] === '=');
7✔
1048
        if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashInt() === $worksheet->getHashInt()) {
7✔
1049
            /**
1050
             * If we delete the entire range that is referenced by a Named Range, MS Excel sets the value to #REF!
1051
             * PhpSpreadsheet still only does a basic adjustment, so the Named Range will still reference Cells.
1052
             * Note that this applies only when deleting columns/rows; subsequent insertion won't fix the #REF!
1053
             * TODO Can we work out a method to identify Named Ranges that cease to be valid, so that we can replace
1054
             *      them with a #REF!
1055
             */
1056
            if ($asFormula === true) {
7✔
1057
                $formula = $this->updateFormulaReferences($cellAddress, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true, true);
5✔
1058
                $definedName->setValue($formula);
5✔
1059
            } else {
1060
                $definedName->setValue($this->updateCellReference(ltrim($cellAddress, '='), true));
2✔
1061
            }
1062
        }
1063
    }
1064

1065
    private function updateNamedFormula(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void
1066
    {
1067
        if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashInt() === $worksheet->getHashInt()) {
4✔
1068
            /**
1069
             * If we delete the entire range that is referenced by a Named Formula, MS Excel sets the value to #REF!
1070
             * PhpSpreadsheet still only does a basic adjustment, so the Named Formula will still reference Cells.
1071
             * Note that this applies only when deleting columns/rows; subsequent insertion won't fix the #REF!
1072
             * TODO Can we work out a method to identify Named Ranges that cease to be valid, so that we can replace
1073
             *      them with a #REF!
1074
             */
1075
            $formula = $definedName->getValue();
3✔
1076
            $formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle(), true);
3✔
1077
            $definedName->setValue($formula);
3✔
1078
        }
1079
    }
1080

1081
    /**
1082
     * Update cell range.
1083
     *
1084
     * @param string $cellRange Cell range    (e.g. 'B2:D4', 'B:C' or '2:3')
1085
     *
1086
     * @return string Updated cell range
1087
     */
1088
    private function updateCellRange(string $cellRange = 'A1:A1', bool $includeAbsoluteReferences = false, bool $onlyAbsoluteReferences = false): string
1089
    {
1090
        if (!Coordinate::coordinateIsRange($cellRange)) {
49✔
UNCOV
1091
            throw new Exception('Only cell ranges may be passed to this method.');
×
1092
        }
1093

1094
        // Update range
1095
        $range = Coordinate::splitRange($cellRange);
49✔
1096
        $ic = count($range);
49✔
1097
        for ($i = 0; $i < $ic; ++$i) {
49✔
1098
            $jc = count($range[$i]);
49✔
1099
            for ($j = 0; $j < $jc; ++$j) {
49✔
1100
                /** @var CellReferenceHelper */
1101
                $cellReferenceHelper = $this->cellReferenceHelper;
49✔
1102
                if (ctype_alpha($range[$i][$j])) {
49✔
UNCOV
1103
                    $range[$i][$j] = Coordinate::coordinateFromString(
×
UNCOV
1104
                        $cellReferenceHelper->updateCellReference($range[$i][$j] . '1', $includeAbsoluteReferences, $onlyAbsoluteReferences, null)
×
UNCOV
1105
                    )[0];
×
1106
                } elseif (ctype_digit($range[$i][$j])) {
49✔
UNCOV
1107
                    $range[$i][$j] = Coordinate::coordinateFromString(
×
UNCOV
1108
                        $cellReferenceHelper->updateCellReference('A' . $range[$i][$j], $includeAbsoluteReferences, $onlyAbsoluteReferences, null)
×
UNCOV
1109
                    )[1];
×
1110
                } else {
1111
                    $range[$i][$j] = $cellReferenceHelper->updateCellReference($range[$i][$j], $includeAbsoluteReferences, $onlyAbsoluteReferences, null);
49✔
1112
                }
1113
            }
1114
        }
1115

1116
        // Recreate range string
1117
        return Coordinate::buildRange($range);
49✔
1118
    }
1119

1120
    private function clearColumnStrips(int $highestRow, int $beforeColumn, int $numberOfColumns, Worksheet $worksheet): void
1121
    {
1122
        $startColumnId = Coordinate::stringFromColumnIndex($beforeColumn + $numberOfColumns);
30✔
1123
        $endColumnId = Coordinate::stringFromColumnIndex($beforeColumn);
30✔
1124

1125
        for ($row = 1; $row <= $highestRow - 1; ++$row) {
30✔
1126
            for ($column = $startColumnId; $column !== $endColumnId; ++$column) {
24✔
1127
                /** @var string $column */
1128
                $coordinate = $column . $row;
24✔
1129
                $this->clearStripCell($worksheet, $coordinate);
24✔
1130
            }
1131
        }
1132
    }
1133

1134
    private function clearRowStrips(string $highestColumn, int $beforeColumn, int $beforeRow, int $numberOfRows, Worksheet $worksheet): void
1135
    {
1136
        $startColumnId = Coordinate::stringFromColumnIndex($beforeColumn);
45✔
1137
        ++$highestColumn;
45✔
1138

1139
        for ($column = $startColumnId; $column !== $highestColumn; ++$column) {
45✔
1140
            /** @var string $column */
1141
            for ($row = $beforeRow + $numberOfRows; $row <= $beforeRow - 1; ++$row) {
45✔
1142
                $coordinate = $column . $row;
45✔
1143
                $this->clearStripCell($worksheet, $coordinate);
45✔
1144
            }
1145
        }
1146
    }
1147

1148
    private function clearStripCell(Worksheet $worksheet, string $coordinate): void
1149
    {
1150
        $worksheet->removeConditionalStyles($coordinate);
52✔
1151
        $worksheet->setHyperlink($coordinate);
52✔
1152
        $worksheet->setDataValidation($coordinate);
52✔
1153
        $worksheet->removeComment($coordinate);
52✔
1154

1155
        if ($worksheet->cellExists($coordinate)) {
52✔
1156
            $worksheet->getCell($coordinate)->setValueExplicit(null, DataType::TYPE_NULL);
21✔
1157
            $worksheet->getCell($coordinate)->setXfIndex(0);
21✔
1158
        }
1159
    }
1160

1161
    private function adjustAutoFilter(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void
1162
    {
1163
        $autoFilter = $worksheet->getAutoFilter();
122✔
1164
        $autoFilterRange = $autoFilter->getRange();
122✔
1165
        if (!empty($autoFilterRange)) {
122✔
1166
            if ($numberOfColumns !== 0) {
4✔
1167
                $autoFilterColumns = $autoFilter->getColumns();
2✔
1168
                if (count($autoFilterColumns) > 0) {
2✔
1169
                    $column = '';
2✔
1170
                    $row = 0;
2✔
1171
                    sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row);
2✔
1172
                    $columnIndex = Coordinate::columnIndexFromString((string) $column);
2✔
1173
                    [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($autoFilterRange);
2✔
1174
                    if ($columnIndex <= $rangeEnd[0]) {
2✔
1175
                        if ($numberOfColumns < 0) {
2✔
1176
                            $this->adjustAutoFilterDeleteRules($columnIndex, $numberOfColumns, $autoFilterColumns, $autoFilter);
1✔
1177
                        }
1178
                        $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0];
2✔
1179

1180
                        //    Shuffle columns in autofilter range
1181
                        if ($numberOfColumns > 0) {
2✔
1182
                            $this->adjustAutoFilterInsert($startCol, $numberOfColumns, $rangeEnd[0], $autoFilter);
1✔
1183
                        } else {
1184
                            $this->adjustAutoFilterDelete($startCol, $numberOfColumns, $rangeEnd[0], $autoFilter);
1✔
1185
                        }
1186
                    }
1187
                }
1188
            }
1189

1190
            $worksheet->setAutoFilter(
4✔
1191
                $this->updateCellReference($autoFilterRange)
4✔
1192
            );
4✔
1193
        }
1194
    }
1195

1196
    /** @param mixed[] $autoFilterColumns */
1197
    private function adjustAutoFilterDeleteRules(int $columnIndex, int $numberOfColumns, array $autoFilterColumns, AutoFilter $autoFilter): void
1198
    {
1199
        // If we're actually deleting any columns that fall within the autofilter range,
1200
        //    then we delete any rules for those columns
1201
        $deleteColumn = $columnIndex + $numberOfColumns - 1;
1✔
1202
        $deleteCount = abs($numberOfColumns);
1✔
1203

1204
        for ($i = 1; $i <= $deleteCount; ++$i) {
1✔
1205
            $columnName = Coordinate::stringFromColumnIndex($deleteColumn + 1);
1✔
1206
            if (isset($autoFilterColumns[$columnName])) {
1✔
1207
                $autoFilter->clearColumn($columnName);
1✔
1208
            }
1209
            ++$deleteColumn;
1✔
1210
        }
1211
    }
1212

1213
    private function adjustAutoFilterInsert(int $startCol, int $numberOfColumns, int $rangeEnd, AutoFilter $autoFilter): void
1214
    {
1215
        $startColRef = $startCol;
1✔
1216
        $endColRef = $rangeEnd;
1✔
1217
        $toColRef = $rangeEnd + $numberOfColumns;
1✔
1218

1219
        do {
1220
            $autoFilter->shiftColumn(Coordinate::stringFromColumnIndex($endColRef), Coordinate::stringFromColumnIndex($toColRef));
1✔
1221
            --$endColRef;
1✔
1222
            --$toColRef;
1✔
1223
        } while ($startColRef <= $endColRef);
1✔
1224
    }
1225

1226
    private function adjustAutoFilterDelete(int $startCol, int $numberOfColumns, int $rangeEnd, AutoFilter $autoFilter): void
1227
    {
1228
        // For delete, we shuffle from beginning to end to avoid overwriting
1229
        $startColID = Coordinate::stringFromColumnIndex($startCol);
1✔
1230
        $toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns);
1✔
1231
        $endColID = Coordinate::stringFromColumnIndex($rangeEnd + 1);
1✔
1232

1233
        do {
1234
            $autoFilter->shiftColumn($startColID, $toColID);
1✔
1235
            /** @var string $toColID */
1236
            ++$toColID;
1✔
1237
            /** @var string $startColID */
1238
            ++$startColID;
1✔
1239
        } while ($startColID !== $endColID);
1✔
1240
    }
1241

1242
    private function adjustTable(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void
1243
    {
1244
        $tableCollection = $worksheet->getTableCollection();
122✔
1245

1246
        foreach ($tableCollection as $table) {
122✔
1247
            $tableRange = $table->getRange();
4✔
1248
            if (!empty($tableRange)) {
4✔
1249
                if ($numberOfColumns !== 0) {
4✔
1250
                    $tableColumns = $table->getColumns();
2✔
1251
                    if (count($tableColumns) > 0) {
2✔
1252
                        $column = '';
2✔
1253
                        $row = 0;
2✔
1254
                        sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row);
2✔
1255
                        $columnIndex = Coordinate::columnIndexFromString((string) $column);
2✔
1256
                        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($tableRange);
2✔
1257
                        if ($columnIndex <= $rangeEnd[0]) {
2✔
1258
                            if ($numberOfColumns < 0) {
2✔
1259
                                $this->adjustTableDeleteRules($columnIndex, $numberOfColumns, $tableColumns, $table);
1✔
1260
                            }
1261
                            $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0];
2✔
1262

1263
                            //    Shuffle columns in table range
1264
                            if ($numberOfColumns > 0) {
2✔
1265
                                $this->adjustTableInsert($startCol, $numberOfColumns, $rangeEnd[0], $table);
1✔
1266
                            } else {
1267
                                $this->adjustTableDelete($startCol, $numberOfColumns, $rangeEnd[0], $table);
1✔
1268
                            }
1269
                        }
1270
                    }
1271
                }
1272

1273
                $table->setRange($this->updateCellReference($tableRange));
4✔
1274
            }
1275
        }
1276
    }
1277

1278
    /** @param mixed[] $tableColumns */
1279
    private function adjustTableDeleteRules(int $columnIndex, int $numberOfColumns, array $tableColumns, Table $table): void
1280
    {
1281
        // If we're actually deleting any columns that fall within the table range,
1282
        //    then we delete any rules for those columns
1283
        $deleteColumn = $columnIndex + $numberOfColumns - 1;
1✔
1284
        $deleteCount = abs($numberOfColumns);
1✔
1285

1286
        for ($i = 1; $i <= $deleteCount; ++$i) {
1✔
1287
            $columnName = Coordinate::stringFromColumnIndex($deleteColumn + 1);
1✔
1288
            if (isset($tableColumns[$columnName])) {
1✔
1289
                $table->clearColumn($columnName);
1✔
1290
            }
1291
            ++$deleteColumn;
1✔
1292
        }
1293
    }
1294

1295
    private function adjustTableInsert(int $startCol, int $numberOfColumns, int $rangeEnd, Table $table): void
1296
    {
1297
        $startColRef = $startCol;
1✔
1298
        $endColRef = $rangeEnd;
1✔
1299
        $toColRef = $rangeEnd + $numberOfColumns;
1✔
1300

1301
        do {
1302
            $table->shiftColumn(Coordinate::stringFromColumnIndex($endColRef), Coordinate::stringFromColumnIndex($toColRef));
1✔
1303
            --$endColRef;
1✔
1304
            --$toColRef;
1✔
1305
        } while ($startColRef <= $endColRef);
1✔
1306
    }
1307

1308
    private function adjustTableDelete(int $startCol, int $numberOfColumns, int $rangeEnd, Table $table): void
1309
    {
1310
        // For delete, we shuffle from beginning to end to avoid overwriting
1311
        $startColID = Coordinate::stringFromColumnIndex($startCol);
1✔
1312
        $toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns);
1✔
1313
        $endColID = Coordinate::stringFromColumnIndex($rangeEnd + 1);
1✔
1314

1315
        do {
1316
            $table->shiftColumn($startColID, $toColID);
1✔
1317
            /** @var string $toColID */
1318
            ++$toColID;
1✔
1319
            /** @var string $startColID */
1320
            ++$startColID;
1✔
1321
        } while ($startColID !== $endColID);
1✔
1322
    }
1323

1324
    private function duplicateStylesByColumn(Worksheet $worksheet, int $beforeColumn, int $beforeRow, int $highestRow, int $numberOfColumns): void
1325
    {
1326
        $beforeColumnName = Coordinate::stringFromColumnIndex($beforeColumn - 1);
31✔
1327
        for ($i = $beforeRow; $i <= $highestRow; ++$i) {
31✔
1328
            // Style
1329
            $coordinate = $beforeColumnName . $i;
31✔
1330
            if ($worksheet->cellExists($coordinate)) {
31✔
1331
                $xfIndex = $worksheet->getCell($coordinate)->getXfIndex();
23✔
1332
                for ($j = $beforeColumn; $j <= $beforeColumn - 1 + $numberOfColumns; ++$j) {
23✔
1333
                    if (!empty($xfIndex) || $worksheet->cellExists([$j, $i])) {
23✔
1334
                        $worksheet->getCell([$j, $i])->setXfIndex($xfIndex);
19✔
1335
                    }
1336
                }
1337
            }
1338
        }
1339
    }
1340

1341
    private function duplicateStylesByRow(Worksheet $worksheet, int $beforeColumn, int $beforeRow, string $highestColumn, int $numberOfRows): void
1342
    {
1343
        $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
38✔
1344
        for ($i = $beforeColumn; $i <= $highestColumnIndex; ++$i) {
38✔
1345
            // Style
1346
            $coordinate = Coordinate::stringFromColumnIndex($i) . ($beforeRow - 1);
38✔
1347
            if ($worksheet->cellExists($coordinate)) {
38✔
1348
                $xfIndex = $worksheet->getCell($coordinate)->getXfIndex();
32✔
1349
                for ($j = $beforeRow; $j <= $beforeRow - 1 + $numberOfRows; ++$j) {
32✔
1350
                    if (!empty($xfIndex) || $worksheet->cellExists([$i, $j])) {
32✔
1351
                        $worksheet->getCell(Coordinate::stringFromColumnIndex($i) . $j)->setXfIndex($xfIndex);
23✔
1352
                    }
1353
                }
1354
            }
1355
        }
1356
    }
1357

1358
    /**
1359
     * __clone implementation. Cloning should not be allowed in a Singleton!
1360
     */
1361
    final public function __clone()
1362
    {
1363
        throw new Exception('Cloning a Singleton is not allowed!');
1✔
1364
    }
1365
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc