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