Completed
Push — master ( 4fc43c...b3109d )
by Mark
35s queued 31s
created

Worksheet::getTableIndexByName()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 3

Importance

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

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

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

}

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

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

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

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

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

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

}

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

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

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

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