Passed
Push — master ( a34695...33ec70 )
by Adrien
09:32
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 array
198
     */
199
    private $protectedCells = [];
200
201
    /**
202
     * Autofilter Range and selection.
203
     *
204
     * @var AutoFilter
205
     */
206
    private $autoFilter;
207
208
    /**
209
     * Freeze pane.
210
     *
211
     * @var null|string
212
     */
213
    private $freezePane;
214
215
    /**
216
     * Default position of the right bottom pane.
217
     *
218
     * @var null|string
219
     */
220
    private $topLeftCell;
221
222
    /**
223
     * Show gridlines?
224
     *
225
     * @var bool
226
     */
227
    private $showGridlines = true;
228
229
    /**
230
     * Print gridlines?
231
     *
232
     * @var bool
233
     */
234
    private $printGridlines = false;
235
236
    /**
237
     * Show row and column headers?
238
     *
239
     * @var bool
240
     */
241
    private $showRowColHeaders = true;
242
243
    /**
244
     * Show summary below? (Row/Column outline).
245
     *
246
     * @var bool
247
     */
248
    private $showSummaryBelow = true;
249
250
    /**
251
     * Show summary right? (Row/Column outline).
252
     *
253
     * @var bool
254
     */
255
    private $showSummaryRight = true;
256
257
    /**
258
     * Collection of comments.
259
     *
260
     * @var Comment[]
261
     */
262
    private $comments = [];
263
264
    /**
265
     * Active cell. (Only one!).
266
     *
267
     * @var string
268
     */
269
    private $activeCell = 'A1';
270
271
    /**
272
     * Selected cells.
273
     *
274
     * @var string
275
     */
276
    private $selectedCells = 'A1';
277
278
    /**
279
     * Cached highest column.
280
     *
281
     * @var int
282
     */
283
    private $cachedHighestColumn = 1;
284
285
    /**
286
     * Cached highest row.
287
     *
288
     * @var int
289
     */
290
    private $cachedHighestRow = 1;
291
292
    /**
293
     * Right-to-left?
294
     *
295
     * @var bool
296
     */
297
    private $rightToLeft = false;
298
299
    /**
300
     * Hyperlinks. Indexed by cell coordinate, e.g. 'A1'.
301
     *
302
     * @var array
303
     */
304
    private $hyperlinkCollection = [];
305
306
    /**
307
     * Data validation objects. Indexed by cell coordinate, e.g. 'A1'.
308
     *
309
     * @var array
310
     */
311
    private $dataValidationCollection = [];
312
313
    /**
314
     * Tab color.
315
     *
316
     * @var null|Color
317
     */
318
    private $tabColor;
319
320
    /**
321
     * Dirty flag.
322
     *
323
     * @var bool
324
     */
325
    private $dirty = true;
326
327
    /**
328
     * Hash.
329
     *
330
     * @var string
331
     */
332
    private $hash;
333
334
    /**
335
     * CodeName.
336
     *
337
     * @var string
338
     */
339
    private $codeName;
340
341
    /**
342
     * Create a new worksheet.
343
     *
344
     * @param Spreadsheet $parent
345
     * @param string $pTitle
346
     */
347 4040
    public function __construct(?Spreadsheet $parent = null, $pTitle = 'Worksheet')
348
    {
349
        // Set parent and title
350 4040
        $this->parent = $parent;
351 4040
        $this->setTitle($pTitle, false);
352
        // setTitle can change $pTitle
353 4040
        $this->setCodeName($this->getTitle());
354 4040
        $this->setSheetState(self::SHEETSTATE_VISIBLE);
355
356 4040
        $this->cellCollection = CellsFactory::getInstance($this);
357
        // Set page setup
358 4040
        $this->pageSetup = new PageSetup();
359
        // Set page margins
360 4040
        $this->pageMargins = new PageMargins();
361
        // Set page header/footer
362 4040
        $this->headerFooter = new HeaderFooter();
363
        // Set sheet view
364 4040
        $this->sheetView = new SheetView();
365
        // Drawing collection
366 4040
        $this->drawingCollection = new ArrayObject();
367
        // Chart collection
368 4040
        $this->chartCollection = new ArrayObject();
369
        // Protection
370 4040
        $this->protection = new Protection();
371
        // Default row dimension
372 4040
        $this->defaultRowDimension = new RowDimension(null);
373
        // Default column dimension
374 4040
        $this->defaultColumnDimension = new ColumnDimension(null);
375 4040
        $this->autoFilter = new AutoFilter(null, $this);
376 4040
    }
377
378
    /**
379
     * Disconnect all cells from this Worksheet object,
380
     * typically so that the worksheet object can be unset.
381
     */
382 2420
    public function disconnectCells(): void
383
    {
384 2420
        if ($this->cellCollection !== null) {
385 2420
            $this->cellCollection->unsetWorksheetCells();
386
            // @phpstan-ignore-next-line
387 2420
            $this->cellCollection = null;
388
        }
389
        //    detach ourself from the workbook, so that it can then delete this worksheet successfully
390
        // @phpstan-ignore-next-line
391 2420
        $this->parent = null;
392 2420
    }
393
394
    /**
395
     * Code to execute when this worksheet is unset().
396
     */
397 17
    public function __destruct()
398
    {
399 17
        Calculation::getInstance($this->parent)->clearCalculationCacheForWorksheet($this->title);
400
401 17
        $this->disconnectCells();
402 17
    }
403
404
    /**
405
     * Return the cell collection.
406
     *
407
     * @return Cells
408
     */
409 3865
    public function getCellCollection()
410
    {
411 3865
        return $this->cellCollection;
412
    }
413
414
    /**
415
     * Get array of invalid characters for sheet title.
416
     *
417
     * @return array
418
     */
419
    public static function getInvalidCharacters()
420
    {
421
        return self::$invalidCharacters;
422
    }
423
424
    /**
425
     * Check sheet code name for valid Excel syntax.
426
     *
427
     * @param string $pValue The string to check
428
     *
429
     * @return string The valid string
430
     */
431 4040
    private static function checkSheetCodeName($pValue)
432
    {
433 4040
        $CharCount = Shared\StringHelper::countCharacters($pValue);
434 4040
        if ($CharCount == 0) {
435
            throw new Exception('Sheet code name cannot be empty.');
436
        }
437
        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ] and  first and last characters cannot be a "'"
438
        if (
439 4040
            (str_replace(self::$invalidCharacters, '', $pValue) !== $pValue) ||
440 4040
            (Shared\StringHelper::substring($pValue, -1, 1) == '\'') ||
441 4040
            (Shared\StringHelper::substring($pValue, 0, 1) == '\'')
442
        ) {
443 1
            throw new Exception('Invalid character found in sheet code name');
444
        }
445
446
        // Enforce maximum characters allowed for sheet title
447 4040
        if ($CharCount > self::SHEET_TITLE_MAXIMUM_LENGTH) {
448 1
            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet code name.');
449
        }
450
451 4040
        return $pValue;
452
    }
453
454
    /**
455
     * Check sheet title for valid Excel syntax.
456
     *
457
     * @param string $pValue The string to check
458
     *
459
     * @return string The valid string
460
     */
461 4040
    private static function checkSheetTitle($pValue)
462
    {
463
        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ]
464 4040
        if (str_replace(self::$invalidCharacters, '', $pValue) !== $pValue) {
465 1
            throw new Exception('Invalid character found in sheet title');
466
        }
467
468
        // Enforce maximum characters allowed for sheet title
469 4040
        if (Shared\StringHelper::countCharacters($pValue) > self::SHEET_TITLE_MAXIMUM_LENGTH) {
470 1
            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet title.');
471
        }
472
473 4040
        return $pValue;
474
    }
475
476
    /**
477
     * Get a sorted list of all cell coordinates currently held in the collection by row and column.
478
     *
479
     * @param bool $sorted Also sort the cell collection?
480
     *
481
     * @return string[]
482
     */
483 404
    public function getCoordinates($sorted = true)
484
    {
485 404
        if ($this->cellCollection == null) {
486
            return [];
487
        }
488
489 404
        if ($sorted) {
490 167
            return $this->cellCollection->getSortedCoordinates();
491
        }
492
493 391
        return $this->cellCollection->getCoordinates();
494
    }
495
496
    /**
497
     * Get collection of row dimensions.
498
     *
499
     * @return RowDimension[]
500
     */
501 327
    public function getRowDimensions()
502
    {
503 327
        return $this->rowDimensions;
504
    }
505
506
    /**
507
     * Get default row dimension.
508
     *
509
     * @return RowDimension
510
     */
511 328
    public function getDefaultRowDimension()
512
    {
513 328
        return $this->defaultRowDimension;
514
    }
515
516
    /**
517
     * Get collection of column dimensions.
518
     *
519
     * @return ColumnDimension[]
520
     */
521 327
    public function getColumnDimensions()
522
    {
523 327
        return $this->columnDimensions;
524
    }
525
526
    /**
527
     * Get default column dimension.
528
     *
529
     * @return ColumnDimension
530
     */
531 152
    public function getDefaultColumnDimension()
532
    {
533 152
        return $this->defaultColumnDimension;
534
    }
535
536
    /**
537
     * Get collection of drawings.
538
     *
539
     * @return ArrayObject<BaseDrawing>
540
     */
541 326
    public function getDrawingCollection()
542
    {
543 326
        return $this->drawingCollection;
544
    }
545
546
    /**
547
     * Get collection of charts.
548
     *
549
     * @return ArrayObject<Chart>
550
     */
551 17
    public function getChartCollection()
552
    {
553 17
        return $this->chartCollection;
554
    }
555
556
    /**
557
     * Add chart.
558
     *
559
     * @param null|int $iChartIndex Index where chart should go (0,1,..., or null for last)
560
     *
561
     * @return Chart
562
     */
563 18
    public function addChart(Chart $pChart, $iChartIndex = null)
564
    {
565 18
        $pChart->setWorksheet($this);
566 18
        if ($iChartIndex === null) {
567 18
            $this->chartCollection[] = $pChart;
568
        } else {
569
            // Insert the chart at the requested index
570
            array_splice($this->chartCollection, $iChartIndex, 0, [$pChart]);
0 ignored issues
show
Bug introduced by
$this->chartCollection of type ArrayObject is incompatible with the type array expected by parameter $array of array_splice(). ( Ignorable by Annotation )

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

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