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

Worksheet::getConditionalStyles()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

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