Passed
Push — master ( fde2cc...f1d90a )
by Mark
17:04 queued 08:09
created

Worksheet::setCodeName()   B

Complexity

Conditions 11
Paths 6

Size

Total Lines 46
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 11.727

Importance

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

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

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