Passed
Push — master ( 9e1378...d02904 )
by Mark
16:48 queued 06:34
created

Worksheet::getChartByIndex()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4.5923

Importance

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