Passed
Push — master ( e5185e...ea2d4b )
by Adrien
27:27
created

Worksheet::calculateColumnWidths()   C

Complexity

Conditions 16
Paths 6

Size

Total Lines 71
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 16.747

Importance

Changes 0
Metric Value
cc 16
eloc 35
c 0
b 0
f 0
nc 6
nop 0
dl 0
loc 71
ccs 30
cts 35
cp 0.8571
crap 16.747
rs 5.5666

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Worksheet;
4
5
use ArrayObject;
6
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
7
use PhpOffice\PhpSpreadsheet\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 string[]
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 4149
    public function __construct(?Spreadsheet $parent = null, $pTitle = 'Worksheet')
348
    {
349
        // Set parent and title
350 4149
        $this->parent = $parent;
351 4149
        $this->setTitle($pTitle, false);
352
        // setTitle can change $pTitle
353 4149
        $this->setCodeName($this->getTitle());
354 4149
        $this->setSheetState(self::SHEETSTATE_VISIBLE);
355
356 4149
        $this->cellCollection = CellsFactory::getInstance($this);
357
        // Set page setup
358 4149
        $this->pageSetup = new PageSetup();
359
        // Set page margins
360 4149
        $this->pageMargins = new PageMargins();
361
        // Set page header/footer
362 4149
        $this->headerFooter = new HeaderFooter();
363
        // Set sheet view
364 4149
        $this->sheetView = new SheetView();
365
        // Drawing collection
366 4149
        $this->drawingCollection = new ArrayObject();
367
        // Chart collection
368 4149
        $this->chartCollection = new ArrayObject();
369
        // Protection
370 4149
        $this->protection = new Protection();
371
        // Default row dimension
372 4149
        $this->defaultRowDimension = new RowDimension(null);
373
        // Default column dimension
374 4149
        $this->defaultColumnDimension = new ColumnDimension(null);
375 4149
        $this->autoFilter = new AutoFilter(null, $this);
376 4149
    }
377
378
    /**
379
     * Disconnect all cells from this Worksheet object,
380
     * typically so that the worksheet object can be unset.
381
     */
382 2477
    public function disconnectCells(): void
383
    {
384 2477
        if ($this->cellCollection !== null) {
385 2477
            $this->cellCollection->unsetWorksheetCells();
386
            // @phpstan-ignore-next-line
387 2477
            $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 2477
        $this->parent = null;
392 2477
    }
393
394
    /**
395
     * Code to execute when this worksheet is unset().
396
     */
397 31
    public function __destruct()
398
    {
399 31
        Calculation::getInstance($this->parent)->clearCalculationCacheForWorksheet($this->title);
400
401 31
        $this->disconnectCells();
402 31
        $this->rowDimensions = [];
403 31
    }
404
405
    /**
406
     * Return the cell collection.
407
     *
408
     * @return Cells
409
     */
410 4019
    public function getCellCollection()
411
    {
412 4019
        return $this->cellCollection;
413
    }
414
415
    /**
416
     * Get array of invalid characters for sheet title.
417
     *
418
     * @return array
419
     */
420
    public static function getInvalidCharacters()
421
    {
422
        return self::$invalidCharacters;
423
    }
424
425
    /**
426
     * Check sheet code name for valid Excel syntax.
427
     *
428
     * @param string $pValue The string to check
429
     *
430
     * @return string The valid string
431
     */
432 4149
    private static function checkSheetCodeName($pValue)
433
    {
434 4149
        $CharCount = Shared\StringHelper::countCharacters($pValue);
435 4149
        if ($CharCount == 0) {
436
            throw new Exception('Sheet code name cannot be empty.');
437
        }
438
        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ] and  first and last characters cannot be a "'"
439
        if (
440 4149
            (str_replace(self::$invalidCharacters, '', $pValue) !== $pValue) ||
441 4149
            (Shared\StringHelper::substring($pValue, -1, 1) == '\'') ||
442 4149
            (Shared\StringHelper::substring($pValue, 0, 1) == '\'')
443
        ) {
444 1
            throw new Exception('Invalid character found in sheet code name');
445
        }
446
447
        // Enforce maximum characters allowed for sheet title
448 4149
        if ($CharCount > self::SHEET_TITLE_MAXIMUM_LENGTH) {
449 1
            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet code name.');
450
        }
451
452 4149
        return $pValue;
453
    }
454
455
    /**
456
     * Check sheet title for valid Excel syntax.
457
     *
458
     * @param string $pValue The string to check
459
     *
460
     * @return string The valid string
461
     */
462 4149
    private static function checkSheetTitle($pValue)
463
    {
464
        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ]
465 4149
        if (str_replace(self::$invalidCharacters, '', $pValue) !== $pValue) {
466 1
            throw new Exception('Invalid character found in sheet title');
467
        }
468
469
        // Enforce maximum characters allowed for sheet title
470 4149
        if (Shared\StringHelper::countCharacters($pValue) > self::SHEET_TITLE_MAXIMUM_LENGTH) {
471 1
            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet title.');
472
        }
473
474 4149
        return $pValue;
475
    }
476
477
    /**
478
     * Get a sorted list of all cell coordinates currently held in the collection by row and column.
479
     *
480
     * @param bool $sorted Also sort the cell collection?
481
     *
482
     * @return string[]
483
     */
484 509
    public function getCoordinates($sorted = true)
485
    {
486 509
        if ($this->cellCollection == null) {
487
            return [];
488
        }
489
490 509
        if ($sorted) {
491 175
            return $this->cellCollection->getSortedCoordinates();
492
        }
493
494 495
        return $this->cellCollection->getCoordinates();
495
    }
496
497
    /**
498
     * Get collection of row dimensions.
499
     *
500
     * @return RowDimension[]
501
     */
502 341
    public function getRowDimensions()
503
    {
504 341
        return $this->rowDimensions;
505
    }
506
507
    /**
508
     * Get default row dimension.
509
     *
510
     * @return RowDimension
511
     */
512 340
    public function getDefaultRowDimension()
513
    {
514 340
        return $this->defaultRowDimension;
515
    }
516
517
    /**
518
     * Get collection of column dimensions.
519
     *
520
     * @return ColumnDimension[]
521
     */
522 341
    public function getColumnDimensions()
523
    {
524 341
        return $this->columnDimensions;
525
    }
526
527
    /**
528
     * Get default column dimension.
529
     *
530
     * @return ColumnDimension
531
     */
532 160
    public function getDefaultColumnDimension()
533
    {
534 160
        return $this->defaultColumnDimension;
535
    }
536
537
    /**
538
     * Get collection of drawings.
539
     *
540
     * @return ArrayObject<BaseDrawing>
541
     */
542 338
    public function getDrawingCollection()
543
    {
544 338
        return $this->drawingCollection;
545
    }
546
547
    /**
548
     * Get collection of charts.
549
     *
550
     * @return ArrayObject<Chart>
551
     */
552 17
    public function getChartCollection()
553
    {
554 17
        return $this->chartCollection;
555
    }
556
557
    /**
558
     * Add chart.
559
     *
560
     * @param null|int $iChartIndex Index where chart should go (0,1,..., or null for last)
561
     *
562
     * @return Chart
563
     */
564 18
    public function addChart(Chart $pChart, $iChartIndex = null)
565
    {
566 18
        $pChart->setWorksheet($this);
567 18
        if ($iChartIndex === null) {
568 18
            $this->chartCollection[] = $pChart;
569
        } else {
570
            // Insert the chart at the requested index
571
            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

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