Passed
Push — master ( c31b1d...a40793 )
by Mark
13:39
created

Worksheet::setCodeName()   B

Complexity

Conditions 11
Paths 6

Size

Total Lines 46
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 11.727

Importance

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

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Worksheet;
4
5
use ArrayObject;
6
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
7
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
8
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
9
use PhpOffice\PhpSpreadsheet\Cell\Cell;
10
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
11
use PhpOffice\PhpSpreadsheet\Cell\CellRange;
12
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
13
use PhpOffice\PhpSpreadsheet\Cell\DataType;
14
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
15
use PhpOffice\PhpSpreadsheet\Cell\Hyperlink;
16
use PhpOffice\PhpSpreadsheet\Chart\Chart;
17
use PhpOffice\PhpSpreadsheet\Collection\Cells;
18
use PhpOffice\PhpSpreadsheet\Collection\CellsFactory;
19
use PhpOffice\PhpSpreadsheet\Comment;
20
use PhpOffice\PhpSpreadsheet\DefinedName;
21
use PhpOffice\PhpSpreadsheet\Exception;
22
use PhpOffice\PhpSpreadsheet\IComparable;
23
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
24
use PhpOffice\PhpSpreadsheet\RichText\RichText;
25
use PhpOffice\PhpSpreadsheet\Shared;
26
use PhpOffice\PhpSpreadsheet\Spreadsheet;
27
use PhpOffice\PhpSpreadsheet\Style\Color;
28
use PhpOffice\PhpSpreadsheet\Style\Conditional;
29
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
30
use PhpOffice\PhpSpreadsheet\Style\Style;
31
32
class Worksheet implements IComparable
33
{
34
    // Break types
35
    public const BREAK_NONE = 0;
36
    public const BREAK_ROW = 1;
37
    public const BREAK_COLUMN = 2;
38
39
    // Sheet state
40
    public const SHEETSTATE_VISIBLE = 'visible';
41
    public const SHEETSTATE_HIDDEN = 'hidden';
42
    public const SHEETSTATE_VERYHIDDEN = 'veryHidden';
43
44
    public const MERGE_CELL_CONTENT_EMPTY = 'empty';
45
    public const MERGE_CELL_CONTENT_HIDE = 'hide';
46
    public const MERGE_CELL_CONTENT_MERGE = 'merge';
47
48
    protected const SHEET_NAME_REQUIRES_NO_QUOTES = '/^[_\p{L}][_\p{L}\p{N}]*$/mui';
49
50
    /**
51
     * Maximum 31 characters allowed for sheet title.
52
     *
53
     * @var int
54
     */
55
    const SHEET_TITLE_MAXIMUM_LENGTH = 31;
56
57
    /**
58
     * Invalid characters in sheet title.
59
     *
60
     * @var array
61
     */
62
    private static $invalidCharacters = ['*', ':', '/', '\\', '?', '[', ']'];
63
64
    /**
65
     * Parent spreadsheet.
66
     *
67
     * @var Spreadsheet
68
     */
69
    private $parent;
70
71
    /**
72
     * Collection of cells.
73
     *
74
     * @var Cells
75
     */
76
    private $cellCollection;
77
78
    /**
79
     * Collection of row dimensions.
80
     *
81
     * @var RowDimension[]
82
     */
83
    private $rowDimensions = [];
84
85
    /**
86
     * Default row dimension.
87
     *
88
     * @var RowDimension
89
     */
90
    private $defaultRowDimension;
91
92
    /**
93
     * Collection of column dimensions.
94
     *
95
     * @var ColumnDimension[]
96
     */
97
    private $columnDimensions = [];
98
99
    /**
100
     * Default column dimension.
101
     *
102
     * @var ColumnDimension
103
     */
104
    private $defaultColumnDimension;
105
106
    /**
107
     * Collection of drawings.
108
     *
109
     * @var ArrayObject<int, BaseDrawing>
110
     */
111
    private $drawingCollection;
112
113
    /**
114
     * Collection of Chart objects.
115
     *
116
     * @var ArrayObject<int, Chart>
117
     */
118
    private $chartCollection;
119
120
    /**
121
     * Collection of Table objects.
122
     *
123
     * @var ArrayObject<int, Table>
124
     */
125
    private $tableCollection;
126
127
    /**
128
     * Worksheet title.
129
     *
130
     * @var string
131
     */
132
    private $title;
133
134
    /**
135
     * Sheet state.
136
     *
137
     * @var string
138
     */
139
    private $sheetState;
140
141
    /**
142
     * Page setup.
143
     *
144
     * @var PageSetup
145
     */
146
    private $pageSetup;
147
148
    /**
149
     * Page margins.
150
     *
151
     * @var PageMargins
152
     */
153
    private $pageMargins;
154
155
    /**
156
     * Page header/footer.
157
     *
158
     * @var HeaderFooter
159
     */
160
    private $headerFooter;
161
162
    /**
163
     * Sheet view.
164
     *
165
     * @var SheetView
166
     */
167
    private $sheetView;
168
169
    /**
170
     * Protection.
171
     *
172
     * @var Protection
173
     */
174
    private $protection;
175
176
    /**
177
     * Collection of styles.
178
     *
179
     * @var Style[]
180
     */
181
    private $styles = [];
182
183
    /**
184
     * Conditional styles. Indexed by cell coordinate, e.g. 'A1'.
185
     *
186
     * @var array
187
     */
188
    private $conditionalStylesCollection = [];
189
190
    /**
191
     * Collection of breaks.
192
     *
193
     * @var int[]
194
     */
195
    private $breaks = [];
196
197
    /**
198
     * Collection of merged cell ranges.
199
     *
200
     * @var string[]
201
     */
202
    private $mergeCells = [];
203
204
    /**
205
     * Collection of protected cell ranges.
206
     *
207
     * @var string[]
208
     */
209
    private $protectedCells = [];
210
211
    /**
212
     * Autofilter Range and selection.
213
     *
214
     * @var AutoFilter
215
     */
216
    private $autoFilter;
217
218
    /**
219
     * Freeze pane.
220
     *
221
     * @var null|string
222
     */
223
    private $freezePane;
224
225
    /**
226
     * Default position of the right bottom pane.
227
     *
228
     * @var null|string
229
     */
230
    private $topLeftCell;
231
232
    /**
233
     * Show gridlines?
234
     *
235
     * @var bool
236
     */
237
    private $showGridlines = true;
238
239
    /**
240
     * Print gridlines?
241
     *
242
     * @var bool
243
     */
244
    private $printGridlines = false;
245
246
    /**
247
     * Show row and column headers?
248
     *
249
     * @var bool
250
     */
251
    private $showRowColHeaders = true;
252
253
    /**
254
     * Show summary below? (Row/Column outline).
255
     *
256
     * @var bool
257
     */
258
    private $showSummaryBelow = true;
259
260
    /**
261
     * Show summary right? (Row/Column outline).
262
     *
263
     * @var bool
264
     */
265
    private $showSummaryRight = true;
266
267
    /**
268
     * Collection of comments.
269
     *
270
     * @var Comment[]
271
     */
272
    private $comments = [];
273
274
    /**
275
     * Active cell. (Only one!).
276
     *
277
     * @var string
278
     */
279
    private $activeCell = 'A1';
280
281
    /**
282
     * Selected cells.
283
     *
284
     * @var string
285
     */
286
    private $selectedCells = 'A1';
287
288
    /**
289
     * Cached highest column.
290
     *
291
     * @var int
292
     */
293
    private $cachedHighestColumn = 1;
294
295
    /**
296
     * Cached highest row.
297
     *
298
     * @var int
299
     */
300
    private $cachedHighestRow = 1;
301
302
    /**
303
     * Right-to-left?
304
     *
305
     * @var bool
306
     */
307
    private $rightToLeft = false;
308
309
    /**
310
     * Hyperlinks. Indexed by cell coordinate, e.g. 'A1'.
311
     *
312
     * @var array
313
     */
314
    private $hyperlinkCollection = [];
315
316
    /**
317
     * Data validation objects. Indexed by cell coordinate, e.g. 'A1'.
318
     *
319
     * @var array
320
     */
321
    private $dataValidationCollection = [];
322
323
    /**
324
     * Tab color.
325
     *
326
     * @var null|Color
327
     */
328
    private $tabColor;
329
330
    /**
331
     * Dirty flag.
332
     *
333
     * @var bool
334
     */
335
    private $dirty = true;
336
337
    /**
338
     * Hash.
339
     *
340
     * @var string
341
     */
342
    private $hash;
343
344
    /**
345
     * CodeName.
346
     *
347
     * @var string
348
     */
349
    private $codeName;
350
351
    /**
352
     * Create a new worksheet.
353
     *
354
     * @param string $title
355
     */
356 9963
    public function __construct(?Spreadsheet $parent = null, $title = 'Worksheet')
357
    {
358
        // Set parent and title
359 9963
        $this->parent = $parent;
360 9963
        $this->setTitle($title, false);
361
        // setTitle can change $pTitle
362 9963
        $this->setCodeName($this->getTitle());
363 9963
        $this->setSheetState(self::SHEETSTATE_VISIBLE);
364
365 9963
        $this->cellCollection = CellsFactory::getInstance($this);
366
        // Set page setup
367 9963
        $this->pageSetup = new PageSetup();
368
        // Set page margins
369 9963
        $this->pageMargins = new PageMargins();
370
        // Set page header/footer
371 9963
        $this->headerFooter = new HeaderFooter();
372
        // Set sheet view
373 9963
        $this->sheetView = new SheetView();
374
        // Drawing collection
375 9963
        $this->drawingCollection = new ArrayObject();
376
        // Chart collection
377 9963
        $this->chartCollection = new ArrayObject();
378
        // Protection
379 9963
        $this->protection = new Protection();
380
        // Default row dimension
381 9963
        $this->defaultRowDimension = new RowDimension(null);
382
        // Default column dimension
383 9963
        $this->defaultColumnDimension = new ColumnDimension(null);
384
        // AutoFilter
385 9963
        $this->autoFilter = new AutoFilter('', $this);
386
        // Table collection
387 9963
        $this->tableCollection = new ArrayObject();
388
    }
389
390
    /**
391
     * Disconnect all cells from this Worksheet object,
392
     * typically so that the worksheet object can be unset.
393
     */
394 8531
    public function disconnectCells(): void
395
    {
396 8531
        if ($this->cellCollection !== null) {
397 8531
            $this->cellCollection->unsetWorksheetCells();
398
            // @phpstan-ignore-next-line
399 8531
            $this->cellCollection = null;
400
        }
401
        //    detach ourself from the workbook, so that it can then delete this worksheet successfully
402
        // @phpstan-ignore-next-line
403 8531
        $this->parent = null;
404
    }
405
406
    /**
407
     * Code to execute when this worksheet is unset().
408
     */
409 104
    public function __destruct()
410
    {
411 104
        Calculation::getInstance($this->parent)->clearCalculationCacheForWorksheet($this->title);
412
413 104
        $this->disconnectCells();
414 104
        $this->rowDimensions = [];
415
    }
416
417
    /**
418
     * Return the cell collection.
419
     *
420
     * @return Cells
421
     */
422 9712
    public function getCellCollection()
423
    {
424 9712
        return $this->cellCollection;
425
    }
426
427
    /**
428
     * Get array of invalid characters for sheet title.
429
     *
430
     * @return array
431
     */
432
    public static function getInvalidCharacters()
433
    {
434
        return self::$invalidCharacters;
435
    }
436
437
    /**
438
     * Check sheet code name for valid Excel syntax.
439
     *
440
     * @param string $sheetCodeName The string to check
441
     *
442
     * @return string The valid string
443
     */
444 9963
    private static function checkSheetCodeName($sheetCodeName)
445
    {
446 9963
        $charCount = Shared\StringHelper::countCharacters($sheetCodeName);
447 9963
        if ($charCount == 0) {
448
            throw new Exception('Sheet code name cannot be empty.');
449
        }
450
        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ] and  first and last characters cannot be a "'"
451
        if (
452 9963
            (str_replace(self::$invalidCharacters, '', $sheetCodeName) !== $sheetCodeName) ||
453 9963
            (Shared\StringHelper::substring($sheetCodeName, -1, 1) == '\'') ||
454 9963
            (Shared\StringHelper::substring($sheetCodeName, 0, 1) == '\'')
455
        ) {
456 1
            throw new Exception('Invalid character found in sheet code name');
457
        }
458
459
        // Enforce maximum characters allowed for sheet title
460 9963
        if ($charCount > self::SHEET_TITLE_MAXIMUM_LENGTH) {
461 1
            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet code name.');
462
        }
463
464 9963
        return $sheetCodeName;
465
    }
466
467
    /**
468
     * Check sheet title for valid Excel syntax.
469
     *
470
     * @param string $sheetTitle The string to check
471
     *
472
     * @return string The valid string
473
     */
474 9963
    private static function checkSheetTitle($sheetTitle)
475
    {
476
        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ]
477 9963
        if (str_replace(self::$invalidCharacters, '', $sheetTitle) !== $sheetTitle) {
478 1
            throw new Exception('Invalid character found in sheet title');
479
        }
480
481
        // Enforce maximum characters allowed for sheet title
482 9963
        if (Shared\StringHelper::countCharacters($sheetTitle) > self::SHEET_TITLE_MAXIMUM_LENGTH) {
483 1
            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet title.');
484
        }
485
486 9963
        return $sheetTitle;
487
    }
488
489
    /**
490
     * Get a sorted list of all cell coordinates currently held in the collection by row and column.
491
     *
492
     * @param bool $sorted Also sort the cell collection?
493
     *
494
     * @return string[]
495
     */
496 1039
    public function getCoordinates($sorted = true)
497
    {
498 1039
        if ($this->cellCollection == null) {
499
            return [];
500
        }
501
502 1039
        if ($sorted) {
503 340
            return $this->cellCollection->getSortedCoordinates();
504
        }
505
506 963
        return $this->cellCollection->getCoordinates();
507
    }
508
509
    /**
510
     * Get collection of row dimensions.
511
     *
512
     * @return RowDimension[]
513
     */
514 807
    public function getRowDimensions()
515
    {
516 807
        return $this->rowDimensions;
517
    }
518
519
    /**
520
     * Get default row dimension.
521
     *
522
     * @return RowDimension
523
     */
524 777
    public function getDefaultRowDimension()
525
    {
526 777
        return $this->defaultRowDimension;
527
    }
528
529
    /**
530
     * Get collection of column dimensions.
531
     *
532
     * @return ColumnDimension[]
533
     */
534 807
    public function getColumnDimensions()
535
    {
536 807
        return $this->columnDimensions;
537
    }
538
539
    /**
540
     * Get default column dimension.
541
     *
542
     * @return ColumnDimension
543
     */
544 351
    public function getDefaultColumnDimension()
545
    {
546 351
        return $this->defaultColumnDimension;
547
    }
548
549
    /**
550
     * Get collection of drawings.
551
     *
552
     * @return ArrayObject<int, BaseDrawing>
553
     */
554 797
    public function getDrawingCollection()
555
    {
556 797
        return $this->drawingCollection;
557
    }
558
559
    /**
560
     * Get collection of charts.
561
     *
562
     * @return ArrayObject<int, Chart>
563
     */
564 70
    public function getChartCollection()
565
    {
566 70
        return $this->chartCollection;
567
    }
568
569
    /**
570
     * Add chart.
571
     *
572
     * @param null|int $chartIndex Index where chart should go (0,1,..., or null for last)
573
     *
574
     * @return Chart
575
     */
576 75
    public function addChart(Chart $chart, $chartIndex = null)
577
    {
578 75
        $chart->setWorksheet($this);
579 75
        if ($chartIndex === null) {
580 75
            $this->chartCollection[] = $chart;
581
        } else {
582
            // Insert the chart at the requested index
583
            // @phpstan-ignore-next-line
584
            array_splice(/** @scrutinizer ignore-type */ $this->chartCollection, $chartIndex, 0, [$chart]);
585
        }
586
587 75
        return $chart;
588
    }
589
590
    /**
591
     * Return the count of charts on this worksheet.
592
     *
593
     * @return int The number of charts
594
     */
595 58
    public function getChartCount()
596
    {
597 58
        return count($this->chartCollection);
598
    }
599
600
    /**
601
     * Get a chart by its index position.
602
     *
603
     * @param ?string $index Chart index position
604
     *
605
     * @return Chart|false
606
     */
607 53
    public function getChartByIndex($index)
608
    {
609 53
        $chartCount = count($this->chartCollection);
610 53
        if ($chartCount == 0) {
611
            return false;
612
        }
613 53
        if ($index === null) {
614
            $index = --$chartCount;
615
        }
616 53
        if (!isset($this->chartCollection[$index])) {
617
            return false;
618
        }
619
620 53
        return $this->chartCollection[$index];
621
    }
622
623
    /**
624
     * Return an array of the names of charts on this worksheet.
625
     *
626
     * @return string[] The names of charts
627
     */
628 4
    public function getChartNames()
629
    {
630 4
        $chartNames = [];
631 4
        foreach ($this->chartCollection as $chart) {
632 4
            $chartNames[] = $chart->getName();
633
        }
634
635 4
        return $chartNames;
636
    }
637
638
    /**
639
     * Get a chart by name.
640
     *
641
     * @param string $chartName Chart name
642
     *
643
     * @return Chart|false
644
     */
645 4
    public function getChartByName($chartName)
646
    {
647 4
        $chartCount = count($this->chartCollection);
648 4
        if ($chartCount == 0) {
649
            return false;
650
        }
651 4
        foreach ($this->chartCollection as $index => $chart) {
652 4
            if ($chart->getName() == $chartName) {
653 4
                return $this->chartCollection[$index];
654
            }
655
        }
656
657
        return false;
658
    }
659
660
    /**
661
     * Refresh column dimensions.
662
     *
663
     * @return $this
664
     */
665 23
    public function refreshColumnDimensions()
666
    {
667 23
        $newColumnDimensions = [];
668 23
        foreach ($this->getColumnDimensions() as $objColumnDimension) {
669 23
            $newColumnDimensions[$objColumnDimension->getColumnIndex()] = $objColumnDimension;
670
        }
671
672 23
        $this->columnDimensions = $newColumnDimensions;
673
674 23
        return $this;
675
    }
676
677
    /**
678
     * Refresh row dimensions.
679
     *
680
     * @return $this
681
     */
682 5
    public function refreshRowDimensions()
683
    {
684 5
        $newRowDimensions = [];
685 5
        foreach ($this->getRowDimensions() as $objRowDimension) {
686 5
            $newRowDimensions[$objRowDimension->getRowIndex()] = $objRowDimension;
687
        }
688
689 5
        $this->rowDimensions = $newRowDimensions;
690
691 5
        return $this;
692
    }
693
694
    /**
695
     * Calculate worksheet dimension.
696
     *
697
     * @return string String containing the dimension of this worksheet
698
     */
699 301
    public function calculateWorksheetDimension()
700
    {
701
        // Return
702 301
        return 'A1:' . $this->getHighestColumn() . $this->getHighestRow();
703
    }
704
705
    /**
706
     * Calculate worksheet data dimension.
707
     *
708
     * @return string String containing the dimension of this worksheet that actually contain data
709
     */
710 420
    public function calculateWorksheetDataDimension()
711
    {
712
        // Return
713 420
        return 'A1:' . $this->getHighestDataColumn() . $this->getHighestDataRow();
714
    }
715
716
    /**
717
     * Calculate widths for auto-size columns.
718
     *
719
     * @return $this
720
     */
721 566
    public function calculateColumnWidths()
722
    {
723
        // initialize $autoSizes array
724 566
        $autoSizes = [];
725 566
        foreach ($this->getColumnDimensions() as $colDimension) {
726 92
            if ($colDimension->getAutoSize()) {
727 45
                $autoSizes[$colDimension->getColumnIndex()] = -1;
728
            }
729
        }
730
731
        // There is only something to do if there are some auto-size columns
732 566
        if (!empty($autoSizes)) {
733
            // build list of cells references that participate in a merge
734 45
            $isMergeCell = [];
735 45
            foreach ($this->getMergeCells() as $cells) {
736 16
                foreach (Coordinate::extractAllCellReferencesInRange($cells) as $cellReference) {
737 16
                    $isMergeCell[$cellReference] = true;
738
                }
739
            }
740
741 45
            $autoFilterRange = $autoFilterFirstRowRange = $this->autoFilter->getRange();
742 45
            if (!empty($autoFilterRange)) {
743 1
                $autoFilterRangeBoundaries = Coordinate::rangeBoundaries($autoFilterRange);
744 1
                $autoFilterFirstRowRange = (string) new CellRange(
745 1
                    CellAddress::fromColumnAndRow($autoFilterRangeBoundaries[0][0], $autoFilterRangeBoundaries[0][1]),
746 1
                    CellAddress::fromColumnAndRow($autoFilterRangeBoundaries[1][0], $autoFilterRangeBoundaries[0][1])
747
                );
748
            }
749
750
            // loop through all cells in the worksheet
751 45
            foreach ($this->getCoordinates(false) as $coordinate) {
752 45
                $cell = $this->getCellOrNull($coordinate);
753
754 45
                if ($cell !== null && isset($autoSizes[$this->cellCollection->getCurrentColumn()])) {
755
                    //Determine if cell is in merge range
756 45
                    $isMerged = isset($isMergeCell[$this->cellCollection->getCurrentCoordinate()]);
757
758
                    //By default merged cells should be ignored
759 45
                    $isMergedButProceed = false;
760
761
                    //The only exception is if it's a merge range value cell of a 'vertical' range (1 column wide)
762 45
                    if ($isMerged && $cell->isMergeRangeValueCell()) {
763
                        $range = $cell->getMergeRange();
764
                        $rangeBoundaries = Coordinate::rangeDimension($range);
765
                        if ($rangeBoundaries[0] === 1) {
766
                            $isMergedButProceed = true;
767
                        }
768
                    }
769
770
                    // Determine width if cell is not part of a merge or does and is a value cell of 1-column wide range
771 45
                    if (!$isMerged || $isMergedButProceed) {
772
                        // Determine if we need to make an adjustment for the first row in an AutoFilter range that
773
                        //    has a column filter dropdown
774 45
                        $filterAdjustment = false;
775 45
                        if (!empty($autoFilterRange) && $cell->isInRange($autoFilterFirstRowRange)) {
776 1
                            $filterAdjustment = true;
777
                        }
778
779 45
                        $indentAdjustment = $cell->getStyle()->getAlignment()->getIndent();
780
781
                        // Calculated value
782
                        // To formatted string
783 45
                        $cellValue = NumberFormat::toFormattedString(
784 45
                            $cell->getCalculatedValue(),
785 45
                            $this->getParent()->getCellXfByIndex($cell->getXfIndex())
786 45
                                ->getNumberFormat()->getFormatCode()
787
                        );
788
789 45
                        if ($cellValue !== null && $cellValue !== '') {
790 45
                            $autoSizes[$this->cellCollection->getCurrentColumn()] = max(
791 45
                                (float) $autoSizes[$this->cellCollection->getCurrentColumn()],
792 45
                                (float) Shared\Font::calculateColumnWidth(
793 45
                                    $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont(),
794
                                    $cellValue,
795 45
                                    $this->getParent()->getCellXfByIndex($cell->getXfIndex())
796 45
                                        ->getAlignment()->getTextRotation(),
797 45
                                    $this->getParent()->getDefaultStyle()->getFont(),
798
                                    $filterAdjustment,
799
                                    $indentAdjustment
800
                                )
801
                            );
802
                        }
803
                    }
804
                }
805
            }
806
807
            // adjust column widths
808 45
            foreach ($autoSizes as $columnIndex => $width) {
809 45
                if ($width == -1) {
810
                    $width = $this->getDefaultColumnDimension()->getWidth();
811
                }
812 45
                $this->getColumnDimension($columnIndex)->setWidth($width);
813
            }
814
        }
815
816 566
        return $this;
817
    }
818
819
    /**
820
     * Get parent.
821
     *
822
     * @return Spreadsheet
823
     */
824 9964
    public function getParent()
825
    {
826 9964
        return $this->parent;
827
    }
828
829
    /**
830
     * Re-bind parent.
831
     *
832
     * @return $this
833
     */
834 53
    public function rebindParent(Spreadsheet $parent)
835
    {
836 53
        if ($this->parent !== null) {
837 4
            $definedNames = $this->parent->getDefinedNames();
838 4
            foreach ($definedNames as $definedName) {
839
                $parent->addDefinedName($definedName);
840
            }
841
842 4
            $this->parent->removeSheetByIndex(
843 4
                $this->parent->getIndex($this)
844
            );
845
        }
846 53
        $this->parent = $parent;
847
848 53
        return $this;
849
    }
850
851
    /**
852
     * Get title.
853
     *
854
     * @return string
855
     */
856 9964
    public function getTitle()
857
    {
858 9964
        return $this->title;
859
    }
860
861
    /**
862
     * Set title.
863
     *
864
     * @param string $title String containing the dimension of this worksheet
865
     * @param bool $updateFormulaCellReferences Flag indicating whether cell references in formulae should
866
     *            be updated to reflect the new sheet name.
867
     *          This should be left as the default true, unless you are
868
     *          certain that no formula cells on any worksheet contain
869
     *          references to this worksheet
870
     * @param bool $validate False to skip validation of new title. WARNING: This should only be set
871
     *                       at parse time (by Readers), where titles can be assumed to be valid.
872
     *
873
     * @return $this
874
     */
875 9963
    public function setTitle($title, $updateFormulaCellReferences = true, $validate = true)
876
    {
877
        // Is this a 'rename' or not?
878 9963
        if ($this->getTitle() == $title) {
879 185
            return $this;
880
        }
881
882
        // Old title
883 9963
        $oldTitle = $this->getTitle();
884
885 9963
        if ($validate) {
886
            // Syntax check
887 9963
            self::checkSheetTitle($title);
888
889 9963
            if ($this->parent) {
890
                // Is there already such sheet name?
891 9954
                if ($this->parent->sheetNameExists($title)) {
892
                    // Use name, but append with lowest possible integer
893
894 138
                    if (Shared\StringHelper::countCharacters($title) > 29) {
895
                        $title = Shared\StringHelper::substring($title, 0, 29);
896
                    }
897 138
                    $i = 1;
898 138
                    while ($this->parent->sheetNameExists($title . ' ' . $i)) {
899 19
                        ++$i;
900 19
                        if ($i == 10) {
901
                            if (Shared\StringHelper::countCharacters($title) > 28) {
902
                                $title = Shared\StringHelper::substring($title, 0, 28);
903
                            }
904 19
                        } elseif ($i == 100) {
905
                            if (Shared\StringHelper::countCharacters($title) > 27) {
906
                                $title = Shared\StringHelper::substring($title, 0, 27);
907
                            }
908
                        }
909
                    }
910
911 138
                    $title .= " $i";
912
                }
913
            }
914
        }
915
916
        // Set title
917 9963
        $this->title = $title;
918 9963
        $this->dirty = true;
919
920 9963
        if ($this->parent && $this->parent->getCalculationEngine()) {
921
            // New title
922 9954
            $newTitle = $this->getTitle();
923 9954
            $this->parent->getCalculationEngine()
924 9954
                ->renameCalculationCacheForWorksheet($oldTitle, $newTitle);
925 9954
            if ($updateFormulaCellReferences) {
926 655
                ReferenceHelper::getInstance()->updateNamedFormulae($this->parent, $oldTitle, $newTitle);
927
            }
928
        }
929
930 9963
        return $this;
931
    }
932
933
    /**
934
     * Get sheet state.
935
     *
936
     * @return string Sheet state (visible, hidden, veryHidden)
937
     */
938 312
    public function getSheetState()
939
    {
940 312
        return $this->sheetState;
941
    }
942
943
    /**
944
     * Set sheet state.
945
     *
946
     * @param string $value Sheet state (visible, hidden, veryHidden)
947
     *
948
     * @return $this
949
     */
950 9963
    public function setSheetState($value)
951
    {
952 9963
        $this->sheetState = $value;
953
954 9963
        return $this;
955
    }
956
957
    /**
958
     * Get page setup.
959
     *
960
     * @return PageSetup
961
     */
962 1099
    public function getPageSetup()
963
    {
964 1099
        return $this->pageSetup;
965
    }
966
967
    /**
968
     * Set page setup.
969
     *
970
     * @return $this
971
     */
972
    public function setPageSetup(PageSetup $pageSetup)
973
    {
974
        $this->pageSetup = $pageSetup;
975
976
        return $this;
977
    }
978
979
    /**
980
     * Get page margins.
981
     *
982
     * @return PageMargins
983
     */
984 1116
    public function getPageMargins()
985
    {
986 1116
        return $this->pageMargins;
987
    }
988
989
    /**
990
     * Set page margins.
991
     *
992
     * @return $this
993
     */
994
    public function setPageMargins(PageMargins $pageMargins)
995
    {
996
        $this->pageMargins = $pageMargins;
997
998
        return $this;
999
    }
1000
1001
    /**
1002
     * Get page header/footer.
1003
     *
1004
     * @return HeaderFooter
1005
     */
1006 362
    public function getHeaderFooter()
1007
    {
1008 362
        return $this->headerFooter;
1009
    }
1010
1011
    /**
1012
     * Set page header/footer.
1013
     *
1014
     * @return $this
1015
     */
1016
    public function setHeaderFooter(HeaderFooter $headerFooter)
1017
    {
1018
        $this->headerFooter = $headerFooter;
1019
1020
        return $this;
1021
    }
1022
1023
    /**
1024
     * Get sheet view.
1025
     *
1026
     * @return SheetView
1027
     */
1028 376
    public function getSheetView()
1029
    {
1030 376
        return $this->sheetView;
1031
    }
1032
1033
    /**
1034
     * Set sheet view.
1035
     *
1036
     * @return $this
1037
     */
1038
    public function setSheetView(SheetView $sheetView)
1039
    {
1040
        $this->sheetView = $sheetView;
1041
1042
        return $this;
1043
    }
1044
1045
    /**
1046
     * Get Protection.
1047
     *
1048
     * @return Protection
1049
     */
1050 392
    public function getProtection()
1051
    {
1052 392
        return $this->protection;
1053
    }
1054
1055
    /**
1056
     * Set Protection.
1057
     *
1058
     * @return $this
1059
     */
1060
    public function setProtection(Protection $protection)
1061
    {
1062
        $this->protection = $protection;
1063
        $this->dirty = true;
1064
1065
        return $this;
1066
    }
1067
1068
    /**
1069
     * Get highest worksheet column.
1070
     *
1071
     * @param null|int|string $row Return the data highest column for the specified row,
1072
     *                                     or the highest column of any row if no row number is passed
1073
     *
1074
     * @return string Highest column name
1075
     */
1076 1068
    public function getHighestColumn($row = null)
1077
    {
1078 1068
        if ($row === null) {
1079 1068
            return Coordinate::stringFromColumnIndex($this->cachedHighestColumn);
1080
        }
1081
1082
        return $this->getHighestDataColumn($row);
1083
    }
1084
1085
    /**
1086
     * Get highest worksheet column that contains data.
1087
     *
1088
     * @param null|int|string $row Return the highest data column for the specified row,
1089
     *                                     or the highest data column of any row if no row number is passed
1090
     *
1091
     * @return string Highest column name that contains data
1092
     */
1093 468
    public function getHighestDataColumn($row = null)
1094
    {
1095 468
        return $this->cellCollection->getHighestColumn($row);
1096
    }
1097
1098
    /**
1099
     * Get highest worksheet row.
1100
     *
1101
     * @param null|string $column Return the highest data row for the specified column,
1102
     *                                     or the highest row of any column if no column letter is passed
1103
     *
1104
     * @return int Highest row number
1105
     */
1106 672
    public function getHighestRow($column = null)
1107
    {
1108 672
        if ($column === null) {
1109 672
            return $this->cachedHighestRow;
1110
        }
1111
1112
        return $this->getHighestDataRow($column);
1113
    }
1114
1115
    /**
1116
     * Get highest worksheet row that contains data.
1117
     *
1118
     * @param null|string $column Return the highest data row for the specified column,
1119
     *                                     or the highest data row of any column if no column letter is passed
1120
     *
1121
     * @return int Highest row number that contains data
1122
     */
1123 477
    public function getHighestDataRow($column = null)
1124
    {
1125 477
        return $this->cellCollection->getHighestRow($column);
1126
    }
1127
1128
    /**
1129
     * Get highest worksheet column and highest row that have cell records.
1130
     *
1131
     * @return array Highest column name and highest row number
1132
     */
1133
    public function getHighestRowAndColumn()
1134
    {
1135
        return $this->cellCollection->getHighestRowAndColumn();
1136
    }
1137
1138
    /**
1139
     * Set a cell value.
1140
     *
1141
     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1142
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1143
     * @param mixed $value Value for the cell
1144
     *
1145
     * @return $this
1146
     */
1147 2196
    public function setCellValue($coordinate, $value)
1148
    {
1149 2196
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
1150 2196
        $this->getCell($cellAddress)->setValue($value);
1151
1152 2196
        return $this;
1153
    }
1154
1155
    /**
1156
     * Set a cell value by using numeric cell coordinates.
1157
     *
1158
     * @deprecated 1.23.0
1159
     *      Use the setCellValue() method with a cell address such as 'C5' instead;,
1160
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1161
     * @see Worksheet::setCellValue()
1162
     *
1163
     * @param int $columnIndex Numeric column coordinate of the cell
1164
     * @param int $row Numeric row coordinate of the cell
1165
     * @param mixed $value Value of the cell
1166
     *
1167
     * @return $this
1168
     */
1169 1
    public function setCellValueByColumnAndRow($columnIndex, $row, $value)
1170
    {
1171 1
        $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row)->setValue($value);
1172
1173 1
        return $this;
1174
    }
1175
1176
    /**
1177
     * Set a cell value.
1178
     *
1179
     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1180
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1181
     * @param mixed $value Value of the cell
1182
     * @param string $dataType Explicit data type, see DataType::TYPE_*
1183
     *        Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this
1184
     *             method, then it is your responsibility as an end-user developer to validate that the value and
1185
     *             the datatype match.
1186
     *       If you do mismatch value and datatpe, then the value you enter may be changed to match the datatype
1187
     *          that you specify.
1188
     *
1189
     * @see DataType
1190
     *
1191
     * @return $this
1192
     */
1193 95
    public function setCellValueExplicit($coordinate, $value, $dataType)
1194
    {
1195 95
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
1196 95
        $this->getCell($cellAddress)->setValueExplicit($value, $dataType);
1197
1198 95
        return $this;
1199
    }
1200
1201
    /**
1202
     * Set a cell value by using numeric cell coordinates.
1203
     *
1204
     * @deprecated 1.23.0
1205
     *      Use the setCellValueExplicit() method with a cell address such as 'C5' instead;,
1206
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1207
     * @see Worksheet::setCellValueExplicit()
1208
     *
1209
     * @param int $columnIndex Numeric column coordinate of the cell
1210
     * @param int $row Numeric row coordinate of the cell
1211
     * @param mixed $value Value of the cell
1212
     * @param string $dataType Explicit data type, see DataType::TYPE_*
1213
     *        Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this
1214
     *             method, then it is your responsibility as an end-user developer to validate that the value and
1215
     *             the datatype match.
1216
     *       If you do mismatch value and datatpe, then the value you enter may be changed to match the datatype
1217
     *          that you specify.
1218
     *
1219
     * @see DataType
1220
     *
1221
     * @return $this
1222
     */
1223 1
    public function setCellValueExplicitByColumnAndRow($columnIndex, $row, $value, $dataType)
1224
    {
1225 1
        $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row)->setValueExplicit($value, $dataType);
1226
1227 1
        return $this;
1228
    }
1229
1230
    /**
1231
     * Get cell at a specific coordinate.
1232
     *
1233
     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1234
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1235
     *
1236
     * @return Cell Cell that was found or created
1237
     *              WARNING: Because the cell collection can be cached to reduce memory, it only allows one
1238
     *              "active" cell at a time in memory. If you assign that cell to a variable, then select
1239
     *              another cell using getCell() or any of its variants, the newly selected cell becomes
1240
     *              the "active" cell, and any previous assignment becomes a disconnected reference because
1241
     *              the active cell has changed.
1242
     */
1243 9696
    public function getCell($coordinate): Cell
1244
    {
1245 9696
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
1246
1247
        // Shortcut for increased performance for the vast majority of simple cases
1248 9696
        if ($this->cellCollection->has($cellAddress)) {
1249
            /** @var Cell $cell */
1250 9335
            $cell = $this->cellCollection->get($cellAddress);
1251
1252 9335
            return $cell;
1253
        }
1254
1255
        /** @var Worksheet $sheet */
1256 9693
        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
1257 9693
        $cell = $sheet->cellCollection->get($finalCoordinate);
1258
1259 9693
        return $cell ?? $sheet->createNewCell($finalCoordinate);
1260
    }
1261
1262
    /**
1263
     * Get the correct Worksheet and coordinate from a coordinate that may
1264
     * contains reference to another sheet or a named range.
1265
     *
1266
     * @return array{0: Worksheet, 1: string}
1267
     */
1268 9694
    private function getWorksheetAndCoordinate(string $coordinate): array
1269
    {
1270 9694
        $sheet = null;
1271 9694
        $finalCoordinate = null;
1272
1273
        // Worksheet reference?
1274 9694
        if (strpos($coordinate, '!') !== false) {
1275
            $worksheetReference = self::extractSheetTitle($coordinate, true);
1276
1277
            $sheet = $this->parent->getSheetByName($worksheetReference[0]);
1278
            $finalCoordinate = strtoupper($worksheetReference[1]);
1279
1280
            if ($sheet === null) {
1281
                throw new Exception('Sheet not found for name: ' . $worksheetReference[0]);
1282
            }
1283
        } elseif (
1284 9694
            !preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $coordinate) &&
1285 9694
            preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/iu', $coordinate)
1286
        ) {
1287
            // Named range?
1288 14
            $namedRange = $this->validateNamedRange($coordinate, true);
1289 14
            if ($namedRange !== null) {
1290 10
                $sheet = $namedRange->getWorksheet();
1291 10
                if ($sheet === null) {
1292
                    throw new Exception('Sheet not found for named range: ' . $namedRange->getName());
1293
                }
1294
1295
                /** @phpstan-ignore-next-line */
1296 10
                $cellCoordinate = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!');
1297 10
                $finalCoordinate = str_replace('$', '', $cellCoordinate);
1298
            }
1299
        }
1300
1301 9694
        if ($sheet === null || $finalCoordinate === null) {
1302 9694
            $sheet = $this;
1303 9694
            $finalCoordinate = strtoupper($coordinate);
1304
        }
1305
1306 9694
        if (Coordinate::coordinateIsRange($finalCoordinate)) {
1307 2
            throw new Exception('Cell coordinate string can not be a range of cells.');
1308 9694
        } elseif (strpos($finalCoordinate, '$') !== false) {
1309
            throw new Exception('Cell coordinate must not be absolute.');
1310
        }
1311
1312 9694
        return [$sheet, $finalCoordinate];
1313
    }
1314
1315
    /**
1316
     * Get an existing cell at a specific coordinate, or null.
1317
     *
1318
     * @param string $coordinate Coordinate of the cell, eg: 'A1'
1319
     *
1320
     * @return null|Cell Cell that was found or null
1321
     */
1322 45
    private function getCellOrNull($coordinate): ?Cell
1323
    {
1324
        // Check cell collection
1325 45
        if ($this->cellCollection->has($coordinate)) {
1326 45
            return $this->cellCollection->get($coordinate);
1327
        }
1328
1329
        return null;
1330
    }
1331
1332
    /**
1333
     * Get cell at a specific coordinate by using numeric cell coordinates.
1334
     *
1335
     * @deprecated 1.23.0
1336
     *      Use the getCell() method with a cell address such as 'C5' instead;,
1337
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1338
     * @see Worksheet::getCell()
1339
     *
1340
     * @param int $columnIndex Numeric column coordinate of the cell
1341
     * @param int $row Numeric row coordinate of the cell
1342
     *
1343
     * @return Cell Cell that was found/created or null
1344
     *              WARNING: Because the cell collection can be cached to reduce memory, it only allows one
1345
     *              "active" cell at a time in memory. If you assign that cell to a variable, then select
1346
     *              another cell using getCell() or any of its variants, the newly selected cell becomes
1347
     *              the "active" cell, and any previous assignment becomes a disconnected reference because
1348
     *              the active cell has changed.
1349
     */
1350 1
    public function getCellByColumnAndRow($columnIndex, $row): Cell
1351
    {
1352 1
        return $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row);
1353
    }
1354
1355
    /**
1356
     * Create a new cell at the specified coordinate.
1357
     *
1358
     * @param string $coordinate Coordinate of the cell
1359
     *
1360
     * @return Cell Cell that was created
1361
     *              WARNING: Because the cell collection can be cached to reduce memory, it only allows one
1362
     *              "active" cell at a time in memory. If you assign that cell to a variable, then select
1363
     *              another cell using getCell() or any of its variants, the newly selected cell becomes
1364
     *              the "active" cell, and any previous assignment becomes a disconnected reference because
1365
     *              the active cell has changed.
1366
     */
1367 9697
    public function createNewCell($coordinate): Cell
1368
    {
1369 9697
        [$column, $row, $columnString] = Coordinate::indexesFromString($coordinate);
1370 9697
        $cell = new Cell(null, DataType::TYPE_NULL, $this);
1371 9697
        $this->cellCollection->add($coordinate, $cell);
1372
1373
        // Coordinates
1374 9697
        if ($column > $this->cachedHighestColumn) {
1375 7262
            $this->cachedHighestColumn = $column;
1376
        }
1377 9697
        if ($row > $this->cachedHighestRow) {
1378 7019
            $this->cachedHighestRow = $row;
1379
        }
1380
1381
        // Cell needs appropriate xfIndex from dimensions records
1382
        //    but don't create dimension records if they don't already exist
1383 9697
        $rowDimension = $this->rowDimensions[$row] ?? null;
1384 9697
        $columnDimension = $this->columnDimensions[$columnString] ?? null;
1385
1386 9697
        if ($rowDimension !== null) {
1387 365
            $rowXf = (int) $rowDimension->getXfIndex();
1388 365
            if ($rowXf > 0) {
1389
                // then there is a row dimension with explicit style, assign it to the cell
1390 365
                $cell->setXfIndex($rowXf);
1391
            }
1392 9648
        } elseif ($columnDimension !== null) {
1393 350
            $colXf = (int) $columnDimension->getXfIndex();
1394 350
            if ($colXf > 0) {
1395
                // then there is a column dimension, assign it to the cell
1396 195
                $cell->setXfIndex($colXf);
1397
            }
1398
        }
1399
1400 9697
        return $cell;
1401
    }
1402
1403
    /**
1404
     * Does the cell at a specific coordinate exist?
1405
     *
1406
     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1407
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1408
     */
1409 8474
    public function cellExists($coordinate): bool
1410
    {
1411 8474
        $cellAddress = Validations::validateCellAddress($coordinate);
1412
        /** @var Worksheet $sheet */
1413 8474
        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
1414
1415 8473
        return $sheet->cellCollection->has($finalCoordinate);
1416
    }
1417
1418
    /**
1419
     * Cell at a specific coordinate by using numeric cell coordinates exists?
1420
     *
1421
     * @deprecated 1.23.0
1422
     *      Use the cellExists() method with a cell address such as 'C5' instead;,
1423
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1424
     * @see Worksheet::cellExists()
1425
     *
1426
     * @param int $columnIndex Numeric column coordinate of the cell
1427
     * @param int $row Numeric row coordinate of the cell
1428
     */
1429 1
    public function cellExistsByColumnAndRow($columnIndex, $row): bool
1430
    {
1431 1
        return $this->cellExists(Coordinate::stringFromColumnIndex($columnIndex) . $row);
1432
    }
1433
1434
    /**
1435
     * Get row dimension at a specific row.
1436
     *
1437
     * @param int $row Numeric index of the row
1438
     */
1439 682
    public function getRowDimension(int $row): RowDimension
1440
    {
1441
        // Get row dimension
1442 682
        if (!isset($this->rowDimensions[$row])) {
1443 682
            $this->rowDimensions[$row] = new RowDimension($row);
1444
1445 682
            $this->cachedHighestRow = max($this->cachedHighestRow, $row);
1446
        }
1447
1448 682
        return $this->rowDimensions[$row];
1449
    }
1450
1451 106
    public function rowDimensionExists(int $row): bool
1452
    {
1453 106
        return isset($this->rowDimensions[$row]);
1454
    }
1455
1456
    /**
1457
     * Get column dimension at a specific column.
1458
     *
1459
     * @param string $column String index of the column eg: 'A'
1460
     */
1461 451
    public function getColumnDimension(string $column): ColumnDimension
1462
    {
1463
        // Uppercase coordinate
1464 451
        $column = strtoupper($column);
1465
1466
        // Fetch dimensions
1467 451
        if (!isset($this->columnDimensions[$column])) {
1468 451
            $this->columnDimensions[$column] = new ColumnDimension($column);
1469
1470 451
            $columnIndex = Coordinate::columnIndexFromString($column);
1471 451
            if ($this->cachedHighestColumn < $columnIndex) {
1472 315
                $this->cachedHighestColumn = $columnIndex;
1473
            }
1474
        }
1475
1476 451
        return $this->columnDimensions[$column];
1477
    }
1478
1479
    /**
1480
     * Get column dimension at a specific column by using numeric cell coordinates.
1481
     *
1482
     * @param int $columnIndex Numeric column coordinate of the cell
1483
     */
1484 66
    public function getColumnDimensionByColumn(int $columnIndex): ColumnDimension
1485
    {
1486 66
        return $this->getColumnDimension(Coordinate::stringFromColumnIndex($columnIndex));
1487
    }
1488
1489
    /**
1490
     * Get styles.
1491
     *
1492
     * @return Style[]
1493
     */
1494
    public function getStyles()
1495
    {
1496
        return $this->styles;
1497
    }
1498
1499
    /**
1500
     * Get style for cell.
1501
     *
1502
     * @param AddressRange|array<int>|CellAddress|int|string $cellCoordinate
1503
     *              A simple string containing a cell address like 'A1' or a cell range like 'A1:E10'
1504
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1505
     *              or a CellAddress or AddressRange object.
1506
     */
1507 9036
    public function getStyle($cellCoordinate): Style
1508
    {
1509 9036
        $cellCoordinate = Validations::validateCellOrCellRange($cellCoordinate);
1510
1511
        // set this sheet as active
1512 9036
        $this->parent->setActiveSheetIndex($this->parent->getIndex($this));
1513
1514
        // set cell coordinate as active
1515 9036
        $this->setSelectedCells($cellCoordinate);
1516
1517 9036
        return $this->parent->getCellXfSupervisor();
1518
    }
1519
1520
    /**
1521
     * Get style for cell by using numeric cell coordinates.
1522
     *
1523
     * @deprecated 1.23.0
1524
     *      Use the getStyle() method with a cell address range such as 'C5:F8' instead;,
1525
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1526
     *          or an AddressRange object.
1527
     * @see Worksheet::getStyle()
1528
     *
1529
     * @param int $columnIndex1 Numeric column coordinate of the cell
1530
     * @param int $row1 Numeric row coordinate of the cell
1531
     * @param null|int $columnIndex2 Numeric column coordinate of the range cell
1532
     * @param null|int $row2 Numeric row coordinate of the range cell
1533
     *
1534
     * @return Style
1535
     */
1536 1
    public function getStyleByColumnAndRow($columnIndex1, $row1, $columnIndex2 = null, $row2 = null)
1537
    {
1538 1
        if ($columnIndex2 !== null && $row2 !== null) {
1539 1
            $cellRange = new CellRange(
1540 1
                CellAddress::fromColumnAndRow($columnIndex1, $row1),
1541 1
                CellAddress::fromColumnAndRow($columnIndex2, $row2)
1542
            );
1543
1544 1
            return $this->getStyle($cellRange);
1545
        }
1546
1547 1
        return $this->getStyle(CellAddress::fromColumnAndRow($columnIndex1, $row1));
1548
    }
1549
1550
    /**
1551
     * Get conditional styles for a cell.
1552
     *
1553
     * @param string $coordinate eg: 'A1' or 'A1:A3'.
1554
     *          If a single cell is referenced, then the array of conditional styles will be returned if the cell is
1555
     *               included in a conditional style range.
1556
     *          If a range of cells is specified, then the styles will only be returned if the range matches the entire
1557
     *               range of the conditional.
1558
     *
1559
     * @return Conditional[]
1560
     */
1561 225
    public function getConditionalStyles(string $coordinate): array
1562
    {
1563 225
        $coordinate = strtoupper($coordinate);
1564 225
        if (strpos($coordinate, ':') !== false) {
1565 45
            return $this->conditionalStylesCollection[$coordinate] ?? [];
1566
        }
1567
1568 204
        $cell = $this->getCell($coordinate);
1569 204
        foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
1570 193
            if ($cell->isInRange($conditionalRange)) {
1571 189
                return $this->conditionalStylesCollection[$conditionalRange];
1572
            }
1573
        }
1574
1575 33
        return [];
1576
    }
1577
1578 178
    public function getConditionalRange(string $coordinate): ?string
1579
    {
1580 178
        $coordinate = strtoupper($coordinate);
1581 178
        $cell = $this->getCell($coordinate);
1582 178
        foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
1583 178
            if ($cell->isInRange($conditionalRange)) {
1584 177
                return $conditionalRange;
1585
            }
1586
        }
1587
1588 1
        return null;
1589
    }
1590
1591
    /**
1592
     * Do conditional styles exist for this cell?
1593
     *
1594
     * @param string $coordinate eg: 'A1' or 'A1:A3'.
1595
     *          If a single cell is specified, then this method will return true if that cell is included in a
1596
     *               conditional style range.
1597
     *          If a range of cells is specified, then true will only be returned if the range matches the entire
1598
     *               range of the conditional.
1599
     */
1600 21
    public function conditionalStylesExists($coordinate): bool
1601
    {
1602 21
        $coordinate = strtoupper($coordinate);
1603 21
        if (strpos($coordinate, ':') !== false) {
1604 10
            return isset($this->conditionalStylesCollection[$coordinate]);
1605
        }
1606
1607 11
        $cell = $this->getCell($coordinate);
1608 11
        foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
1609 11
            if ($cell->isInRange($conditionalRange)) {
1610 7
                return true;
1611
            }
1612
        }
1613
1614 4
        return false;
1615
    }
1616
1617
    /**
1618
     * Removes conditional styles for a cell.
1619
     *
1620
     * @param string $coordinate eg: 'A1'
1621
     *
1622
     * @return $this
1623
     */
1624 42
    public function removeConditionalStyles($coordinate)
1625
    {
1626 42
        unset($this->conditionalStylesCollection[strtoupper($coordinate)]);
1627
1628 42
        return $this;
1629
    }
1630
1631
    /**
1632
     * Get collection of conditional styles.
1633
     *
1634
     * @return array
1635
     */
1636 373
    public function getConditionalStylesCollection()
1637
    {
1638 373
        return $this->conditionalStylesCollection;
1639
    }
1640
1641
    /**
1642
     * Set conditional styles.
1643
     *
1644
     * @param string $coordinate eg: 'A1'
1645
     * @param Conditional[] $styles
1646
     *
1647
     * @return $this
1648
     */
1649 276
    public function setConditionalStyles($coordinate, $styles)
1650
    {
1651 276
        $this->conditionalStylesCollection[strtoupper($coordinate)] = $styles;
1652
1653 276
        return $this;
1654
    }
1655
1656
    /**
1657
     * Duplicate cell style to a range of cells.
1658
     *
1659
     * Please note that this will overwrite existing cell styles for cells in range!
1660
     *
1661
     * @param Style $style Cell style to duplicate
1662
     * @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
1663
     *
1664
     * @return $this
1665
     */
1666 2
    public function duplicateStyle(Style $style, $range)
1667
    {
1668
        // Add the style to the workbook if necessary
1669 2
        $workbook = $this->parent;
1670 2
        if ($existingStyle = $this->parent->getCellXfByHashCode($style->getHashCode())) {
1671
            // there is already such cell Xf in our collection
1672 1
            $xfIndex = $existingStyle->getIndex();
1673
        } else {
1674
            // we don't have such a cell Xf, need to add
1675 2
            $workbook->addCellXf($style);
1676 2
            $xfIndex = $style->getIndex();
1677
        }
1678
1679
        // Calculate range outer borders
1680 2
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
1681
1682
        // Make sure we can loop upwards on rows and columns
1683 2
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
1684
            $tmp = $rangeStart;
1685
            $rangeStart = $rangeEnd;
1686
            $rangeEnd = $tmp;
1687
        }
1688
1689
        // Loop through cells and apply styles
1690 2
        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
1691 2
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
1692 2
                $this->getCell(Coordinate::stringFromColumnIndex($col) . $row)->setXfIndex($xfIndex);
1693
            }
1694
        }
1695
1696 2
        return $this;
1697
    }
1698
1699
    /**
1700
     * Duplicate conditional style to a range of cells.
1701
     *
1702
     * Please note that this will overwrite existing cell styles for cells in range!
1703
     *
1704
     * @param Conditional[] $styles Cell style to duplicate
1705
     * @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
1706
     *
1707
     * @return $this
1708
     */
1709 18
    public function duplicateConditionalStyle(array $styles, $range = '')
1710
    {
1711 18
        foreach ($styles as $cellStyle) {
1712 18
            if (!($cellStyle instanceof Conditional)) {
1713
                throw new Exception('Style is not a conditional style');
1714
            }
1715
        }
1716
1717
        // Calculate range outer borders
1718 18
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
1719
1720
        // Make sure we can loop upwards on rows and columns
1721 18
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
1722
            $tmp = $rangeStart;
1723
            $rangeStart = $rangeEnd;
1724
            $rangeEnd = $tmp;
1725
        }
1726
1727
        // Loop through cells and apply styles
1728 18
        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
1729 18
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
1730 18
                $this->setConditionalStyles(Coordinate::stringFromColumnIndex($col) . $row, $styles);
1731
            }
1732
        }
1733
1734 18
        return $this;
1735
    }
1736
1737
    /**
1738
     * Set break on a cell.
1739
     *
1740
     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1741
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1742
     * @param int $break Break type (type of Worksheet::BREAK_*)
1743
     *
1744
     * @return $this
1745
     */
1746 14
    public function setBreak($coordinate, $break)
1747
    {
1748 14
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
1749
1750 14
        if ($break === self::BREAK_NONE) {
1751 2
            if (isset($this->breaks[$cellAddress])) {
1752 2
                unset($this->breaks[$cellAddress]);
1753
            }
1754
        } else {
1755 14
            $this->breaks[$cellAddress] = $break;
1756
        }
1757
1758 14
        return $this;
1759
    }
1760
1761
    /**
1762
     * Set break on a cell by using numeric cell coordinates.
1763
     *
1764
     * @deprecated 1.23.0
1765
     *      Use the setBreak() method with a cell address such as 'C5' instead;,
1766
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1767
     * @see Worksheet::setBreak()
1768
     *
1769
     * @param int $columnIndex Numeric column coordinate of the cell
1770
     * @param int $row Numeric row coordinate of the cell
1771
     * @param int $break Break type (type of Worksheet::BREAK_*)
1772
     *
1773
     * @return $this
1774
     */
1775 1
    public function setBreakByColumnAndRow($columnIndex, $row, $break)
1776
    {
1777 1
        return $this->setBreak(Coordinate::stringFromColumnIndex($columnIndex) . $row, $break);
1778
    }
1779
1780
    /**
1781
     * Get breaks.
1782
     *
1783
     * @return int[]
1784
     */
1785 782
    public function getBreaks()
1786
    {
1787 782
        return $this->breaks;
1788
    }
1789
1790
    /**
1791
     * Set merge on a cell range.
1792
     *
1793
     * @param AddressRange|array<int>|string $range A simple string containing a Cell range like 'A1:E10'
1794
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1795
     *              or an AddressRange.
1796
     * @param string $behaviour How the merged cells should behave.
1797
     *               Possible values are:
1798
     *                   MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells
1799
     *                   MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells
1800
     *                   MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell
1801
     *
1802
     * @return $this
1803
     */
1804 136
    public function mergeCells($range, $behaviour = self::MERGE_CELL_CONTENT_EMPTY)
1805
    {
1806 136
        $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range));
1807
1808 135
        if (strpos($range, ':') === false) {
1809 1
            $range .= ":{$range}";
1810
        }
1811
1812 135
        if (preg_match('/^([A-Z]+)(\\d+):([A-Z]+)(\\d+)$/', $range, $matches) !== 1) {
1813 1
            throw new Exception('Merge must be on a valid range of cells.');
1814
        }
1815
1816 134
        $this->mergeCells[$range] = $range;
1817 134
        $firstRow = (int) $matches[2];
1818 134
        $lastRow = (int) $matches[4];
1819 134
        $firstColumn = $matches[1];
1820 134
        $lastColumn = $matches[3];
1821 134
        $firstColumnIndex = Coordinate::columnIndexFromString($firstColumn);
1822 134
        $lastColumnIndex = Coordinate::columnIndexFromString($lastColumn);
1823 134
        $numberRows = $lastRow - $firstRow;
1824 134
        $numberColumns = $lastColumnIndex - $firstColumnIndex;
1825
1826 134
        if ($numberRows === 1 && $numberColumns === 1) {
1827 29
            return $this;
1828
        }
1829
1830
        // create upper left cell if it does not already exist
1831 127
        $upperLeft = "{$firstColumn}{$firstRow}";
1832 127
        if (!$this->cellExists($upperLeft)) {
1833 28
            $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL);
1834
        }
1835
1836 127
        if ($behaviour !== self::MERGE_CELL_CONTENT_HIDE) {
1837
            // Blank out the rest of the cells in the range (if they exist)
1838 46
            if ($numberRows > $numberColumns) {
1839 10
                $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft, $behaviour);
1840
            } else {
1841 36
                $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft, $behaviour);
1842
            }
1843
        }
1844
1845 127
        return $this;
1846
    }
1847
1848 10
    private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
1849
    {
1850 10
        $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
1851
            ? [$this->getCell($upperLeft)->getFormattedValue()]
1852 10
            : [];
1853
1854 10
        foreach ($this->getColumnIterator($firstColumn, $lastColumn) as $column) {
1855 10
            $iterator = $column->getCellIterator($firstRow);
1856 10
            $iterator->setIterateOnlyExistingCells(true);
1857 10
            foreach ($iterator as $cell) {
1858 10
                if ($cell !== null) {
1859 10
                    $row = $cell->getRow();
1860 10
                    if ($row > $lastRow) {
1861 7
                        break;
1862
                    }
1863 10
                    $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue);
1864
                }
1865
            }
1866
        }
1867
1868 10
        if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
1869
            $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
1870
        }
1871
    }
1872
1873 36
    private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
1874
    {
1875 36
        $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
1876 4
            ? [$this->getCell($upperLeft)->getFormattedValue()]
1877 36
            : [];
1878
1879 36
        foreach ($this->getRowIterator($firstRow, $lastRow) as $row) {
1880 36
            $iterator = $row->getCellIterator($firstColumn);
1881 36
            $iterator->setIterateOnlyExistingCells(true);
1882 36
            foreach ($iterator as $cell) {
1883 36
                if ($cell !== null) {
1884 36
                    $column = $cell->getColumn();
1885 36
                    $columnIndex = Coordinate::columnIndexFromString($column);
1886 36
                    if ($columnIndex > $lastColumnIndex) {
1887 7
                        break;
1888
                    }
1889 36
                    $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue);
1890
                }
1891
            }
1892
        }
1893
1894 36
        if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
1895 4
            $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
1896
        }
1897
    }
1898
1899 46
    public function mergeCellBehaviour(Cell $cell, string $upperLeft, string $behaviour, array $leftCellValue): array
1900
    {
1901 46
        if ($cell->getCoordinate() !== $upperLeft) {
1902 22
            Calculation::getInstance($cell->getWorksheet()->getParent())->flushInstance();
1903 22
            if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
1904 4
                $cellValue = $cell->getFormattedValue();
1905 4
                if ($cellValue !== '') {
1906 4
                    $leftCellValue[] = $cellValue;
1907
                }
1908
            }
1909 22
            $cell->setValueExplicit(null, DataType::TYPE_NULL);
1910
        }
1911
1912 46
        return $leftCellValue;
1913
    }
1914
1915
    /**
1916
     * Set merge on a cell range by using numeric cell coordinates.
1917
     *
1918
     * @deprecated 1.23.0
1919
     *      Use the mergeCells() method with a cell address range such as 'C5:F8' instead;,
1920
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1921
     *          or an AddressRange object.
1922
     * @see Worksheet::mergeCells()
1923
     *
1924
     * @param int $columnIndex1 Numeric column coordinate of the first cell
1925
     * @param int $row1 Numeric row coordinate of the first cell
1926
     * @param int $columnIndex2 Numeric column coordinate of the last cell
1927
     * @param int $row2 Numeric row coordinate of the last cell
1928
     * @param string $behaviour How the merged cells should behave.
1929
     *               Possible values are:
1930
     *                   MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells
1931
     *                   MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells
1932
     *                   MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell
1933
     *
1934
     * @return $this
1935
     */
1936 1
    public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $behaviour = self::MERGE_CELL_CONTENT_EMPTY)
1937
    {
1938 1
        $cellRange = new CellRange(
1939 1
            CellAddress::fromColumnAndRow($columnIndex1, $row1),
1940 1
            CellAddress::fromColumnAndRow($columnIndex2, $row2)
1941
        );
1942
1943 1
        return $this->mergeCells($cellRange, $behaviour);
1944
    }
1945
1946
    /**
1947
     * Remove merge on a cell range.
1948
     *
1949
     * @param AddressRange|array<int>|string $range A simple string containing a Cell range like 'A1:E10'
1950
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1951
     *              or an AddressRange.
1952
     *
1953
     * @return $this
1954
     */
1955 22
    public function unmergeCells($range)
1956
    {
1957 22
        $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range));
1958
1959 22
        if (strpos($range, ':') !== false) {
1960 22
            if (isset($this->mergeCells[$range])) {
1961 22
                unset($this->mergeCells[$range]);
1962
            } else {
1963 22
                throw new Exception('Cell range ' . $range . ' not known as merged.');
1964
            }
1965
        } else {
1966
            throw new Exception('Merge can only be removed from a range of cells.');
1967
        }
1968
1969 22
        return $this;
1970
    }
1971
1972
    /**
1973
     * Remove merge on a cell range by using numeric cell coordinates.
1974
     *
1975
     * @deprecated 1.23.0
1976
     *      Use the unmergeCells() method with a cell address range such as 'C5:F8' instead;,
1977
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1978
     *          or an AddressRange object.
1979
     * @see Worksheet::unmergeCells()
1980
     *
1981
     * @param int $columnIndex1 Numeric column coordinate of the first cell
1982
     * @param int $row1 Numeric row coordinate of the first cell
1983
     * @param int $columnIndex2 Numeric column coordinate of the last cell
1984
     * @param int $row2 Numeric row coordinate of the last cell
1985
     *
1986
     * @return $this
1987
     */
1988 1
    public function unmergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
1989
    {
1990 1
        $cellRange = new CellRange(
1991 1
            CellAddress::fromColumnAndRow($columnIndex1, $row1),
1992 1
            CellAddress::fromColumnAndRow($columnIndex2, $row2)
1993
        );
1994
1995 1
        return $this->unmergeCells($cellRange);
1996
    }
1997
1998
    /**
1999
     * Get merge cells array.
2000
     *
2001
     * @return string[]
2002
     */
2003 815
    public function getMergeCells()
2004
    {
2005 815
        return $this->mergeCells;
2006
    }
2007
2008
    /**
2009
     * Set merge cells array for the entire sheet. Use instead mergeCells() to merge
2010
     * a single cell range.
2011
     *
2012
     * @param string[] $mergeCells
2013
     *
2014
     * @return $this
2015
     */
2016 68
    public function setMergeCells(array $mergeCells)
2017
    {
2018 68
        $this->mergeCells = $mergeCells;
2019
2020 68
        return $this;
2021
    }
2022
2023
    /**
2024
     * Set protection on a cell or cell range.
2025
     *
2026
     * @param AddressRange|array<int>|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10'
2027
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2028
     *              or a CellAddress or AddressRange object.
2029
     * @param string $password Password to unlock the protection
2030
     * @param bool $alreadyHashed If the password has already been hashed, set this to true
2031
     *
2032
     * @return $this
2033
     */
2034 21
    public function protectCells($range, $password, $alreadyHashed = false)
2035
    {
2036 21
        $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
2037
2038 21
        if (!$alreadyHashed) {
2039 21
            $password = Shared\PasswordHasher::hashPassword($password);
2040
        }
2041 21
        $this->protectedCells[$range] = $password;
2042
2043 21
        return $this;
2044
    }
2045
2046
    /**
2047
     * Set protection on a cell range by using numeric cell coordinates.
2048
     *
2049
     * @deprecated 1.23.0
2050
     *      Use the protectCells() method with a cell address range such as 'C5:F8' instead;,
2051
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2052
     *          or an AddressRange object.
2053
     * @see Worksheet::protectCells()
2054
     *
2055
     * @param int $columnIndex1 Numeric column coordinate of the first cell
2056
     * @param int $row1 Numeric row coordinate of the first cell
2057
     * @param int $columnIndex2 Numeric column coordinate of the last cell
2058
     * @param int $row2 Numeric row coordinate of the last cell
2059
     * @param string $password Password to unlock the protection
2060
     * @param bool $alreadyHashed If the password has already been hashed, set this to true
2061
     *
2062
     * @return $this
2063
     */
2064 1
    public function protectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $password, $alreadyHashed = false)
2065
    {
2066 1
        $cellRange = new CellRange(
2067 1
            CellAddress::fromColumnAndRow($columnIndex1, $row1),
2068 1
            CellAddress::fromColumnAndRow($columnIndex2, $row2)
2069
        );
2070
2071 1
        return $this->protectCells($cellRange, $password, $alreadyHashed);
2072
    }
2073
2074
    /**
2075
     * Remove protection on a cell or cell range.
2076
     *
2077
     * @param AddressRange|array<int>|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10'
2078
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2079
     *              or a CellAddress or AddressRange object.
2080
     *
2081
     * @return $this
2082
     */
2083 19
    public function unprotectCells($range)
2084
    {
2085 19
        $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
2086
2087 19
        if (isset($this->protectedCells[$range])) {
2088 19
            unset($this->protectedCells[$range]);
2089
        } else {
2090
            throw new Exception('Cell range ' . $range . ' not known as protected.');
2091
        }
2092
2093 19
        return $this;
2094
    }
2095
2096
    /**
2097
     * Remove protection on a cell range by using numeric cell coordinates.
2098
     *
2099
     * @deprecated 1.23.0
2100
     *      Use the unprotectCells() method with a cell address range such as 'C5:F8' instead;,
2101
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2102
     *          or an AddressRange object.
2103
     * @see Worksheet::unprotectCells()
2104
     *
2105
     * @param int $columnIndex1 Numeric column coordinate of the first cell
2106
     * @param int $row1 Numeric row coordinate of the first cell
2107
     * @param int $columnIndex2 Numeric column coordinate of the last cell
2108
     * @param int $row2 Numeric row coordinate of the last cell
2109
     *
2110
     * @return $this
2111
     */
2112 1
    public function unprotectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
2113
    {
2114 1
        $cellRange = new CellRange(
2115 1
            CellAddress::fromColumnAndRow($columnIndex1, $row1),
2116 1
            CellAddress::fromColumnAndRow($columnIndex2, $row2)
2117
        );
2118
2119 1
        return $this->unprotectCells($cellRange);
2120
    }
2121
2122
    /**
2123
     * Get protected cells.
2124
     *
2125
     * @return string[]
2126
     */
2127 376
    public function getProtectedCells()
2128
    {
2129 376
        return $this->protectedCells;
2130
    }
2131
2132
    /**
2133
     * Get Autofilter.
2134
     *
2135
     * @return AutoFilter
2136
     */
2137 538
    public function getAutoFilter()
2138
    {
2139 538
        return $this->autoFilter;
2140
    }
2141
2142
    /**
2143
     * Set AutoFilter.
2144
     *
2145
     * @param AddressRange|array<int>|AutoFilter|string $autoFilterOrRange
2146
     *            A simple string containing a Cell range like 'A1:E10' is permitted for backward compatibility
2147
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2148
     *              or an AddressRange.
2149
     *
2150
     * @return $this
2151
     */
2152 14
    public function setAutoFilter($autoFilterOrRange)
2153
    {
2154 14
        if (is_object($autoFilterOrRange) && ($autoFilterOrRange instanceof AutoFilter)) {
2155
            $this->autoFilter = $autoFilterOrRange;
2156
        } else {
2157 14
            $cellRange = Functions::trimSheetFromCellReference(Validations::validateCellRange($autoFilterOrRange));
2158
2159 14
            $this->autoFilter->setRange($cellRange);
2160
        }
2161
2162 14
        return $this;
2163
    }
2164
2165
    /**
2166
     * Set Autofilter Range by using numeric cell coordinates.
2167
     *
2168
     * @deprecated 1.23.0
2169
     *      Use the setAutoFilter() method with a cell address range such as 'C5:F8' instead;,
2170
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2171
     *          or an AddressRange object or AutoFilter object.
2172
     * @see Worksheet::setAutoFilter()
2173
     *
2174
     * @param int $columnIndex1 Numeric column coordinate of the first cell
2175
     * @param int $row1 Numeric row coordinate of the first cell
2176
     * @param int $columnIndex2 Numeric column coordinate of the second cell
2177
     * @param int $row2 Numeric row coordinate of the second cell
2178
     *
2179
     * @return $this
2180
     */
2181 1
    public function setAutoFilterByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
2182
    {
2183 1
        $cellRange = new CellRange(
2184 1
            CellAddress::fromColumnAndRow($columnIndex1, $row1),
2185 1
            CellAddress::fromColumnAndRow($columnIndex2, $row2)
2186
        );
2187
2188 1
        return $this->setAutoFilter($cellRange);
2189
    }
2190
2191
    /**
2192
     * Remove autofilter.
2193
     */
2194 1
    public function removeAutoFilter(): self
2195
    {
2196 1
        $this->autoFilter->setRange('');
2197
2198 1
        return $this;
2199
    }
2200
2201
    /**
2202
     * Get collection of Tables.
2203
     *
2204
     * @return ArrayObject<int, Table>
2205
     */
2206 9654
    public function getTableCollection()
2207
    {
2208 9654
        return $this->tableCollection;
2209
    }
2210
2211
    /**
2212
     * Add Table.
2213
     *
2214
     * @return $this
2215
     */
2216 24
    public function addTable(Table $table): self
2217
    {
2218 24
        $table->setWorksheet($this);
2219 24
        $this->tableCollection[] = $table;
2220
2221 24
        return $this;
2222
    }
2223
2224
    /**
2225
     * @return string[] array of Table names
2226
     */
2227 1
    public function getTableNames(): array
2228
    {
2229 1
        $tableNames = [];
2230
2231 1
        foreach ($this->tableCollection as $table) {
2232
            /** @var Table $table */
2233 1
            $tableNames[] = $table->getName();
2234
        }
2235
2236 1
        return $tableNames;
2237
    }
2238
2239
    /**
2240
     * @param string $name the table name to search
2241
     *
2242
     * @return null|Table The table from the tables collection, or null if not found
2243
     */
2244 1
    public function getTableByName(string $name): ?Table
2245
    {
2246 1
        $tableIndex = $this->getTableIndexByName($name);
1 ignored issue
show
Bug introduced by
Are you sure the assignment to $tableIndex is correct as $this->getTableIndexByName($name) targeting PhpOffice\PhpSpreadsheet...::getTableIndexByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
2247
2248 1
        return ($tableIndex === null) ? null : $this->tableCollection[$tableIndex];
1 ignored issue
show
introduced by
The condition $tableIndex === null is always true.
Loading history...
2249
    }
2250
2251
    /**
2252
     * @param string $name the table name to search
2253
     *
2254
     * @return null|int The index of the located table in the tables collection, or null if not found
2255
     */
2256 2
    protected function getTableIndexByName(string $name): ?int
2257
    {
2258 2
        $name = Shared\StringHelper::strToUpper($name);
2259 2
        foreach ($this->tableCollection as $index => $table) {
2260
            /** @var Table $table */
2261 2
            if (Shared\StringHelper::strToUpper($table->getName()) === $name) {
2262 2
                return $index;
2263
            }
2264
        }
2265
2266 1
        return null;
2267
    }
2268
2269
    /**
2270
     * Remove Table by name.
2271
     *
2272
     * @param string $name Table name
2273
     *
2274
     * @return $this
2275
     */
2276 1
    public function removeTableByName(string $name): self
2277
    {
2278 1
        $tableIndex = $this->getTableIndexByName($name);
1 ignored issue
show
Bug introduced by
Are you sure the assignment to $tableIndex is correct as $this->getTableIndexByName($name) targeting PhpOffice\PhpSpreadsheet...::getTableIndexByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
2279
2280 1
        if ($tableIndex !== null) {
0 ignored issues
show
introduced by
The condition $tableIndex !== null is always false.
Loading history...
2281 1
            unset($this->tableCollection[$tableIndex]);
2282
        }
2283
2284 1
        return $this;
2285
    }
2286
2287
    /**
2288
     * Remove collection of Tables.
2289
     */
2290 1
    public function removeTableCollection(): self
2291
    {
2292 1
        $this->tableCollection = new ArrayObject();
2293
2294 1
        return $this;
2295
    }
2296
2297
    /**
2298
     * Get Freeze Pane.
2299
     *
2300
     * @return null|string
2301
     */
2302 398
    public function getFreezePane()
2303
    {
2304 398
        return $this->freezePane;
2305
    }
2306
2307
    /**
2308
     * Freeze Pane.
2309
     *
2310
     * Examples:
2311
     *
2312
     *     - A2 will freeze the rows above cell A2 (i.e row 1)
2313
     *     - B1 will freeze the columns to the left of cell B1 (i.e column A)
2314
     *     - B2 will freeze the rows above and to the left of cell B2 (i.e row 1 and column A)
2315
     *
2316
     * @param null|array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
2317
     *            or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2318
     *        Passing a null value for this argument will clear any existing freeze pane for this worksheet.
2319
     * @param null|array<int>|CellAddress|string $topLeftCell default position of the right bottom pane
2320
     *            Coordinate of the cell as a string, eg: 'C5'; or as an array of [$columnIndex, $row] (e.g. [3, 5]),
2321
     *            or a CellAddress object.
2322
     *
2323
     * @return $this
2324
     */
2325 30
    public function freezePane($coordinate, $topLeftCell = null)
2326
    {
2327 30
        $cellAddress = ($coordinate !== null)
2328 30
            ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate))
2329 30
            : null;
2330 30
        if ($cellAddress !== null && Coordinate::coordinateIsRange($cellAddress)) {
2331
            throw new Exception('Freeze pane can not be set on a range of cells.');
2332
        }
2333 30
        $topLeftCell = ($topLeftCell !== null)
2334 24
            ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($topLeftCell))
2335 30
            : null;
2336
2337 30
        if ($cellAddress !== null && $topLeftCell === null) {
2338 9
            $coordinate = Coordinate::coordinateFromString($cellAddress);
2339 9
            $topLeftCell = $coordinate[0] . $coordinate[1];
2340
        }
2341
2342 30
        $this->freezePane = $cellAddress;
2343 30
        $this->topLeftCell = $topLeftCell;
2344
2345 30
        return $this;
2346
    }
2347
2348 11
    public function setTopLeftCell(string $topLeftCell): self
2349
    {
2350 11
        $this->topLeftCell = $topLeftCell;
2351
2352 11
        return $this;
2353
    }
2354
2355
    /**
2356
     * Freeze Pane by using numeric cell coordinates.
2357
     *
2358
     * @deprecated 1.23.0
2359
     *      Use the freezePane() method with a cell address such as 'C5' instead;,
2360
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2361
     * @see Worksheet::freezePane()
2362
     *
2363
     * @param int $columnIndex Numeric column coordinate of the cell
2364
     * @param int $row Numeric row coordinate of the cell
2365
     *
2366
     * @return $this
2367
     */
2368 1
    public function freezePaneByColumnAndRow($columnIndex, $row)
2369
    {
2370 1
        return $this->freezePane(Coordinate::stringFromColumnIndex($columnIndex) . $row);
2371
    }
2372
2373
    /**
2374
     * Unfreeze Pane.
2375
     *
2376
     * @return $this
2377
     */
2378
    public function unfreezePane()
2379
    {
2380
        return $this->freezePane(null);
2381
    }
2382
2383
    /**
2384
     * Get the default position of the right bottom pane.
2385
     *
2386
     * @return null|string
2387
     */
2388 292
    public function getTopLeftCell()
2389
    {
2390 292
        return $this->topLeftCell;
2391
    }
2392
2393
    /**
2394
     * Insert a new row, updating all possible related data.
2395
     *
2396
     * @param int $before Insert before this one
2397
     * @param int $numberOfRows Number of rows to insert
2398
     *
2399
     * @return $this
2400
     */
2401 30
    public function insertNewRowBefore($before, $numberOfRows = 1)
2402
    {
2403 30
        if ($before >= 1) {
2404 30
            $objReferenceHelper = ReferenceHelper::getInstance();
2405 30
            $objReferenceHelper->insertNewBefore('A' . $before, 0, $numberOfRows, $this);
2406
        } else {
2407
            throw new Exception('Rows can only be inserted before at least row 1.');
2408
        }
2409
2410 30
        return $this;
2411
    }
2412
2413
    /**
2414
     * Insert a new column, updating all possible related data.
2415
     *
2416
     * @param string $before Insert before this one, eg: 'A'
2417
     * @param int $numberOfColumns Number of columns to insert
2418
     *
2419
     * @return $this
2420
     */
2421 23
    public function insertNewColumnBefore($before, $numberOfColumns = 1)
2422
    {
2423 23
        if (!is_numeric($before)) {
2424 23
            $objReferenceHelper = ReferenceHelper::getInstance();
2425 23
            $objReferenceHelper->insertNewBefore($before . '1', $numberOfColumns, 0, $this);
2426
        } else {
2427
            throw new Exception('Column references should not be numeric.');
2428
        }
2429
2430 23
        return $this;
2431
    }
2432
2433
    /**
2434
     * Insert a new column, updating all possible related data.
2435
     *
2436
     * @param int $beforeColumnIndex Insert before this one (numeric column coordinate of the cell)
2437
     * @param int $numberOfColumns Number of columns to insert
2438
     *
2439
     * @return $this
2440
     */
2441
    public function insertNewColumnBeforeByIndex($beforeColumnIndex, $numberOfColumns = 1)
2442
    {
2443
        if ($beforeColumnIndex >= 1) {
2444
            return $this->insertNewColumnBefore(Coordinate::stringFromColumnIndex($beforeColumnIndex), $numberOfColumns);
2445
        }
2446
2447
        throw new Exception('Columns can only be inserted before at least column A (1).');
2448
    }
2449
2450
    /**
2451
     * Delete a row, updating all possible related data.
2452
     *
2453
     * @param int $row Remove starting with this one
2454
     * @param int $numberOfRows Number of rows to remove
2455
     *
2456
     * @return $this
2457
     */
2458 39
    public function removeRow($row, $numberOfRows = 1)
2459
    {
2460 39
        if ($row < 1) {
2461
            throw new Exception('Rows to be deleted should at least start from row 1.');
2462
        }
2463
2464 39
        $holdRowDimensions = $this->removeRowDimensions($row, $numberOfRows);
2465 39
        $highestRow = $this->getHighestDataRow();
2466 39
        $removedRowsCounter = 0;
2467
2468 39
        for ($r = 0; $r < $numberOfRows; ++$r) {
2469 39
            if ($row + $r <= $highestRow) {
2470 35
                $this->getCellCollection()->removeRow($row + $r);
2471 35
                ++$removedRowsCounter;
2472
            }
2473
        }
2474
2475 39
        $objReferenceHelper = ReferenceHelper::getInstance();
2476 39
        $objReferenceHelper->insertNewBefore('A' . ($row + $numberOfRows), 0, -$numberOfRows, $this);
2477 39
        for ($r = 0; $r < $removedRowsCounter; ++$r) {
2478 35
            $this->getCellCollection()->removeRow($highestRow);
2479 35
            --$highestRow;
2480
        }
2481
2482 39
        $this->rowDimensions = $holdRowDimensions;
2483
2484 39
        return $this;
2485
    }
2486
2487 39
    private function removeRowDimensions(int $row, int $numberOfRows): array
2488
    {
2489 39
        $highRow = $row + $numberOfRows - 1;
2490 39
        $holdRowDimensions = [];
2491 39
        foreach ($this->rowDimensions as $rowDimension) {
2492 4
            $num = $rowDimension->getRowIndex();
2493 4
            if ($num < $row) {
2494 3
                $holdRowDimensions[$num] = $rowDimension;
2495 4
            } elseif ($num > $highRow) {
2496 4
                $num -= $numberOfRows;
2497 4
                $cloneDimension = clone $rowDimension;
2498 4
                $cloneDimension->setRowIndex($num);
2499 4
                $holdRowDimensions[$num] = $cloneDimension;
2500
            }
2501
        }
2502
2503 39
        return $holdRowDimensions;
2504
    }
2505
2506
    /**
2507
     * Remove a column, updating all possible related data.
2508
     *
2509
     * @param string $column Remove starting with this one, eg: 'A'
2510
     * @param int $numberOfColumns Number of columns to remove
2511
     *
2512
     * @return $this
2513
     */
2514 29
    public function removeColumn($column, $numberOfColumns = 1)
2515
    {
2516 29
        if (is_numeric($column)) {
2517
            throw new Exception('Column references should not be numeric.');
2518
        }
2519
2520 29
        $highestColumn = $this->getHighestDataColumn();
2521 29
        $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
2522 29
        $pColumnIndex = Coordinate::columnIndexFromString($column);
2523
2524 29
        $holdColumnDimensions = $this->removeColumnDimensions($pColumnIndex, $numberOfColumns);
2525
2526 29
        $column = Coordinate::stringFromColumnIndex($pColumnIndex + $numberOfColumns);
2527 29
        $objReferenceHelper = ReferenceHelper::getInstance();
2528 29
        $objReferenceHelper->insertNewBefore($column . '1', -$numberOfColumns, 0, $this);
2529
2530 29
        $this->columnDimensions = $holdColumnDimensions;
2531
2532 29
        if ($pColumnIndex > $highestColumnIndex) {
2533 2
            return $this;
2534
        }
2535
2536 27
        $maxPossibleColumnsToBeRemoved = $highestColumnIndex - $pColumnIndex + 1;
2537
2538 27
        for ($c = 0, $n = min($maxPossibleColumnsToBeRemoved, $numberOfColumns); $c < $n; ++$c) {
2539 27
            $this->getCellCollection()->removeColumn($highestColumn);
2540 27
            $highestColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($highestColumn) - 1);
2541
        }
2542
2543 27
        $this->garbageCollect();
2544
2545 27
        return $this;
2546
    }
2547
2548 29
    private function removeColumnDimensions(int $pColumnIndex, int $numberOfColumns): array
2549
    {
2550 29
        $highCol = $pColumnIndex + $numberOfColumns - 1;
2551 29
        $holdColumnDimensions = [];
2552 29
        foreach ($this->columnDimensions as $columnDimension) {
2553 18
            $num = $columnDimension->getColumnNumeric();
2554 18
            if ($num < $pColumnIndex) {
2555 18
                $str = $columnDimension->getColumnIndex();
2556 18
                $holdColumnDimensions[$str] = $columnDimension;
2557 18
            } elseif ($num > $highCol) {
2558 18
                $cloneDimension = clone $columnDimension;
2559 18
                $cloneDimension->setColumnNumeric($num - $numberOfColumns);
2560 18
                $str = $cloneDimension->getColumnIndex();
2561 18
                $holdColumnDimensions[$str] = $cloneDimension;
2562
            }
2563
        }
2564
2565 29
        return $holdColumnDimensions;
2566
    }
2567
2568
    /**
2569
     * Remove a column, updating all possible related data.
2570
     *
2571
     * @param int $columnIndex Remove starting with this one (numeric column coordinate of the cell)
2572
     * @param int $numColumns Number of columns to remove
2573
     *
2574
     * @return $this
2575
     */
2576
    public function removeColumnByIndex($columnIndex, $numColumns = 1)
2577
    {
2578
        if ($columnIndex >= 1) {
2579
            return $this->removeColumn(Coordinate::stringFromColumnIndex($columnIndex), $numColumns);
2580
        }
2581
2582
        throw new Exception('Columns to be deleted should at least start from column A (1)');
2583
    }
2584
2585
    /**
2586
     * Show gridlines?
2587
     */
2588 730
    public function getShowGridlines(): bool
2589
    {
2590 730
        return $this->showGridlines;
2591
    }
2592
2593
    /**
2594
     * Set show gridlines.
2595
     *
2596
     * @param bool $showGridLines Show gridlines (true/false)
2597
     *
2598
     * @return $this
2599
     */
2600 237
    public function setShowGridlines(bool $showGridLines): self
2601
    {
2602 237
        $this->showGridlines = $showGridLines;
2603
2604 237
        return $this;
2605
    }
2606
2607
    /**
2608
     * Print gridlines?
2609
     */
2610 732
    public function getPrintGridlines(): bool
2611
    {
2612 732
        return $this->printGridlines;
2613
    }
2614
2615
    /**
2616
     * Set print gridlines.
2617
     *
2618
     * @param bool $printGridLines Print gridlines (true/false)
2619
     *
2620
     * @return $this
2621
     */
2622 85
    public function setPrintGridlines(bool $printGridLines): self
2623
    {
2624 85
        $this->printGridlines = $printGridLines;
2625
2626 85
        return $this;
2627
    }
2628
2629
    /**
2630
     * Show row and column headers?
2631
     */
2632 319
    public function getShowRowColHeaders(): bool
2633
    {
2634 319
        return $this->showRowColHeaders;
2635
    }
2636
2637
    /**
2638
     * Set show row and column headers.
2639
     *
2640
     * @param bool $showRowColHeaders Show row and column headers (true/false)
2641
     *
2642
     * @return $this
2643
     */
2644 227
    public function setShowRowColHeaders(bool $showRowColHeaders): self
2645
    {
2646 227
        $this->showRowColHeaders = $showRowColHeaders;
2647
2648 227
        return $this;
2649
    }
2650
2651
    /**
2652
     * Show summary below? (Row/Column outlining).
2653
     */
2654 319
    public function getShowSummaryBelow(): bool
2655
    {
2656 319
        return $this->showSummaryBelow;
2657
    }
2658
2659
    /**
2660
     * Set show summary below.
2661
     *
2662
     * @param bool $showSummaryBelow Show summary below (true/false)
2663
     *
2664
     * @return $this
2665
     */
2666 230
    public function setShowSummaryBelow(bool $showSummaryBelow): self
2667
    {
2668 230
        $this->showSummaryBelow = $showSummaryBelow;
2669
2670 230
        return $this;
2671
    }
2672
2673
    /**
2674
     * Show summary right? (Row/Column outlining).
2675
     */
2676 319
    public function getShowSummaryRight(): bool
2677
    {
2678 319
        return $this->showSummaryRight;
2679
    }
2680
2681
    /**
2682
     * Set show summary right.
2683
     *
2684
     * @param bool $showSummaryRight Show summary right (true/false)
2685
     *
2686
     * @return $this
2687
     */
2688 230
    public function setShowSummaryRight(bool $showSummaryRight): self
2689
    {
2690 230
        $this->showSummaryRight = $showSummaryRight;
2691
2692 230
        return $this;
2693
    }
2694
2695
    /**
2696
     * Get comments.
2697
     *
2698
     * @return Comment[]
2699
     */
2700 755
    public function getComments()
2701
    {
2702 755
        return $this->comments;
2703
    }
2704
2705
    /**
2706
     * Set comments array for the entire sheet.
2707
     *
2708
     * @param Comment[] $comments
2709
     *
2710
     * @return $this
2711
     */
2712 68
    public function setComments(array $comments): self
2713
    {
2714 68
        $this->comments = $comments;
2715
2716 68
        return $this;
2717
    }
2718
2719
    /**
2720
     * Remove comment from cell.
2721
     *
2722
     * @param array<int>|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
2723
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2724
     *
2725
     * @return $this
2726
     */
2727 41
    public function removeComment($cellCoordinate): self
2728
    {
2729 41
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
2730
2731 41
        if (Coordinate::coordinateIsRange($cellAddress)) {
2732
            throw new Exception('Cell coordinate string can not be a range of cells.');
2733 41
        } elseif (strpos($cellAddress, '$') !== false) {
2734
            throw new Exception('Cell coordinate string must not be absolute.');
2735 41
        } elseif ($cellAddress == '') {
2736
            throw new Exception('Cell coordinate can not be zero-length string.');
2737
        }
2738
        // Check if we have a comment for this cell and delete it
2739 41
        if (isset($this->comments[$cellAddress])) {
2740 2
            unset($this->comments[$cellAddress]);
2741
        }
2742
2743 41
        return $this;
2744
    }
2745
2746
    /**
2747
     * Get comment for cell.
2748
     *
2749
     * @param array<int>|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
2750
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2751
     */
2752 79
    public function getComment($cellCoordinate): Comment
2753
    {
2754 79
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
2755
2756 79
        if (Coordinate::coordinateIsRange($cellAddress)) {
2757
            throw new Exception('Cell coordinate string can not be a range of cells.');
2758 79
        } elseif (strpos($cellAddress, '$') !== false) {
2759
            throw new Exception('Cell coordinate string must not be absolute.');
2760 79
        } elseif ($cellAddress == '') {
2761
            throw new Exception('Cell coordinate can not be zero-length string.');
2762
        }
2763
2764
        // Check if we already have a comment for this cell.
2765 79
        if (isset($this->comments[$cellAddress])) {
2766 51
            return $this->comments[$cellAddress];
2767
        }
2768
2769
        // If not, create a new comment.
2770 79
        $newComment = new Comment();
2771 79
        $this->comments[$cellAddress] = $newComment;
2772
2773 79
        return $newComment;
2774
    }
2775
2776
    /**
2777
     * Get comment for cell by using numeric cell coordinates.
2778
     *
2779
     * @deprecated 1.23.0
2780
     *      Use the getComment() method with a cell address such as 'C5' instead;,
2781
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2782
     * @see Worksheet::getComment()
2783
     *
2784
     * @param int $columnIndex Numeric column coordinate of the cell
2785
     * @param int $row Numeric row coordinate of the cell
2786
     */
2787 1
    public function getCommentByColumnAndRow($columnIndex, $row): Comment
2788
    {
2789 1
        return $this->getComment(Coordinate::stringFromColumnIndex($columnIndex) . $row);
2790
    }
2791
2792
    /**
2793
     * Get active cell.
2794
     *
2795
     * @return string Example: 'A1'
2796
     */
2797 8599
    public function getActiveCell()
2798
    {
2799 8599
        return $this->activeCell;
2800
    }
2801
2802
    /**
2803
     * Get selected cells.
2804
     *
2805
     * @return string
2806
     */
2807 9185
    public function getSelectedCells()
2808
    {
2809 9185
        return $this->selectedCells;
2810
    }
2811
2812
    /**
2813
     * Selected cell.
2814
     *
2815
     * @param string $coordinate Cell (i.e. A1)
2816
     *
2817
     * @return $this
2818
     */
2819 21
    public function setSelectedCell($coordinate)
2820
    {
2821 21
        return $this->setSelectedCells($coordinate);
2822
    }
2823
2824
    /**
2825
     * Select a range of cells.
2826
     *
2827
     * @param AddressRange|array<int>|CellAddress|int|string $coordinate A simple string containing a Cell range like 'A1:E10'
2828
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2829
     *              or a CellAddress or AddressRange object.
2830
     *
2831
     * @return $this
2832
     */
2833 9247
    public function setSelectedCells($coordinate)
2834
    {
2835 9247
        if (is_string($coordinate)) {
2836 9247
            $coordinate = Validations::definedNameToCoordinate($coordinate, $this);
2837
        }
2838 9247
        $coordinate = Validations::validateCellOrCellRange($coordinate);
2839
2840 9247
        if (Coordinate::coordinateIsRange($coordinate)) {
2841 410
            [$first] = Coordinate::splitRange($coordinate);
2842 410
            $this->activeCell = $first[0];
2843
        } else {
2844 9191
            $this->activeCell = $coordinate;
2845
        }
2846 9247
        $this->selectedCells = $coordinate;
2847
2848 9247
        return $this;
2849
    }
2850
2851
    /**
2852
     * Selected cell by using numeric cell coordinates.
2853
     *
2854
     * @deprecated 1.23.0
2855
     *      Use the setSelectedCells() method with a cell address such as 'C5' instead;,
2856
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2857
     * @see Worksheet::setSelectedCells()
2858
     *
2859
     * @param int $columnIndex Numeric column coordinate of the cell
2860
     * @param int $row Numeric row coordinate of the cell
2861
     *
2862
     * @return $this
2863
     */
2864
    public function setSelectedCellByColumnAndRow($columnIndex, $row)
2865
    {
2866
        return $this->setSelectedCells(Coordinate::stringFromColumnIndex($columnIndex) . $row);
2867
    }
2868
2869
    /**
2870
     * Get right-to-left.
2871
     *
2872
     * @return bool
2873
     */
2874 319
    public function getRightToLeft()
2875
    {
2876 319
        return $this->rightToLeft;
2877
    }
2878
2879
    /**
2880
     * Set right-to-left.
2881
     *
2882
     * @param bool $value Right-to-left true/false
2883
     *
2884
     * @return $this
2885
     */
2886 85
    public function setRightToLeft($value)
2887
    {
2888 85
        $this->rightToLeft = $value;
2889
2890 85
        return $this;
2891
    }
2892
2893
    /**
2894
     * Fill worksheet from values in array.
2895
     *
2896
     * @param array $source Source array
2897
     * @param mixed $nullValue Value in source array that stands for blank cell
2898
     * @param string $startCell Insert array starting from this cell address as the top left coordinate
2899
     * @param bool $strictNullComparison Apply strict comparison when testing for null values in the array
2900
     *
2901
     * @return $this
2902
     */
2903 496
    public function fromArray(array $source, $nullValue = null, $startCell = 'A1', $strictNullComparison = false)
2904
    {
2905
        //    Convert a 1-D array to 2-D (for ease of looping)
2906 496
        if (!is_array(end($source))) {
2907 31
            $source = [$source];
2908
        }
2909
2910
        // start coordinate
2911 496
        [$startColumn, $startRow] = Coordinate::coordinateFromString($startCell);
2912
2913
        // Loop through $source
2914 496
        foreach ($source as $rowData) {
2915 496
            $currentColumn = $startColumn;
2916 496
            foreach ($rowData as $cellValue) {
2917 495
                if ($strictNullComparison) {
2918 233
                    if ($cellValue !== $nullValue) {
2919
                        // Set cell value
2920 233
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
2921
                    }
2922
                } else {
2923 262
                    if ($cellValue != $nullValue) {
2924
                        // Set cell value
2925 259
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
2926
                    }
2927
                }
2928 495
                ++$currentColumn;
2929
            }
2930 496
            ++$startRow;
2931
        }
2932
2933 496
        return $this;
2934
    }
2935
2936
    /**
2937
     * Create array from a range of cells.
2938
     *
2939
     * @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
2940
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
2941
     * @param bool $calculateFormulas Should formulas be calculated?
2942
     * @param bool $formatData Should formatting be applied to cell values?
2943
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
2944
     *                               True - Return rows and columns indexed by their actual row and column IDs
2945
     *
2946
     * @return array
2947
     */
2948 92
    public function rangeToArray($range, $nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
2949
    {
2950
        // Returnvalue
2951 92
        $returnValue = [];
2952
        //    Identify the range that we need to extract from the worksheet
2953 92
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range);
2954 92
        $minCol = Coordinate::stringFromColumnIndex($rangeStart[0]);
2955 92
        $minRow = $rangeStart[1];
2956 92
        $maxCol = Coordinate::stringFromColumnIndex($rangeEnd[0]);
2957 92
        $maxRow = $rangeEnd[1];
2958
2959 92
        ++$maxCol;
2960
        // Loop through rows
2961 92
        $r = -1;
2962 92
        for ($row = $minRow; $row <= $maxRow; ++$row) {
2963 92
            $rRef = $returnCellRef ? $row : ++$r;
2964 92
            $c = -1;
2965
            // Loop through columns in the current row
2966 92
            for ($col = $minCol; $col != $maxCol; ++$col) {
2967 92
                $cRef = $returnCellRef ? $col : ++$c;
2968
                //    Using getCell() will create a new cell if it doesn't already exist. We don't want that to happen
2969
                //        so we test and retrieve directly against cellCollection
2970 92
                if ($this->cellCollection->has($col . $row)) {
2971
                    // Cell exists
2972 92
                    $cell = $this->cellCollection->get($col . $row);
2973 92
                    if ($cell->getValue() !== null) {
2974 92
                        if ($cell->getValue() instanceof RichText) {
2975 2
                            $returnValue[$rRef][$cRef] = $cell->getValue()->getPlainText();
2976
                        } else {
2977 92
                            if ($calculateFormulas) {
2978 87
                                $returnValue[$rRef][$cRef] = $cell->getCalculatedValue();
2979
                            } else {
2980 6
                                $returnValue[$rRef][$cRef] = $cell->getValue();
2981
                            }
2982
                        }
2983
2984 92
                        if ($formatData) {
2985 77
                            $style = $this->parent->getCellXfByIndex($cell->getXfIndex());
2986 77
                            $returnValue[$rRef][$cRef] = NumberFormat::toFormattedString(
2987 77
                                $returnValue[$rRef][$cRef],
2988 92
                                ($style && $style->getNumberFormat()) ? $style->getNumberFormat()->getFormatCode() : NumberFormat::FORMAT_GENERAL
2989
                            );
2990
                        }
2991
                    } else {
2992
                        // Cell holds a NULL
2993 92
                        $returnValue[$rRef][$cRef] = $nullValue;
2994
                    }
2995
                } else {
2996
                    // Cell doesn't exist
2997 24
                    $returnValue[$rRef][$cRef] = $nullValue;
2998
                }
2999
            }
3000
        }
3001
3002
        // Return
3003 92
        return $returnValue;
3004
    }
3005
3006 16
    private function validateNamedRange(string $definedName, bool $returnNullIfInvalid = false): ?DefinedName
3007
    {
3008 16
        $namedRange = DefinedName::resolveName($definedName, $this);
3009 16
        if ($namedRange === null) {
3010 5
            if ($returnNullIfInvalid) {
3011 4
                return null;
3012
            }
3013
3014 1
            throw new Exception('Named Range ' . $definedName . ' does not exist.');
3015
        }
3016
3017 11
        if ($namedRange->isFormula()) {
3018
            if ($returnNullIfInvalid) {
3019
                return null;
3020
            }
3021
3022
            throw new Exception('Defined Named ' . $definedName . ' is a formula, not a range or cell.');
3023
        }
3024
3025 11
        if ($namedRange->getLocalOnly() && $this->getHashCode() !== $namedRange->getWorksheet()->getHashCode()) {
3026
            if ($returnNullIfInvalid) {
3027
                return null;
3028
            }
3029
3030
            throw new Exception(
3031
                'Named range ' . $definedName . ' is not accessible from within sheet ' . $this->getTitle()
3032
            );
3033
        }
3034
3035 11
        return $namedRange;
3036
    }
3037
3038
    /**
3039
     * Create array from a range of cells.
3040
     *
3041
     * @param string $definedName The Named Range that should be returned
3042
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
3043
     * @param bool $calculateFormulas Should formulas be calculated?
3044
     * @param bool $formatData Should formatting be applied to cell values?
3045
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3046
     *                                True - Return rows and columns indexed by their actual row and column IDs
3047
     *
3048
     * @return array
3049
     */
3050 2
    public function namedRangeToArray(string $definedName, $nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
3051
    {
3052 2
        $namedRange = $this->validateNamedRange($definedName);
3053 1
        $workSheet = $namedRange->getWorksheet();
3054
        /** @phpstan-ignore-next-line */
3055 1
        $cellRange = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!');
3056 1
        $cellRange = str_replace('$', '', $cellRange);
3057
3058 1
        return $workSheet->rangeToArray($cellRange, $nullValue, $calculateFormulas, $formatData, $returnCellRef);
3059
    }
3060
3061
    /**
3062
     * Create array from worksheet.
3063
     *
3064
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
3065
     * @param bool $calculateFormulas Should formulas be calculated?
3066
     * @param bool $formatData Should formatting be applied to cell values?
3067
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3068
     *                               True - Return rows and columns indexed by their actual row and column IDs
3069
     *
3070
     * @return array
3071
     */
3072 41
    public function toArray($nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
3073
    {
3074
        // Garbage collect...
3075 41
        $this->garbageCollect();
3076
3077
        //    Identify the range that we need to extract from the worksheet
3078 41
        $maxCol = $this->getHighestColumn();
3079 41
        $maxRow = $this->getHighestRow();
3080
3081
        // Return
3082 41
        return $this->rangeToArray('A1:' . $maxCol . $maxRow, $nullValue, $calculateFormulas, $formatData, $returnCellRef);
3083
    }
3084
3085
    /**
3086
     * Get row iterator.
3087
     *
3088
     * @param int $startRow The row number at which to start iterating
3089
     * @param int $endRow The row number at which to stop iterating
3090
     *
3091
     * @return RowIterator
3092
     */
3093 63
    public function getRowIterator($startRow = 1, $endRow = null)
3094
    {
3095 63
        return new RowIterator($this, $startRow, $endRow);
3096
    }
3097
3098
    /**
3099
     * Get column iterator.
3100
     *
3101
     * @param string $startColumn The column address at which to start iterating
3102
     * @param string $endColumn The column address at which to stop iterating
3103
     *
3104
     * @return ColumnIterator
3105
     */
3106 16
    public function getColumnIterator($startColumn = 'A', $endColumn = null)
3107
    {
3108 16
        return new ColumnIterator($this, $startColumn, $endColumn);
3109
    }
3110
3111
    /**
3112
     * Run PhpSpreadsheet garbage collector.
3113
     *
3114
     * @return $this
3115
     */
3116 794
    public function garbageCollect()
3117
    {
3118
        // Flush cache
3119 794
        $this->cellCollection->get('A1');
3120
3121
        // Lookup highest column and highest row if cells are cleaned
3122 794
        $colRow = $this->cellCollection->getHighestRowAndColumn();
3123 794
        $highestRow = $colRow['row'];
3124 794
        $highestColumn = Coordinate::columnIndexFromString($colRow['column']);
3125
3126
        // Loop through column dimensions
3127 794
        foreach ($this->columnDimensions as $dimension) {
3128 103
            $highestColumn = max($highestColumn, Coordinate::columnIndexFromString($dimension->getColumnIndex()));
3129
        }
3130
3131
        // Loop through row dimensions
3132 794
        foreach ($this->rowDimensions as $dimension) {
3133 96
            $highestRow = max($highestRow, $dimension->getRowIndex());
3134
        }
3135
3136
        // Cache values
3137 794
        if ($highestColumn < 1) {
3138
            $this->cachedHighestColumn = 1;
3139
        } else {
3140 794
            $this->cachedHighestColumn = $highestColumn;
3141
        }
3142 794
        $this->cachedHighestRow = $highestRow;
3143
3144
        // Return
3145 794
        return $this;
3146
    }
3147
3148
    /**
3149
     * Get hash code.
3150
     *
3151
     * @return string Hash code
3152
     */
3153 9272
    public function getHashCode()
3154
    {
3155 9272
        if ($this->dirty) {
3156 9272
            $this->hash = md5($this->title . $this->autoFilter . ($this->protection->isProtectionEnabled() ? 't' : 'f') . __CLASS__);
3157 9272
            $this->dirty = false;
3158
        }
3159
3160 9272
        return $this->hash;
3161
    }
3162
3163
    /**
3164
     * Extract worksheet title from range.
3165
     *
3166
     * Example: extractSheetTitle("testSheet!A1") ==> 'A1'
3167
     * Example: extractSheetTitle("testSheet!A1:C3") ==> 'A1:C3'
3168
     * Example: extractSheetTitle("'testSheet 1'!A1", true) ==> ['testSheet 1', 'A1'];
3169
     * Example: extractSheetTitle("'testSheet 1'!A1:C3", true) ==> ['testSheet 1', 'A1:C3'];
3170
     * Example: extractSheetTitle("A1", true) ==> ['', 'A1'];
3171
     * Example: extractSheetTitle("A1:C3", true) ==> ['', 'A1:C3']
3172
     *
3173
     * @param string $range Range to extract title from
3174
     * @param bool $returnRange Return range? (see example)
3175
     *
3176
     * @return mixed
3177
     */
3178 9873
    public static function extractSheetTitle($range, $returnRange = false)
3179
    {
3180 9873
        if (empty($range)) {
3181 10
            return $returnRange ? [null, null] : null;
3182
        }
3183
3184
        // Sheet title included?
3185 9873
        if (($sep = strrpos($range, '!')) === false) {
3186 9847
            return $returnRange ? ['', $range] : '';
3187
        }
3188
3189 1125
        if ($returnRange) {
3190 1125
            return [substr($range, 0, $sep), substr($range, $sep + 1)];
3191
        }
3192
3193 7
        return substr($range, $sep + 1);
3194
    }
3195
3196
    /**
3197
     * Get hyperlink.
3198
     *
3199
     * @param string $cellCoordinate Cell coordinate to get hyperlink for, eg: 'A1'
3200
     *
3201
     * @return Hyperlink
3202
     */
3203 57
    public function getHyperlink($cellCoordinate)
3204
    {
3205
        // return hyperlink if we already have one
3206 57
        if (isset($this->hyperlinkCollection[$cellCoordinate])) {
3207 31
            return $this->hyperlinkCollection[$cellCoordinate];
3208
        }
3209
3210
        // else create hyperlink
3211 57
        $this->hyperlinkCollection[$cellCoordinate] = new Hyperlink();
3212
3213 57
        return $this->hyperlinkCollection[$cellCoordinate];
3214
    }
3215
3216
    /**
3217
     * Set hyperlink.
3218
     *
3219
     * @param string $cellCoordinate Cell coordinate to insert hyperlink, eg: 'A1'
3220
     *
3221
     * @return $this
3222
     */
3223 41
    public function setHyperlink($cellCoordinate, ?Hyperlink $hyperlink = null)
3224
    {
3225 41
        if ($hyperlink === null) {
3226 41
            unset($this->hyperlinkCollection[$cellCoordinate]);
3227
        } else {
3228 20
            $this->hyperlinkCollection[$cellCoordinate] = $hyperlink;
3229
        }
3230
3231 41
        return $this;
3232
    }
3233
3234
    /**
3235
     * Hyperlink at a specific coordinate exists?
3236
     *
3237
     * @param string $coordinate eg: 'A1'
3238
     *
3239
     * @return bool
3240
     */
3241 420
    public function hyperlinkExists($coordinate)
3242
    {
3243 420
        return isset($this->hyperlinkCollection[$coordinate]);
3244
    }
3245
3246
    /**
3247
     * Get collection of hyperlinks.
3248
     *
3249
     * @return Hyperlink[]
3250
     */
3251 372
    public function getHyperlinkCollection()
3252
    {
3253 372
        return $this->hyperlinkCollection;
3254
    }
3255
3256
    /**
3257
     * Get data validation.
3258
     *
3259
     * @param string $cellCoordinate Cell coordinate to get data validation for, eg: 'A1'
3260
     *
3261
     * @return DataValidation
3262
     */
3263 20
    public function getDataValidation($cellCoordinate)
3264
    {
3265
        // return data validation if we already have one
3266 20
        if (isset($this->dataValidationCollection[$cellCoordinate])) {
3267 14
            return $this->dataValidationCollection[$cellCoordinate];
3268
        }
3269
3270
        // else create data validation
3271 20
        $this->dataValidationCollection[$cellCoordinate] = new DataValidation();
3272
3273 20
        return $this->dataValidationCollection[$cellCoordinate];
3274
    }
3275
3276
    /**
3277
     * Set data validation.
3278
     *
3279
     * @param string $cellCoordinate Cell coordinate to insert data validation, eg: 'A1'
3280
     *
3281
     * @return $this
3282
     */
3283 43
    public function setDataValidation($cellCoordinate, ?DataValidation $dataValidation = null)
3284
    {
3285 43
        if ($dataValidation === null) {
3286 43
            unset($this->dataValidationCollection[$cellCoordinate]);
3287
        } else {
3288 4
            $this->dataValidationCollection[$cellCoordinate] = $dataValidation;
3289
        }
3290
3291 43
        return $this;
3292
    }
3293
3294
    /**
3295
     * Data validation at a specific coordinate exists?
3296
     *
3297
     * @param string $coordinate eg: 'A1'
3298
     *
3299
     * @return bool
3300
     */
3301 14
    public function dataValidationExists($coordinate)
3302
    {
3303 14
        return isset($this->dataValidationCollection[$coordinate]);
3304
    }
3305
3306
    /**
3307
     * Get collection of data validations.
3308
     *
3309
     * @return DataValidation[]
3310
     */
3311 372
    public function getDataValidationCollection()
3312
    {
3313 372
        return $this->dataValidationCollection;
3314
    }
3315
3316
    /**
3317
     * Accepts a range, returning it as a range that falls within the current highest row and column of the worksheet.
3318
     *
3319
     * @param string $range
3320
     *
3321
     * @return string Adjusted range value
3322
     */
3323 11
    public function shrinkRangeToFit($range)
3324
    {
3325 11
        $maxCol = $this->getHighestColumn();
3326 11
        $maxRow = $this->getHighestRow();
3327 11
        $maxCol = Coordinate::columnIndexFromString($maxCol);
3328
3329 11
        $rangeBlocks = explode(' ', $range);
3330 11
        foreach ($rangeBlocks as &$rangeSet) {
3331 11
            $rangeBoundaries = Coordinate::getRangeBoundaries($rangeSet);
3332
3333 11
            if (Coordinate::columnIndexFromString($rangeBoundaries[0][0]) > $maxCol) {
3334
                $rangeBoundaries[0][0] = Coordinate::stringFromColumnIndex($maxCol);
3335
            }
3336 11
            if ($rangeBoundaries[0][1] > $maxRow) {
3337
                $rangeBoundaries[0][1] = $maxRow;
3338
            }
3339 11
            if (Coordinate::columnIndexFromString($rangeBoundaries[1][0]) > $maxCol) {
3340
                $rangeBoundaries[1][0] = Coordinate::stringFromColumnIndex($maxCol);
3341
            }
3342 11
            if ($rangeBoundaries[1][1] > $maxRow) {
3343 1
                $rangeBoundaries[1][1] = $maxRow;
3344
            }
3345 11
            $rangeSet = $rangeBoundaries[0][0] . $rangeBoundaries[0][1] . ':' . $rangeBoundaries[1][0] . $rangeBoundaries[1][1];
3346
        }
3347 11
        unset($rangeSet);
3348
3349 11
        return implode(' ', $rangeBlocks);
3350
    }
3351
3352
    /**
3353
     * Get tab color.
3354
     *
3355
     * @return Color
3356
     */
3357 18
    public function getTabColor()
3358
    {
3359 18
        if ($this->tabColor === null) {
3360 18
            $this->tabColor = new Color();
3361
        }
3362
3363 18
        return $this->tabColor;
3364
    }
3365
3366
    /**
3367
     * Reset tab color.
3368
     *
3369
     * @return $this
3370
     */
3371
    public function resetTabColor()
3372
    {
3373
        $this->tabColor = null;
3374
3375
        return $this;
3376
    }
3377
3378
    /**
3379
     * Tab color set?
3380
     *
3381
     * @return bool
3382
     */
3383 328
    public function isTabColorSet()
3384
    {
3385 328
        return $this->tabColor !== null;
3386
    }
3387
3388
    /**
3389
     * Copy worksheet (!= clone!).
3390
     *
3391
     * @return static
3392
     */
3393
    public function copy()
3394
    {
3395
        return clone $this;
3396
    }
3397
3398
    /**
3399
     * Returns a boolean true if the specified row contains no cells. By default, this means that no cell records
3400
     *          exist in the collection for this row. false will be returned otherwise.
3401
     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
3402
     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
3403
     *                  cells, then the row will be considered empty.
3404
     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
3405
     *                  string value cells, then the row will be considered empty.
3406
     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3407
     *                  If the only cells in the collection are null value or empty string value cells, then the row
3408
     *                  will be considered empty.
3409
     *
3410
     * @param int $definitionOfEmptyFlags
3411
     *              Possible Flag Values are:
3412
     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
3413
     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3414
     */
3415 9
    public function isEmptyRow(int $rowId, int $definitionOfEmptyFlags = 0): bool
3416
    {
3417
        try {
3418 9
            $iterator = new RowIterator($this, $rowId, $rowId);
3419 8
            $iterator->seek($rowId);
3420 8
            $row = $iterator->current();
3421 1
        } catch (Exception $e) {
3422 1
            return true;
3423
        }
3424
3425 8
        return $row->isEmpty($definitionOfEmptyFlags);
3426
    }
3427
3428
    /**
3429
     * Returns a boolean true if the specified column contains no cells. By default, this means that no cell records
3430
     *          exist in the collection for this column. false will be returned otherwise.
3431
     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
3432
     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
3433
     *                  cells, then the column will be considered empty.
3434
     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
3435
     *                  string value cells, then the column will be considered empty.
3436
     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3437
     *                  If the only cells in the collection are null value or empty string value cells, then the column
3438
     *                  will be considered empty.
3439
     *
3440
     * @param int $definitionOfEmptyFlags
3441
     *              Possible Flag Values are:
3442
     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
3443
     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3444
     */
3445 9
    public function isEmptyColumn(string $columnId, int $definitionOfEmptyFlags = 0): bool
3446
    {
3447
        try {
3448 9
            $iterator = new ColumnIterator($this, $columnId, $columnId);
3449 8
            $iterator->seek($columnId);
3450 8
            $column = $iterator->current();
3451 1
        } catch (Exception $e) {
3452 1
            return true;
3453
        }
3454
3455 8
        return $column->isEmpty($definitionOfEmptyFlags);
3456
    }
3457
3458
    /**
3459
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
3460
     */
3461 5
    public function __clone()
3462
    {
3463
        // @phpstan-ignore-next-line
3464 5
        foreach ($this as $key => $val) {
3465 5
            if ($key == 'parent') {
3466 5
                continue;
3467
            }
3468
3469 5
            if (is_object($val) || (is_array($val))) {
3470 5
                if ($key == 'cellCollection') {
3471 5
                    $newCollection = $this->cellCollection->cloneCellCollection($this);
3472 5
                    $this->cellCollection = $newCollection;
3473 5
                } elseif ($key == 'drawingCollection') {
3474 5
                    $currentCollection = $this->drawingCollection;
3475 5
                    $this->drawingCollection = new ArrayObject();
3476 5
                    foreach ($currentCollection as $item) {
3477 3
                        if (is_object($item)) {
3478 3
                            $newDrawing = clone $item;
3479 3
                            $newDrawing->setWorksheet($this);
3480
                        }
3481
                    }
3482 5
                } elseif (($key == 'autoFilter') && ($this->autoFilter instanceof AutoFilter)) {
3483 5
                    $newAutoFilter = clone $this->autoFilter;
3484 5
                    $this->autoFilter = $newAutoFilter;
3485 5
                    $this->autoFilter->setParent($this);
3486
                } else {
3487 5
                    $this->{$key} = unserialize(serialize($val));
3488
                }
3489
            }
3490
        }
3491
    }
3492
3493
    /**
3494
     * Define the code name of the sheet.
3495
     *
3496
     * @param string $codeName Same rule as Title minus space not allowed (but, like Excel, change
3497
     *                       silently space to underscore)
3498
     * @param bool $validate False to skip validation of new title. WARNING: This should only be set
3499
     *                       at parse time (by Readers), where titles can be assumed to be valid.
3500
     *
3501
     * @return $this
3502
     */
3503 9963
    public function setCodeName($codeName, $validate = true)
3504
    {
3505
        // Is this a 'rename' or not?
3506 9963
        if ($this->getCodeName() == $codeName) {
3507
            return $this;
3508
        }
3509
3510 9963
        if ($validate) {
3511 9963
            $codeName = str_replace(' ', '_', $codeName); //Excel does this automatically without flinching, we are doing the same
3512
3513
            // Syntax check
3514
            // throw an exception if not valid
3515 9963
            self::checkSheetCodeName($codeName);
3516
3517
            // We use the same code that setTitle to find a valid codeName else not using a space (Excel don't like) but a '_'
3518
3519 9963
            if ($this->getParent()) {
3520
                // Is there already such sheet name?
3521 9954
                if ($this->getParent()->sheetCodeNameExists($codeName)) {
3522
                    // Use name, but append with lowest possible integer
3523
3524 454
                    if (Shared\StringHelper::countCharacters($codeName) > 29) {
3525
                        $codeName = Shared\StringHelper::substring($codeName, 0, 29);
3526
                    }
3527 454
                    $i = 1;
3528 454
                    while ($this->getParent()->sheetCodeNameExists($codeName . '_' . $i)) {
3529 226
                        ++$i;
3530 226
                        if ($i == 10) {
3531 1
                            if (Shared\StringHelper::countCharacters($codeName) > 28) {
3532 1
                                $codeName = Shared\StringHelper::substring($codeName, 0, 28);
3533
                            }
3534 226
                        } elseif ($i == 100) {
3535
                            if (Shared\StringHelper::countCharacters($codeName) > 27) {
3536
                                $codeName = Shared\StringHelper::substring($codeName, 0, 27);
3537
                            }
3538
                        }
3539
                    }
3540
3541 454
                    $codeName .= '_' . $i; // ok, we have a valid name
3542
                }
3543
            }
3544
        }
3545
3546 9963
        $this->codeName = $codeName;
3547
3548 9963
        return $this;
3549
    }
3550
3551
    /**
3552
     * Return the code name of the sheet.
3553
     *
3554
     * @return null|string
3555
     */
3556 9963
    public function getCodeName()
3557
    {
3558 9963
        return $this->codeName;
3559
    }
3560
3561
    /**
3562
     * Sheet has a code name ?
3563
     *
3564
     * @return bool
3565
     */
3566 2
    public function hasCodeName()
3567
    {
3568 2
        return $this->codeName !== null;
3569
    }
3570
3571 4
    public static function nameRequiresQuotes(string $sheetName): bool
3572
    {
3573 4
        return preg_match(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName) !== 1;
3574
    }
3575
}
3576