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

Worksheet::setCodeName()   B

Complexity

Conditions 11
Paths 6

Size

Total Lines 46
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 11.727

Importance

Changes 0
Metric Value
cc 11
eloc 21
c 0
b 0
f 0
nc 6
nop 2
dl 0
loc 46
ccs 18
cts 22
cp 0.8182
crap 11.727
rs 7.3166

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\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