Passed
Push — master ( a34695...33ec70 )
by Adrien
09:32
created

Worksheet::checkSheetTitle()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 1
dl 0
loc 13
ccs 6
cts 6
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Worksheet;
4
5
use ArrayObject;
6
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
7
use PhpOffice\PhpSpreadsheet\Cell\Cell;
8
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
9
use PhpOffice\PhpSpreadsheet\Cell\DataType;
10
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
11
use PhpOffice\PhpSpreadsheet\Cell\Hyperlink;
12
use PhpOffice\PhpSpreadsheet\Chart\Chart;
13
use PhpOffice\PhpSpreadsheet\Collection\Cells;
14
use PhpOffice\PhpSpreadsheet\Collection\CellsFactory;
15
use PhpOffice\PhpSpreadsheet\Comment;
16
use PhpOffice\PhpSpreadsheet\DefinedName;
17
use PhpOffice\PhpSpreadsheet\Exception;
18
use PhpOffice\PhpSpreadsheet\IComparable;
19
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
20
use PhpOffice\PhpSpreadsheet\RichText\RichText;
21
use PhpOffice\PhpSpreadsheet\Shared;
22
use PhpOffice\PhpSpreadsheet\Spreadsheet;
23
use PhpOffice\PhpSpreadsheet\Style\Color;
24
use PhpOffice\PhpSpreadsheet\Style\Conditional;
25
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
26
use PhpOffice\PhpSpreadsheet\Style\Style;
27
28
class Worksheet implements IComparable
29
{
30
    // Break types
31
    const BREAK_NONE = 0;
32
    const BREAK_ROW = 1;
33
    const BREAK_COLUMN = 2;
34
35
    // Sheet state
36
    const SHEETSTATE_VISIBLE = 'visible';
37
    const SHEETSTATE_HIDDEN = 'hidden';
38
    const SHEETSTATE_VERYHIDDEN = 'veryHidden';
39
40
    /**
41
     * Maximum 31 characters allowed for sheet title.
42
     *
43
     * @var int
44
     */
45
    const SHEET_TITLE_MAXIMUM_LENGTH = 31;
46
47
    /**
48
     * Invalid characters in sheet title.
49
     *
50
     * @var array
51
     */
52
    private static $invalidCharacters = ['*', ':', '/', '\\', '?', '[', ']'];
53
54
    /**
55
     * Parent spreadsheet.
56
     *
57
     * @var Spreadsheet
58
     */
59
    private $parent;
60
61
    /**
62
     * Collection of cells.
63
     *
64
     * @var Cells
65
     */
66
    private $cellCollection;
67
68
    /**
69
     * Collection of row dimensions.
70
     *
71
     * @var RowDimension[]
72
     */
73
    private $rowDimensions = [];
74
75
    /**
76
     * Default row dimension.
77
     *
78
     * @var RowDimension
79
     */
80
    private $defaultRowDimension;
81
82
    /**
83
     * Collection of column dimensions.
84
     *
85
     * @var ColumnDimension[]
86
     */
87
    private $columnDimensions = [];
88
89
    /**
90
     * Default column dimension.
91
     *
92
     * @var ColumnDimension
93
     */
94
    private $defaultColumnDimension;
95
96
    /**
97
     * Collection of drawings.
98
     *
99
     * @var ArrayObject<BaseDrawing>
100
     */
101
    private $drawingCollection;
102
103
    /**
104
     * Collection of Chart objects.
105
     *
106
     * @var ArrayObject<Chart>
107
     */
108
    private $chartCollection;
109
110
    /**
111
     * Worksheet title.
112
     *
113
     * @var string
114
     */
115
    private $title;
116
117
    /**
118
     * Sheet state.
119
     *
120
     * @var string
121
     */
122
    private $sheetState;
123
124
    /**
125
     * Page setup.
126
     *
127
     * @var PageSetup
128
     */
129
    private $pageSetup;
130
131
    /**
132
     * Page margins.
133
     *
134
     * @var PageMargins
135
     */
136
    private $pageMargins;
137
138
    /**
139
     * Page header/footer.
140
     *
141
     * @var HeaderFooter
142
     */
143
    private $headerFooter;
144
145
    /**
146
     * Sheet view.
147
     *
148
     * @var SheetView
149
     */
150
    private $sheetView;
151
152
    /**
153
     * Protection.
154
     *
155
     * @var Protection
156
     */
157
    private $protection;
158
159
    /**
160
     * Collection of styles.
161
     *
162
     * @var Style[]
163
     */
164
    private $styles = [];
165
166
    /**
167
     * Conditional styles. Indexed by cell coordinate, e.g. 'A1'.
168
     *
169
     * @var array
170
     */
171
    private $conditionalStylesCollection = [];
172
173
    /**
174
     * Is the current cell collection sorted already?
175
     *
176
     * @var bool
177
     */
178
    private $cellCollectionIsSorted = false;
179
180
    /**
181
     * Collection of breaks.
182
     *
183
     * @var int[]
184
     */
185
    private $breaks = [];
186
187
    /**
188
     * Collection of merged cell ranges.
189
     *
190
     * @var string[]
191
     */
192
    private $mergeCells = [];
193
194
    /**
195
     * Collection of protected cell ranges.
196
     *
197
     * @var array
198
     */
199
    private $protectedCells = [];
200
201
    /**
202
     * Autofilter Range and selection.
203
     *
204
     * @var AutoFilter
205
     */
206
    private $autoFilter;
207
208
    /**
209
     * Freeze pane.
210
     *
211
     * @var null|string
212
     */
213
    private $freezePane;
214
215
    /**
216
     * Default position of the right bottom pane.
217
     *
218
     * @var null|string
219
     */
220
    private $topLeftCell;
221
222
    /**
223
     * Show gridlines?
224
     *
225
     * @var bool
226
     */
227
    private $showGridlines = true;
228
229
    /**
230
     * Print gridlines?
231
     *
232
     * @var bool
233
     */
234
    private $printGridlines = false;
235
236
    /**
237
     * Show row and column headers?
238
     *
239
     * @var bool
240
     */
241
    private $showRowColHeaders = true;
242
243
    /**
244
     * Show summary below? (Row/Column outline).
245
     *
246
     * @var bool
247
     */
248
    private $showSummaryBelow = true;
249
250
    /**
251
     * Show summary right? (Row/Column outline).
252
     *
253
     * @var bool
254
     */
255
    private $showSummaryRight = true;
256
257
    /**
258
     * Collection of comments.
259
     *
260
     * @var Comment[]
261
     */
262
    private $comments = [];
263
264
    /**
265
     * Active cell. (Only one!).
266
     *
267
     * @var string
268
     */
269
    private $activeCell = 'A1';
270
271
    /**
272
     * Selected cells.
273
     *
274
     * @var string
275
     */
276
    private $selectedCells = 'A1';
277
278
    /**
279
     * Cached highest column.
280
     *
281
     * @var int
282
     */
283
    private $cachedHighestColumn = 1;
284
285
    /**
286
     * Cached highest row.
287
     *
288
     * @var int
289
     */
290
    private $cachedHighestRow = 1;
291
292
    /**
293
     * Right-to-left?
294
     *
295
     * @var bool
296
     */
297
    private $rightToLeft = false;
298
299
    /**
300
     * Hyperlinks. Indexed by cell coordinate, e.g. 'A1'.
301
     *
302
     * @var array
303
     */
304
    private $hyperlinkCollection = [];
305
306
    /**
307
     * Data validation objects. Indexed by cell coordinate, e.g. 'A1'.
308
     *
309
     * @var array
310
     */
311
    private $dataValidationCollection = [];
312
313
    /**
314
     * Tab color.
315
     *
316
     * @var null|Color
317
     */
318
    private $tabColor;
319
320
    /**
321
     * Dirty flag.
322
     *
323
     * @var bool
324
     */
325
    private $dirty = true;
326
327
    /**
328
     * Hash.
329
     *
330
     * @var string
331
     */
332
    private $hash;
333
334
    /**
335
     * CodeName.
336
     *
337
     * @var string
338
     */
339
    private $codeName;
340
341
    /**
342
     * Create a new worksheet.
343
     *
344
     * @param Spreadsheet $parent
345
     * @param string $pTitle
346
     */
347 4040
    public function __construct(?Spreadsheet $parent = null, $pTitle = 'Worksheet')
348
    {
349
        // Set parent and title
350 4040
        $this->parent = $parent;
351 4040
        $this->setTitle($pTitle, false);
352
        // setTitle can change $pTitle
353 4040
        $this->setCodeName($this->getTitle());
354 4040
        $this->setSheetState(self::SHEETSTATE_VISIBLE);
355
356 4040
        $this->cellCollection = CellsFactory::getInstance($this);
357
        // Set page setup
358 4040
        $this->pageSetup = new PageSetup();
359
        // Set page margins
360 4040
        $this->pageMargins = new PageMargins();
361
        // Set page header/footer
362 4040
        $this->headerFooter = new HeaderFooter();
363
        // Set sheet view
364 4040
        $this->sheetView = new SheetView();
365
        // Drawing collection
366 4040
        $this->drawingCollection = new ArrayObject();
367
        // Chart collection
368 4040
        $this->chartCollection = new ArrayObject();
369
        // Protection
370 4040
        $this->protection = new Protection();
371
        // Default row dimension
372 4040
        $this->defaultRowDimension = new RowDimension(null);
373
        // Default column dimension
374 4040
        $this->defaultColumnDimension = new ColumnDimension(null);
375 4040
        $this->autoFilter = new AutoFilter(null, $this);
376 4040
    }
377
378
    /**
379
     * Disconnect all cells from this Worksheet object,
380
     * typically so that the worksheet object can be unset.
381
     */
382 2420
    public function disconnectCells(): void
383
    {
384 2420
        if ($this->cellCollection !== null) {
385 2420
            $this->cellCollection->unsetWorksheetCells();
386
            // @phpstan-ignore-next-line
387 2420
            $this->cellCollection = null;
388
        }
389
        //    detach ourself from the workbook, so that it can then delete this worksheet successfully
390
        // @phpstan-ignore-next-line
391 2420
        $this->parent = null;
392 2420
    }
393
394
    /**
395
     * Code to execute when this worksheet is unset().
396
     */
397 17
    public function __destruct()
398
    {
399 17
        Calculation::getInstance($this->parent)->clearCalculationCacheForWorksheet($this->title);
400
401 17
        $this->disconnectCells();
402 17
    }
403
404
    /**
405
     * Return the cell collection.
406
     *
407
     * @return Cells
408
     */
409 3865
    public function getCellCollection()
410
    {
411 3865
        return $this->cellCollection;
412
    }
413
414
    /**
415
     * Get array of invalid characters for sheet title.
416
     *
417
     * @return array
418
     */
419
    public static function getInvalidCharacters()
420
    {
421
        return self::$invalidCharacters;
422
    }
423
424
    /**
425
     * Check sheet code name for valid Excel syntax.
426
     *
427
     * @param string $pValue The string to check
428
     *
429
     * @return string The valid string
430
     */
431 4040
    private static function checkSheetCodeName($pValue)
432
    {
433 4040
        $CharCount = Shared\StringHelper::countCharacters($pValue);
434 4040
        if ($CharCount == 0) {
435
            throw new Exception('Sheet code name cannot be empty.');
436
        }
437
        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ] and  first and last characters cannot be a "'"
438
        if (
439 4040
            (str_replace(self::$invalidCharacters, '', $pValue) !== $pValue) ||
440 4040
            (Shared\StringHelper::substring($pValue, -1, 1) == '\'') ||
441 4040
            (Shared\StringHelper::substring($pValue, 0, 1) == '\'')
442
        ) {
443 1
            throw new Exception('Invalid character found in sheet code name');
444
        }
445
446
        // Enforce maximum characters allowed for sheet title
447 4040
        if ($CharCount > self::SHEET_TITLE_MAXIMUM_LENGTH) {
448 1
            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet code name.');
449
        }
450
451 4040
        return $pValue;
452
    }
453
454
    /**
455
     * Check sheet title for valid Excel syntax.
456
     *
457
     * @param string $pValue The string to check
458
     *
459
     * @return string The valid string
460
     */
461 4040
    private static function checkSheetTitle($pValue)
462
    {
463
        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ]
464 4040
        if (str_replace(self::$invalidCharacters, '', $pValue) !== $pValue) {
465 1
            throw new Exception('Invalid character found in sheet title');
466
        }
467
468
        // Enforce maximum characters allowed for sheet title
469 4040
        if (Shared\StringHelper::countCharacters($pValue) > self::SHEET_TITLE_MAXIMUM_LENGTH) {
470 1
            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet title.');
471
        }
472
473 4040
        return $pValue;
474
    }
475
476
    /**
477
     * Get a sorted list of all cell coordinates currently held in the collection by row and column.
478
     *
479
     * @param bool $sorted Also sort the cell collection?
480
     *
481
     * @return string[]
482
     */
483 404
    public function getCoordinates($sorted = true)
484
    {
485 404
        if ($this->cellCollection == null) {
486
            return [];
487
        }
488
489 404
        if ($sorted) {
490 167
            return $this->cellCollection->getSortedCoordinates();
491
        }
492
493 391
        return $this->cellCollection->getCoordinates();
494
    }
495
496
    /**
497
     * Get collection of row dimensions.
498
     *
499
     * @return RowDimension[]
500
     */
501 327
    public function getRowDimensions()
502
    {
503 327
        return $this->rowDimensions;
504
    }
505
506
    /**
507
     * Get default row dimension.
508
     *
509
     * @return RowDimension
510
     */
511 328
    public function getDefaultRowDimension()
512
    {
513 328
        return $this->defaultRowDimension;
514
    }
515
516
    /**
517
     * Get collection of column dimensions.
518
     *
519
     * @return ColumnDimension[]
520
     */
521 327
    public function getColumnDimensions()
522
    {
523 327
        return $this->columnDimensions;
524
    }
525
526
    /**
527
     * Get default column dimension.
528
     *
529
     * @return ColumnDimension
530
     */
531 152
    public function getDefaultColumnDimension()
532
    {
533 152
        return $this->defaultColumnDimension;
534
    }
535
536
    /**
537
     * Get collection of drawings.
538
     *
539
     * @return ArrayObject<BaseDrawing>
540
     */
541 326
    public function getDrawingCollection()
542
    {
543 326
        return $this->drawingCollection;
544
    }
545
546
    /**
547
     * Get collection of charts.
548
     *
549
     * @return ArrayObject<Chart>
550
     */
551 17
    public function getChartCollection()
552
    {
553 17
        return $this->chartCollection;
554
    }
555
556
    /**
557
     * Add chart.
558
     *
559
     * @param null|int $iChartIndex Index where chart should go (0,1,..., or null for last)
560
     *
561
     * @return Chart
562
     */
563 18
    public function addChart(Chart $pChart, $iChartIndex = null)
564
    {
565 18
        $pChart->setWorksheet($this);
566 18
        if ($iChartIndex === null) {
567 18
            $this->chartCollection[] = $pChart;
568
        } else {
569
            // Insert the chart at the requested index
570
            array_splice($this->chartCollection, $iChartIndex, 0, [$pChart]);
0 ignored issues
show
Bug introduced by
$this->chartCollection of type ArrayObject is incompatible with the type array expected by parameter $array of array_splice(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

570
            array_splice(/** @scrutinizer ignore-type */ $this->chartCollection, $iChartIndex, 0, [$pChart]);
Loading history...
571
        }
572
573 18
        return $pChart;
574
    }
575
576
    /**
577
     * Return the count of charts on this worksheet.
578
     *
579
     * @return int The number of charts
580
     */
581 17
    public function getChartCount()
582
    {
583 17
        return count($this->chartCollection);
584
    }
585
586
    /**
587
     * Get a chart by its index position.
588
     *
589
     * @param string $index Chart index position
590
     *
591
     * @return Chart|false
592
     */
593 15
    public function getChartByIndex($index)
594
    {
595 15
        $chartCount = count($this->chartCollection);
596 15
        if ($chartCount == 0) {
597
            return false;
598
        }
599 15
        if ($index === null) {
0 ignored issues
show
introduced by
The condition $index === null is always false.
Loading history...
600
            $index = --$chartCount;
601
        }
602 15
        if (!isset($this->chartCollection[$index])) {
603
            return false;
604
        }
605
606 15
        return $this->chartCollection[$index];
607
    }
608
609
    /**
610
     * Return an array of the names of charts on this worksheet.
611
     *
612
     * @return string[] The names of charts
613
     */
614 2
    public function getChartNames()
615
    {
616 2
        $chartNames = [];
617 2
        foreach ($this->chartCollection as $chart) {
618 2
            $chartNames[] = $chart->getName();
619
        }
620
621 2
        return $chartNames;
622
    }
623
624
    /**
625
     * Get a chart by name.
626
     *
627
     * @param string $chartName Chart name
628
     *
629
     * @return Chart|false
630
     */
631 2
    public function getChartByName($chartName)
632
    {
633 2
        $chartCount = count($this->chartCollection);
634 2
        if ($chartCount == 0) {
635
            return false;
636
        }
637 2
        foreach ($this->chartCollection as $index => $chart) {
638 2
            if ($chart->getName() == $chartName) {
639 2
                return $this->chartCollection[$index];
640
            }
641
        }
642
643
        return false;
644
    }
645
646
    /**
647
     * Refresh column dimensions.
648
     *
649
     * @return $this
650
     */
651 20
    public function refreshColumnDimensions()
652
    {
653 20
        $currentColumnDimensions = $this->getColumnDimensions();
654 20
        $newColumnDimensions = [];
655
656 20
        foreach ($currentColumnDimensions as $objColumnDimension) {
657 20
            $newColumnDimensions[$objColumnDimension->getColumnIndex()] = $objColumnDimension;
658
        }
659
660 20
        $this->columnDimensions = $newColumnDimensions;
661
662 20
        return $this;
663
    }
664
665
    /**
666
     * Refresh row dimensions.
667
     *
668
     * @return $this
669
     */
670 3
    public function refreshRowDimensions()
671
    {
672 3
        $currentRowDimensions = $this->getRowDimensions();
673 3
        $newRowDimensions = [];
674
675 3
        foreach ($currentRowDimensions as $objRowDimension) {
676 3
            $newRowDimensions[$objRowDimension->getRowIndex()] = $objRowDimension;
677
        }
678
679 3
        $this->rowDimensions = $newRowDimensions;
680
681 3
        return $this;
682
    }
683
684
    /**
685
     * Calculate worksheet dimension.
686
     *
687
     * @return string String containing the dimension of this worksheet
688
     */
689 130
    public function calculateWorksheetDimension()
690
    {
691
        // Return
692 130
        return 'A1:' . $this->getHighestColumn() . $this->getHighestRow();
693
    }
694
695
    /**
696
     * Calculate worksheet data dimension.
697
     *
698
     * @return string String containing the dimension of this worksheet that actually contain data
699
     */
700 163
    public function calculateWorksheetDataDimension()
701
    {
702
        // Return
703 163
        return 'A1:' . $this->getHighestDataColumn() . $this->getHighestDataRow();
704
    }
705
706
    /**
707
     * Calculate widths for auto-size columns.
708
     *
709
     * @return $this
710
     */
711 228
    public function calculateColumnWidths()
712
    {
713
        // initialize $autoSizes array
714 228
        $autoSizes = [];
715 228
        foreach ($this->getColumnDimensions() as $colDimension) {
716 42
            if ($colDimension->getAutoSize()) {
717 20
                $autoSizes[$colDimension->getColumnIndex()] = -1;
718
            }
719
        }
720
721
        // There is only something to do if there are some auto-size columns
722 228
        if (!empty($autoSizes)) {
723
            // build list of cells references that participate in a merge
724 20
            $isMergeCell = [];
725 20
            foreach ($this->getMergeCells() as $cells) {
726 15
                foreach (Coordinate::extractAllCellReferencesInRange($cells) as $cellReference) {
727 15
                    $isMergeCell[$cellReference] = true;
728
                }
729
            }
730
731
            // loop through all cells in the worksheet
732 20
            foreach ($this->getCoordinates(false) as $coordinate) {
733 20
                $cell = $this->getCellOrNull($coordinate);
734 20
                if ($cell !== null && isset($autoSizes[$this->cellCollection->getCurrentColumn()])) {
735
                    //Determine if cell is in merge range
736 20
                    $isMerged = isset($isMergeCell[$this->cellCollection->getCurrentCoordinate()]);
737
738
                    //By default merged cells should be ignored
739 20
                    $isMergedButProceed = false;
740
741
                    //The only exception is if it's a merge range value cell of a 'vertical' randge (1 column wide)
742 20
                    if ($isMerged && $cell->isMergeRangeValueCell()) {
743
                        $range = $cell->getMergeRange();
744
                        $rangeBoundaries = Coordinate::rangeDimension($range);
745
                        if ($rangeBoundaries[0] == 1) {
746
                            $isMergedButProceed = true;
747
                        }
748
                    }
749
750
                    // Determine width if cell does not participate in a merge or does and is a value cell of 1-column wide range
751 20
                    if (!$isMerged || $isMergedButProceed) {
752
                        // Calculated value
753
                        // To formatted string
754 20
                        $cellValue = NumberFormat::toFormattedString(
755 20
                            $cell->getCalculatedValue(),
756 20
                            $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode()
757
                        );
758
759 20
                        $autoSizes[$this->cellCollection->getCurrentColumn()] = max(
760 20
                            (float) $autoSizes[$this->cellCollection->getCurrentColumn()],
761 20
                            (float) Shared\Font::calculateColumnWidth(
762 20
                                $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont(),
763
                                $cellValue,
764 20
                                $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getAlignment()->getTextRotation(),
765 20
                                $this->getParent()->getDefaultStyle()->getFont()
766
                            )
767
                        );
768
                    }
769
                }
770
            }
771
772
            // adjust column widths
773 20
            foreach ($autoSizes as $columnIndex => $width) {
774 20
                if ($width == -1) {
775
                    $width = $this->getDefaultColumnDimension()->getWidth();
776
                }
777 20
                $this->getColumnDimension($columnIndex)->setWidth($width);
778
            }
779
        }
780
781 228
        return $this;
782
    }
783
784
    /**
785
     * Get parent.
786
     *
787
     * @return Spreadsheet
788
     */
789 4040
    public function getParent()
790
    {
791 4040
        return $this->parent;
792
    }
793
794
    /**
795
     * Re-bind parent.
796
     *
797
     * @return $this
798
     */
799 46
    public function rebindParent(Spreadsheet $parent)
800
    {
801 46
        if ($this->parent !== null) {
802 2
            $definedNames = $this->parent->getDefinedNames();
803 2
            foreach ($definedNames as $definedName) {
804
                $parent->addDefinedName($definedName);
805
            }
806
807 2
            $this->parent->removeSheetByIndex(
808 2
                $this->parent->getIndex($this)
809
            );
810
        }
811 46
        $this->parent = $parent;
812
813 46
        return $this;
814
    }
815
816
    /**
817
     * Get title.
818
     *
819
     * @return string
820
     */
821 4040
    public function getTitle()
822
    {
823 4040
        return $this->title;
824
    }
825
826
    /**
827
     * Set title.
828
     *
829
     * @param string $title String containing the dimension of this worksheet
830
     * @param bool $updateFormulaCellReferences Flag indicating whether cell references in formulae should
831
     *            be updated to reflect the new sheet name.
832
     *          This should be left as the default true, unless you are
833
     *          certain that no formula cells on any worksheet contain
834
     *          references to this worksheet
835
     * @param bool $validate False to skip validation of new title. WARNING: This should only be set
836
     *                       at parse time (by Readers), where titles can be assumed to be valid.
837
     *
838
     * @return $this
839
     */
840 4040
    public function setTitle($title, $updateFormulaCellReferences = true, $validate = true)
841
    {
842
        // Is this a 'rename' or not?
843 4040
        if ($this->getTitle() == $title) {
844 98
            return $this;
845
        }
846
847
        // Old title
848 4040
        $oldTitle = $this->getTitle();
849
850 4040
        if ($validate) {
851
            // Syntax check
852 4040
            self::checkSheetTitle($title);
853
854 4040
            if ($this->parent) {
855
                // Is there already such sheet name?
856 4002
                if ($this->parent->sheetNameExists($title)) {
857
                    // Use name, but append with lowest possible integer
858
859 46
                    if (Shared\StringHelper::countCharacters($title) > 29) {
860
                        $title = Shared\StringHelper::substring($title, 0, 29);
861
                    }
862 46
                    $i = 1;
863 46
                    while ($this->parent->sheetNameExists($title . ' ' . $i)) {
864 14
                        ++$i;
865 14
                        if ($i == 10) {
866
                            if (Shared\StringHelper::countCharacters($title) > 28) {
867
                                $title = Shared\StringHelper::substring($title, 0, 28);
868
                            }
869 14
                        } elseif ($i == 100) {
870
                            if (Shared\StringHelper::countCharacters($title) > 27) {
871
                                $title = Shared\StringHelper::substring($title, 0, 27);
872
                            }
873
                        }
874
                    }
875
876 46
                    $title .= " $i";
877
                }
878
            }
879
        }
880
881
        // Set title
882 4040
        $this->title = $title;
883 4040
        $this->dirty = true;
884
885 4040
        if ($this->parent && $this->parent->getCalculationEngine()) {
886
            // New title
887 4002
            $newTitle = $this->getTitle();
888 4002
            $this->parent->getCalculationEngine()
889 4002
                ->renameCalculationCacheForWorksheet($oldTitle, $newTitle);
890 4002
            if ($updateFormulaCellReferences) {
891 251
                ReferenceHelper::getInstance()->updateNamedFormulas($this->parent, $oldTitle, $newTitle);
892
            }
893
        }
894
895 4040
        return $this;
896
    }
897
898
    /**
899
     * Get sheet state.
900
     *
901
     * @return string Sheet state (visible, hidden, veryHidden)
902
     */
903 146
    public function getSheetState()
904
    {
905 146
        return $this->sheetState;
906
    }
907
908
    /**
909
     * Set sheet state.
910
     *
911
     * @param string $value Sheet state (visible, hidden, veryHidden)
912
     *
913
     * @return $this
914
     */
915 4040
    public function setSheetState($value)
916
    {
917 4040
        $this->sheetState = $value;
918
919 4040
        return $this;
920
    }
921
922
    /**
923
     * Get page setup.
924
     *
925
     * @return PageSetup
926
     */
927 381
    public function getPageSetup()
928
    {
929 381
        return $this->pageSetup;
930
    }
931
932
    /**
933
     * Set page setup.
934
     *
935
     * @return $this
936
     */
937
    public function setPageSetup(PageSetup $pValue)
938
    {
939
        $this->pageSetup = $pValue;
940
941
        return $this;
942
    }
943
944
    /**
945
     * Get page margins.
946
     *
947
     * @return PageMargins
948
     */
949 413
    public function getPageMargins()
950
    {
951 413
        return $this->pageMargins;
952
    }
953
954
    /**
955
     * Set page margins.
956
     *
957
     * @return $this
958
     */
959
    public function setPageMargins(PageMargins $pValue)
960
    {
961
        $this->pageMargins = $pValue;
962
963
        return $this;
964
    }
965
966
    /**
967
     * Get page header/footer.
968
     *
969
     * @return HeaderFooter
970
     */
971 160
    public function getHeaderFooter()
972
    {
973 160
        return $this->headerFooter;
974
    }
975
976
    /**
977
     * Set page header/footer.
978
     *
979
     * @return $this
980
     */
981
    public function setHeaderFooter(HeaderFooter $pValue)
982
    {
983
        $this->headerFooter = $pValue;
984
985
        return $this;
986
    }
987
988
    /**
989
     * Get sheet view.
990
     *
991
     * @return SheetView
992
     */
993 164
    public function getSheetView()
994
    {
995 164
        return $this->sheetView;
996
    }
997
998
    /**
999
     * Set sheet view.
1000
     *
1001
     * @return $this
1002
     */
1003
    public function setSheetView(SheetView $pValue)
1004
    {
1005
        $this->sheetView = $pValue;
1006
1007
        return $this;
1008
    }
1009
1010
    /**
1011
     * Get Protection.
1012
     *
1013
     * @return Protection
1014
     */
1015 190
    public function getProtection()
1016
    {
1017 190
        return $this->protection;
1018
    }
1019
1020
    /**
1021
     * Set Protection.
1022
     *
1023
     * @return $this
1024
     */
1025
    public function setProtection(Protection $pValue)
1026
    {
1027
        $this->protection = $pValue;
1028
        $this->dirty = true;
1029
1030
        return $this;
1031
    }
1032
1033
    /**
1034
     * Get highest worksheet column.
1035
     *
1036
     * @param string $row Return the data highest column for the specified row,
1037
     *                                     or the highest column of any row if no row number is passed
1038
     *
1039
     * @return string Highest column name
1040
     */
1041 413
    public function getHighestColumn($row = null)
1042
    {
1043 413
        if ($row == null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $row of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
1044 413
            return Coordinate::stringFromColumnIndex($this->cachedHighestColumn);
1045
        }
1046
1047
        return $this->getHighestDataColumn($row);
1048
    }
1049
1050
    /**
1051
     * Get highest worksheet column that contains data.
1052
     *
1053
     * @param string $row Return the highest data column for the specified row,
1054
     *                                     or the highest data column of any row if no row number is passed
1055
     *
1056
     * @return string Highest column name that contains data
1057
     */
1058 187
    public function getHighestDataColumn($row = null)
1059
    {
1060 187
        return $this->cellCollection->getHighestColumn($row);
1061
    }
1062
1063
    /**
1064
     * Get highest worksheet row.
1065
     *
1066
     * @param string $column Return the highest data row for the specified column,
1067
     *                                     or the highest row of any column if no column letter is passed
1068
     *
1069
     * @return int Highest row number
1070
     */
1071 260
    public function getHighestRow($column = null)
1072
    {
1073 260
        if ($column == null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $column of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
1074 260
            return $this->cachedHighestRow;
1075
        }
1076
1077
        return $this->getHighestDataRow($column);
1078
    }
1079
1080
    /**
1081
     * Get highest worksheet row that contains data.
1082
     *
1083
     * @param string $column Return the highest data row for the specified column,
1084
     *                                     or the highest data row of any column if no column letter is passed
1085
     *
1086
     * @return int Highest row number that contains data
1087
     */
1088 193
    public function getHighestDataRow($column = null)
1089
    {
1090 193
        return $this->cellCollection->getHighestRow($column);
1091
    }
1092
1093
    /**
1094
     * Get highest worksheet column and highest row that have cell records.
1095
     *
1096
     * @return array Highest column name and highest row number
1097
     */
1098
    public function getHighestRowAndColumn()
1099
    {
1100
        return $this->cellCollection->getHighestRowAndColumn();
1101
    }
1102
1103
    /**
1104
     * Set a cell value.
1105
     *
1106
     * @param string $pCoordinate Coordinate of the cell, eg: 'A1'
1107
     * @param mixed $pValue Value of the cell
1108
     *
1109
     * @return $this
1110
     */
1111 1771
    public function setCellValue($pCoordinate, $pValue)
1112
    {
1113 1771
        $this->getCell($pCoordinate)->setValue($pValue);
1114
1115 1771
        return $this;
1116
    }
1117
1118
    /**
1119
     * Set a cell value by using numeric cell coordinates.
1120
     *
1121
     * @param int $columnIndex Numeric column coordinate of the cell
1122
     * @param int $row Numeric row coordinate of the cell
1123
     * @param mixed $value Value of the cell
1124
     *
1125
     * @return $this
1126
     */
1127
    public function setCellValueByColumnAndRow($columnIndex, $row, $value)
1128
    {
1129
        $this->getCellByColumnAndRow($columnIndex, $row)->setValue($value);
1130
1131
        return $this;
1132
    }
1133
1134
    /**
1135
     * Set a cell value.
1136
     *
1137
     * @param string $pCoordinate Coordinate of the cell, eg: 'A1'
1138
     * @param mixed $pValue Value of the cell
1139
     * @param string $pDataType Explicit data type, see DataType::TYPE_*
1140
     *
1141
     * @return $this
1142
     */
1143 3
    public function setCellValueExplicit($pCoordinate, $pValue, $pDataType)
1144
    {
1145
        // Set value
1146 3
        $this->getCell($pCoordinate)->setValueExplicit($pValue, $pDataType);
1147
1148 3
        return $this;
1149
    }
1150
1151
    /**
1152
     * Set a cell value by using numeric cell coordinates.
1153
     *
1154
     * @param int $columnIndex Numeric column coordinate of the cell
1155
     * @param int $row Numeric row coordinate of the cell
1156
     * @param mixed $value Value of the cell
1157
     * @param string $dataType Explicit data type, see DataType::TYPE_*
1158
     *
1159
     * @return $this
1160
     */
1161
    public function setCellValueExplicitByColumnAndRow($columnIndex, $row, $value, $dataType)
1162
    {
1163
        $this->getCellByColumnAndRow($columnIndex, $row)->setValueExplicit($value, $dataType);
1164
1165
        return $this;
1166
    }
1167
1168
    /**
1169
     * Get cell at a specific coordinate.
1170
     *
1171
     * @param string $coordinate Coordinate of the cell, eg: 'A1'
1172
     *
1173
     * @return Cell Cell that was found or created
1174
     */
1175 3861
    public function getCell(string $coordinate): Cell
1176
    {
1177
        // Shortcut for increased performance for the vast majority of simple cases
1178 3861
        if ($this->cellCollection->has($coordinate)) {
1179
            /** @var Cell $cell */
1180 3778
            $cell = $this->cellCollection->get($coordinate);
1181
1182 3778
            return $cell;
1183
        }
1184
1185
        /** @var Worksheet $sheet */
1186 3847
        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($coordinate);
1187 3847
        $cell = $sheet->cellCollection->get($finalCoordinate);
1188
1189 3847
        return $cell ?? $sheet->createNewCell($finalCoordinate);
1190
    }
1191
1192
    /**
1193
     * Get the correct Worksheet and coordinate from a coordinate that may
1194
     * contains reference to another sheet or a named range.
1195
     *
1196
     * @return array{0: Worksheet, 1: string}
1197
     */
1198 3861
    private function getWorksheetAndCoordinate(string $pCoordinate): array
1199
    {
1200 3861
        $sheet = null;
1201 3861
        $finalCoordinate = null;
1202
1203
        // Worksheet reference?
1204 3861
        if (strpos($pCoordinate, '!') !== false) {
1205 1
            $worksheetReference = self::extractSheetTitle($pCoordinate, true);
1206
1207 1
            $sheet = $this->parent->getSheetByName($worksheetReference[0]);
1208 1
            $finalCoordinate = strtoupper($worksheetReference[1]);
1209
1210 1
            if (!$sheet) {
1211 1
                throw new Exception('Sheet not found for name: ' . $worksheetReference[0]);
1212
            }
1213
        } elseif (
1214 3861
            !preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $pCoordinate) &&
1215 3861
            preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/i', $pCoordinate)
1216
        ) {
1217
            // Named range?
1218 10
            $namedRange = $this->validateNamedRange($pCoordinate, true);
1219 10
            if ($namedRange !== null) {
1220 6
                $sheet = $namedRange->getWorksheet();
1221 6
                if (!$sheet) {
0 ignored issues
show
introduced by
$sheet is of type PhpOffice\PhpSpreadsheet\Worksheet\Worksheet, thus it always evaluated to true.
Loading history...
1222
                    throw new Exception('Sheet not found for named range: ' . $namedRange->getName());
1223
                }
1224
1225 6
                $cellCoordinate = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!');
1226 6
                $finalCoordinate = str_replace('$', '', $cellCoordinate);
1227
            }
1228
        }
1229
1230 3861
        if (!$sheet || !$finalCoordinate) {
1231 3861
            $sheet = $this;
1232 3861
            $finalCoordinate = strtoupper($pCoordinate);
1233
        }
1234
1235 3861
        if (Coordinate::coordinateIsRange($finalCoordinate)) {
1236 2
            throw new Exception('Cell coordinate string can not be a range of cells.');
1237 3861
        } elseif (strpos($finalCoordinate, '$') !== false) {
1238
            throw new Exception('Cell coordinate must not be absolute.');
1239
        }
1240
1241 3861
        return [$sheet, $finalCoordinate];
1242
    }
1243
1244
    /**
1245
     * Get an existing cell at a specific coordinate, or null.
1246
     *
1247
     * @param string $coordinate Coordinate of the cell, eg: 'A1'
1248
     *
1249
     * @return null|Cell Cell that was found or null
1250
     */
1251 20
    private function getCellOrNull($coordinate): ?Cell
1252
    {
1253
        // Check cell collection
1254 20
        if ($this->cellCollection->has($coordinate)) {
1255 20
            return $this->cellCollection->get($coordinate);
1256
        }
1257
1258
        return null;
1259
    }
1260
1261
    /**
1262
     * Get cell at a specific coordinate by using numeric cell coordinates.
1263
     *
1264
     * @param int $columnIndex Numeric column coordinate of the cell
1265
     * @param int $row Numeric row coordinate of the cell
1266
     * @param bool $createIfNotExists Flag indicating whether a new cell should be created if it doesn't
1267
     *                                       already exist, or a null should be returned instead
1268
     *
1269
     * @return null|Cell Cell that was found/created or null
1270
     */
1271 286
    public function getCellByColumnAndRow($columnIndex, $row, $createIfNotExists = true)
1272
    {
1273 286
        $columnLetter = Coordinate::stringFromColumnIndex($columnIndex);
1274 286
        $coordinate = $columnLetter . $row;
1275
1276 286
        if ($this->cellCollection->has($coordinate)) {
1277 281
            return $this->cellCollection->get($coordinate);
1278
        }
1279
1280
        // Create new cell object, if required
1281 87
        return $createIfNotExists ? $this->createNewCell($coordinate) : null;
1282
    }
1283
1284
    /**
1285
     * Create a new cell at the specified coordinate.
1286
     *
1287
     * @param string $pCoordinate Coordinate of the cell
1288
     *
1289
     * @return Cell Cell that was created
1290
     */
1291 3865
    private function createNewCell($pCoordinate)
1292
    {
1293 3865
        $cell = new Cell(null, DataType::TYPE_NULL, $this);
1294 3865
        $this->cellCollection->add($pCoordinate, $cell);
1295 3865
        $this->cellCollectionIsSorted = false;
1296
1297
        // Coordinates
1298 3865
        $aCoordinates = Coordinate::coordinateFromString($pCoordinate);
1299 3865
        $aIndexes = Coordinate::indexesFromString($pCoordinate);
1300 3865
        if ($this->cachedHighestColumn < $aIndexes[0]) {
1301 2056
            $this->cachedHighestColumn = $aIndexes[0];
1302
        }
1303 3865
        if ($aIndexes[1] > $this->cachedHighestRow) {
1304 2663
            $this->cachedHighestRow = $aIndexes[1];
1305
        }
1306
1307
        // Cell needs appropriate xfIndex from dimensions records
1308
        //    but don't create dimension records if they don't already exist
1309 3865
        $rowDimension = $this->getRowDimension($aCoordinates[1], false);
1310 3865
        $columnDimension = $this->getColumnDimension($aCoordinates[0], false);
1311
1312 3865
        if ($rowDimension !== null && $rowDimension->getXfIndex() > 0) {
1313
            // then there is a row dimension with explicit style, assign it to the cell
1314 3
            $cell->setXfIndex($rowDimension->getXfIndex());
1315 3865
        } elseif ($columnDimension !== null && $columnDimension->getXfIndex() > 0) {
1316
            // then there is a column dimension, assign it to the cell
1317 2
            $cell->setXfIndex($columnDimension->getXfIndex());
1318
        }
1319
1320 3865
        return $cell;
1321
    }
1322
1323
    /**
1324
     * Does the cell at a specific coordinate exist?
1325
     *
1326
     * @param string $coordinate Coordinate of the cell eg: 'A1'
1327
     *
1328
     * @return bool
1329
     */
1330 3643
    public function cellExists($coordinate)
1331
    {
1332
        /** @var Worksheet $sheet */
1333 3643
        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($coordinate);
1334
1335 3642
        return $sheet->cellCollection->has($finalCoordinate);
1336
    }
1337
1338
    /**
1339
     * Cell at a specific coordinate by using numeric cell coordinates exists?
1340
     *
1341
     * @param int $columnIndex Numeric column coordinate of the cell
1342
     * @param int $row Numeric row coordinate of the cell
1343
     *
1344
     * @return bool
1345
     */
1346 171
    public function cellExistsByColumnAndRow($columnIndex, $row)
1347
    {
1348 171
        return $this->cellExists(Coordinate::stringFromColumnIndex($columnIndex) . $row);
1349
    }
1350
1351
    /**
1352
     * Get row dimension at a specific row.
1353
     *
1354
     * @param int $pRow Numeric index of the row
1355
     * @param bool $create
1356
     *
1357
     * @return null|RowDimension
1358
     */
1359 3868
    public function getRowDimension($pRow, $create = true)
1360
    {
1361
        // Found
1362 3868
        $found = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $found is dead and can be removed.
Loading history...
1363
1364
        // Get row dimension
1365 3868
        if (!isset($this->rowDimensions[$pRow])) {
1366 3868
            if (!$create) {
1367 3845
                return null;
1368
            }
1369 192
            $this->rowDimensions[$pRow] = new RowDimension($pRow);
1370
1371 192
            $this->cachedHighestRow = max($this->cachedHighestRow, $pRow);
1372
        }
1373
1374 192
        return $this->rowDimensions[$pRow];
1375
    }
1376
1377
    /**
1378
     * Get column dimension at a specific column.
1379
     *
1380
     * @param string $pColumn String index of the column eg: 'A'
1381
     * @param bool $create
1382
     *
1383
     * @return null|ColumnDimension
1384
     */
1385 3868
    public function getColumnDimension($pColumn, $create = true)
1386
    {
1387
        // Uppercase coordinate
1388 3868
        $pColumn = strtoupper($pColumn);
1389
1390
        // Fetch dimensions
1391 3868
        if (!isset($this->columnDimensions[$pColumn])) {
1392 3868
            if (!$create) {
1393 3851
                return null;
1394
            }
1395 122
            $this->columnDimensions[$pColumn] = new ColumnDimension($pColumn);
1396
1397 122
            $columnIndex = Coordinate::columnIndexFromString($pColumn);
1398 122
            if ($this->cachedHighestColumn < $columnIndex) {
1399 62
                $this->cachedHighestColumn = $columnIndex;
1400
            }
1401
        }
1402
1403 122
        return $this->columnDimensions[$pColumn];
1404
    }
1405
1406
    /**
1407
     * Get column dimension at a specific column by using numeric cell coordinates.
1408
     *
1409
     * @param int $columnIndex Numeric column coordinate of the cell
1410
     *
1411
     * @return null|ColumnDimension
1412
     */
1413 34
    public function getColumnDimensionByColumn($columnIndex)
1414
    {
1415 34
        return $this->getColumnDimension(Coordinate::stringFromColumnIndex($columnIndex));
1416
    }
1417
1418
    /**
1419
     * Get styles.
1420
     *
1421
     * @return Style[]
1422
     */
1423
    public function getStyles()
1424
    {
1425
        return $this->styles;
1426
    }
1427
1428
    /**
1429
     * Get style for cell.
1430
     *
1431
     * @param string $pCellCoordinate Cell coordinate (or range) to get style for, eg: 'A1'
1432
     *
1433
     * @return Style
1434
     */
1435 3627
    public function getStyle($pCellCoordinate)
1436
    {
1437
        // set this sheet as active
1438 3627
        $this->parent->setActiveSheetIndex($this->parent->getIndex($this));
1439
1440
        // set cell coordinate as active
1441 3627
        $this->setSelectedCells($pCellCoordinate);
1442
1443 3627
        return $this->parent->getCellXfSupervisor();
1444
    }
1445
1446
    /**
1447
     * Get conditional styles for a cell.
1448
     *
1449
     * @param string $pCoordinate eg: 'A1'
1450
     *
1451
     * @return Conditional[]
1452
     */
1453 13
    public function getConditionalStyles($pCoordinate)
1454
    {
1455 13
        $pCoordinate = strtoupper($pCoordinate);
1456 13
        if (!isset($this->conditionalStylesCollection[$pCoordinate])) {
1457 2
            $this->conditionalStylesCollection[$pCoordinate] = [];
1458
        }
1459
1460 13
        return $this->conditionalStylesCollection[$pCoordinate];
1461
    }
1462
1463
    /**
1464
     * Do conditional styles exist for this cell?
1465
     *
1466
     * @param string $pCoordinate eg: 'A1'
1467
     *
1468
     * @return bool
1469
     */
1470 18
    public function conditionalStylesExists($pCoordinate)
1471
    {
1472 18
        return isset($this->conditionalStylesCollection[strtoupper($pCoordinate)]);
1473
    }
1474
1475
    /**
1476
     * Removes conditional styles for a cell.
1477
     *
1478
     * @param string $pCoordinate eg: 'A1'
1479
     *
1480
     * @return $this
1481
     */
1482 25
    public function removeConditionalStyles($pCoordinate)
1483
    {
1484 25
        unset($this->conditionalStylesCollection[strtoupper($pCoordinate)]);
1485
1486 25
        return $this;
1487
    }
1488
1489
    /**
1490
     * Get collection of conditional styles.
1491
     *
1492
     * @return array
1493
     */
1494 146
    public function getConditionalStylesCollection()
1495
    {
1496 146
        return $this->conditionalStylesCollection;
1497
    }
1498
1499
    /**
1500
     * Set conditional styles.
1501
     *
1502
     * @param string $pCoordinate eg: 'A1'
1503
     * @param Conditional[] $pValue
1504
     *
1505
     * @return $this
1506
     */
1507 13
    public function setConditionalStyles($pCoordinate, $pValue)
1508
    {
1509 13
        $this->conditionalStylesCollection[strtoupper($pCoordinate)] = $pValue;
1510
1511 13
        return $this;
1512
    }
1513
1514
    /**
1515
     * Get style for cell by using numeric cell coordinates.
1516
     *
1517
     * @param int $columnIndex1 Numeric column coordinate of the cell
1518
     * @param int $row1 Numeric row coordinate of the cell
1519
     * @param null|int $columnIndex2 Numeric column coordinate of the range cell
1520
     * @param null|int $row2 Numeric row coordinate of the range cell
1521
     *
1522
     * @return Style
1523
     */
1524
    public function getStyleByColumnAndRow($columnIndex1, $row1, $columnIndex2 = null, $row2 = null)
1525
    {
1526
        if ($columnIndex2 !== null && $row2 !== null) {
1527
            $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;
1528
1529
            return $this->getStyle($cellRange);
1530
        }
1531
1532
        return $this->getStyle(Coordinate::stringFromColumnIndex($columnIndex1) . $row1);
1533
    }
1534
1535
    /**
1536
     * Duplicate cell style to a range of cells.
1537
     *
1538
     * Please note that this will overwrite existing cell styles for cells in range!
1539
     *
1540
     * @param Style $pCellStyle Cell style to duplicate
1541
     * @param string $pRange Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
1542
     *
1543
     * @return $this
1544
     */
1545 2
    public function duplicateStyle(Style $pCellStyle, $pRange)
1546
    {
1547
        // Add the style to the workbook if necessary
1548 2
        $workbook = $this->parent;
1549 2
        if ($existingStyle = $this->parent->getCellXfByHashCode($pCellStyle->getHashCode())) {
1550
            // there is already such cell Xf in our collection
1551 1
            $xfIndex = $existingStyle->getIndex();
1552
        } else {
1553
            // we don't have such a cell Xf, need to add
1554 2
            $workbook->addCellXf($pCellStyle);
1555 2
            $xfIndex = $pCellStyle->getIndex();
1556
        }
1557
1558
        // Calculate range outer borders
1559 2
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($pRange . ':' . $pRange);
1560
1561
        // Make sure we can loop upwards on rows and columns
1562 2
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
1563
            $tmp = $rangeStart;
1564
            $rangeStart = $rangeEnd;
1565
            $rangeEnd = $tmp;
1566
        }
1567
1568
        // Loop through cells and apply styles
1569 2
        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
1570 2
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
1571 2
                $this->getCell(Coordinate::stringFromColumnIndex($col) . $row)->setXfIndex($xfIndex);
1572
            }
1573
        }
1574
1575 2
        return $this;
1576
    }
1577
1578
    /**
1579
     * Duplicate conditional style to a range of cells.
1580
     *
1581
     * Please note that this will overwrite existing cell styles for cells in range!
1582
     *
1583
     * @param Conditional[] $pCellStyle Cell style to duplicate
1584
     * @param string $pRange Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
1585
     *
1586
     * @return $this
1587
     */
1588 2
    public function duplicateConditionalStyle(array $pCellStyle, $pRange = '')
1589
    {
1590 2
        foreach ($pCellStyle as $cellStyle) {
1591 2
            if (!($cellStyle instanceof Conditional)) {
1592
                throw new Exception('Style is not a conditional style');
1593
            }
1594
        }
1595
1596
        // Calculate range outer borders
1597 2
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($pRange . ':' . $pRange);
1598
1599
        // Make sure we can loop upwards on rows and columns
1600 2
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
1601
            $tmp = $rangeStart;
1602
            $rangeStart = $rangeEnd;
1603
            $rangeEnd = $tmp;
1604
        }
1605
1606
        // Loop through cells and apply styles
1607 2
        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
1608 2
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
1609 2
                $this->setConditionalStyles(Coordinate::stringFromColumnIndex($col) . $row, $pCellStyle);
1610
            }
1611
        }
1612
1613 2
        return $this;
1614
    }
1615
1616
    /**
1617
     * Set break on a cell.
1618
     *
1619
     * @param string $pCoordinate Cell coordinate (e.g. A1)
1620
     * @param int $pBreak Break type (type of Worksheet::BREAK_*)
1621
     *
1622
     * @return $this
1623
     */
1624 4
    public function setBreak($pCoordinate, $pBreak)
1625
    {
1626
        // Uppercase coordinate
1627 4
        $pCoordinate = strtoupper($pCoordinate);
1628
1629 4
        if ($pCoordinate != '') {
1630 4
            if ($pBreak == self::BREAK_NONE) {
1631
                if (isset($this->breaks[$pCoordinate])) {
1632
                    unset($this->breaks[$pCoordinate]);
1633
                }
1634
            } else {
1635 4
                $this->breaks[$pCoordinate] = $pBreak;
1636
            }
1637
        } else {
1638
            throw new Exception('No cell coordinate specified.');
1639
        }
1640
1641 4
        return $this;
1642
    }
1643
1644
    /**
1645
     * Set break on a cell by using numeric cell coordinates.
1646
     *
1647
     * @param int $columnIndex Numeric column coordinate of the cell
1648
     * @param int $row Numeric row coordinate of the cell
1649
     * @param int $break Break type (type of Worksheet::BREAK_*)
1650
     *
1651
     * @return $this
1652
     */
1653
    public function setBreakByColumnAndRow($columnIndex, $row, $break)
1654
    {
1655
        return $this->setBreak(Coordinate::stringFromColumnIndex($columnIndex) . $row, $break);
1656
    }
1657
1658
    /**
1659
     * Get breaks.
1660
     *
1661
     * @return int[]
1662
     */
1663 320
    public function getBreaks()
1664
    {
1665 320
        return $this->breaks;
1666
    }
1667
1668
    /**
1669
     * Set merge on a cell range.
1670
     *
1671
     * @param string $pRange Cell range (e.g. A1:E1)
1672
     *
1673
     * @return $this
1674
     */
1675 62
    public function mergeCells($pRange)
1676
    {
1677
        // Uppercase coordinate
1678 62
        $pRange = strtoupper($pRange);
1679
1680 62
        if (strpos($pRange, ':') !== false) {
1681 62
            $this->mergeCells[$pRange] = $pRange;
1682
1683
            // make sure cells are created
1684
1685
            // get the cells in the range
1686 62
            $aReferences = Coordinate::extractAllCellReferencesInRange($pRange);
1687
1688
            // create upper left cell if it does not already exist
1689 62
            $upperLeft = $aReferences[0];
1690 62
            if (!$this->cellExists($upperLeft)) {
1691 24
                $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL);
1692
            }
1693
1694
            // Blank out the rest of the cells in the range (if they exist)
1695 62
            $count = count($aReferences);
1696 62
            for ($i = 1; $i < $count; ++$i) {
1697 62
                if ($this->cellExists($aReferences[$i])) {
1698 22
                    $this->getCell($aReferences[$i])->setValueExplicit(null, DataType::TYPE_NULL);
1699
                }
1700
            }
1701
        } else {
1702
            throw new Exception('Merge must be set on a range of cells.');
1703
        }
1704
1705 62
        return $this;
1706
    }
1707
1708
    /**
1709
     * Set merge on a cell range by using numeric cell coordinates.
1710
     *
1711
     * @param int $columnIndex1 Numeric column coordinate of the first cell
1712
     * @param int $row1 Numeric row coordinate of the first cell
1713
     * @param int $columnIndex2 Numeric column coordinate of the last cell
1714
     * @param int $row2 Numeric row coordinate of the last cell
1715
     *
1716
     * @return $this
1717
     */
1718
    public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
1719
    {
1720
        $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;
1721
1722
        return $this->mergeCells($cellRange);
1723
    }
1724
1725
    /**
1726
     * Remove merge on a cell range.
1727
     *
1728
     * @param string $pRange Cell range (e.g. A1:E1)
1729
     *
1730
     * @return $this
1731
     */
1732 16
    public function unmergeCells($pRange)
1733
    {
1734
        // Uppercase coordinate
1735 16
        $pRange = strtoupper($pRange);
1736
1737 16
        if (strpos($pRange, ':') !== false) {
1738 16
            if (isset($this->mergeCells[$pRange])) {
1739 16
                unset($this->mergeCells[$pRange]);
1740
            } else {
1741 16
                throw new Exception('Cell range ' . $pRange . ' not known as merged.');
1742
            }
1743
        } else {
1744
            throw new Exception('Merge can only be removed from a range of cells.');
1745
        }
1746
1747 16
        return $this;
1748
    }
1749
1750
    /**
1751
     * Remove merge on a cell range by using numeric cell coordinates.
1752
     *
1753
     * @param int $columnIndex1 Numeric column coordinate of the first cell
1754
     * @param int $row1 Numeric row coordinate of the first cell
1755
     * @param int $columnIndex2 Numeric column coordinate of the last cell
1756
     * @param int $row2 Numeric row coordinate of the last cell
1757
     *
1758
     * @return $this
1759
     */
1760
    public function unmergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
1761
    {
1762
        $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;
1763
1764
        return $this->unmergeCells($cellRange);
1765
    }
1766
1767
    /**
1768
     * Get merge cells array.
1769
     *
1770
     * @return string[]
1771
     */
1772 330
    public function getMergeCells()
1773
    {
1774 330
        return $this->mergeCells;
1775
    }
1776
1777
    /**
1778
     * Set merge cells array for the entire sheet. Use instead mergeCells() to merge
1779
     * a single cell range.
1780
     *
1781
     * @param string[] $pValue
1782
     *
1783
     * @return $this
1784
     */
1785 32
    public function setMergeCells(array $pValue)
1786
    {
1787 32
        $this->mergeCells = $pValue;
1788
1789 32
        return $this;
1790
    }
1791
1792
    /**
1793
     * Set protection on a cell range.
1794
     *
1795
     * @param string $pRange Cell (e.g. A1) or cell range (e.g. A1:E1)
1796
     * @param string $pPassword Password to unlock the protection
1797
     * @param bool $pAlreadyHashed If the password has already been hashed, set this to true
1798
     *
1799
     * @return $this
1800
     */
1801 16
    public function protectCells($pRange, $pPassword, $pAlreadyHashed = false)
1802
    {
1803
        // Uppercase coordinate
1804 16
        $pRange = strtoupper($pRange);
1805
1806 16
        if (!$pAlreadyHashed) {
1807 16
            $pPassword = Shared\PasswordHasher::hashPassword($pPassword);
1808
        }
1809 16
        $this->protectedCells[$pRange] = $pPassword;
1810
1811 16
        return $this;
1812
    }
1813
1814
    /**
1815
     * Set protection on a cell range by using numeric cell coordinates.
1816
     *
1817
     * @param int $columnIndex1 Numeric column coordinate of the first cell
1818
     * @param int $row1 Numeric row coordinate of the first cell
1819
     * @param int $columnIndex2 Numeric column coordinate of the last cell
1820
     * @param int $row2 Numeric row coordinate of the last cell
1821
     * @param string $password Password to unlock the protection
1822
     * @param bool $alreadyHashed If the password has already been hashed, set this to true
1823
     *
1824
     * @return $this
1825
     */
1826
    public function protectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $password, $alreadyHashed = false)
1827
    {
1828
        $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;
1829
1830
        return $this->protectCells($cellRange, $password, $alreadyHashed);
1831
    }
1832
1833
    /**
1834
     * Remove protection on a cell range.
1835
     *
1836
     * @param string $pRange Cell (e.g. A1) or cell range (e.g. A1:E1)
1837
     *
1838
     * @return $this
1839
     */
1840 16
    public function unprotectCells($pRange)
1841
    {
1842
        // Uppercase coordinate
1843 16
        $pRange = strtoupper($pRange);
1844
1845 16
        if (isset($this->protectedCells[$pRange])) {
1846 16
            unset($this->protectedCells[$pRange]);
1847
        } else {
1848
            throw new Exception('Cell range ' . $pRange . ' not known as protected.');
1849
        }
1850
1851 16
        return $this;
1852
    }
1853
1854
    /**
1855
     * Remove protection on a cell range by using numeric cell coordinates.
1856
     *
1857
     * @param int $columnIndex1 Numeric column coordinate of the first cell
1858
     * @param int $row1 Numeric row coordinate of the first cell
1859
     * @param int $columnIndex2 Numeric column coordinate of the last cell
1860
     * @param int $row2 Numeric row coordinate of the last cell
1861
     *
1862
     * @return $this
1863
     */
1864
    public function unprotectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
1865
    {
1866
        $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;
1867
1868
        return $this->unprotectCells($cellRange);
1869
    }
1870
1871
    /**
1872
     * Get protected cells.
1873
     *
1874
     * @return array[]
1875
     */
1876 166
    public function getProtectedCells()
1877
    {
1878 166
        return $this->protectedCells;
1879
    }
1880
1881
    /**
1882
     * Get Autofilter.
1883
     *
1884
     * @return AutoFilter
1885
     */
1886 168
    public function getAutoFilter()
1887
    {
1888 168
        return $this->autoFilter;
1889
    }
1890
1891
    /**
1892
     * Set AutoFilter.
1893
     *
1894
     * @param AutoFilter|string $pValue
1895
     *            A simple string containing a Cell range like 'A1:E10' is permitted for backward compatibility
1896
     *
1897
     * @return $this
1898
     */
1899 4
    public function setAutoFilter($pValue)
1900
    {
1901 4
        if (is_string($pValue)) {
1902 4
            $this->autoFilter->setRange($pValue);
1903
        } elseif (is_object($pValue) && ($pValue instanceof AutoFilter)) {
1904
            $this->autoFilter = $pValue;
1905
        }
1906
1907 4
        return $this;
1908
    }
1909
1910
    /**
1911
     * Set Autofilter Range by using numeric cell coordinates.
1912
     *
1913
     * @param int $columnIndex1 Numeric column coordinate of the first cell
1914
     * @param int $row1 Numeric row coordinate of the first cell
1915
     * @param int $columnIndex2 Numeric column coordinate of the second cell
1916
     * @param int $row2 Numeric row coordinate of the second cell
1917
     *
1918
     * @return $this
1919
     */
1920
    public function setAutoFilterByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
1921
    {
1922
        return $this->setAutoFilter(
1923
            Coordinate::stringFromColumnIndex($columnIndex1) . $row1
1924
            . ':' .
1925
            Coordinate::stringFromColumnIndex($columnIndex2) . $row2
1926
        );
1927
    }
1928
1929
    /**
1930
     * Remove autofilter.
1931
     *
1932
     * @return $this
1933
     */
1934
    public function removeAutoFilter()
1935
    {
1936
        $this->autoFilter->setRange(null);
1937
1938
        return $this;
1939
    }
1940
1941
    /**
1942
     * Get Freeze Pane.
1943
     *
1944
     * @return string
1945
     */
1946 166
    public function getFreezePane()
1947
    {
1948 166
        return $this->freezePane;
1949
    }
1950
1951
    /**
1952
     * Freeze Pane.
1953
     *
1954
     * Examples:
1955
     *
1956
     *     - A2 will freeze the rows above cell A2 (i.e row 1)
1957
     *     - B1 will freeze the columns to the left of cell B1 (i.e column A)
1958
     *     - B2 will freeze the rows above and to the left of cell B2 (i.e row 1 and column A)
1959
     *
1960
     * @param null|string $cell Position of the split
1961
     * @param null|string $topLeftCell default position of the right bottom pane
1962
     *
1963
     * @return $this
1964
     */
1965 15
    public function freezePane($cell, $topLeftCell = null)
1966
    {
1967 15
        if (is_string($cell) && Coordinate::coordinateIsRange($cell)) {
1968
            throw new Exception('Freeze pane can not be set on a range of cells.');
1969
        }
1970
1971 15
        if ($cell !== null && $topLeftCell === null) {
1972 7
            $coordinate = Coordinate::coordinateFromString($cell);
1973 7
            $topLeftCell = $coordinate[0] . $coordinate[1];
1974
        }
1975
1976 15
        $this->freezePane = $cell;
1977 15
        $this->topLeftCell = $topLeftCell;
1978
1979 15
        return $this;
1980
    }
1981
1982
    /**
1983
     * Freeze Pane by using numeric cell coordinates.
1984
     *
1985
     * @param int $columnIndex Numeric column coordinate of the cell
1986
     * @param int $row Numeric row coordinate of the cell
1987
     *
1988
     * @return $this
1989
     */
1990
    public function freezePaneByColumnAndRow($columnIndex, $row)
1991
    {
1992
        return $this->freezePane(Coordinate::stringFromColumnIndex($columnIndex) . $row);
1993
    }
1994
1995
    /**
1996
     * Unfreeze Pane.
1997
     *
1998
     * @return $this
1999
     */
2000
    public function unfreezePane()
2001
    {
2002
        return $this->freezePane(null);
2003
    }
2004
2005
    /**
2006
     * Get the default position of the right bottom pane.
2007
     *
2008
     * @return null|string
2009
     */
2010 10
    public function getTopLeftCell()
2011
    {
2012 10
        return $this->topLeftCell;
2013
    }
2014
2015
    /**
2016
     * Insert a new row, updating all possible related data.
2017
     *
2018
     * @param int $pBefore Insert before this one
2019
     * @param int $pNumRows Number of rows to insert
2020
     *
2021
     * @return $this
2022
     */
2023 18
    public function insertNewRowBefore($pBefore, $pNumRows = 1)
2024
    {
2025 18
        if ($pBefore >= 1) {
2026 18
            $objReferenceHelper = ReferenceHelper::getInstance();
2027 18
            $objReferenceHelper->insertNewBefore('A' . $pBefore, 0, $pNumRows, $this);
2028
        } else {
2029
            throw new Exception('Rows can only be inserted before at least row 1.');
2030
        }
2031
2032 18
        return $this;
2033
    }
2034
2035
    /**
2036
     * Insert a new column, updating all possible related data.
2037
     *
2038
     * @param string $pBefore Insert before this one, eg: 'A'
2039
     * @param int $pNumCols Number of columns to insert
2040
     *
2041
     * @return $this
2042
     */
2043 16
    public function insertNewColumnBefore($pBefore, $pNumCols = 1)
2044
    {
2045 16
        if (!is_numeric($pBefore)) {
2046 16
            $objReferenceHelper = ReferenceHelper::getInstance();
2047 16
            $objReferenceHelper->insertNewBefore($pBefore . '1', $pNumCols, 0, $this);
2048
        } else {
2049
            throw new Exception('Column references should not be numeric.');
2050
        }
2051
2052 16
        return $this;
2053
    }
2054
2055
    /**
2056
     * Insert a new column, updating all possible related data.
2057
     *
2058
     * @param int $beforeColumnIndex Insert before this one (numeric column coordinate of the cell)
2059
     * @param int $pNumCols Number of columns to insert
2060
     *
2061
     * @return $this
2062
     */
2063
    public function insertNewColumnBeforeByIndex($beforeColumnIndex, $pNumCols = 1)
2064
    {
2065
        if ($beforeColumnIndex >= 1) {
2066
            return $this->insertNewColumnBefore(Coordinate::stringFromColumnIndex($beforeColumnIndex), $pNumCols);
2067
        }
2068
2069
        throw new Exception('Columns can only be inserted before at least column A (1).');
2070
    }
2071
2072
    /**
2073
     * Delete a row, updating all possible related data.
2074
     *
2075
     * @param int $pRow Remove starting with this one
2076
     * @param int $pNumRows Number of rows to remove
2077
     *
2078
     * @return $this
2079
     */
2080 28
    public function removeRow($pRow, $pNumRows = 1)
2081
    {
2082 28
        if ($pRow < 1) {
2083
            throw new Exception('Rows to be deleted should at least start from row 1.');
2084
        }
2085
2086 28
        $highestRow = $this->getHighestDataRow();
2087 28
        $removedRowsCounter = 0;
2088
2089 28
        for ($r = 0; $r < $pNumRows; ++$r) {
2090 28
            if ($pRow + $r <= $highestRow) {
2091 27
                $this->getCellCollection()->removeRow($pRow + $r);
2092 27
                ++$removedRowsCounter;
2093
            }
2094
        }
2095
2096 28
        $objReferenceHelper = ReferenceHelper::getInstance();
2097 28
        $objReferenceHelper->insertNewBefore('A' . ($pRow + $pNumRows), 0, -$pNumRows, $this);
2098 28
        for ($r = 0; $r < $removedRowsCounter; ++$r) {
2099 27
            $this->getCellCollection()->removeRow($highestRow);
2100 27
            --$highestRow;
2101
        }
2102
2103 28
        return $this;
2104
    }
2105
2106
    /**
2107
     * Remove a column, updating all possible related data.
2108
     *
2109
     * @param string $pColumn Remove starting with this one, eg: 'A'
2110
     * @param int $pNumCols Number of columns to remove
2111
     *
2112
     * @return $this
2113
     */
2114 21
    public function removeColumn($pColumn, $pNumCols = 1)
2115
    {
2116 21
        if (is_numeric($pColumn)) {
2117
            throw new Exception('Column references should not be numeric.');
2118
        }
2119
2120 21
        $highestColumn = $this->getHighestDataColumn();
2121 21
        $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
2122 21
        $pColumnIndex = Coordinate::columnIndexFromString($pColumn);
2123
2124 21
        if ($pColumnIndex > $highestColumnIndex) {
2125 1
            return $this;
2126
        }
2127
2128 20
        $pColumn = Coordinate::stringFromColumnIndex($pColumnIndex + $pNumCols);
2129 20
        $objReferenceHelper = ReferenceHelper::getInstance();
2130 20
        $objReferenceHelper->insertNewBefore($pColumn . '1', -$pNumCols, 0, $this);
2131
2132 20
        $maxPossibleColumnsToBeRemoved = $highestColumnIndex - $pColumnIndex + 1;
2133
2134 20
        for ($c = 0, $n = min($maxPossibleColumnsToBeRemoved, $pNumCols); $c < $n; ++$c) {
2135 20
            $this->getCellCollection()->removeColumn($highestColumn);
2136 20
            $highestColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($highestColumn) - 1);
2137
        }
2138
2139 20
        $this->garbageCollect();
2140
2141 20
        return $this;
2142
    }
2143
2144
    /**
2145
     * Remove a column, updating all possible related data.
2146
     *
2147
     * @param int $columnIndex Remove starting with this one (numeric column coordinate of the cell)
2148
     * @param int $numColumns Number of columns to remove
2149
     *
2150
     * @return $this
2151
     */
2152
    public function removeColumnByIndex($columnIndex, $numColumns = 1)
2153
    {
2154
        if ($columnIndex >= 1) {
2155
            return $this->removeColumn(Coordinate::stringFromColumnIndex($columnIndex), $numColumns);
2156
        }
2157
2158
        throw new Exception('Columns to be deleted should at least start from column A (1)');
2159
    }
2160
2161
    /**
2162
     * Show gridlines?
2163
     *
2164
     * @return bool
2165
     */
2166 304
    public function getShowGridlines()
2167
    {
2168 304
        return $this->showGridlines;
2169
    }
2170
2171
    /**
2172
     * Set show gridlines.
2173
     *
2174
     * @param bool $pValue Show gridlines (true/false)
2175
     *
2176
     * @return $this
2177
     */
2178 107
    public function setShowGridlines($pValue)
2179
    {
2180 107
        $this->showGridlines = $pValue;
2181
2182 107
        return $this;
2183
    }
2184
2185
    /**
2186
     * Print gridlines?
2187
     *
2188
     * @return bool
2189
     */
2190 307
    public function getPrintGridlines()
2191
    {
2192 307
        return $this->printGridlines;
2193
    }
2194
2195
    /**
2196
     * Set print gridlines.
2197
     *
2198
     * @param bool $pValue Print gridlines (true/false)
2199
     *
2200
     * @return $this
2201
     */
2202 38
    public function setPrintGridlines($pValue)
2203
    {
2204 38
        $this->printGridlines = $pValue;
2205
2206 38
        return $this;
2207
    }
2208
2209
    /**
2210
     * Show row and column headers?
2211
     *
2212
     * @return bool
2213
     */
2214 146
    public function getShowRowColHeaders()
2215
    {
2216 146
        return $this->showRowColHeaders;
2217
    }
2218
2219
    /**
2220
     * Set show row and column headers.
2221
     *
2222
     * @param bool $pValue Show row and column headers (true/false)
2223
     *
2224
     * @return $this
2225
     */
2226 99
    public function setShowRowColHeaders($pValue)
2227
    {
2228 99
        $this->showRowColHeaders = $pValue;
2229
2230 99
        return $this;
2231
    }
2232
2233
    /**
2234
     * Show summary below? (Row/Column outlining).
2235
     *
2236
     * @return bool
2237
     */
2238 146
    public function getShowSummaryBelow()
2239
    {
2240 146
        return $this->showSummaryBelow;
2241
    }
2242
2243
    /**
2244
     * Set show summary below.
2245
     *
2246
     * @param bool $pValue Show summary below (true/false)
2247
     *
2248
     * @return $this
2249
     */
2250 100
    public function setShowSummaryBelow($pValue)
2251
    {
2252 100
        $this->showSummaryBelow = $pValue;
2253
2254 100
        return $this;
2255
    }
2256
2257
    /**
2258
     * Show summary right? (Row/Column outlining).
2259
     *
2260
     * @return bool
2261
     */
2262 146
    public function getShowSummaryRight()
2263
    {
2264 146
        return $this->showSummaryRight;
2265
    }
2266
2267
    /**
2268
     * Set show summary right.
2269
     *
2270
     * @param bool $pValue Show summary right (true/false)
2271
     *
2272
     * @return $this
2273
     */
2274 100
    public function setShowSummaryRight($pValue)
2275
    {
2276 100
        $this->showSummaryRight = $pValue;
2277
2278 100
        return $this;
2279
    }
2280
2281
    /**
2282
     * Get comments.
2283
     *
2284
     * @return Comment[]
2285
     */
2286 307
    public function getComments()
2287
    {
2288 307
        return $this->comments;
2289
    }
2290
2291
    /**
2292
     * Set comments array for the entire sheet.
2293
     *
2294
     * @param Comment[] $pValue
2295
     *
2296
     * @return $this
2297
     */
2298 32
    public function setComments(array $pValue)
2299
    {
2300 32
        $this->comments = $pValue;
2301
2302 32
        return $this;
2303
    }
2304
2305
    /**
2306
     * Get comment for cell.
2307
     *
2308
     * @param string $pCellCoordinate Cell coordinate to get comment for, eg: 'A1'
2309
     *
2310
     * @return Comment
2311
     */
2312 55
    public function getComment($pCellCoordinate)
2313
    {
2314
        // Uppercase coordinate
2315 55
        $pCellCoordinate = strtoupper($pCellCoordinate);
2316
2317 55
        if (Coordinate::coordinateIsRange($pCellCoordinate)) {
2318
            throw new Exception('Cell coordinate string can not be a range of cells.');
2319 55
        } elseif (strpos($pCellCoordinate, '$') !== false) {
2320
            throw new Exception('Cell coordinate string must not be absolute.');
2321 55
        } elseif ($pCellCoordinate == '') {
2322
            throw new Exception('Cell coordinate can not be zero-length string.');
2323
        }
2324
2325
        // Check if we already have a comment for this cell.
2326 55
        if (isset($this->comments[$pCellCoordinate])) {
2327 31
            return $this->comments[$pCellCoordinate];
2328
        }
2329
2330
        // If not, create a new comment.
2331 55
        $newComment = new Comment();
2332 55
        $this->comments[$pCellCoordinate] = $newComment;
2333
2334 55
        return $newComment;
2335
    }
2336
2337
    /**
2338
     * Get comment for cell by using numeric cell coordinates.
2339
     *
2340
     * @param int $columnIndex Numeric column coordinate of the cell
2341
     * @param int $row Numeric row coordinate of the cell
2342
     *
2343
     * @return Comment
2344
     */
2345 3
    public function getCommentByColumnAndRow($columnIndex, $row)
2346
    {
2347 3
        return $this->getComment(Coordinate::stringFromColumnIndex($columnIndex) . $row);
2348
    }
2349
2350
    /**
2351
     * Get active cell.
2352
     *
2353
     * @return string Example: 'A1'
2354
     */
2355 3528
    public function getActiveCell()
2356
    {
2357 3528
        return $this->activeCell;
2358
    }
2359
2360
    /**
2361
     * Get selected cells.
2362
     *
2363
     * @return string
2364
     */
2365 3691
    public function getSelectedCells()
2366
    {
2367 3691
        return $this->selectedCells;
2368
    }
2369
2370
    /**
2371
     * Selected cell.
2372
     *
2373
     * @param string $pCoordinate Cell (i.e. A1)
2374
     *
2375
     * @return $this
2376
     */
2377 8
    public function setSelectedCell($pCoordinate)
2378
    {
2379 8
        return $this->setSelectedCells($pCoordinate);
2380
    }
2381
2382
    /**
2383
     * Select a range of cells.
2384
     *
2385
     * @param string $pCoordinate Cell range, examples: 'A1', 'B2:G5', 'A:C', '3:6'
2386
     *
2387
     * @return $this
2388
     */
2389 3723
    public function setSelectedCells($pCoordinate)
2390
    {
2391
        // Uppercase coordinate
2392 3723
        $pCoordinate = strtoupper($pCoordinate);
2393
2394
        // Convert 'A' to 'A:A'
2395 3723
        $pCoordinate = preg_replace('/^([A-Z]+)$/', '${1}:${1}', $pCoordinate);
2396
2397
        // Convert '1' to '1:1'
2398 3723
        $pCoordinate = preg_replace('/^(\d+)$/', '${1}:${1}', $pCoordinate);
2399
2400
        // Convert 'A:C' to 'A1:C1048576'
2401 3723
        $pCoordinate = preg_replace('/^([A-Z]+):([A-Z]+)$/', '${1}1:${2}1048576', $pCoordinate);
2402
2403
        // Convert '1:3' to 'A1:XFD3'
2404 3723
        $pCoordinate = preg_replace('/^(\d+):(\d+)$/', 'A${1}:XFD${2}', $pCoordinate);
2405
2406 3723
        if (Coordinate::coordinateIsRange($pCoordinate)) {
2407 136
            [$first] = Coordinate::splitRange($pCoordinate);
2408 136
            $this->activeCell = $first[0];
2409
        } else {
2410 3699
            $this->activeCell = $pCoordinate;
2411
        }
2412 3723
        $this->selectedCells = $pCoordinate;
2413
2414 3723
        return $this;
2415
    }
2416
2417
    /**
2418
     * Selected cell by using numeric cell coordinates.
2419
     *
2420
     * @param int $columnIndex Numeric column coordinate of the cell
2421
     * @param int $row Numeric row coordinate of the cell
2422
     *
2423
     * @return $this
2424
     */
2425 19
    public function setSelectedCellByColumnAndRow($columnIndex, $row)
2426
    {
2427 19
        return $this->setSelectedCells(Coordinate::stringFromColumnIndex($columnIndex) . $row);
2428
    }
2429
2430
    /**
2431
     * Get right-to-left.
2432
     *
2433
     * @return bool
2434
     */
2435 146
    public function getRightToLeft()
2436
    {
2437 146
        return $this->rightToLeft;
2438
    }
2439
2440
    /**
2441
     * Set right-to-left.
2442
     *
2443
     * @param bool $value Right-to-left true/false
2444
     *
2445
     * @return $this
2446
     */
2447 39
    public function setRightToLeft($value)
2448
    {
2449 39
        $this->rightToLeft = $value;
2450
2451 39
        return $this;
2452
    }
2453
2454
    /**
2455
     * Fill worksheet from values in array.
2456
     *
2457
     * @param array $source Source array
2458
     * @param mixed $nullValue Value in source array that stands for blank cell
2459
     * @param string $startCell Insert array starting from this cell address as the top left coordinate
2460
     * @param bool $strictNullComparison Apply strict comparison when testing for null values in the array
2461
     *
2462
     * @return $this
2463
     */
2464 177
    public function fromArray(array $source, $nullValue = null, $startCell = 'A1', $strictNullComparison = false)
2465
    {
2466
        //    Convert a 1-D array to 2-D (for ease of looping)
2467 177
        if (!is_array(end($source))) {
2468 20
            $source = [$source];
2469
        }
2470
2471
        // start coordinate
2472 177
        [$startColumn, $startRow] = Coordinate::coordinateFromString($startCell);
2473
2474
        // Loop through $source
2475 177
        foreach ($source as $rowData) {
2476 177
            $currentColumn = $startColumn;
2477 177
            foreach ($rowData as $cellValue) {
2478 177
                if ($strictNullComparison) {
2479 76
                    if ($cellValue !== $nullValue) {
2480
                        // Set cell value
2481 76
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
2482
                    }
2483
                } else {
2484 101
                    if ($cellValue != $nullValue) {
2485
                        // Set cell value
2486 98
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
2487
                    }
2488
                }
2489 177
                ++$currentColumn;
2490
            }
2491 177
            ++$startRow;
2492
        }
2493
2494 177
        return $this;
2495
    }
2496
2497
    /**
2498
     * Create array from a range of cells.
2499
     *
2500
     * @param string $pRange Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
2501
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
2502
     * @param bool $calculateFormulas Should formulas be calculated?
2503
     * @param bool $formatData Should formatting be applied to cell values?
2504
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
2505
     *                               True - Return rows and columns indexed by their actual row and column IDs
2506
     *
2507
     * @return array
2508
     */
2509 51
    public function rangeToArray($pRange, $nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
2510
    {
2511
        // Returnvalue
2512 51
        $returnValue = [];
2513
        //    Identify the range that we need to extract from the worksheet
2514 51
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($pRange);
2515 51
        $minCol = Coordinate::stringFromColumnIndex($rangeStart[0]);
2516 51
        $minRow = $rangeStart[1];
2517 51
        $maxCol = Coordinate::stringFromColumnIndex($rangeEnd[0]);
2518 51
        $maxRow = $rangeEnd[1];
2519
2520 51
        ++$maxCol;
2521
        // Loop through rows
2522 51
        $r = -1;
2523 51
        for ($row = $minRow; $row <= $maxRow; ++$row) {
2524 51
            $rRef = $returnCellRef ? $row : ++$r;
2525 51
            $c = -1;
2526
            // Loop through columns in the current row
2527 51
            for ($col = $minCol; $col != $maxCol; ++$col) {
2528 51
                $cRef = $returnCellRef ? $col : ++$c;
2529
                //    Using getCell() will create a new cell if it doesn't already exist. We don't want that to happen
2530
                //        so we test and retrieve directly against cellCollection
2531 51
                if ($this->cellCollection->has($col . $row)) {
2532
                    // Cell exists
2533 51
                    $cell = $this->cellCollection->get($col . $row);
2534 51
                    if ($cell->getValue() !== null) {
2535 51
                        if ($cell->getValue() instanceof RichText) {
2536 2
                            $returnValue[$rRef][$cRef] = $cell->getValue()->getPlainText();
2537
                        } else {
2538 51
                            if ($calculateFormulas) {
2539 50
                                $returnValue[$rRef][$cRef] = $cell->getCalculatedValue();
2540
                            } else {
2541 2
                                $returnValue[$rRef][$cRef] = $cell->getValue();
2542
                            }
2543
                        }
2544
2545 51
                        if ($formatData) {
2546 50
                            $style = $this->parent->getCellXfByIndex($cell->getXfIndex());
2547 50
                            $returnValue[$rRef][$cRef] = NumberFormat::toFormattedString(
2548 50
                                $returnValue[$rRef][$cRef],
2549 51
                                ($style && $style->getNumberFormat()) ? $style->getNumberFormat()->getFormatCode() : NumberFormat::FORMAT_GENERAL
2550
                            );
2551
                        }
2552
                    } else {
2553
                        // Cell holds a NULL
2554 51
                        $returnValue[$rRef][$cRef] = $nullValue;
2555
                    }
2556
                } else {
2557
                    // Cell doesn't exist
2558 13
                    $returnValue[$rRef][$cRef] = $nullValue;
2559
                }
2560
            }
2561
        }
2562
2563
        // Return
2564 51
        return $returnValue;
2565
    }
2566
2567 12
    private function validateNamedRange(string $definedName, bool $returnNullIfInvalid = false): ?DefinedName
2568
    {
2569 12
        $namedRange = DefinedName::resolveName($definedName, $this);
2570 12
        if ($namedRange === null) {
2571 5
            if ($returnNullIfInvalid) {
2572 4
                return null;
2573
            }
2574
2575 1
            throw new Exception('Named Range ' . $definedName . ' does not exist.');
2576
        }
2577
2578 7
        if ($namedRange->isFormula()) {
2579
            if ($returnNullIfInvalid) {
2580
                return null;
2581
            }
2582
2583
            throw new Exception('Defined Named ' . $definedName . ' is a formula, not a range or cell.');
2584
        }
2585
2586 7
        if ($namedRange->getLocalOnly() && $this->getHashCode() !== $namedRange->getWorksheet()->getHashCode()) {
2587
            if ($returnNullIfInvalid) {
2588
                return null;
2589
            }
2590
2591
            throw new Exception(
2592
                'Named range ' . $definedName . ' is not accessible from within sheet ' . $this->getTitle()
2593
            );
2594
        }
2595
2596 7
        return $namedRange;
2597
    }
2598
2599
    /**
2600
     * Create array from a range of cells.
2601
     *
2602
     * @param string $definedName The Named Range that should be returned
2603
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
2604
     * @param bool $calculateFormulas Should formulas be calculated?
2605
     * @param bool $formatData Should formatting be applied to cell values?
2606
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
2607
     *                                True - Return rows and columns indexed by their actual row and column IDs
2608
     *
2609
     * @return array
2610
     */
2611 2
    public function namedRangeToArray(string $definedName, $nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
2612
    {
2613 2
        $namedRange = $this->validateNamedRange($definedName);
2614 1
        $workSheet = $namedRange->getWorksheet();
2615 1
        $cellRange = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!');
2616 1
        $cellRange = str_replace('$', '', $cellRange);
2617
2618 1
        return $workSheet->rangeToArray($cellRange, $nullValue, $calculateFormulas, $formatData, $returnCellRef);
2619
    }
2620
2621
    /**
2622
     * Create array from worksheet.
2623
     *
2624
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
2625
     * @param bool $calculateFormulas Should formulas be calculated?
2626
     * @param bool $formatData Should formatting be applied to cell values?
2627
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
2628
     *                               True - Return rows and columns indexed by their actual row and column IDs
2629
     *
2630
     * @return array
2631
     */
2632 27
    public function toArray($nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
2633
    {
2634
        // Garbage collect...
2635 27
        $this->garbageCollect();
2636
2637
        //    Identify the range that we need to extract from the worksheet
2638 27
        $maxCol = $this->getHighestColumn();
2639 27
        $maxRow = $this->getHighestRow();
2640
2641
        // Return
2642 27
        return $this->rangeToArray('A1:' . $maxCol . $maxRow, $nullValue, $calculateFormulas, $formatData, $returnCellRef);
2643
    }
2644
2645
    /**
2646
     * Get row iterator.
2647
     *
2648
     * @param int $startRow The row number at which to start iterating
2649
     * @param int $endRow The row number at which to stop iterating
2650
     *
2651
     * @return RowIterator
2652
     */
2653 13
    public function getRowIterator($startRow = 1, $endRow = null)
2654
    {
2655 13
        return new RowIterator($this, $startRow, $endRow);
2656
    }
2657
2658
    /**
2659
     * Get column iterator.
2660
     *
2661
     * @param string $startColumn The column address at which to start iterating
2662
     * @param string $endColumn The column address at which to stop iterating
2663
     *
2664
     * @return ColumnIterator
2665
     */
2666 3
    public function getColumnIterator($startColumn = 'A', $endColumn = null)
2667
    {
2668 3
        return new ColumnIterator($this, $startColumn, $endColumn);
2669
    }
2670
2671
    /**
2672
     * Run PhpSpreadsheet garbage collector.
2673
     *
2674
     * @return $this
2675
     */
2676 342
    public function garbageCollect()
2677
    {
2678
        // Flush cache
2679 342
        $this->cellCollection->get('A1');
2680
2681
        // Lookup highest column and highest row if cells are cleaned
2682 342
        $colRow = $this->cellCollection->getHighestRowAndColumn();
2683 342
        $highestRow = $colRow['row'];
2684 342
        $highestColumn = Coordinate::columnIndexFromString($colRow['column']);
2685
2686
        // Loop through column dimensions
2687 342
        foreach ($this->columnDimensions as $dimension) {
2688 50
            $highestColumn = max($highestColumn, Coordinate::columnIndexFromString($dimension->getColumnIndex()));
2689
        }
2690
2691
        // Loop through row dimensions
2692 342
        foreach ($this->rowDimensions as $dimension) {
2693 60
            $highestRow = max($highestRow, $dimension->getRowIndex());
2694
        }
2695
2696
        // Cache values
2697 342
        if ($highestColumn < 1) {
2698
            $this->cachedHighestColumn = 1;
2699
        } else {
2700 342
            $this->cachedHighestColumn = $highestColumn;
2701
        }
2702 342
        $this->cachedHighestRow = $highestRow;
2703
2704
        // Return
2705 342
        return $this;
2706
    }
2707
2708
    /**
2709
     * Get hash code.
2710
     *
2711
     * @return string Hash code
2712
     */
2713 3748
    public function getHashCode()
2714
    {
2715 3748
        if ($this->dirty) {
2716 3748
            $this->hash = md5($this->title . $this->autoFilter . ($this->protection->isProtectionEnabled() ? 't' : 'f') . __CLASS__);
2717 3748
            $this->dirty = false;
2718
        }
2719
2720 3748
        return $this->hash;
2721
    }
2722
2723
    /**
2724
     * Extract worksheet title from range.
2725
     *
2726
     * Example: extractSheetTitle("testSheet!A1") ==> 'A1'
2727
     * Example: extractSheetTitle("'testSheet 1'!A1", true) ==> ['testSheet 1', 'A1'];
2728
     *
2729
     * @param string $pRange Range to extract title from
2730
     * @param bool $returnRange Return range? (see example)
2731
     *
2732
     * @return mixed
2733
     */
2734 459
    public static function extractSheetTitle($pRange, $returnRange = false)
2735
    {
2736
        // Sheet title included?
2737 459
        if (($sep = strrpos($pRange, '!')) === false) {
2738 65
            return $returnRange ? ['', $pRange] : '';
2739
        }
2740
2741 423
        if ($returnRange) {
2742 423
            return [substr($pRange, 0, $sep), substr($pRange, $sep + 1)];
2743
        }
2744
2745 7
        return substr($pRange, $sep + 1);
2746
    }
2747
2748
    /**
2749
     * Get hyperlink.
2750
     *
2751
     * @param string $pCellCoordinate Cell coordinate to get hyperlink for, eg: 'A1'
2752
     *
2753
     * @return Hyperlink
2754
     */
2755 36
    public function getHyperlink($pCellCoordinate)
2756
    {
2757
        // return hyperlink if we already have one
2758 36
        if (isset($this->hyperlinkCollection[$pCellCoordinate])) {
2759 22
            return $this->hyperlinkCollection[$pCellCoordinate];
2760
        }
2761
2762
        // else create hyperlink
2763 36
        $this->hyperlinkCollection[$pCellCoordinate] = new Hyperlink();
2764
2765 36
        return $this->hyperlinkCollection[$pCellCoordinate];
2766
    }
2767
2768
    /**
2769
     * Set hyperlink.
2770
     *
2771
     * @param string $pCellCoordinate Cell coordinate to insert hyperlink, eg: 'A1'
2772
     *
2773
     * @return $this
2774
     */
2775 17
    public function setHyperlink($pCellCoordinate, ?Hyperlink $pHyperlink = null)
2776
    {
2777 17
        if ($pHyperlink === null) {
2778 17
            unset($this->hyperlinkCollection[$pCellCoordinate]);
2779
        } else {
2780 17
            $this->hyperlinkCollection[$pCellCoordinate] = $pHyperlink;
2781
        }
2782
2783 17
        return $this;
2784
    }
2785
2786
    /**
2787
     * Hyperlink at a specific coordinate exists?
2788
     *
2789
     * @param string $pCoordinate eg: 'A1'
2790
     *
2791
     * @return bool
2792
     */
2793 163
    public function hyperlinkExists($pCoordinate)
2794
    {
2795 163
        return isset($this->hyperlinkCollection[$pCoordinate]);
2796
    }
2797
2798
    /**
2799
     * Get collection of hyperlinks.
2800
     *
2801
     * @return Hyperlink[]
2802
     */
2803 166
    public function getHyperlinkCollection()
2804
    {
2805 166
        return $this->hyperlinkCollection;
2806
    }
2807
2808
    /**
2809
     * Get data validation.
2810
     *
2811
     * @param string $pCellCoordinate Cell coordinate to get data validation for, eg: 'A1'
2812
     *
2813
     * @return DataValidation
2814
     */
2815 5
    public function getDataValidation($pCellCoordinate)
2816
    {
2817
        // return data validation if we already have one
2818 5
        if (isset($this->dataValidationCollection[$pCellCoordinate])) {
2819 2
            return $this->dataValidationCollection[$pCellCoordinate];
2820
        }
2821
2822
        // else create data validation
2823 5
        $this->dataValidationCollection[$pCellCoordinate] = new DataValidation();
2824
2825 5
        return $this->dataValidationCollection[$pCellCoordinate];
2826
    }
2827
2828
    /**
2829
     * Set data validation.
2830
     *
2831
     * @param string $pCellCoordinate Cell coordinate to insert data validation, eg: 'A1'
2832
     *
2833
     * @return $this
2834
     */
2835
    public function setDataValidation($pCellCoordinate, ?DataValidation $pDataValidation = null)
2836
    {
2837
        if ($pDataValidation === null) {
2838
            unset($this->dataValidationCollection[$pCellCoordinate]);
2839
        } else {
2840
            $this->dataValidationCollection[$pCellCoordinate] = $pDataValidation;
2841
        }
2842
2843
        return $this;
2844
    }
2845
2846
    /**
2847
     * Data validation at a specific coordinate exists?
2848
     *
2849
     * @param string $pCoordinate eg: 'A1'
2850
     *
2851
     * @return bool
2852
     */
2853 4
    public function dataValidationExists($pCoordinate)
2854
    {
2855 4
        return isset($this->dataValidationCollection[$pCoordinate]);
2856
    }
2857
2858
    /**
2859
     * Get collection of data validations.
2860
     *
2861
     * @return DataValidation[]
2862
     */
2863 166
    public function getDataValidationCollection()
2864
    {
2865 166
        return $this->dataValidationCollection;
2866
    }
2867
2868
    /**
2869
     * Accepts a range, returning it as a range that falls within the current highest row and column of the worksheet.
2870
     *
2871
     * @param string $range
2872
     *
2873
     * @return string Adjusted range value
2874
     */
2875 1
    public function shrinkRangeToFit($range)
2876
    {
2877 1
        $maxCol = $this->getHighestColumn();
2878 1
        $maxRow = $this->getHighestRow();
2879 1
        $maxCol = Coordinate::columnIndexFromString($maxCol);
2880
2881 1
        $rangeBlocks = explode(' ', $range);
2882 1
        foreach ($rangeBlocks as &$rangeSet) {
2883 1
            $rangeBoundaries = Coordinate::getRangeBoundaries($rangeSet);
2884
2885 1
            if (Coordinate::columnIndexFromString($rangeBoundaries[0][0]) > $maxCol) {
2886
                $rangeBoundaries[0][0] = Coordinate::stringFromColumnIndex($maxCol);
2887
            }
2888 1
            if ($rangeBoundaries[0][1] > $maxRow) {
2889
                $rangeBoundaries[0][1] = $maxRow;
2890
            }
2891 1
            if (Coordinate::columnIndexFromString($rangeBoundaries[1][0]) > $maxCol) {
2892
                $rangeBoundaries[1][0] = Coordinate::stringFromColumnIndex($maxCol);
2893
            }
2894 1
            if ($rangeBoundaries[1][1] > $maxRow) {
2895
                $rangeBoundaries[1][1] = $maxRow;
2896
            }
2897 1
            $rangeSet = $rangeBoundaries[0][0] . $rangeBoundaries[0][1] . ':' . $rangeBoundaries[1][0] . $rangeBoundaries[1][1];
2898
        }
2899 1
        unset($rangeSet);
2900
2901 1
        return implode(' ', $rangeBlocks);
2902
    }
2903
2904
    /**
2905
     * Get tab color.
2906
     *
2907
     * @return Color
2908
     */
2909 16
    public function getTabColor()
2910
    {
2911 16
        if ($this->tabColor === null) {
2912 16
            $this->tabColor = new Color();
2913
        }
2914
2915 16
        return $this->tabColor;
2916
    }
2917
2918
    /**
2919
     * Reset tab color.
2920
     *
2921
     * @return $this
2922
     */
2923
    public function resetTabColor()
2924
    {
2925
        $this->tabColor = null;
2926
2927
        return $this;
2928
    }
2929
2930
    /**
2931
     * Tab color set?
2932
     *
2933
     * @return bool
2934
     */
2935 155
    public function isTabColorSet()
2936
    {
2937 155
        return $this->tabColor !== null;
2938
    }
2939
2940
    /**
2941
     * Copy worksheet (!= clone!).
2942
     *
2943
     * @return static
2944
     */
2945
    public function copy()
2946
    {
2947
        return clone $this;
2948
    }
2949
2950
    /**
2951
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
2952
     */
2953 5
    public function __clone()
2954
    {
2955
        // @phpstan-ignore-next-line
2956 5
        foreach ($this as $key => $val) {
2957 5
            if ($key == 'parent') {
2958 5
                continue;
2959
            }
2960
2961 5
            if (is_object($val) || (is_array($val))) {
2962 5
                if ($key == 'cellCollection') {
2963 5
                    $newCollection = $this->cellCollection->cloneCellCollection($this);
2964 5
                    $this->cellCollection = $newCollection;
2965 5
                } elseif ($key == 'drawingCollection') {
2966 5
                    $currentCollection = $this->drawingCollection;
2967 5
                    $this->drawingCollection = new ArrayObject();
2968 5
                    foreach ($currentCollection as $item) {
2969 3
                        if (is_object($item)) {
2970 3
                            $newDrawing = clone $item;
2971 3
                            $newDrawing->setWorksheet($this);
2972
                        }
2973
                    }
2974 5
                } elseif (($key == 'autoFilter') && ($this->autoFilter instanceof AutoFilter)) {
2975 5
                    $newAutoFilter = clone $this->autoFilter;
2976 5
                    $this->autoFilter = $newAutoFilter;
2977 5
                    $this->autoFilter->setParent($this);
2978
                } else {
2979 5
                    $this->{$key} = unserialize(serialize($val));
2980
                }
2981
            }
2982
        }
2983 5
    }
2984
2985
    /**
2986
     * Define the code name of the sheet.
2987
     *
2988
     * @param string $pValue Same rule as Title minus space not allowed (but, like Excel, change
2989
     *                       silently space to underscore)
2990
     * @param bool $validate False to skip validation of new title. WARNING: This should only be set
2991
     *                       at parse time (by Readers), where titles can be assumed to be valid.
2992
     *
2993
     * @return $this
2994
     */
2995 4040
    public function setCodeName($pValue, $validate = true)
2996
    {
2997
        // Is this a 'rename' or not?
2998 4040
        if ($this->getCodeName() == $pValue) {
2999
            return $this;
3000
        }
3001
3002 4040
        if ($validate) {
3003 4040
            $pValue = str_replace(' ', '_', $pValue); //Excel does this automatically without flinching, we are doing the same
3004
3005
            // Syntax check
3006
            // throw an exception if not valid
3007 4040
            self::checkSheetCodeName($pValue);
3008
3009
            // We use the same code that setTitle to find a valid codeName else not using a space (Excel don't like) but a '_'
3010
3011 4040
            if ($this->getParent()) {
3012
                // Is there already such sheet name?
3013 4002
                if ($this->getParent()->sheetCodeNameExists($pValue)) {
3014
                    // Use name, but append with lowest possible integer
3015
3016 92
                    if (Shared\StringHelper::countCharacters($pValue) > 29) {
3017
                        $pValue = Shared\StringHelper::substring($pValue, 0, 29);
3018
                    }
3019 92
                    $i = 1;
3020 92
                    while ($this->getParent()->sheetCodeNameExists($pValue . '_' . $i)) {
3021 27
                        ++$i;
3022 27
                        if ($i == 10) {
3023
                            if (Shared\StringHelper::countCharacters($pValue) > 28) {
3024
                                $pValue = Shared\StringHelper::substring($pValue, 0, 28);
3025
                            }
3026 27
                        } elseif ($i == 100) {
3027
                            if (Shared\StringHelper::countCharacters($pValue) > 27) {
3028
                                $pValue = Shared\StringHelper::substring($pValue, 0, 27);
3029
                            }
3030
                        }
3031
                    }
3032
3033 92
                    $pValue .= '_' . $i; // ok, we have a valid name
3034
                }
3035
            }
3036
        }
3037
3038 4040
        $this->codeName = $pValue;
3039
3040 4040
        return $this;
3041
    }
3042
3043
    /**
3044
     * Return the code name of the sheet.
3045
     *
3046
     * @return null|string
3047
     */
3048 4040
    public function getCodeName()
3049
    {
3050 4040
        return $this->codeName;
3051
    }
3052
3053
    /**
3054
     * Sheet has a code name ?
3055
     *
3056
     * @return bool
3057
     */
3058 1
    public function hasCodeName()
3059
    {
3060 1
        return $this->codeName !== null;
3061
    }
3062
}
3063