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