Completed
Push — master ( bc0154...042bac )
by Mark
32s queued 28s
created

Worksheet::rowDimensionExists()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Worksheet;
4
5
use ArrayObject;
6
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
7
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
8
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
9
use PhpOffice\PhpSpreadsheet\Cell\Cell;
10
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
11
use PhpOffice\PhpSpreadsheet\Cell\CellRange;
12
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
13
use PhpOffice\PhpSpreadsheet\Cell\DataType;
14
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
15
use PhpOffice\PhpSpreadsheet\Cell\Hyperlink;
16
use PhpOffice\PhpSpreadsheet\Chart\Chart;
17
use PhpOffice\PhpSpreadsheet\Collection\Cells;
18
use PhpOffice\PhpSpreadsheet\Collection\CellsFactory;
19
use PhpOffice\PhpSpreadsheet\Comment;
20
use PhpOffice\PhpSpreadsheet\DefinedName;
21
use PhpOffice\PhpSpreadsheet\Exception;
22
use PhpOffice\PhpSpreadsheet\IComparable;
23
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
24
use PhpOffice\PhpSpreadsheet\RichText\RichText;
25
use PhpOffice\PhpSpreadsheet\Shared;
26
use PhpOffice\PhpSpreadsheet\Spreadsheet;
27
use PhpOffice\PhpSpreadsheet\Style\Color;
28
use PhpOffice\PhpSpreadsheet\Style\Conditional;
29
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
30
use PhpOffice\PhpSpreadsheet\Style\Style;
31
32
class Worksheet implements IComparable
33
{
34
    // Break types
35
    public const BREAK_NONE = 0;
36
    public const BREAK_ROW = 1;
37
    public const BREAK_COLUMN = 2;
38
39
    // Sheet state
40
    public const SHEETSTATE_VISIBLE = 'visible';
41
    public const SHEETSTATE_HIDDEN = 'hidden';
42
    public const SHEETSTATE_VERYHIDDEN = 'veryHidden';
43
44
    public const MERGE_CELL_CONTENT_EMPTY = 'empty';
45
    public const MERGE_CELL_CONTENT_HIDE = 'hide';
46
    public const MERGE_CELL_CONTENT_MERGE = 'merge';
47
48
    protected const SHEET_NAME_REQUIRES_NO_QUOTES = '/^[_\p{L}][_\p{L}\p{N}]*$/mui';
49
50
    /**
51
     * Maximum 31 characters allowed for sheet title.
52
     *
53
     * @var int
54
     */
55
    const SHEET_TITLE_MAXIMUM_LENGTH = 31;
56
57
    /**
58
     * Invalid characters in sheet title.
59
     *
60
     * @var array
61
     */
62
    private static $invalidCharacters = ['*', ':', '/', '\\', '?', '[', ']'];
63
64
    /**
65
     * Parent spreadsheet.
66
     *
67
     * @var Spreadsheet
68
     */
69
    private $parent;
70
71
    /**
72
     * Collection of cells.
73
     *
74
     * @var Cells
75
     */
76
    private $cellCollection;
77
78
    /**
79
     * Collection of row dimensions.
80
     *
81
     * @var RowDimension[]
82
     */
83
    private $rowDimensions = [];
84
85
    /**
86
     * Default row dimension.
87
     *
88
     * @var RowDimension
89
     */
90
    private $defaultRowDimension;
91
92
    /**
93
     * Collection of column dimensions.
94
     *
95
     * @var ColumnDimension[]
96
     */
97
    private $columnDimensions = [];
98
99
    /**
100
     * Default column dimension.
101
     *
102
     * @var ColumnDimension
103
     */
104
    private $defaultColumnDimension;
105
106
    /**
107
     * Collection of drawings.
108
     *
109
     * @var ArrayObject<int, BaseDrawing>
110
     */
111
    private $drawingCollection;
112
113
    /**
114
     * Collection of Chart objects.
115
     *
116
     * @var ArrayObject<int, Chart>
117
     */
118
    private $chartCollection;
119
120
    /**
121
     * Collection of Table objects.
122
     *
123
     * @var ArrayObject<int, Table>
124
     */
125
    private $tableCollection;
126
127
    /**
128
     * Worksheet title.
129
     *
130
     * @var string
131
     */
132
    private $title;
133
134
    /**
135
     * Sheet state.
136
     *
137
     * @var string
138
     */
139
    private $sheetState;
140
141
    /**
142
     * Page setup.
143
     *
144
     * @var PageSetup
145
     */
146
    private $pageSetup;
147
148
    /**
149
     * Page margins.
150
     *
151
     * @var PageMargins
152
     */
153
    private $pageMargins;
154
155
    /**
156
     * Page header/footer.
157
     *
158
     * @var HeaderFooter
159
     */
160
    private $headerFooter;
161
162
    /**
163
     * Sheet view.
164
     *
165
     * @var SheetView
166
     */
167
    private $sheetView;
168
169
    /**
170
     * Protection.
171
     *
172
     * @var Protection
173
     */
174
    private $protection;
175
176
    /**
177
     * Collection of styles.
178
     *
179
     * @var Style[]
180
     */
181
    private $styles = [];
182
183
    /**
184
     * Conditional styles. Indexed by cell coordinate, e.g. 'A1'.
185
     *
186
     * @var array
187
     */
188
    private $conditionalStylesCollection = [];
189
190
    /**
191
     * Collection of breaks.
192
     *
193
     * @var int[]
194
     */
195
    private $breaks = [];
196
197
    /**
198
     * Collection of merged cell ranges.
199
     *
200
     * @var string[]
201
     */
202
    private $mergeCells = [];
203
204
    /**
205
     * Collection of protected cell ranges.
206
     *
207
     * @var string[]
208
     */
209
    private $protectedCells = [];
210
211
    /**
212
     * Autofilter Range and selection.
213
     *
214
     * @var AutoFilter
215
     */
216
    private $autoFilter;
217
218
    /**
219
     * Freeze pane.
220
     *
221
     * @var null|string
222
     */
223
    private $freezePane;
224
225
    /**
226
     * Default position of the right bottom pane.
227
     *
228
     * @var null|string
229
     */
230
    private $topLeftCell;
231
232
    /**
233
     * Show gridlines?
234
     *
235
     * @var bool
236
     */
237
    private $showGridlines = true;
238
239
    /**
240
     * Print gridlines?
241
     *
242
     * @var bool
243
     */
244
    private $printGridlines = false;
245
246
    /**
247
     * Show row and column headers?
248
     *
249
     * @var bool
250
     */
251
    private $showRowColHeaders = true;
252
253
    /**
254
     * Show summary below? (Row/Column outline).
255
     *
256
     * @var bool
257
     */
258
    private $showSummaryBelow = true;
259
260
    /**
261
     * Show summary right? (Row/Column outline).
262
     *
263
     * @var bool
264
     */
265
    private $showSummaryRight = true;
266
267
    /**
268
     * Collection of comments.
269
     *
270
     * @var Comment[]
271
     */
272
    private $comments = [];
273
274
    /**
275
     * Active cell. (Only one!).
276
     *
277
     * @var string
278
     */
279
    private $activeCell = 'A1';
280
281
    /**
282
     * Selected cells.
283
     *
284
     * @var string
285
     */
286
    private $selectedCells = 'A1';
287
288
    /**
289
     * Cached highest column.
290
     *
291
     * @var int
292
     */
293
    private $cachedHighestColumn = 1;
294
295
    /**
296
     * Cached highest row.
297
     *
298
     * @var int
299
     */
300
    private $cachedHighestRow = 1;
301
302
    /**
303
     * Right-to-left?
304
     *
305
     * @var bool
306
     */
307
    private $rightToLeft = false;
308
309
    /**
310
     * Hyperlinks. Indexed by cell coordinate, e.g. 'A1'.
311
     *
312
     * @var array
313
     */
314
    private $hyperlinkCollection = [];
315
316
    /**
317
     * Data validation objects. Indexed by cell coordinate, e.g. 'A1'.
318
     *
319
     * @var array
320
     */
321
    private $dataValidationCollection = [];
322
323
    /**
324
     * Tab color.
325
     *
326
     * @var null|Color
327
     */
328
    private $tabColor;
329
330
    /**
331
     * Dirty flag.
332
     *
333
     * @var bool
334
     */
335
    private $dirty = true;
336
337
    /**
338
     * Hash.
339
     *
340
     * @var string
341
     */
342
    private $hash;
343
344
    /**
345
     * CodeName.
346
     *
347
     * @var string
348
     */
349
    private $codeName;
350
351
    /**
352
     * Create a new worksheet.
353
     *
354
     * @param string $title
355
     */
356 9861
    public function __construct(?Spreadsheet $parent = null, $title = 'Worksheet')
357
    {
358
        // Set parent and title
359 9861
        $this->parent = $parent;
360 9861
        $this->setTitle($title, false);
361
        // setTitle can change $pTitle
362 9861
        $this->setCodeName($this->getTitle());
363 9861
        $this->setSheetState(self::SHEETSTATE_VISIBLE);
364
365 9861
        $this->cellCollection = CellsFactory::getInstance($this);
366
        // Set page setup
367 9861
        $this->pageSetup = new PageSetup();
368
        // Set page margins
369 9861
        $this->pageMargins = new PageMargins();
370
        // Set page header/footer
371 9861
        $this->headerFooter = new HeaderFooter();
372
        // Set sheet view
373 9861
        $this->sheetView = new SheetView();
374
        // Drawing collection
375 9861
        $this->drawingCollection = new ArrayObject();
376
        // Chart collection
377 9861
        $this->chartCollection = new ArrayObject();
378
        // Protection
379 9861
        $this->protection = new Protection();
380
        // Default row dimension
381 9861
        $this->defaultRowDimension = new RowDimension(null);
382
        // Default column dimension
383 9861
        $this->defaultColumnDimension = new ColumnDimension(null);
384
        // AutoFilter
385 9861
        $this->autoFilter = new AutoFilter('', $this);
386
        // Table collection
387 9861
        $this->tableCollection = new ArrayObject();
388
    }
389
390
    /**
391
     * Disconnect all cells from this Worksheet object,
392
     * typically so that the worksheet object can be unset.
393
     */
394 8433
    public function disconnectCells(): void
395
    {
396 8433
        if ($this->cellCollection !== null) {
397 8432
            $this->cellCollection->unsetWorksheetCells();
398
            // @phpstan-ignore-next-line
399 8432
            $this->cellCollection = null;
400
        }
401
        //    detach ourself from the workbook, so that it can then delete this worksheet successfully
402
        // @phpstan-ignore-next-line
403 8433
        $this->parent = null;
404
    }
405
406
    /**
407
     * Code to execute when this worksheet is unset().
408
     */
409 110
    public function __destruct()
410
    {
411 110
        Calculation::getInstance($this->parent)->clearCalculationCacheForWorksheet($this->title);
412
413 110
        $this->disconnectCells();
414 110
        $this->rowDimensions = [];
415
    }
416
417
    /**
418
     * Return the cell collection.
419
     *
420
     * @return Cells
421
     */
422 9612
    public function getCellCollection()
423
    {
424 9612
        return $this->cellCollection;
425
    }
426
427
    /**
428
     * Get array of invalid characters for sheet title.
429
     *
430
     * @return array
431
     */
432
    public static function getInvalidCharacters()
433
    {
434
        return self::$invalidCharacters;
435
    }
436
437
    /**
438
     * Check sheet code name for valid Excel syntax.
439
     *
440
     * @param string $sheetCodeName The string to check
441
     *
442
     * @return string The valid string
443
     */
444 9861
    private static function checkSheetCodeName($sheetCodeName)
445
    {
446 9861
        $charCount = Shared\StringHelper::countCharacters($sheetCodeName);
447 9861
        if ($charCount == 0) {
448
            throw new Exception('Sheet code name cannot be empty.');
449
        }
450
        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ] and  first and last characters cannot be a "'"
451
        if (
452 9861
            (str_replace(self::$invalidCharacters, '', $sheetCodeName) !== $sheetCodeName) ||
453 9861
            (Shared\StringHelper::substring($sheetCodeName, -1, 1) == '\'') ||
454 9861
            (Shared\StringHelper::substring($sheetCodeName, 0, 1) == '\'')
455
        ) {
456 1
            throw new Exception('Invalid character found in sheet code name');
457
        }
458
459
        // Enforce maximum characters allowed for sheet title
460 9861
        if ($charCount > self::SHEET_TITLE_MAXIMUM_LENGTH) {
461 1
            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet code name.');
462
        }
463
464 9861
        return $sheetCodeName;
465
    }
466
467
    /**
468
     * Check sheet title for valid Excel syntax.
469
     *
470
     * @param string $sheetTitle The string to check
471
     *
472
     * @return string The valid string
473
     */
474 9861
    private static function checkSheetTitle($sheetTitle)
475
    {
476
        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ]
477 9861
        if (str_replace(self::$invalidCharacters, '', $sheetTitle) !== $sheetTitle) {
478 1
            throw new Exception('Invalid character found in sheet title');
479
        }
480
481
        // Enforce maximum characters allowed for sheet title
482 9861
        if (Shared\StringHelper::countCharacters($sheetTitle) > self::SHEET_TITLE_MAXIMUM_LENGTH) {
483 1
            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet title.');
484
        }
485
486 9861
        return $sheetTitle;
487
    }
488
489
    /**
490
     * Get a sorted list of all cell coordinates currently held in the collection by row and column.
491
     *
492
     * @param bool $sorted Also sort the cell collection?
493
     *
494
     * @return string[]
495
     */
496 1031
    public function getCoordinates($sorted = true)
497
    {
498 1031
        if ($this->cellCollection == null) {
499
            return [];
500
        }
501
502 1031
        if ($sorted) {
503 337
            return $this->cellCollection->getSortedCoordinates();
504
        }
505
506 954
        return $this->cellCollection->getCoordinates();
507
    }
508
509
    /**
510
     * Get collection of row dimensions.
511
     *
512
     * @return RowDimension[]
513
     */
514 803
    public function getRowDimensions()
515
    {
516 803
        return $this->rowDimensions;
517
    }
518
519
    /**
520
     * Get default row dimension.
521
     *
522
     * @return RowDimension
523
     */
524 769
    public function getDefaultRowDimension()
525
    {
526 769
        return $this->defaultRowDimension;
527
    }
528
529
    /**
530
     * Get collection of column dimensions.
531
     *
532
     * @return ColumnDimension[]
533
     */
534 803
    public function getColumnDimensions()
535
    {
536 803
        return $this->columnDimensions;
537
    }
538
539
    /**
540
     * Get default column dimension.
541
     *
542
     * @return ColumnDimension
543
     */
544 343
    public function getDefaultColumnDimension()
545
    {
546 343
        return $this->defaultColumnDimension;
547
    }
548
549
    /**
550
     * Get collection of drawings.
551
     *
552
     * @return ArrayObject<int, BaseDrawing>
553
     */
554 793
    public function getDrawingCollection()
555
    {
556 793
        return $this->drawingCollection;
557
    }
558
559
    /**
560
     * Get collection of charts.
561
     *
562
     * @return ArrayObject<int, Chart>
563
     */
564 70
    public function getChartCollection()
565
    {
566 70
        return $this->chartCollection;
567
    }
568
569
    /**
570
     * Add chart.
571
     *
572
     * @param null|int $chartIndex Index where chart should go (0,1,..., or null for last)
573
     *
574
     * @return Chart
575
     */
576 75
    public function addChart(Chart $chart, $chartIndex = null)
577
    {
578 75
        $chart->setWorksheet($this);
579 75
        if ($chartIndex === null) {
580 75
            $this->chartCollection[] = $chart;
581
        } else {
582
            // Insert the chart at the requested index
583
            // @phpstan-ignore-next-line
584
            array_splice(/** @scrutinizer ignore-type */ $this->chartCollection, $chartIndex, 0, [$chart]);
585
        }
586
587 75
        return $chart;
588
    }
589
590
    /**
591
     * Return the count of charts on this worksheet.
592
     *
593
     * @return int The number of charts
594
     */
595 58
    public function getChartCount()
596
    {
597 58
        return count($this->chartCollection);
598
    }
599
600
    /**
601
     * Get a chart by its index position.
602
     *
603
     * @param ?string $index Chart index position
604
     *
605
     * @return Chart|false
606
     */
607 53
    public function getChartByIndex($index)
608
    {
609 53
        $chartCount = count($this->chartCollection);
610 53
        if ($chartCount == 0) {
611
            return false;
612
        }
613 53
        if ($index === null) {
614
            $index = --$chartCount;
615
        }
616 53
        if (!isset($this->chartCollection[$index])) {
617
            return false;
618
        }
619
620 53
        return $this->chartCollection[$index];
621
    }
622
623
    /**
624
     * Return an array of the names of charts on this worksheet.
625
     *
626
     * @return string[] The names of charts
627
     */
628 4
    public function getChartNames()
629
    {
630 4
        $chartNames = [];
631 4
        foreach ($this->chartCollection as $chart) {
632 4
            $chartNames[] = $chart->getName();
633
        }
634
635 4
        return $chartNames;
636
    }
637
638
    /**
639
     * Get a chart by name.
640
     *
641
     * @param string $chartName Chart name
642
     *
643
     * @return Chart|false
644
     */
645 4
    public function getChartByName($chartName)
646
    {
647 4
        $chartCount = count($this->chartCollection);
648 4
        if ($chartCount == 0) {
649
            return false;
650
        }
651 4
        foreach ($this->chartCollection as $index => $chart) {
652 4
            if ($chart->getName() == $chartName) {
653 4
                return $this->chartCollection[$index];
654
            }
655
        }
656
657
        return false;
658
    }
659
660
    /**
661
     * Refresh column dimensions.
662
     *
663
     * @return $this
664
     */
665 23
    public function refreshColumnDimensions()
666
    {
667 23
        $newColumnDimensions = [];
668 23
        foreach ($this->getColumnDimensions() as $objColumnDimension) {
669 23
            $newColumnDimensions[$objColumnDimension->getColumnIndex()] = $objColumnDimension;
670
        }
671
672 23
        $this->columnDimensions = $newColumnDimensions;
673
674 23
        return $this;
675
    }
676
677
    /**
678
     * Refresh row dimensions.
679
     *
680
     * @return $this
681
     */
682 5
    public function refreshRowDimensions()
683
    {
684 5
        $newRowDimensions = [];
685 5
        foreach ($this->getRowDimensions() as $objRowDimension) {
686 5
            $newRowDimensions[$objRowDimension->getRowIndex()] = $objRowDimension;
687
        }
688
689 5
        $this->rowDimensions = $newRowDimensions;
690
691 5
        return $this;
692
    }
693
694
    /**
695
     * Calculate worksheet dimension.
696
     *
697
     * @return string String containing the dimension of this worksheet
698
     */
699 298
    public function calculateWorksheetDimension()
700
    {
701
        // Return
702 298
        return 'A1:' . $this->getHighestColumn() . $this->getHighestRow();
703
    }
704
705
    /**
706
     * Calculate worksheet data dimension.
707
     *
708
     * @return string String containing the dimension of this worksheet that actually contain data
709
     */
710 420
    public function calculateWorksheetDataDimension()
711
    {
712
        // Return
713 420
        return 'A1:' . $this->getHighestDataColumn() . $this->getHighestDataRow();
714
    }
715
716
    /**
717
     * Calculate widths for auto-size columns.
718
     *
719
     * @return $this
720
     */
721 565
    public function calculateColumnWidths()
722
    {
723
        // initialize $autoSizes array
724 565
        $autoSizes = [];
725 565
        foreach ($this->getColumnDimensions() as $colDimension) {
726 92
            if ($colDimension->getAutoSize()) {
727 45
                $autoSizes[$colDimension->getColumnIndex()] = -1;
728
            }
729
        }
730
731
        // There is only something to do if there are some auto-size columns
732 565
        if (!empty($autoSizes)) {
733
            // build list of cells references that participate in a merge
734 45
            $isMergeCell = [];
735 45
            foreach ($this->getMergeCells() as $cells) {
736 16
                foreach (Coordinate::extractAllCellReferencesInRange($cells) as $cellReference) {
737 16
                    $isMergeCell[$cellReference] = true;
738
                }
739
            }
740
741 45
            $autoFilterRange = $autoFilterFirstRowRange = $this->autoFilter->getRange();
742 45
            if (!empty($autoFilterRange)) {
743 1
                $autoFilterRangeBoundaries = Coordinate::rangeBoundaries($autoFilterRange);
744 1
                $autoFilterFirstRowRange = (string) new CellRange(
745 1
                    CellAddress::fromColumnAndRow($autoFilterRangeBoundaries[0][0], $autoFilterRangeBoundaries[0][1]),
746 1
                    CellAddress::fromColumnAndRow($autoFilterRangeBoundaries[1][0], $autoFilterRangeBoundaries[0][1])
747
                );
748
            }
749
750
            // loop through all cells in the worksheet
751 45
            foreach ($this->getCoordinates(false) as $coordinate) {
752 45
                $cell = $this->getCellOrNull($coordinate);
753
754 45
                if ($cell !== null && isset($autoSizes[$this->cellCollection->getCurrentColumn()])) {
755
                    //Determine if cell is in merge range
756 45
                    $isMerged = isset($isMergeCell[$this->cellCollection->getCurrentCoordinate()]);
757
758
                    //By default merged cells should be ignored
759 45
                    $isMergedButProceed = false;
760
761
                    //The only exception is if it's a merge range value cell of a 'vertical' range (1 column wide)
762 45
                    if ($isMerged && $cell->isMergeRangeValueCell()) {
763
                        $range = $cell->getMergeRange();
764
                        $rangeBoundaries = Coordinate::rangeDimension($range);
765
                        if ($rangeBoundaries[0] === 1) {
766
                            $isMergedButProceed = true;
767
                        }
768
                    }
769
770
                    // Determine width if cell is not part of a merge or does and is a value cell of 1-column wide range
771 45
                    if (!$isMerged || $isMergedButProceed) {
772
                        // Determine if we need to make an adjustment for the first row in an AutoFilter range that
773
                        //    has a column filter dropdown
774 45
                        $filterAdjustment = false;
775 45
                        if (!empty($autoFilterRange) && $cell->isInRange($autoFilterFirstRowRange)) {
776 1
                            $filterAdjustment = true;
777
                        }
778
779 45
                        $indentAdjustment = $cell->getStyle()->getAlignment()->getIndent();
780
781
                        // Calculated value
782
                        // To formatted string
783 45
                        $cellValue = NumberFormat::toFormattedString(
784 45
                            $cell->getCalculatedValue(),
785 45
                            $this->getParent()->getCellXfByIndex($cell->getXfIndex())
786 45
                                ->getNumberFormat()->getFormatCode()
787
                        );
788
789 45
                        if ($cellValue !== null && $cellValue !== '') {
790 45
                            $autoSizes[$this->cellCollection->getCurrentColumn()] = max(
791 45
                                (float) $autoSizes[$this->cellCollection->getCurrentColumn()],
792 45
                                (float) Shared\Font::calculateColumnWidth(
793 45
                                    $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont(),
794
                                    $cellValue,
795 45
                                    $this->getParent()->getCellXfByIndex($cell->getXfIndex())
796 45
                                        ->getAlignment()->getTextRotation(),
797 45
                                    $this->getParent()->getDefaultStyle()->getFont(),
798
                                    $filterAdjustment,
799
                                    $indentAdjustment
800
                                )
801
                            );
802
                        }
803
                    }
804
                }
805
            }
806
807
            // adjust column widths
808 45
            foreach ($autoSizes as $columnIndex => $width) {
809 45
                if ($width == -1) {
810
                    $width = $this->getDefaultColumnDimension()->getWidth();
811
                }
812 45
                $this->getColumnDimension($columnIndex)->setWidth($width);
813
            }
814
        }
815
816 565
        return $this;
817
    }
818
819
    /**
820
     * Get parent.
821
     *
822
     * @return Spreadsheet
823
     */
824 9862
    public function getParent()
825
    {
826 9862
        return $this->parent;
827
    }
828
829
    /**
830
     * Re-bind parent.
831
     *
832
     * @return $this
833
     */
834 53
    public function rebindParent(Spreadsheet $parent)
835
    {
836 53
        if ($this->parent !== null) {
837 4
            $definedNames = $this->parent->getDefinedNames();
838 4
            foreach ($definedNames as $definedName) {
839
                $parent->addDefinedName($definedName);
840
            }
841
842 4
            $this->parent->removeSheetByIndex(
843 4
                $this->parent->getIndex($this)
844
            );
845
        }
846 53
        $this->parent = $parent;
847
848 53
        return $this;
849
    }
850
851
    /**
852
     * Get title.
853
     *
854
     * @return string
855
     */
856 9862
    public function getTitle()
857
    {
858 9862
        return $this->title;
859
    }
860
861
    /**
862
     * Set title.
863
     *
864
     * @param string $title String containing the dimension of this worksheet
865
     * @param bool $updateFormulaCellReferences Flag indicating whether cell references in formulae should
866
     *            be updated to reflect the new sheet name.
867
     *          This should be left as the default true, unless you are
868
     *          certain that no formula cells on any worksheet contain
869
     *          references to this worksheet
870
     * @param bool $validate False to skip validation of new title. WARNING: This should only be set
871
     *                       at parse time (by Readers), where titles can be assumed to be valid.
872
     *
873
     * @return $this
874
     */
875 9861
    public function setTitle($title, $updateFormulaCellReferences = true, $validate = true)
876
    {
877
        // Is this a 'rename' or not?
878 9861
        if ($this->getTitle() == $title) {
879 182
            return $this;
880
        }
881
882
        // Old title
883 9861
        $oldTitle = $this->getTitle();
884
885 9861
        if ($validate) {
886
            // Syntax check
887 9861
            self::checkSheetTitle($title);
888
889 9861
            if ($this->parent) {
890
                // Is there already such sheet name?
891 9852
                if ($this->parent->sheetNameExists($title)) {
892
                    // Use name, but append with lowest possible integer
893
894 138
                    if (Shared\StringHelper::countCharacters($title) > 29) {
895
                        $title = Shared\StringHelper::substring($title, 0, 29);
896
                    }
897 138
                    $i = 1;
898 138
                    while ($this->parent->sheetNameExists($title . ' ' . $i)) {
899 19
                        ++$i;
900 19
                        if ($i == 10) {
901
                            if (Shared\StringHelper::countCharacters($title) > 28) {
902
                                $title = Shared\StringHelper::substring($title, 0, 28);
903
                            }
904 19
                        } elseif ($i == 100) {
905
                            if (Shared\StringHelper::countCharacters($title) > 27) {
906
                                $title = Shared\StringHelper::substring($title, 0, 27);
907
                            }
908
                        }
909
                    }
910
911 138
                    $title .= " $i";
912
                }
913
            }
914
        }
915
916
        // Set title
917 9861
        $this->title = $title;
918 9861
        $this->dirty = true;
919
920 9861
        if ($this->parent && $this->parent->getCalculationEngine()) {
921
            // New title
922 9852
            $newTitle = $this->getTitle();
923 9852
            $this->parent->getCalculationEngine()
924 9852
                ->renameCalculationCacheForWorksheet($oldTitle, $newTitle);
925 9852
            if ($updateFormulaCellReferences) {
926 655
                ReferenceHelper::getInstance()->updateNamedFormulae($this->parent, $oldTitle, $newTitle);
927
            }
928
        }
929
930 9861
        return $this;
931
    }
932
933
    /**
934
     * Get sheet state.
935
     *
936
     * @return string Sheet state (visible, hidden, veryHidden)
937
     */
938 308
    public function getSheetState()
939
    {
940 308
        return $this->sheetState;
941
    }
942
943
    /**
944
     * Set sheet state.
945
     *
946
     * @param string $value Sheet state (visible, hidden, veryHidden)
947
     *
948
     * @return $this
949
     */
950 9861
    public function setSheetState($value)
951
    {
952 9861
        $this->sheetState = $value;
953
954 9861
        return $this;
955
    }
956
957
    /**
958
     * Get page setup.
959
     *
960
     * @return PageSetup
961
     */
962 1087
    public function getPageSetup()
963
    {
964 1087
        return $this->pageSetup;
965
    }
966
967
    /**
968
     * Set page setup.
969
     *
970
     * @return $this
971
     */
972
    public function setPageSetup(PageSetup $pageSetup)
973
    {
974
        $this->pageSetup = $pageSetup;
975
976
        return $this;
977
    }
978
979
    /**
980
     * Get page margins.
981
     *
982
     * @return PageMargins
983
     */
984 1104
    public function getPageMargins()
985
    {
986 1104
        return $this->pageMargins;
987
    }
988
989
    /**
990
     * Set page margins.
991
     *
992
     * @return $this
993
     */
994
    public function setPageMargins(PageMargins $pageMargins)
995
    {
996
        $this->pageMargins = $pageMargins;
997
998
        return $this;
999
    }
1000
1001
    /**
1002
     * Get page header/footer.
1003
     *
1004
     * @return HeaderFooter
1005
     */
1006 358
    public function getHeaderFooter()
1007
    {
1008 358
        return $this->headerFooter;
1009
    }
1010
1011
    /**
1012
     * Set page header/footer.
1013
     *
1014
     * @return $this
1015
     */
1016
    public function setHeaderFooter(HeaderFooter $headerFooter)
1017
    {
1018
        $this->headerFooter = $headerFooter;
1019
1020
        return $this;
1021
    }
1022
1023
    /**
1024
     * Get sheet view.
1025
     *
1026
     * @return SheetView
1027
     */
1028 372
    public function getSheetView()
1029
    {
1030 372
        return $this->sheetView;
1031
    }
1032
1033
    /**
1034
     * Set sheet view.
1035
     *
1036
     * @return $this
1037
     */
1038
    public function setSheetView(SheetView $sheetView)
1039
    {
1040
        $this->sheetView = $sheetView;
1041
1042
        return $this;
1043
    }
1044
1045
    /**
1046
     * Get Protection.
1047
     *
1048
     * @return Protection
1049
     */
1050 388
    public function getProtection()
1051
    {
1052 388
        return $this->protection;
1053
    }
1054
1055
    /**
1056
     * Set Protection.
1057
     *
1058
     * @return $this
1059
     */
1060
    public function setProtection(Protection $protection)
1061
    {
1062
        $this->protection = $protection;
1063
        $this->dirty = true;
1064
1065
        return $this;
1066
    }
1067
1068
    /**
1069
     * Get highest worksheet column.
1070
     *
1071
     * @param null|int|string $row Return the data highest column for the specified row,
1072
     *                                     or the highest column of any row if no row number is passed
1073
     *
1074
     * @return string Highest column name
1075
     */
1076 1064
    public function getHighestColumn($row = null)
1077
    {
1078 1064
        if ($row === null) {
1079 1064
            return Coordinate::stringFromColumnIndex($this->cachedHighestColumn);
1080
        }
1081
1082
        return $this->getHighestDataColumn($row);
1083
    }
1084
1085
    /**
1086
     * Get highest worksheet column that contains data.
1087
     *
1088
     * @param null|int|string $row Return the highest data column for the specified row,
1089
     *                                     or the highest data column of any row if no row number is passed
1090
     *
1091
     * @return string Highest column name that contains data
1092
     */
1093 466
    public function getHighestDataColumn($row = null)
1094
    {
1095 466
        return $this->cellCollection->getHighestColumn($row);
1096
    }
1097
1098
    /**
1099
     * Get highest worksheet row.
1100
     *
1101
     * @param null|string $column Return the highest data row for the specified column,
1102
     *                                     or the highest row of any column if no column letter is passed
1103
     *
1104
     * @return int Highest row number
1105
     */
1106 668
    public function getHighestRow($column = null)
1107
    {
1108 668
        if ($column === null) {
1109 668
            return $this->cachedHighestRow;
1110
        }
1111
1112
        return $this->getHighestDataRow($column);
1113
    }
1114
1115
    /**
1116
     * Get highest worksheet row that contains data.
1117
     *
1118
     * @param null|string $column Return the highest data row for the specified column,
1119
     *                                     or the highest data row of any column if no column letter is passed
1120
     *
1121
     * @return int Highest row number that contains data
1122
     */
1123 475
    public function getHighestDataRow($column = null)
1124
    {
1125 475
        return $this->cellCollection->getHighestRow($column);
1126
    }
1127
1128
    /**
1129
     * Get highest worksheet column and highest row that have cell records.
1130
     *
1131
     * @return array Highest column name and highest row number
1132
     */
1133
    public function getHighestRowAndColumn()
1134
    {
1135
        return $this->cellCollection->getHighestRowAndColumn();
1136
    }
1137
1138
    /**
1139
     * Set a cell value.
1140
     *
1141
     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1142
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1143
     * @param mixed $value Value for the cell
1144
     *
1145
     * @return $this
1146
     */
1147 2196
    public function setCellValue($coordinate, $value)
1148
    {
1149 2196
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
1150 2196
        $this->getCell($cellAddress)->setValue($value);
1151
1152 2196
        return $this;
1153
    }
1154
1155
    /**
1156
     * Set a cell value by using numeric cell coordinates.
1157
     *
1158
     * @deprecated 1.23.0
1159
     *      Use the setCellValue() method with a cell address such as 'C5' instead;,
1160
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1161
     * @see Worksheet::setCellValue()
1162
     *
1163
     * @param int $columnIndex Numeric column coordinate of the cell
1164
     * @param int $row Numeric row coordinate of the cell
1165
     * @param mixed $value Value of the cell
1166
     *
1167
     * @return $this
1168
     */
1169 1
    public function setCellValueByColumnAndRow($columnIndex, $row, $value)
1170
    {
1171 1
        $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row)->setValue($value);
1172
1173 1
        return $this;
1174
    }
1175
1176
    /**
1177
     * Set a cell value.
1178
     *
1179
     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1180
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1181
     * @param mixed $value Value of the cell
1182
     * @param string $dataType Explicit data type, see DataType::TYPE_*
1183
     *        Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this
1184
     *             method, then it is your responsibility as an end-user developer to validate that the value and
1185
     *             the datatype match.
1186
     *       If you do mismatch value and datatpe, then the value you enter may be changed to match the datatype
1187
     *          that you specify.
1188
     *
1189
     * @see DataType
1190
     *
1191
     * @return $this
1192
     */
1193 95
    public function setCellValueExplicit($coordinate, $value, $dataType)
1194
    {
1195 95
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
1196 95
        $this->getCell($cellAddress)->setValueExplicit($value, $dataType);
1197
1198 95
        return $this;
1199
    }
1200
1201
    /**
1202
     * Set a cell value by using numeric cell coordinates.
1203
     *
1204
     * @deprecated 1.23.0
1205
     *      Use the setCellValueExplicit() method with a cell address such as 'C5' instead;,
1206
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1207
     * @see Worksheet::setCellValueExplicit()
1208
     *
1209
     * @param int $columnIndex Numeric column coordinate of the cell
1210
     * @param int $row Numeric row coordinate of the cell
1211
     * @param mixed $value Value of the cell
1212
     * @param string $dataType Explicit data type, see DataType::TYPE_*
1213
     *        Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this
1214
     *             method, then it is your responsibility as an end-user developer to validate that the value and
1215
     *             the datatype match.
1216
     *       If you do mismatch value and datatpe, then the value you enter may be changed to match the datatype
1217
     *          that you specify.
1218
     *
1219
     * @see DataType
1220
     *
1221
     * @return $this
1222
     */
1223 1
    public function setCellValueExplicitByColumnAndRow($columnIndex, $row, $value, $dataType)
1224
    {
1225 1
        $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row)->setValueExplicit($value, $dataType);
1226
1227 1
        return $this;
1228
    }
1229
1230
    /**
1231
     * Get cell at a specific coordinate.
1232
     *
1233
     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1234
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1235
     *
1236
     * @return Cell Cell that was found or created
1237
     */
1238 9597
    public function getCell($coordinate): Cell
1239
    {
1240 9597
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
1241
1242
        // Shortcut for increased performance for the vast majority of simple cases
1243 9597
        if ($this->cellCollection->has($cellAddress)) {
1244
            /** @var Cell $cell */
1245 9240
            $cell = $this->cellCollection->get($cellAddress);
1246
1247 9240
            return $cell;
1248
        }
1249
1250
        /** @var Worksheet $sheet */
1251 9594
        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
1252 9594
        $cell = $sheet->cellCollection->get($finalCoordinate);
1253
1254 9594
        return $cell ?? $sheet->createNewCell($finalCoordinate);
1255
    }
1256
1257
    /**
1258
     * Get the correct Worksheet and coordinate from a coordinate that may
1259
     * contains reference to another sheet or a named range.
1260
     *
1261
     * @return array{0: Worksheet, 1: string}
1262
     */
1263 9595
    private function getWorksheetAndCoordinate(string $coordinate): array
1264
    {
1265 9595
        $sheet = null;
1266 9595
        $finalCoordinate = null;
1267
1268
        // Worksheet reference?
1269 9595
        if (strpos($coordinate, '!') !== false) {
1270
            $worksheetReference = self::extractSheetTitle($coordinate, true);
1271
1272
            $sheet = $this->parent->getSheetByName($worksheetReference[0]);
1273
            $finalCoordinate = strtoupper($worksheetReference[1]);
1274
1275
            if ($sheet === null) {
1276
                throw new Exception('Sheet not found for name: ' . $worksheetReference[0]);
1277
            }
1278
        } elseif (
1279 9595
            !preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $coordinate) &&
1280 9595
            preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/iu', $coordinate)
1281
        ) {
1282
            // Named range?
1283 14
            $namedRange = $this->validateNamedRange($coordinate, true);
1284 14
            if ($namedRange !== null) {
1285 10
                $sheet = $namedRange->getWorksheet();
1286 10
                if ($sheet === null) {
1287
                    throw new Exception('Sheet not found for named range: ' . $namedRange->getName());
1288
                }
1289
1290
                /** @phpstan-ignore-next-line */
1291 10
                $cellCoordinate = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!');
1292 10
                $finalCoordinate = str_replace('$', '', $cellCoordinate);
1293
            }
1294
        }
1295
1296 9595
        if ($sheet === null || $finalCoordinate === null) {
1297 9595
            $sheet = $this;
1298 9595
            $finalCoordinate = strtoupper($coordinate);
1299
        }
1300
1301 9595
        if (Coordinate::coordinateIsRange($finalCoordinate)) {
1302 2
            throw new Exception('Cell coordinate string can not be a range of cells.');
1303 9595
        } elseif (strpos($finalCoordinate, '$') !== false) {
1304
            throw new Exception('Cell coordinate must not be absolute.');
1305
        }
1306
1307 9595
        return [$sheet, $finalCoordinate];
1308
    }
1309
1310
    /**
1311
     * Get an existing cell at a specific coordinate, or null.
1312
     *
1313
     * @param string $coordinate Coordinate of the cell, eg: 'A1'
1314
     *
1315
     * @return null|Cell Cell that was found or null
1316
     */
1317 45
    private function getCellOrNull($coordinate): ?Cell
1318
    {
1319
        // Check cell collection
1320 45
        if ($this->cellCollection->has($coordinate)) {
1321 45
            return $this->cellCollection->get($coordinate);
1322
        }
1323
1324
        return null;
1325
    }
1326
1327
    /**
1328
     * Get cell at a specific coordinate by using numeric cell coordinates.
1329
     *
1330
     * @deprecated 1.23.0
1331
     *      Use the getCell() method with a cell address such as 'C5' instead;,
1332
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1333
     * @see Worksheet::getCell()
1334
     *
1335
     * @param int $columnIndex Numeric column coordinate of the cell
1336
     * @param int $row Numeric row coordinate of the cell
1337
     *
1338
     * @return Cell Cell that was found/created or null
1339
     */
1340 1
    public function getCellByColumnAndRow($columnIndex, $row): Cell
1341
    {
1342 1
        return $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row);
1343
    }
1344
1345
    /**
1346
     * Create a new cell at the specified coordinate.
1347
     *
1348
     * @param string $coordinate Coordinate of the cell
1349
     *
1350
     * @return Cell Cell that was created
1351
     */
1352 9598
    public function createNewCell($coordinate): Cell
1353
    {
1354 9598
        [$column, $row, $columnString] = Coordinate::indexesFromString($coordinate);
1355 9598
        $cell = new Cell(null, DataType::TYPE_NULL, $this);
1356 9598
        $this->cellCollection->add($coordinate, $cell);
1357
1358
        // Coordinates
1359 9598
        if ($column > $this->cachedHighestColumn) {
1360 7171
            $this->cachedHighestColumn = $column;
1361
        }
1362 9598
        if ($row > $this->cachedHighestRow) {
1363 6921
            $this->cachedHighestRow = $row;
1364
        }
1365
1366
        // Cell needs appropriate xfIndex from dimensions records
1367
        //    but don't create dimension records if they don't already exist
1368 9598
        $rowDimension = $this->rowDimensions[$row] ?? null;
1369 9598
        $columnDimension = $this->columnDimensions[$columnString] ?? null;
1370
1371 9598
        if ($rowDimension !== null) {
1372 358
            $rowXf = (int) $rowDimension->getXfIndex();
1373 358
            if ($rowXf > 0) {
1374
                // then there is a row dimension with explicit style, assign it to the cell
1375 358
                $cell->setXfIndex($rowXf);
1376
            }
1377 9549
        } elseif ($columnDimension !== null) {
1378 342
            $colXf = (int) $columnDimension->getXfIndex();
1379 342
            if ($colXf > 0) {
1380
                // then there is a column dimension, assign it to the cell
1381 195
                $cell->setXfIndex($colXf);
1382
            }
1383
        }
1384
1385 9598
        return $cell;
1386
    }
1387
1388
    /**
1389
     * Does the cell at a specific coordinate exist?
1390
     *
1391
     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1392
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1393
     */
1394 8385
    public function cellExists($coordinate): bool
1395
    {
1396 8385
        $cellAddress = Validations::validateCellAddress($coordinate);
1397
        /** @var Worksheet $sheet */
1398 8385
        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
1399
1400 8384
        return $sheet->cellCollection->has($finalCoordinate);
1401
    }
1402
1403
    /**
1404
     * Cell at a specific coordinate by using numeric cell coordinates exists?
1405
     *
1406
     * @deprecated 1.23.0
1407
     *      Use the cellExists() method with a cell address such as 'C5' instead;,
1408
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1409
     * @see Worksheet::cellExists()
1410
     *
1411
     * @param int $columnIndex Numeric column coordinate of the cell
1412
     * @param int $row Numeric row coordinate of the cell
1413
     */
1414 1
    public function cellExistsByColumnAndRow($columnIndex, $row): bool
1415
    {
1416 1
        return $this->cellExists(Coordinate::stringFromColumnIndex($columnIndex) . $row);
1417
    }
1418
1419
    /**
1420
     * Get row dimension at a specific row.
1421
     *
1422
     * @param int $row Numeric index of the row
1423
     */
1424 672
    public function getRowDimension(int $row): RowDimension
1425
    {
1426
        // Get row dimension
1427 672
        if (!isset($this->rowDimensions[$row])) {
1428 672
            $this->rowDimensions[$row] = new RowDimension($row);
1429
1430 672
            $this->cachedHighestRow = max($this->cachedHighestRow, $row);
1431
        }
1432
1433 672
        return $this->rowDimensions[$row];
1434
    }
1435
1436 105
    public function rowDimensionExists(int $row): bool
1437
    {
1438 105
        return isset($this->rowDimensions[$row]);
1439
    }
1440
1441
    /**
1442
     * Get column dimension at a specific column.
1443
     *
1444
     * @param string $column String index of the column eg: 'A'
1445
     */
1446 442
    public function getColumnDimension(string $column): ColumnDimension
1447
    {
1448
        // Uppercase coordinate
1449 442
        $column = strtoupper($column);
1450
1451
        // Fetch dimensions
1452 442
        if (!isset($this->columnDimensions[$column])) {
1453 442
            $this->columnDimensions[$column] = new ColumnDimension($column);
1454
1455 442
            $columnIndex = Coordinate::columnIndexFromString($column);
1456 442
            if ($this->cachedHighestColumn < $columnIndex) {
1457 308
                $this->cachedHighestColumn = $columnIndex;
1458
            }
1459
        }
1460
1461 442
        return $this->columnDimensions[$column];
1462
    }
1463
1464
    /**
1465
     * Get column dimension at a specific column by using numeric cell coordinates.
1466
     *
1467
     * @param int $columnIndex Numeric column coordinate of the cell
1468
     */
1469 65
    public function getColumnDimensionByColumn(int $columnIndex): ColumnDimension
1470
    {
1471 65
        return $this->getColumnDimension(Coordinate::stringFromColumnIndex($columnIndex));
1472
    }
1473
1474
    /**
1475
     * Get styles.
1476
     *
1477
     * @return Style[]
1478
     */
1479
    public function getStyles()
1480
    {
1481
        return $this->styles;
1482
    }
1483
1484
    /**
1485
     * Get style for cell.
1486
     *
1487
     * @param AddressRange|array<int>|CellAddress|int|string $cellCoordinate
1488
     *              A simple string containing a cell address like 'A1' or a cell range like 'A1:E10'
1489
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1490
     *              or a CellAddress or AddressRange object.
1491
     */
1492 8947
    public function getStyle($cellCoordinate): Style
1493
    {
1494 8947
        $cellCoordinate = Validations::validateCellOrCellRange($cellCoordinate);
1495
1496
        // set this sheet as active
1497 8947
        $this->parent->setActiveSheetIndex($this->parent->getIndex($this));
1498
1499
        // set cell coordinate as active
1500 8947
        $this->setSelectedCells($cellCoordinate);
1501
1502 8947
        return $this->parent->getCellXfSupervisor();
1503
    }
1504
1505
    /**
1506
     * Get style for cell by using numeric cell coordinates.
1507
     *
1508
     * @deprecated 1.23.0
1509
     *      Use the getStyle() method with a cell address range such as 'C5:F8' instead;,
1510
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1511
     *          or an AddressRange object.
1512
     * @see Worksheet::getStyle()
1513
     *
1514
     * @param int $columnIndex1 Numeric column coordinate of the cell
1515
     * @param int $row1 Numeric row coordinate of the cell
1516
     * @param null|int $columnIndex2 Numeric column coordinate of the range cell
1517
     * @param null|int $row2 Numeric row coordinate of the range cell
1518
     *
1519
     * @return Style
1520
     */
1521 1
    public function getStyleByColumnAndRow($columnIndex1, $row1, $columnIndex2 = null, $row2 = null)
1522
    {
1523 1
        if ($columnIndex2 !== null && $row2 !== null) {
1524 1
            $cellRange = new CellRange(
1525 1
                CellAddress::fromColumnAndRow($columnIndex1, $row1),
1526 1
                CellAddress::fromColumnAndRow($columnIndex2, $row2)
1527
            );
1528
1529 1
            return $this->getStyle($cellRange);
1530
        }
1531
1532 1
        return $this->getStyle(CellAddress::fromColumnAndRow($columnIndex1, $row1));
1533
    }
1534
1535
    /**
1536
     * Get conditional styles for a cell.
1537
     *
1538
     * @param string $coordinate eg: 'A1' or 'A1:A3'.
1539
     *          If a single cell is referenced, then the array of conditional styles will be returned if the cell is
1540
     *               included in a conditional style range.
1541
     *          If a range of cells is specified, then the styles will only be returned if the range matches the entire
1542
     *               range of the conditional.
1543
     *
1544
     * @return Conditional[]
1545
     */
1546 225
    public function getConditionalStyles(string $coordinate): array
1547
    {
1548 225
        $coordinate = strtoupper($coordinate);
1549 225
        if (strpos($coordinate, ':') !== false) {
1550 45
            return $this->conditionalStylesCollection[$coordinate] ?? [];
1551
        }
1552
1553 204
        $cell = $this->getCell($coordinate);
1554 204
        foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
1555 193
            if ($cell->isInRange($conditionalRange)) {
1556 189
                return $this->conditionalStylesCollection[$conditionalRange];
1557
            }
1558
        }
1559
1560 33
        return [];
1561
    }
1562
1563 178
    public function getConditionalRange(string $coordinate): ?string
1564
    {
1565 178
        $coordinate = strtoupper($coordinate);
1566 178
        $cell = $this->getCell($coordinate);
1567 178
        foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
1568 178
            if ($cell->isInRange($conditionalRange)) {
1569 177
                return $conditionalRange;
1570
            }
1571
        }
1572
1573 1
        return null;
1574
    }
1575
1576
    /**
1577
     * Do conditional styles exist for this cell?
1578
     *
1579
     * @param string $coordinate eg: 'A1' or 'A1:A3'.
1580
     *          If a single cell is specified, then this method will return true if that cell is included in a
1581
     *               conditional style range.
1582
     *          If a range of cells is specified, then true will only be returned if the range matches the entire
1583
     *               range of the conditional.
1584
     */
1585 21
    public function conditionalStylesExists($coordinate): bool
1586
    {
1587 21
        $coordinate = strtoupper($coordinate);
1588 21
        if (strpos($coordinate, ':') !== false) {
1589 10
            return isset($this->conditionalStylesCollection[$coordinate]);
1590
        }
1591
1592 11
        $cell = $this->getCell($coordinate);
1593 11
        foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
1594 11
            if ($cell->isInRange($conditionalRange)) {
1595 7
                return true;
1596
            }
1597
        }
1598
1599 4
        return false;
1600
    }
1601
1602
    /**
1603
     * Removes conditional styles for a cell.
1604
     *
1605
     * @param string $coordinate eg: 'A1'
1606
     *
1607
     * @return $this
1608
     */
1609 42
    public function removeConditionalStyles($coordinate)
1610
    {
1611 42
        unset($this->conditionalStylesCollection[strtoupper($coordinate)]);
1612
1613 42
        return $this;
1614
    }
1615
1616
    /**
1617
     * Get collection of conditional styles.
1618
     *
1619
     * @return array
1620
     */
1621 369
    public function getConditionalStylesCollection()
1622
    {
1623 369
        return $this->conditionalStylesCollection;
1624
    }
1625
1626
    /**
1627
     * Set conditional styles.
1628
     *
1629
     * @param string $coordinate eg: 'A1'
1630
     * @param Conditional[] $styles
1631
     *
1632
     * @return $this
1633
     */
1634 275
    public function setConditionalStyles($coordinate, $styles)
1635
    {
1636 275
        $this->conditionalStylesCollection[strtoupper($coordinate)] = $styles;
1637
1638 275
        return $this;
1639
    }
1640
1641
    /**
1642
     * Duplicate cell style to a range of cells.
1643
     *
1644
     * Please note that this will overwrite existing cell styles for cells in range!
1645
     *
1646
     * @param Style $style Cell style to duplicate
1647
     * @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
1648
     *
1649
     * @return $this
1650
     */
1651 2
    public function duplicateStyle(Style $style, $range)
1652
    {
1653
        // Add the style to the workbook if necessary
1654 2
        $workbook = $this->parent;
1655 2
        if ($existingStyle = $this->parent->getCellXfByHashCode($style->getHashCode())) {
1656
            // there is already such cell Xf in our collection
1657 1
            $xfIndex = $existingStyle->getIndex();
1658
        } else {
1659
            // we don't have such a cell Xf, need to add
1660 2
            $workbook->addCellXf($style);
1661 2
            $xfIndex = $style->getIndex();
1662
        }
1663
1664
        // Calculate range outer borders
1665 2
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
1666
1667
        // Make sure we can loop upwards on rows and columns
1668 2
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
1669
            $tmp = $rangeStart;
1670
            $rangeStart = $rangeEnd;
1671
            $rangeEnd = $tmp;
1672
        }
1673
1674
        // Loop through cells and apply styles
1675 2
        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
1676 2
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
1677 2
                $this->getCell(Coordinate::stringFromColumnIndex($col) . $row)->setXfIndex($xfIndex);
1678
            }
1679
        }
1680
1681 2
        return $this;
1682
    }
1683
1684
    /**
1685
     * Duplicate conditional style to a range of cells.
1686
     *
1687
     * Please note that this will overwrite existing cell styles for cells in range!
1688
     *
1689
     * @param Conditional[] $styles Cell style to duplicate
1690
     * @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
1691
     *
1692
     * @return $this
1693
     */
1694 18
    public function duplicateConditionalStyle(array $styles, $range = '')
1695
    {
1696 18
        foreach ($styles as $cellStyle) {
1697 18
            if (!($cellStyle instanceof Conditional)) {
1698
                throw new Exception('Style is not a conditional style');
1699
            }
1700
        }
1701
1702
        // Calculate range outer borders
1703 18
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
1704
1705
        // Make sure we can loop upwards on rows and columns
1706 18
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
1707
            $tmp = $rangeStart;
1708
            $rangeStart = $rangeEnd;
1709
            $rangeEnd = $tmp;
1710
        }
1711
1712
        // Loop through cells and apply styles
1713 18
        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
1714 18
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
1715 18
                $this->setConditionalStyles(Coordinate::stringFromColumnIndex($col) . $row, $styles);
1716
            }
1717
        }
1718
1719 18
        return $this;
1720
    }
1721
1722
    /**
1723
     * Set break on a cell.
1724
     *
1725
     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1726
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1727
     * @param int $break Break type (type of Worksheet::BREAK_*)
1728
     *
1729
     * @return $this
1730
     */
1731 14
    public function setBreak($coordinate, $break)
1732
    {
1733 14
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
1734
1735 14
        if ($break === self::BREAK_NONE) {
1736 2
            if (isset($this->breaks[$cellAddress])) {
1737 2
                unset($this->breaks[$cellAddress]);
1738
            }
1739
        } else {
1740 14
            $this->breaks[$cellAddress] = $break;
1741
        }
1742
1743 14
        return $this;
1744
    }
1745
1746
    /**
1747
     * Set break on a cell by using numeric cell coordinates.
1748
     *
1749
     * @deprecated 1.23.0
1750
     *      Use the setBreak() method with a cell address such as 'C5' instead;,
1751
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1752
     * @see Worksheet::setBreak()
1753
     *
1754
     * @param int $columnIndex Numeric column coordinate of the cell
1755
     * @param int $row Numeric row coordinate of the cell
1756
     * @param int $break Break type (type of Worksheet::BREAK_*)
1757
     *
1758
     * @return $this
1759
     */
1760 1
    public function setBreakByColumnAndRow($columnIndex, $row, $break)
1761
    {
1762 1
        return $this->setBreak(Coordinate::stringFromColumnIndex($columnIndex) . $row, $break);
1763
    }
1764
1765
    /**
1766
     * Get breaks.
1767
     *
1768
     * @return int[]
1769
     */
1770 778
    public function getBreaks()
1771
    {
1772 778
        return $this->breaks;
1773
    }
1774
1775
    /**
1776
     * Set merge on a cell range.
1777
     *
1778
     * @param AddressRange|array<int>|string $range A simple string containing a Cell range like 'A1:E10'
1779
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1780
     *              or an AddressRange.
1781
     * @param string $behaviour How the merged cells should behave.
1782
     *               Possible values are:
1783
     *                   MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells
1784
     *                   MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells
1785
     *                   MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell
1786
     *
1787
     * @return $this
1788
     */
1789 135
    public function mergeCells($range, $behaviour = self::MERGE_CELL_CONTENT_EMPTY)
1790
    {
1791 135
        $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range));
1792
1793 134
        if (strpos($range, ':') === false) {
1794 1
            $range .= ":{$range}";
1795
        }
1796
1797 134
        if (preg_match('/^([A-Z]+)(\\d+):([A-Z]+)(\\d+)$/', $range, $matches) !== 1) {
1798 1
            throw new Exception('Merge must be on a valid range of cells.');
1799
        }
1800
1801 133
        $this->mergeCells[$range] = $range;
1802 133
        $firstRow = (int) $matches[2];
1803 133
        $lastRow = (int) $matches[4];
1804 133
        $firstColumn = $matches[1];
1805 133
        $lastColumn = $matches[3];
1806 133
        $firstColumnIndex = Coordinate::columnIndexFromString($firstColumn);
1807 133
        $lastColumnIndex = Coordinate::columnIndexFromString($lastColumn);
1808 133
        $numberRows = $lastRow - $firstRow;
1809 133
        $numberColumns = $lastColumnIndex - $firstColumnIndex;
1810
1811 133
        if ($numberRows === 1 && $numberColumns === 1) {
1812 29
            return $this;
1813
        }
1814
1815
        // create upper left cell if it does not already exist
1816 126
        $upperLeft = "{$firstColumn}{$firstRow}";
1817 126
        if (!$this->cellExists($upperLeft)) {
1818 28
            $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL);
1819
        }
1820
1821 126
        if ($behaviour !== self::MERGE_CELL_CONTENT_HIDE) {
1822
            // Blank out the rest of the cells in the range (if they exist)
1823 46
            if ($numberRows > $numberColumns) {
1824 10
                $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft, $behaviour);
1825
            } else {
1826 36
                $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft, $behaviour);
1827
            }
1828
        }
1829
1830 126
        return $this;
1831
    }
1832
1833 10
    private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
1834
    {
1835 10
        $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
1836
            ? [$this->getCell($upperLeft)->getFormattedValue()]
1837 10
            : [];
1838
1839 10
        foreach ($this->getColumnIterator($firstColumn, $lastColumn) as $column) {
1840 10
            $iterator = $column->getCellIterator($firstRow);
1841 10
            $iterator->setIterateOnlyExistingCells(true);
1842 10
            foreach ($iterator as $cell) {
1843 10
                if ($cell !== null) {
1844 10
                    $row = $cell->getRow();
1845 10
                    if ($row > $lastRow) {
1846 7
                        break;
1847
                    }
1848 10
                    $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue);
1849
                }
1850
            }
1851
        }
1852
1853 10
        if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
1854
            $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
1855
        }
1856
    }
1857
1858 36
    private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
1859
    {
1860 36
        $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
1861 4
            ? [$this->getCell($upperLeft)->getFormattedValue()]
1862 36
            : [];
1863
1864 36
        foreach ($this->getRowIterator($firstRow, $lastRow) as $row) {
1865 36
            $iterator = $row->getCellIterator($firstColumn);
1866 36
            $iterator->setIterateOnlyExistingCells(true);
1867 36
            foreach ($iterator as $cell) {
1868 36
                if ($cell !== null) {
1869 36
                    $column = $cell->getColumn();
1870 36
                    $columnIndex = Coordinate::columnIndexFromString($column);
1871 36
                    if ($columnIndex > $lastColumnIndex) {
1872 7
                        break;
1873
                    }
1874 36
                    $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue);
1875
                }
1876
            }
1877
        }
1878
1879 36
        if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
1880 4
            $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
1881
        }
1882
    }
1883
1884 46
    public function mergeCellBehaviour(Cell $cell, string $upperLeft, string $behaviour, array $leftCellValue): array
1885
    {
1886 46
        if ($cell->getCoordinate() !== $upperLeft) {
1887 22
            Calculation::getInstance($cell->getWorksheet()->getParent())->flushInstance();
1888 22
            if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
1889 4
                $cellValue = $cell->getFormattedValue();
1890 4
                if ($cellValue !== '') {
1891 4
                    $leftCellValue[] = $cellValue;
1892
                }
1893
            }
1894 22
            $cell->setValueExplicit(null, DataType::TYPE_NULL);
1895
        }
1896
1897 46
        return $leftCellValue;
1898
    }
1899
1900
    /**
1901
     * Set merge on a cell range by using numeric cell coordinates.
1902
     *
1903
     * @deprecated 1.23.0
1904
     *      Use the mergeCells() method with a cell address range such as 'C5:F8' instead;,
1905
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1906
     *          or an AddressRange object.
1907
     * @see Worksheet::mergeCells()
1908
     *
1909
     * @param int $columnIndex1 Numeric column coordinate of the first cell
1910
     * @param int $row1 Numeric row coordinate of the first cell
1911
     * @param int $columnIndex2 Numeric column coordinate of the last cell
1912
     * @param int $row2 Numeric row coordinate of the last cell
1913
     * @param string $behaviour How the merged cells should behave.
1914
     *               Possible values are:
1915
     *                   MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells
1916
     *                   MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells
1917
     *                   MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell
1918
     *
1919
     * @return $this
1920
     */
1921 1
    public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $behaviour = self::MERGE_CELL_CONTENT_EMPTY)
1922
    {
1923 1
        $cellRange = new CellRange(
1924 1
            CellAddress::fromColumnAndRow($columnIndex1, $row1),
1925 1
            CellAddress::fromColumnAndRow($columnIndex2, $row2)
1926
        );
1927
1928 1
        return $this->mergeCells($cellRange, $behaviour);
1929
    }
1930
1931
    /**
1932
     * Remove merge on a cell range.
1933
     *
1934
     * @param AddressRange|array<int>|string $range A simple string containing a Cell range like 'A1:E10'
1935
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1936
     *              or an AddressRange.
1937
     *
1938
     * @return $this
1939
     */
1940 22
    public function unmergeCells($range)
1941
    {
1942 22
        $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range));
1943
1944 22
        if (strpos($range, ':') !== false) {
1945 22
            if (isset($this->mergeCells[$range])) {
1946 22
                unset($this->mergeCells[$range]);
1947
            } else {
1948 22
                throw new Exception('Cell range ' . $range . ' not known as merged.');
1949
            }
1950
        } else {
1951
            throw new Exception('Merge can only be removed from a range of cells.');
1952
        }
1953
1954 22
        return $this;
1955
    }
1956
1957
    /**
1958
     * Remove merge on a cell range by using numeric cell coordinates.
1959
     *
1960
     * @deprecated 1.23.0
1961
     *      Use the unmergeCells() method with a cell address range such as 'C5:F8' instead;,
1962
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1963
     *          or an AddressRange object.
1964
     * @see Worksheet::unmergeCells()
1965
     *
1966
     * @param int $columnIndex1 Numeric column coordinate of the first cell
1967
     * @param int $row1 Numeric row coordinate of the first cell
1968
     * @param int $columnIndex2 Numeric column coordinate of the last cell
1969
     * @param int $row2 Numeric row coordinate of the last cell
1970
     *
1971
     * @return $this
1972
     */
1973 1
    public function unmergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
1974
    {
1975 1
        $cellRange = new CellRange(
1976 1
            CellAddress::fromColumnAndRow($columnIndex1, $row1),
1977 1
            CellAddress::fromColumnAndRow($columnIndex2, $row2)
1978
        );
1979
1980 1
        return $this->unmergeCells($cellRange);
1981
    }
1982
1983
    /**
1984
     * Get merge cells array.
1985
     *
1986
     * @return string[]
1987
     */
1988 811
    public function getMergeCells()
1989
    {
1990 811
        return $this->mergeCells;
1991
    }
1992
1993
    /**
1994
     * Set merge cells array for the entire sheet. Use instead mergeCells() to merge
1995
     * a single cell range.
1996
     *
1997
     * @param string[] $mergeCells
1998
     *
1999
     * @return $this
2000
     */
2001 68
    public function setMergeCells(array $mergeCells)
2002
    {
2003 68
        $this->mergeCells = $mergeCells;
2004
2005 68
        return $this;
2006
    }
2007
2008
    /**
2009
     * Set protection on a cell or cell range.
2010
     *
2011
     * @param AddressRange|array<int>|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10'
2012
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2013
     *              or a CellAddress or AddressRange object.
2014
     * @param string $password Password to unlock the protection
2015
     * @param bool $alreadyHashed If the password has already been hashed, set this to true
2016
     *
2017
     * @return $this
2018
     */
2019 21
    public function protectCells($range, $password, $alreadyHashed = false)
2020
    {
2021 21
        $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
2022
2023 21
        if (!$alreadyHashed) {
2024 21
            $password = Shared\PasswordHasher::hashPassword($password);
2025
        }
2026 21
        $this->protectedCells[$range] = $password;
2027
2028 21
        return $this;
2029
    }
2030
2031
    /**
2032
     * Set protection on a cell range by using numeric cell coordinates.
2033
     *
2034
     * @deprecated 1.23.0
2035
     *      Use the protectCells() method with a cell address range such as 'C5:F8' instead;,
2036
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2037
     *          or an AddressRange object.
2038
     * @see Worksheet::protectCells()
2039
     *
2040
     * @param int $columnIndex1 Numeric column coordinate of the first cell
2041
     * @param int $row1 Numeric row coordinate of the first cell
2042
     * @param int $columnIndex2 Numeric column coordinate of the last cell
2043
     * @param int $row2 Numeric row coordinate of the last cell
2044
     * @param string $password Password to unlock the protection
2045
     * @param bool $alreadyHashed If the password has already been hashed, set this to true
2046
     *
2047
     * @return $this
2048
     */
2049 1
    public function protectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $password, $alreadyHashed = false)
2050
    {
2051 1
        $cellRange = new CellRange(
2052 1
            CellAddress::fromColumnAndRow($columnIndex1, $row1),
2053 1
            CellAddress::fromColumnAndRow($columnIndex2, $row2)
2054
        );
2055
2056 1
        return $this->protectCells($cellRange, $password, $alreadyHashed);
2057
    }
2058
2059
    /**
2060
     * Remove protection on a cell or cell range.
2061
     *
2062
     * @param AddressRange|array<int>|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10'
2063
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2064
     *              or a CellAddress or AddressRange object.
2065
     *
2066
     * @return $this
2067
     */
2068 19
    public function unprotectCells($range)
2069
    {
2070 19
        $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
2071
2072 19
        if (isset($this->protectedCells[$range])) {
2073 19
            unset($this->protectedCells[$range]);
2074
        } else {
2075
            throw new Exception('Cell range ' . $range . ' not known as protected.');
2076
        }
2077
2078 19
        return $this;
2079
    }
2080
2081
    /**
2082
     * Remove protection on a cell range by using numeric cell coordinates.
2083
     *
2084
     * @deprecated 1.23.0
2085
     *      Use the unprotectCells() method with a cell address range such as 'C5:F8' instead;,
2086
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2087
     *          or an AddressRange object.
2088
     * @see Worksheet::unprotectCells()
2089
     *
2090
     * @param int $columnIndex1 Numeric column coordinate of the first cell
2091
     * @param int $row1 Numeric row coordinate of the first cell
2092
     * @param int $columnIndex2 Numeric column coordinate of the last cell
2093
     * @param int $row2 Numeric row coordinate of the last cell
2094
     *
2095
     * @return $this
2096
     */
2097 1
    public function unprotectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
2098
    {
2099 1
        $cellRange = new CellRange(
2100 1
            CellAddress::fromColumnAndRow($columnIndex1, $row1),
2101 1
            CellAddress::fromColumnAndRow($columnIndex2, $row2)
2102
        );
2103
2104 1
        return $this->unprotectCells($cellRange);
2105
    }
2106
2107
    /**
2108
     * Get protected cells.
2109
     *
2110
     * @return string[]
2111
     */
2112 372
    public function getProtectedCells()
2113
    {
2114 372
        return $this->protectedCells;
2115
    }
2116
2117
    /**
2118
     * Get Autofilter.
2119
     *
2120
     * @return AutoFilter
2121
     */
2122 534
    public function getAutoFilter()
2123
    {
2124 534
        return $this->autoFilter;
2125
    }
2126
2127
    /**
2128
     * Set AutoFilter.
2129
     *
2130
     * @param AddressRange|array<int>|AutoFilter|string $autoFilterOrRange
2131
     *            A simple string containing a Cell range like 'A1:E10' is permitted for backward compatibility
2132
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2133
     *              or an AddressRange.
2134
     *
2135
     * @return $this
2136
     */
2137 14
    public function setAutoFilter($autoFilterOrRange)
2138
    {
2139 14
        if (is_object($autoFilterOrRange) && ($autoFilterOrRange instanceof AutoFilter)) {
2140
            $this->autoFilter = $autoFilterOrRange;
2141
        } else {
2142 14
            $cellRange = Functions::trimSheetFromCellReference(Validations::validateCellRange($autoFilterOrRange));
2143
2144 14
            $this->autoFilter->setRange($cellRange);
2145
        }
2146
2147 14
        return $this;
2148
    }
2149
2150
    /**
2151
     * Set Autofilter Range by using numeric cell coordinates.
2152
     *
2153
     * @deprecated 1.23.0
2154
     *      Use the setAutoFilter() method with a cell address range such as 'C5:F8' instead;,
2155
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2156
     *          or an AddressRange object or AutoFilter object.
2157
     * @see Worksheet::setAutoFilter()
2158
     *
2159
     * @param int $columnIndex1 Numeric column coordinate of the first cell
2160
     * @param int $row1 Numeric row coordinate of the first cell
2161
     * @param int $columnIndex2 Numeric column coordinate of the second cell
2162
     * @param int $row2 Numeric row coordinate of the second cell
2163
     *
2164
     * @return $this
2165
     */
2166 1
    public function setAutoFilterByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
2167
    {
2168 1
        $cellRange = new CellRange(
2169 1
            CellAddress::fromColumnAndRow($columnIndex1, $row1),
2170 1
            CellAddress::fromColumnAndRow($columnIndex2, $row2)
2171
        );
2172
2173 1
        return $this->setAutoFilter($cellRange);
2174
    }
2175
2176
    /**
2177
     * Remove autofilter.
2178
     */
2179 1
    public function removeAutoFilter(): self
2180
    {
2181 1
        $this->autoFilter->setRange('');
2182
2183 1
        return $this;
2184
    }
2185
2186
    /**
2187
     * Get collection of Tables.
2188
     *
2189
     * @return ArrayObject<int, Table>
2190
     */
2191 342
    public function getTableCollection()
2192
    {
2193 342
        return $this->tableCollection;
2194
    }
2195
2196
    /**
2197
     * Add Table.
2198
     *
2199
     * @return $this
2200
     */
2201 15
    public function addTable(Table $table): self
2202
    {
2203 15
        $table->setWorksheet($this);
2204 15
        $this->tableCollection[] = $table;
2205
2206 15
        return $this;
2207
    }
2208
2209
    /**
2210
     * Remove Table by name.
2211
     *
2212
     * @param string $name Table name
2213
     *
2214
     * @return $this
2215
     */
2216 1
    public function removeTableByName(string $name): self
2217
    {
2218 1
        $name = Shared\StringHelper::strToUpper($name);
2219 1
        foreach ($this->tableCollection as $key => $table) {
2220 1
            if (Shared\StringHelper::strToUpper($table->getName()) === $name) {
2221 1
                unset($this->tableCollection[$key]);
2222
            }
2223
        }
2224
2225 1
        return $this;
2226
    }
2227
2228
    /**
2229
     * Remove collection of Tables.
2230
     */
2231 1
    public function removeTableCollection(): self
2232
    {
2233 1
        $this->tableCollection = new ArrayObject();
2234
2235 1
        return $this;
2236
    }
2237
2238
    /**
2239
     * Get Freeze Pane.
2240
     *
2241
     * @return null|string
2242
     */
2243 394
    public function getFreezePane()
2244
    {
2245 394
        return $this->freezePane;
2246
    }
2247
2248
    /**
2249
     * Freeze Pane.
2250
     *
2251
     * Examples:
2252
     *
2253
     *     - A2 will freeze the rows above cell A2 (i.e row 1)
2254
     *     - B1 will freeze the columns to the left of cell B1 (i.e column A)
2255
     *     - B2 will freeze the rows above and to the left of cell B2 (i.e row 1 and column A)
2256
     *
2257
     * @param null|array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
2258
     *            or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2259
     *        Passing a null value for this argument will clear any existing freeze pane for this worksheet.
2260
     * @param null|array<int>|CellAddress|string $topLeftCell default position of the right bottom pane
2261
     *            Coordinate of the cell as a string, eg: 'C5'; or as an array of [$columnIndex, $row] (e.g. [3, 5]),
2262
     *            or a CellAddress object.
2263
     *
2264
     * @return $this
2265
     */
2266 30
    public function freezePane($coordinate, $topLeftCell = null)
2267
    {
2268 30
        $cellAddress = ($coordinate !== null)
2269 30
            ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate))
2270 30
            : null;
2271 30
        if ($cellAddress !== null && Coordinate::coordinateIsRange($cellAddress)) {
2272
            throw new Exception('Freeze pane can not be set on a range of cells.');
2273
        }
2274 30
        $topLeftCell = ($topLeftCell !== null)
2275 24
            ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($topLeftCell))
2276 30
            : null;
2277
2278 30
        if ($cellAddress !== null && $topLeftCell === null) {
2279 9
            $coordinate = Coordinate::coordinateFromString($cellAddress);
2280 9
            $topLeftCell = $coordinate[0] . $coordinate[1];
2281
        }
2282
2283 30
        $this->freezePane = $cellAddress;
2284 30
        $this->topLeftCell = $topLeftCell;
2285
2286 30
        return $this;
2287
    }
2288
2289 11
    public function setTopLeftCell(string $topLeftCell): self
2290
    {
2291 11
        $this->topLeftCell = $topLeftCell;
2292
2293 11
        return $this;
2294
    }
2295
2296
    /**
2297
     * Freeze Pane by using numeric cell coordinates.
2298
     *
2299
     * @deprecated 1.23.0
2300
     *      Use the freezePane() method with a cell address such as 'C5' instead;,
2301
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2302
     * @see Worksheet::freezePane()
2303
     *
2304
     * @param int $columnIndex Numeric column coordinate of the cell
2305
     * @param int $row Numeric row coordinate of the cell
2306
     *
2307
     * @return $this
2308
     */
2309 1
    public function freezePaneByColumnAndRow($columnIndex, $row)
2310
    {
2311 1
        return $this->freezePane(Coordinate::stringFromColumnIndex($columnIndex) . $row);
2312
    }
2313
2314
    /**
2315
     * Unfreeze Pane.
2316
     *
2317
     * @return $this
2318
     */
2319
    public function unfreezePane()
2320
    {
2321
        return $this->freezePane(null);
2322
    }
2323
2324
    /**
2325
     * Get the default position of the right bottom pane.
2326
     *
2327
     * @return null|string
2328
     */
2329 289
    public function getTopLeftCell()
2330
    {
2331 289
        return $this->topLeftCell;
2332
    }
2333
2334
    /**
2335
     * Insert a new row, updating all possible related data.
2336
     *
2337
     * @param int $before Insert before this one
2338
     * @param int $numberOfRows Number of rows to insert
2339
     *
2340
     * @return $this
2341
     */
2342 30
    public function insertNewRowBefore($before, $numberOfRows = 1)
2343
    {
2344 30
        if ($before >= 1) {
2345 30
            $objReferenceHelper = ReferenceHelper::getInstance();
2346 30
            $objReferenceHelper->insertNewBefore('A' . $before, 0, $numberOfRows, $this);
2347
        } else {
2348
            throw new Exception('Rows can only be inserted before at least row 1.');
2349
        }
2350
2351 30
        return $this;
2352
    }
2353
2354
    /**
2355
     * Insert a new column, updating all possible related data.
2356
     *
2357
     * @param string $before Insert before this one, eg: 'A'
2358
     * @param int $numberOfColumns Number of columns to insert
2359
     *
2360
     * @return $this
2361
     */
2362 23
    public function insertNewColumnBefore($before, $numberOfColumns = 1)
2363
    {
2364 23
        if (!is_numeric($before)) {
2365 23
            $objReferenceHelper = ReferenceHelper::getInstance();
2366 23
            $objReferenceHelper->insertNewBefore($before . '1', $numberOfColumns, 0, $this);
2367
        } else {
2368
            throw new Exception('Column references should not be numeric.');
2369
        }
2370
2371 23
        return $this;
2372
    }
2373
2374
    /**
2375
     * Insert a new column, updating all possible related data.
2376
     *
2377
     * @param int $beforeColumnIndex Insert before this one (numeric column coordinate of the cell)
2378
     * @param int $numberOfColumns Number of columns to insert
2379
     *
2380
     * @return $this
2381
     */
2382
    public function insertNewColumnBeforeByIndex($beforeColumnIndex, $numberOfColumns = 1)
2383
    {
2384
        if ($beforeColumnIndex >= 1) {
2385
            return $this->insertNewColumnBefore(Coordinate::stringFromColumnIndex($beforeColumnIndex), $numberOfColumns);
2386
        }
2387
2388
        throw new Exception('Columns can only be inserted before at least column A (1).');
2389
    }
2390
2391
    /**
2392
     * Delete a row, updating all possible related data.
2393
     *
2394
     * @param int $row Remove starting with this one
2395
     * @param int $numberOfRows Number of rows to remove
2396
     *
2397
     * @return $this
2398
     */
2399 39
    public function removeRow($row, $numberOfRows = 1)
2400
    {
2401 39
        if ($row < 1) {
2402
            throw new Exception('Rows to be deleted should at least start from row 1.');
2403
        }
2404
2405 39
        $holdRowDimensions = $this->removeRowDimensions($row, $numberOfRows);
2406 39
        $highestRow = $this->getHighestDataRow();
2407 39
        $removedRowsCounter = 0;
2408
2409 39
        for ($r = 0; $r < $numberOfRows; ++$r) {
2410 39
            if ($row + $r <= $highestRow) {
2411 35
                $this->getCellCollection()->removeRow($row + $r);
2412 35
                ++$removedRowsCounter;
2413
            }
2414
        }
2415
2416 39
        $objReferenceHelper = ReferenceHelper::getInstance();
2417 39
        $objReferenceHelper->insertNewBefore('A' . ($row + $numberOfRows), 0, -$numberOfRows, $this);
2418 39
        for ($r = 0; $r < $removedRowsCounter; ++$r) {
2419 35
            $this->getCellCollection()->removeRow($highestRow);
2420 35
            --$highestRow;
2421
        }
2422
2423 39
        $this->rowDimensions = $holdRowDimensions;
2424
2425 39
        return $this;
2426
    }
2427
2428 39
    private function removeRowDimensions(int $row, int $numberOfRows): array
2429
    {
2430 39
        $highRow = $row + $numberOfRows - 1;
2431 39
        $holdRowDimensions = [];
2432 39
        foreach ($this->rowDimensions as $rowDimension) {
2433 4
            $num = $rowDimension->getRowIndex();
2434 4
            if ($num < $row) {
2435 3
                $holdRowDimensions[$num] = $rowDimension;
2436 4
            } elseif ($num > $highRow) {
2437 4
                $num -= $numberOfRows;
2438 4
                $cloneDimension = clone $rowDimension;
2439 4
                $cloneDimension->setRowIndex($num);
2440 4
                $holdRowDimensions[$num] = $cloneDimension;
2441
            }
2442
        }
2443
2444 39
        return $holdRowDimensions;
2445
    }
2446
2447
    /**
2448
     * Remove a column, updating all possible related data.
2449
     *
2450
     * @param string $column Remove starting with this one, eg: 'A'
2451
     * @param int $numberOfColumns Number of columns to remove
2452
     *
2453
     * @return $this
2454
     */
2455 29
    public function removeColumn($column, $numberOfColumns = 1)
2456
    {
2457 29
        if (is_numeric($column)) {
2458
            throw new Exception('Column references should not be numeric.');
2459
        }
2460
2461 29
        $highestColumn = $this->getHighestDataColumn();
2462 29
        $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
2463 29
        $pColumnIndex = Coordinate::columnIndexFromString($column);
2464
2465 29
        $holdColumnDimensions = $this->removeColumnDimensions($pColumnIndex, $numberOfColumns);
2466
2467 29
        $column = Coordinate::stringFromColumnIndex($pColumnIndex + $numberOfColumns);
2468 29
        $objReferenceHelper = ReferenceHelper::getInstance();
2469 29
        $objReferenceHelper->insertNewBefore($column . '1', -$numberOfColumns, 0, $this);
2470
2471 29
        $this->columnDimensions = $holdColumnDimensions;
2472
2473 29
        if ($pColumnIndex > $highestColumnIndex) {
2474 2
            return $this;
2475
        }
2476
2477 27
        $maxPossibleColumnsToBeRemoved = $highestColumnIndex - $pColumnIndex + 1;
2478
2479 27
        for ($c = 0, $n = min($maxPossibleColumnsToBeRemoved, $numberOfColumns); $c < $n; ++$c) {
2480 27
            $this->getCellCollection()->removeColumn($highestColumn);
2481 27
            $highestColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($highestColumn) - 1);
2482
        }
2483
2484 27
        $this->garbageCollect();
2485
2486 27
        return $this;
2487
    }
2488
2489 29
    private function removeColumnDimensions(int $pColumnIndex, int $numberOfColumns): array
2490
    {
2491 29
        $highCol = $pColumnIndex + $numberOfColumns - 1;
2492 29
        $holdColumnDimensions = [];
2493 29
        foreach ($this->columnDimensions as $columnDimension) {
2494 18
            $num = $columnDimension->getColumnNumeric();
2495 18
            if ($num < $pColumnIndex) {
2496 18
                $str = $columnDimension->getColumnIndex();
2497 18
                $holdColumnDimensions[$str] = $columnDimension;
2498 18
            } elseif ($num > $highCol) {
2499 18
                $cloneDimension = clone $columnDimension;
2500 18
                $cloneDimension->setColumnNumeric($num - $numberOfColumns);
2501 18
                $str = $cloneDimension->getColumnIndex();
2502 18
                $holdColumnDimensions[$str] = $cloneDimension;
2503
            }
2504
        }
2505
2506 29
        return $holdColumnDimensions;
2507
    }
2508
2509
    /**
2510
     * Remove a column, updating all possible related data.
2511
     *
2512
     * @param int $columnIndex Remove starting with this one (numeric column coordinate of the cell)
2513
     * @param int $numColumns Number of columns to remove
2514
     *
2515
     * @return $this
2516
     */
2517
    public function removeColumnByIndex($columnIndex, $numColumns = 1)
2518
    {
2519
        if ($columnIndex >= 1) {
2520
            return $this->removeColumn(Coordinate::stringFromColumnIndex($columnIndex), $numColumns);
2521
        }
2522
2523
        throw new Exception('Columns to be deleted should at least start from column A (1)');
2524
    }
2525
2526
    /**
2527
     * Show gridlines?
2528
     */
2529 726
    public function getShowGridlines(): bool
2530
    {
2531 726
        return $this->showGridlines;
2532
    }
2533
2534
    /**
2535
     * Set show gridlines.
2536
     *
2537
     * @param bool $showGridLines Show gridlines (true/false)
2538
     *
2539
     * @return $this
2540
     */
2541 233
    public function setShowGridlines(bool $showGridLines): self
2542
    {
2543 233
        $this->showGridlines = $showGridLines;
2544
2545 233
        return $this;
2546
    }
2547
2548
    /**
2549
     * Print gridlines?
2550
     */
2551 728
    public function getPrintGridlines(): bool
2552
    {
2553 728
        return $this->printGridlines;
2554
    }
2555
2556
    /**
2557
     * Set print gridlines.
2558
     *
2559
     * @param bool $printGridLines Print gridlines (true/false)
2560
     *
2561
     * @return $this
2562
     */
2563 84
    public function setPrintGridlines(bool $printGridLines): self
2564
    {
2565 84
        $this->printGridlines = $printGridLines;
2566
2567 84
        return $this;
2568
    }
2569
2570
    /**
2571
     * Show row and column headers?
2572
     */
2573 315
    public function getShowRowColHeaders(): bool
2574
    {
2575 315
        return $this->showRowColHeaders;
2576
    }
2577
2578
    /**
2579
     * Set show row and column headers.
2580
     *
2581
     * @param bool $showRowColHeaders Show row and column headers (true/false)
2582
     *
2583
     * @return $this
2584
     */
2585 223
    public function setShowRowColHeaders(bool $showRowColHeaders): self
2586
    {
2587 223
        $this->showRowColHeaders = $showRowColHeaders;
2588
2589 223
        return $this;
2590
    }
2591
2592
    /**
2593
     * Show summary below? (Row/Column outlining).
2594
     */
2595 315
    public function getShowSummaryBelow(): bool
2596
    {
2597 315
        return $this->showSummaryBelow;
2598
    }
2599
2600
    /**
2601
     * Set show summary below.
2602
     *
2603
     * @param bool $showSummaryBelow Show summary below (true/false)
2604
     *
2605
     * @return $this
2606
     */
2607 226
    public function setShowSummaryBelow(bool $showSummaryBelow): self
2608
    {
2609 226
        $this->showSummaryBelow = $showSummaryBelow;
2610
2611 226
        return $this;
2612
    }
2613
2614
    /**
2615
     * Show summary right? (Row/Column outlining).
2616
     */
2617 315
    public function getShowSummaryRight(): bool
2618
    {
2619 315
        return $this->showSummaryRight;
2620
    }
2621
2622
    /**
2623
     * Set show summary right.
2624
     *
2625
     * @param bool $showSummaryRight Show summary right (true/false)
2626
     *
2627
     * @return $this
2628
     */
2629 226
    public function setShowSummaryRight(bool $showSummaryRight): self
2630
    {
2631 226
        $this->showSummaryRight = $showSummaryRight;
2632
2633 226
        return $this;
2634
    }
2635
2636
    /**
2637
     * Get comments.
2638
     *
2639
     * @return Comment[]
2640
     */
2641 752
    public function getComments()
2642
    {
2643 752
        return $this->comments;
2644
    }
2645
2646
    /**
2647
     * Set comments array for the entire sheet.
2648
     *
2649
     * @param Comment[] $comments
2650
     *
2651
     * @return $this
2652
     */
2653 68
    public function setComments(array $comments): self
2654
    {
2655 68
        $this->comments = $comments;
2656
2657 68
        return $this;
2658
    }
2659
2660
    /**
2661
     * Remove comment from cell.
2662
     *
2663
     * @param array<int>|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
2664
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2665
     *
2666
     * @return $this
2667
     */
2668 41
    public function removeComment($cellCoordinate): self
2669
    {
2670 41
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
2671
2672 41
        if (Coordinate::coordinateIsRange($cellAddress)) {
2673
            throw new Exception('Cell coordinate string can not be a range of cells.');
2674 41
        } elseif (strpos($cellAddress, '$') !== false) {
2675
            throw new Exception('Cell coordinate string must not be absolute.');
2676 41
        } elseif ($cellAddress == '') {
2677
            throw new Exception('Cell coordinate can not be zero-length string.');
2678
        }
2679
        // Check if we have a comment for this cell and delete it
2680 41
        if (isset($this->comments[$cellAddress])) {
2681 2
            unset($this->comments[$cellAddress]);
2682
        }
2683
2684 41
        return $this;
2685
    }
2686
2687
    /**
2688
     * Get comment for cell.
2689
     *
2690
     * @param array<int>|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
2691
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2692
     */
2693 79
    public function getComment($cellCoordinate): Comment
2694
    {
2695 79
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
2696
2697 79
        if (Coordinate::coordinateIsRange($cellAddress)) {
2698
            throw new Exception('Cell coordinate string can not be a range of cells.');
2699 79
        } elseif (strpos($cellAddress, '$') !== false) {
2700
            throw new Exception('Cell coordinate string must not be absolute.');
2701 79
        } elseif ($cellAddress == '') {
2702
            throw new Exception('Cell coordinate can not be zero-length string.');
2703
        }
2704
2705
        // Check if we already have a comment for this cell.
2706 79
        if (isset($this->comments[$cellAddress])) {
2707 51
            return $this->comments[$cellAddress];
2708
        }
2709
2710
        // If not, create a new comment.
2711 79
        $newComment = new Comment();
2712 79
        $this->comments[$cellAddress] = $newComment;
2713
2714 79
        return $newComment;
2715
    }
2716
2717
    /**
2718
     * Get comment for cell by using numeric cell coordinates.
2719
     *
2720
     * @deprecated 1.23.0
2721
     *      Use the getComment() method with a cell address such as 'C5' instead;,
2722
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2723
     * @see Worksheet::getComment()
2724
     *
2725
     * @param int $columnIndex Numeric column coordinate of the cell
2726
     * @param int $row Numeric row coordinate of the cell
2727
     */
2728 1
    public function getCommentByColumnAndRow($columnIndex, $row): Comment
2729
    {
2730 1
        return $this->getComment(Coordinate::stringFromColumnIndex($columnIndex) . $row);
2731
    }
2732
2733
    /**
2734
     * Get active cell.
2735
     *
2736
     * @return string Example: 'A1'
2737
     */
2738 8507
    public function getActiveCell()
2739
    {
2740 8507
        return $this->activeCell;
2741
    }
2742
2743
    /**
2744
     * Get selected cells.
2745
     *
2746
     * @return string
2747
     */
2748 9081
    public function getSelectedCells()
2749
    {
2750 9081
        return $this->selectedCells;
2751
    }
2752
2753
    /**
2754
     * Selected cell.
2755
     *
2756
     * @param string $coordinate Cell (i.e. A1)
2757
     *
2758
     * @return $this
2759
     */
2760 21
    public function setSelectedCell($coordinate)
2761
    {
2762 21
        return $this->setSelectedCells($coordinate);
2763
    }
2764
2765
    /**
2766
     * Select a range of cells.
2767
     *
2768
     * @param AddressRange|array<int>|CellAddress|int|string $coordinate A simple string containing a Cell range like 'A1:E10'
2769
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2770
     *              or a CellAddress or AddressRange object.
2771
     *
2772
     * @return $this
2773
     */
2774 9147
    public function setSelectedCells($coordinate)
2775
    {
2776 9147
        if (is_string($coordinate)) {
2777 9147
            $coordinate = Validations::definedNameToCoordinate($coordinate, $this);
2778
        }
2779 9147
        $coordinate = Validations::validateCellOrCellRange($coordinate);
2780
2781 9147
        if (Coordinate::coordinateIsRange($coordinate)) {
2782 408
            [$first] = Coordinate::splitRange($coordinate);
2783 408
            $this->activeCell = $first[0];
2784
        } else {
2785 9090
            $this->activeCell = $coordinate;
2786
        }
2787 9147
        $this->selectedCells = $coordinate;
2788
2789 9147
        return $this;
2790
    }
2791
2792
    /**
2793
     * Selected cell by using numeric cell coordinates.
2794
     *
2795
     * @deprecated 1.23.0
2796
     *      Use the setSelectedCells() method with a cell address such as 'C5' instead;,
2797
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2798
     * @see Worksheet::setSelectedCells()
2799
     *
2800
     * @param int $columnIndex Numeric column coordinate of the cell
2801
     * @param int $row Numeric row coordinate of the cell
2802
     *
2803
     * @return $this
2804
     */
2805
    public function setSelectedCellByColumnAndRow($columnIndex, $row)
2806
    {
2807
        return $this->setSelectedCells(Coordinate::stringFromColumnIndex($columnIndex) . $row);
2808
    }
2809
2810
    /**
2811
     * Get right-to-left.
2812
     *
2813
     * @return bool
2814
     */
2815 315
    public function getRightToLeft()
2816
    {
2817 315
        return $this->rightToLeft;
2818
    }
2819
2820
    /**
2821
     * Set right-to-left.
2822
     *
2823
     * @param bool $value Right-to-left true/false
2824
     *
2825
     * @return $this
2826
     */
2827 84
    public function setRightToLeft($value)
2828
    {
2829 84
        $this->rightToLeft = $value;
2830
2831 84
        return $this;
2832
    }
2833
2834
    /**
2835
     * Fill worksheet from values in array.
2836
     *
2837
     * @param array $source Source array
2838
     * @param mixed $nullValue Value in source array that stands for blank cell
2839
     * @param string $startCell Insert array starting from this cell address as the top left coordinate
2840
     * @param bool $strictNullComparison Apply strict comparison when testing for null values in the array
2841
     *
2842
     * @return $this
2843
     */
2844 492
    public function fromArray(array $source, $nullValue = null, $startCell = 'A1', $strictNullComparison = false)
2845
    {
2846
        //    Convert a 1-D array to 2-D (for ease of looping)
2847 492
        if (!is_array(end($source))) {
2848 31
            $source = [$source];
2849
        }
2850
2851
        // start coordinate
2852 492
        [$startColumn, $startRow] = Coordinate::coordinateFromString($startCell);
2853
2854
        // Loop through $source
2855 492
        foreach ($source as $rowData) {
2856 492
            $currentColumn = $startColumn;
2857 492
            foreach ($rowData as $cellValue) {
2858 491
                if ($strictNullComparison) {
2859 233
                    if ($cellValue !== $nullValue) {
2860
                        // Set cell value
2861 233
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
2862
                    }
2863
                } else {
2864 258
                    if ($cellValue != $nullValue) {
2865
                        // Set cell value
2866 255
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
2867
                    }
2868
                }
2869 491
                ++$currentColumn;
2870
            }
2871 492
            ++$startRow;
2872
        }
2873
2874 492
        return $this;
2875
    }
2876
2877
    /**
2878
     * Create array from a range of cells.
2879
     *
2880
     * @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
2881
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
2882
     * @param bool $calculateFormulas Should formulas be calculated?
2883
     * @param bool $formatData Should formatting be applied to cell values?
2884
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
2885
     *                               True - Return rows and columns indexed by their actual row and column IDs
2886
     *
2887
     * @return array
2888
     */
2889 92
    public function rangeToArray($range, $nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
2890
    {
2891
        // Returnvalue
2892 92
        $returnValue = [];
2893
        //    Identify the range that we need to extract from the worksheet
2894 92
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range);
2895 92
        $minCol = Coordinate::stringFromColumnIndex($rangeStart[0]);
2896 92
        $minRow = $rangeStart[1];
2897 92
        $maxCol = Coordinate::stringFromColumnIndex($rangeEnd[0]);
2898 92
        $maxRow = $rangeEnd[1];
2899
2900 92
        ++$maxCol;
2901
        // Loop through rows
2902 92
        $r = -1;
2903 92
        for ($row = $minRow; $row <= $maxRow; ++$row) {
2904 92
            $rRef = $returnCellRef ? $row : ++$r;
2905 92
            $c = -1;
2906
            // Loop through columns in the current row
2907 92
            for ($col = $minCol; $col != $maxCol; ++$col) {
2908 92
                $cRef = $returnCellRef ? $col : ++$c;
2909
                //    Using getCell() will create a new cell if it doesn't already exist. We don't want that to happen
2910
                //        so we test and retrieve directly against cellCollection
2911 92
                if ($this->cellCollection->has($col . $row)) {
2912
                    // Cell exists
2913 92
                    $cell = $this->cellCollection->get($col . $row);
2914 92
                    if ($cell->getValue() !== null) {
2915 92
                        if ($cell->getValue() instanceof RichText) {
2916 2
                            $returnValue[$rRef][$cRef] = $cell->getValue()->getPlainText();
2917
                        } else {
2918 92
                            if ($calculateFormulas) {
2919 87
                                $returnValue[$rRef][$cRef] = $cell->getCalculatedValue();
2920
                            } else {
2921 6
                                $returnValue[$rRef][$cRef] = $cell->getValue();
2922
                            }
2923
                        }
2924
2925 92
                        if ($formatData) {
2926 77
                            $style = $this->parent->getCellXfByIndex($cell->getXfIndex());
2927 77
                            $returnValue[$rRef][$cRef] = NumberFormat::toFormattedString(
2928 77
                                $returnValue[$rRef][$cRef],
2929 92
                                ($style && $style->getNumberFormat()) ? $style->getNumberFormat()->getFormatCode() : NumberFormat::FORMAT_GENERAL
2930
                            );
2931
                        }
2932
                    } else {
2933
                        // Cell holds a NULL
2934 92
                        $returnValue[$rRef][$cRef] = $nullValue;
2935
                    }
2936
                } else {
2937
                    // Cell doesn't exist
2938 24
                    $returnValue[$rRef][$cRef] = $nullValue;
2939
                }
2940
            }
2941
        }
2942
2943
        // Return
2944 92
        return $returnValue;
2945
    }
2946
2947 16
    private function validateNamedRange(string $definedName, bool $returnNullIfInvalid = false): ?DefinedName
2948
    {
2949 16
        $namedRange = DefinedName::resolveName($definedName, $this);
2950 16
        if ($namedRange === null) {
2951 5
            if ($returnNullIfInvalid) {
2952 4
                return null;
2953
            }
2954
2955 1
            throw new Exception('Named Range ' . $definedName . ' does not exist.');
2956
        }
2957
2958 11
        if ($namedRange->isFormula()) {
2959
            if ($returnNullIfInvalid) {
2960
                return null;
2961
            }
2962
2963
            throw new Exception('Defined Named ' . $definedName . ' is a formula, not a range or cell.');
2964
        }
2965
2966 11
        if ($namedRange->getLocalOnly() && $this->getHashCode() !== $namedRange->getWorksheet()->getHashCode()) {
2967
            if ($returnNullIfInvalid) {
2968
                return null;
2969
            }
2970
2971
            throw new Exception(
2972
                'Named range ' . $definedName . ' is not accessible from within sheet ' . $this->getTitle()
2973
            );
2974
        }
2975
2976 11
        return $namedRange;
2977
    }
2978
2979
    /**
2980
     * Create array from a range of cells.
2981
     *
2982
     * @param string $definedName The Named Range that should be returned
2983
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
2984
     * @param bool $calculateFormulas Should formulas be calculated?
2985
     * @param bool $formatData Should formatting be applied to cell values?
2986
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
2987
     *                                True - Return rows and columns indexed by their actual row and column IDs
2988
     *
2989
     * @return array
2990
     */
2991 2
    public function namedRangeToArray(string $definedName, $nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
2992
    {
2993 2
        $namedRange = $this->validateNamedRange($definedName);
2994 1
        $workSheet = $namedRange->getWorksheet();
2995
        /** @phpstan-ignore-next-line */
2996 1
        $cellRange = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!');
2997 1
        $cellRange = str_replace('$', '', $cellRange);
2998
2999 1
        return $workSheet->rangeToArray($cellRange, $nullValue, $calculateFormulas, $formatData, $returnCellRef);
3000
    }
3001
3002
    /**
3003
     * Create array from worksheet.
3004
     *
3005
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
3006
     * @param bool $calculateFormulas Should formulas be calculated?
3007
     * @param bool $formatData Should formatting be applied to cell values?
3008
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3009
     *                               True - Return rows and columns indexed by their actual row and column IDs
3010
     *
3011
     * @return array
3012
     */
3013 41
    public function toArray($nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
3014
    {
3015
        // Garbage collect...
3016 41
        $this->garbageCollect();
3017
3018
        //    Identify the range that we need to extract from the worksheet
3019 41
        $maxCol = $this->getHighestColumn();
3020 41
        $maxRow = $this->getHighestRow();
3021
3022
        // Return
3023 41
        return $this->rangeToArray('A1:' . $maxCol . $maxRow, $nullValue, $calculateFormulas, $formatData, $returnCellRef);
3024
    }
3025
3026
    /**
3027
     * Get row iterator.
3028
     *
3029
     * @param int $startRow The row number at which to start iterating
3030
     * @param int $endRow The row number at which to stop iterating
3031
     *
3032
     * @return RowIterator
3033
     */
3034 63
    public function getRowIterator($startRow = 1, $endRow = null)
3035
    {
3036 63
        return new RowIterator($this, $startRow, $endRow);
3037
    }
3038
3039
    /**
3040
     * Get column iterator.
3041
     *
3042
     * @param string $startColumn The column address at which to start iterating
3043
     * @param string $endColumn The column address at which to stop iterating
3044
     *
3045
     * @return ColumnIterator
3046
     */
3047 16
    public function getColumnIterator($startColumn = 'A', $endColumn = null)
3048
    {
3049 16
        return new ColumnIterator($this, $startColumn, $endColumn);
3050
    }
3051
3052
    /**
3053
     * Run PhpSpreadsheet garbage collector.
3054
     *
3055
     * @return $this
3056
     */
3057 790
    public function garbageCollect()
3058
    {
3059
        // Flush cache
3060 790
        $this->cellCollection->get('A1');
3061
3062
        // Lookup highest column and highest row if cells are cleaned
3063 790
        $colRow = $this->cellCollection->getHighestRowAndColumn();
3064 790
        $highestRow = $colRow['row'];
3065 790
        $highestColumn = Coordinate::columnIndexFromString($colRow['column']);
3066
3067
        // Loop through column dimensions
3068 790
        foreach ($this->columnDimensions as $dimension) {
3069 103
            $highestColumn = max($highestColumn, Coordinate::columnIndexFromString($dimension->getColumnIndex()));
3070
        }
3071
3072
        // Loop through row dimensions
3073 790
        foreach ($this->rowDimensions as $dimension) {
3074 96
            $highestRow = max($highestRow, $dimension->getRowIndex());
3075
        }
3076
3077
        // Cache values
3078 790
        if ($highestColumn < 1) {
3079
            $this->cachedHighestColumn = 1;
3080
        } else {
3081 790
            $this->cachedHighestColumn = $highestColumn;
3082
        }
3083 790
        $this->cachedHighestRow = $highestRow;
3084
3085
        // Return
3086 790
        return $this;
3087
    }
3088
3089
    /**
3090
     * Get hash code.
3091
     *
3092
     * @return string Hash code
3093
     */
3094 9179
    public function getHashCode()
3095
    {
3096 9179
        if ($this->dirty) {
3097 9179
            $this->hash = md5($this->title . $this->autoFilter . ($this->protection->isProtectionEnabled() ? 't' : 'f') . __CLASS__);
3098 9179
            $this->dirty = false;
3099
        }
3100
3101 9179
        return $this->hash;
3102
    }
3103
3104
    /**
3105
     * Extract worksheet title from range.
3106
     *
3107
     * Example: extractSheetTitle("testSheet!A1") ==> 'A1'
3108
     * Example: extractSheetTitle("testSheet!A1:C3") ==> 'A1:C3'
3109
     * Example: extractSheetTitle("'testSheet 1'!A1", true) ==> ['testSheet 1', 'A1'];
3110
     * Example: extractSheetTitle("'testSheet 1'!A1:C3", true) ==> ['testSheet 1', 'A1:C3'];
3111
     * Example: extractSheetTitle("A1", true) ==> ['', 'A1'];
3112
     * Example: extractSheetTitle("A1:C3", true) ==> ['', 'A1:C3']
3113
     *
3114
     * @param string $range Range to extract title from
3115
     * @param bool $returnRange Return range? (see example)
3116
     *
3117
     * @return mixed
3118
     */
3119 9773
    public static function extractSheetTitle($range, $returnRange = false)
3120
    {
3121 9773
        if (empty($range)) {
3122 10
            return $returnRange ? [null, null] : null;
3123
        }
3124
3125
        // Sheet title included?
3126 9773
        if (($sep = strrpos($range, '!')) === false) {
3127 9747
            return $returnRange ? ['', $range] : '';
3128
        }
3129
3130 1045
        if ($returnRange) {
3131 1045
            return [substr($range, 0, $sep), substr($range, $sep + 1)];
3132
        }
3133
3134 7
        return substr($range, $sep + 1);
3135
    }
3136
3137
    /**
3138
     * Get hyperlink.
3139
     *
3140
     * @param string $cellCoordinate Cell coordinate to get hyperlink for, eg: 'A1'
3141
     *
3142
     * @return Hyperlink
3143
     */
3144 57
    public function getHyperlink($cellCoordinate)
3145
    {
3146
        // return hyperlink if we already have one
3147 57
        if (isset($this->hyperlinkCollection[$cellCoordinate])) {
3148 31
            return $this->hyperlinkCollection[$cellCoordinate];
3149
        }
3150
3151
        // else create hyperlink
3152 57
        $this->hyperlinkCollection[$cellCoordinate] = new Hyperlink();
3153
3154 57
        return $this->hyperlinkCollection[$cellCoordinate];
3155
    }
3156
3157
    /**
3158
     * Set hyperlink.
3159
     *
3160
     * @param string $cellCoordinate Cell coordinate to insert hyperlink, eg: 'A1'
3161
     *
3162
     * @return $this
3163
     */
3164 41
    public function setHyperlink($cellCoordinate, ?Hyperlink $hyperlink = null)
3165
    {
3166 41
        if ($hyperlink === null) {
3167 41
            unset($this->hyperlinkCollection[$cellCoordinate]);
3168
        } else {
3169 20
            $this->hyperlinkCollection[$cellCoordinate] = $hyperlink;
3170
        }
3171
3172 41
        return $this;
3173
    }
3174
3175
    /**
3176
     * Hyperlink at a specific coordinate exists?
3177
     *
3178
     * @param string $coordinate eg: 'A1'
3179
     *
3180
     * @return bool
3181
     */
3182 420
    public function hyperlinkExists($coordinate)
3183
    {
3184 420
        return isset($this->hyperlinkCollection[$coordinate]);
3185
    }
3186
3187
    /**
3188
     * Get collection of hyperlinks.
3189
     *
3190
     * @return Hyperlink[]
3191
     */
3192 368
    public function getHyperlinkCollection()
3193
    {
3194 368
        return $this->hyperlinkCollection;
3195
    }
3196
3197
    /**
3198
     * Get data validation.
3199
     *
3200
     * @param string $cellCoordinate Cell coordinate to get data validation for, eg: 'A1'
3201
     *
3202
     * @return DataValidation
3203
     */
3204 20
    public function getDataValidation($cellCoordinate)
3205
    {
3206
        // return data validation if we already have one
3207 20
        if (isset($this->dataValidationCollection[$cellCoordinate])) {
3208 14
            return $this->dataValidationCollection[$cellCoordinate];
3209
        }
3210
3211
        // else create data validation
3212 20
        $this->dataValidationCollection[$cellCoordinate] = new DataValidation();
3213
3214 20
        return $this->dataValidationCollection[$cellCoordinate];
3215
    }
3216
3217
    /**
3218
     * Set data validation.
3219
     *
3220
     * @param string $cellCoordinate Cell coordinate to insert data validation, eg: 'A1'
3221
     *
3222
     * @return $this
3223
     */
3224 43
    public function setDataValidation($cellCoordinate, ?DataValidation $dataValidation = null)
3225
    {
3226 43
        if ($dataValidation === null) {
3227 43
            unset($this->dataValidationCollection[$cellCoordinate]);
3228
        } else {
3229 4
            $this->dataValidationCollection[$cellCoordinate] = $dataValidation;
3230
        }
3231
3232 43
        return $this;
3233
    }
3234
3235
    /**
3236
     * Data validation at a specific coordinate exists?
3237
     *
3238
     * @param string $coordinate eg: 'A1'
3239
     *
3240
     * @return bool
3241
     */
3242 14
    public function dataValidationExists($coordinate)
3243
    {
3244 14
        return isset($this->dataValidationCollection[$coordinate]);
3245
    }
3246
3247
    /**
3248
     * Get collection of data validations.
3249
     *
3250
     * @return DataValidation[]
3251
     */
3252 368
    public function getDataValidationCollection()
3253
    {
3254 368
        return $this->dataValidationCollection;
3255
    }
3256
3257
    /**
3258
     * Accepts a range, returning it as a range that falls within the current highest row and column of the worksheet.
3259
     *
3260
     * @param string $range
3261
     *
3262
     * @return string Adjusted range value
3263
     */
3264 11
    public function shrinkRangeToFit($range)
3265
    {
3266 11
        $maxCol = $this->getHighestColumn();
3267 11
        $maxRow = $this->getHighestRow();
3268 11
        $maxCol = Coordinate::columnIndexFromString($maxCol);
3269
3270 11
        $rangeBlocks = explode(' ', $range);
3271 11
        foreach ($rangeBlocks as &$rangeSet) {
3272 11
            $rangeBoundaries = Coordinate::getRangeBoundaries($rangeSet);
3273
3274 11
            if (Coordinate::columnIndexFromString($rangeBoundaries[0][0]) > $maxCol) {
3275
                $rangeBoundaries[0][0] = Coordinate::stringFromColumnIndex($maxCol);
3276
            }
3277 11
            if ($rangeBoundaries[0][1] > $maxRow) {
3278
                $rangeBoundaries[0][1] = $maxRow;
3279
            }
3280 11
            if (Coordinate::columnIndexFromString($rangeBoundaries[1][0]) > $maxCol) {
3281
                $rangeBoundaries[1][0] = Coordinate::stringFromColumnIndex($maxCol);
3282
            }
3283 11
            if ($rangeBoundaries[1][1] > $maxRow) {
3284 1
                $rangeBoundaries[1][1] = $maxRow;
3285
            }
3286 11
            $rangeSet = $rangeBoundaries[0][0] . $rangeBoundaries[0][1] . ':' . $rangeBoundaries[1][0] . $rangeBoundaries[1][1];
3287
        }
3288 11
        unset($rangeSet);
3289
3290 11
        return implode(' ', $rangeBlocks);
3291
    }
3292
3293
    /**
3294
     * Get tab color.
3295
     *
3296
     * @return Color
3297
     */
3298 18
    public function getTabColor()
3299
    {
3300 18
        if ($this->tabColor === null) {
3301 18
            $this->tabColor = new Color();
3302
        }
3303
3304 18
        return $this->tabColor;
3305
    }
3306
3307
    /**
3308
     * Reset tab color.
3309
     *
3310
     * @return $this
3311
     */
3312
    public function resetTabColor()
3313
    {
3314
        $this->tabColor = null;
3315
3316
        return $this;
3317
    }
3318
3319
    /**
3320
     * Tab color set?
3321
     *
3322
     * @return bool
3323
     */
3324 324
    public function isTabColorSet()
3325
    {
3326 324
        return $this->tabColor !== null;
3327
    }
3328
3329
    /**
3330
     * Copy worksheet (!= clone!).
3331
     *
3332
     * @return static
3333
     */
3334
    public function copy()
3335
    {
3336
        return clone $this;
3337
    }
3338
3339
    /**
3340
     * Returns a boolean true if the specified row contains no cells. By default, this means that no cell records
3341
     *          exist in the collection for this row. false will be returned otherwise.
3342
     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
3343
     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
3344
     *                  cells, then the row will be considered empty.
3345
     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
3346
     *                  string value cells, then the row will be considered empty.
3347
     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3348
     *                  If the only cells in the collection are null value or empty string value cells, then the row
3349
     *                  will be considered empty.
3350
     *
3351
     * @param int $definitionOfEmptyFlags
3352
     *              Possible Flag Values are:
3353
     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
3354
     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3355
     */
3356 9
    public function isEmptyRow(int $rowId, int $definitionOfEmptyFlags = 0): bool
3357
    {
3358
        try {
3359 9
            $iterator = new RowIterator($this, $rowId, $rowId);
3360 8
            $iterator->seek($rowId);
3361 8
            $row = $iterator->current();
3362 1
        } catch (Exception $e) {
3363 1
            return true;
3364
        }
3365
3366 8
        return $row->isEmpty($definitionOfEmptyFlags);
3367
    }
3368
3369
    /**
3370
     * Returns a boolean true if the specified column contains no cells. By default, this means that no cell records
3371
     *          exist in the collection for this column. false will be returned otherwise.
3372
     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
3373
     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
3374
     *                  cells, then the column will be considered empty.
3375
     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
3376
     *                  string value cells, then the column will be considered empty.
3377
     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3378
     *                  If the only cells in the collection are null value or empty string value cells, then the column
3379
     *                  will be considered empty.
3380
     *
3381
     * @param int $definitionOfEmptyFlags
3382
     *              Possible Flag Values are:
3383
     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
3384
     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3385
     */
3386 9
    public function isEmptyColumn(string $columnId, int $definitionOfEmptyFlags = 0): bool
3387
    {
3388
        try {
3389 9
            $iterator = new ColumnIterator($this, $columnId, $columnId);
3390 8
            $iterator->seek($columnId);
3391 8
            $column = $iterator->current();
3392 1
        } catch (Exception $e) {
3393 1
            return true;
3394
        }
3395
3396 8
        return $column->isEmpty($definitionOfEmptyFlags);
3397
    }
3398
3399
    /**
3400
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
3401
     */
3402 5
    public function __clone()
3403
    {
3404
        // @phpstan-ignore-next-line
3405 5
        foreach ($this as $key => $val) {
3406 5
            if ($key == 'parent') {
3407 5
                continue;
3408
            }
3409
3410 5
            if (is_object($val) || (is_array($val))) {
3411 5
                if ($key == 'cellCollection') {
3412 5
                    $newCollection = $this->cellCollection->cloneCellCollection($this);
3413 5
                    $this->cellCollection = $newCollection;
3414 5
                } elseif ($key == 'drawingCollection') {
3415 5
                    $currentCollection = $this->drawingCollection;
3416 5
                    $this->drawingCollection = new ArrayObject();
3417 5
                    foreach ($currentCollection as $item) {
3418 3
                        if (is_object($item)) {
3419 3
                            $newDrawing = clone $item;
3420 3
                            $newDrawing->setWorksheet($this);
3421
                        }
3422
                    }
3423 5
                } elseif (($key == 'autoFilter') && ($this->autoFilter instanceof AutoFilter)) {
3424 5
                    $newAutoFilter = clone $this->autoFilter;
3425 5
                    $this->autoFilter = $newAutoFilter;
3426 5
                    $this->autoFilter->setParent($this);
3427
                } else {
3428 5
                    $this->{$key} = unserialize(serialize($val));
3429
                }
3430
            }
3431
        }
3432
    }
3433
3434
    /**
3435
     * Define the code name of the sheet.
3436
     *
3437
     * @param string $codeName Same rule as Title minus space not allowed (but, like Excel, change
3438
     *                       silently space to underscore)
3439
     * @param bool $validate False to skip validation of new title. WARNING: This should only be set
3440
     *                       at parse time (by Readers), where titles can be assumed to be valid.
3441
     *
3442
     * @return $this
3443
     */
3444 9861
    public function setCodeName($codeName, $validate = true)
3445
    {
3446
        // Is this a 'rename' or not?
3447 9861
        if ($this->getCodeName() == $codeName) {
3448
            return $this;
3449
        }
3450
3451 9861
        if ($validate) {
3452 9861
            $codeName = str_replace(' ', '_', $codeName); //Excel does this automatically without flinching, we are doing the same
3453
3454
            // Syntax check
3455
            // throw an exception if not valid
3456 9861
            self::checkSheetCodeName($codeName);
3457
3458
            // We use the same code that setTitle to find a valid codeName else not using a space (Excel don't like) but a '_'
3459
3460 9861
            if ($this->getParent()) {
3461
                // Is there already such sheet name?
3462 9852
                if ($this->getParent()->sheetCodeNameExists($codeName)) {
3463
                    // Use name, but append with lowest possible integer
3464
3465 453
                    if (Shared\StringHelper::countCharacters($codeName) > 29) {
3466
                        $codeName = Shared\StringHelper::substring($codeName, 0, 29);
3467
                    }
3468 453
                    $i = 1;
3469 453
                    while ($this->getParent()->sheetCodeNameExists($codeName . '_' . $i)) {
3470 225
                        ++$i;
3471 225
                        if ($i == 10) {
3472 1
                            if (Shared\StringHelper::countCharacters($codeName) > 28) {
3473 1
                                $codeName = Shared\StringHelper::substring($codeName, 0, 28);
3474
                            }
3475 225
                        } elseif ($i == 100) {
3476
                            if (Shared\StringHelper::countCharacters($codeName) > 27) {
3477
                                $codeName = Shared\StringHelper::substring($codeName, 0, 27);
3478
                            }
3479
                        }
3480
                    }
3481
3482 453
                    $codeName .= '_' . $i; // ok, we have a valid name
3483
                }
3484
            }
3485
        }
3486
3487 9861
        $this->codeName = $codeName;
3488
3489 9861
        return $this;
3490
    }
3491
3492
    /**
3493
     * Return the code name of the sheet.
3494
     *
3495
     * @return null|string
3496
     */
3497 9861
    public function getCodeName()
3498
    {
3499 9861
        return $this->codeName;
3500
    }
3501
3502
    /**
3503
     * Sheet has a code name ?
3504
     *
3505
     * @return bool
3506
     */
3507 2
    public function hasCodeName()
3508
    {
3509 2
        return $this->codeName !== null;
3510
    }
3511
3512 4
    public static function nameRequiresQuotes(string $sheetName): bool
3513
    {
3514 4
        return preg_match(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName) !== 1;
3515
    }
3516
}
3517