Completed
Push — master ( f4e897...378d46 )
by Mark
40s queued 29s
created

Worksheet::removeColumnDimensions()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 4

Importance

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