Failed Conditions
Push — master ( 02f37d...f95322 )
by Adrien
09:26
created

Worksheet::removeColumn()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4.0032

Importance

Changes 0
Metric Value
cc 4
eloc 16
nc 4
nop 2
dl 0
loc 28
ccs 16
cts 17
cp 0.9412
crap 4.0032
rs 9.7333
c 0
b 0
f 0
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Worksheet;
4
5
use ArrayObject;
6
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
7
use PhpOffice\PhpSpreadsheet\Cell\Cell;
8
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
9
use PhpOffice\PhpSpreadsheet\Cell\DataType;
10
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
11
use PhpOffice\PhpSpreadsheet\Cell\Hyperlink;
12
use PhpOffice\PhpSpreadsheet\Chart\Chart;
13
use PhpOffice\PhpSpreadsheet\Collection\Cells;
14
use PhpOffice\PhpSpreadsheet\Collection\CellsFactory;
15
use PhpOffice\PhpSpreadsheet\Comment;
16
use PhpOffice\PhpSpreadsheet\DefinedName;
17
use PhpOffice\PhpSpreadsheet\Exception;
18
use PhpOffice\PhpSpreadsheet\IComparable;
19
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
20
use PhpOffice\PhpSpreadsheet\RichText\RichText;
21
use PhpOffice\PhpSpreadsheet\Shared;
22
use PhpOffice\PhpSpreadsheet\Spreadsheet;
23
use PhpOffice\PhpSpreadsheet\Style\Color;
24
use PhpOffice\PhpSpreadsheet\Style\Conditional;
25
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
26
use PhpOffice\PhpSpreadsheet\Style\Style;
27
28
class Worksheet implements IComparable
29
{
30
    // Break types
31
    const BREAK_NONE = 0;
32
    const BREAK_ROW = 1;
33
    const BREAK_COLUMN = 2;
34
35
    // Sheet state
36
    const SHEETSTATE_VISIBLE = 'visible';
37
    const SHEETSTATE_HIDDEN = 'hidden';
38
    const SHEETSTATE_VERYHIDDEN = 'veryHidden';
39
40
    /**
41
     * Maximum 31 characters allowed for sheet title.
42
     *
43
     * @var int
44
     */
45
    const SHEET_TITLE_MAXIMUM_LENGTH = 31;
46
47
    /**
48
     * Invalid characters in sheet title.
49
     *
50
     * @var array
51
     */
52
    private static $invalidCharacters = ['*', ':', '/', '\\', '?', '[', ']'];
53
54
    /**
55
     * Parent spreadsheet.
56
     *
57
     * @var Spreadsheet
58
     */
59
    private $parent;
60
61
    /**
62
     * Collection of cells.
63
     *
64
     * @var Cells
65
     */
66
    private $cellCollection;
67
68
    /**
69
     * Collection of row dimensions.
70
     *
71
     * @var RowDimension[]
72
     */
73
    private $rowDimensions = [];
74
75
    /**
76
     * Default row dimension.
77
     *
78
     * @var RowDimension
79
     */
80
    private $defaultRowDimension;
81
82
    /**
83
     * Collection of column dimensions.
84
     *
85
     * @var ColumnDimension[]
86
     */
87
    private $columnDimensions = [];
88
89
    /**
90
     * Default column dimension.
91
     *
92
     * @var ColumnDimension
93
     */
94
    private $defaultColumnDimension;
95
96
    /**
97
     * Collection of drawings.
98
     *
99
     * @var ArrayObject<BaseDrawing>
100
     */
101
    private $drawingCollection;
102
103
    /**
104
     * Collection of Chart objects.
105
     *
106
     * @var ArrayObject<Chart>
107
     */
108
    private $chartCollection;
109
110
    /**
111
     * Worksheet title.
112
     *
113
     * @var string
114
     */
115
    private $title;
116
117
    /**
118
     * Sheet state.
119
     *
120
     * @var string
121
     */
122
    private $sheetState;
123
124
    /**
125
     * Page setup.
126
     *
127
     * @var PageSetup
128
     */
129
    private $pageSetup;
130
131
    /**
132
     * Page margins.
133
     *
134
     * @var PageMargins
135
     */
136
    private $pageMargins;
137
138
    /**
139
     * Page header/footer.
140
     *
141
     * @var HeaderFooter
142
     */
143
    private $headerFooter;
144
145
    /**
146
     * Sheet view.
147
     *
148
     * @var SheetView
149
     */
150
    private $sheetView;
151
152
    /**
153
     * Protection.
154
     *
155
     * @var Protection
156
     */
157
    private $protection;
158
159
    /**
160
     * Collection of styles.
161
     *
162
     * @var Style[]
163
     */
164
    private $styles = [];
165
166
    /**
167
     * Conditional styles. Indexed by cell coordinate, e.g. 'A1'.
168
     *
169
     * @var array
170
     */
171
    private $conditionalStylesCollection = [];
172
173
    /**
174
     * Is the current cell collection sorted already?
175
     *
176
     * @var bool
177
     */
178
    private $cellCollectionIsSorted = false;
179
180
    /**
181
     * Collection of breaks.
182
     *
183
     * @var int[]
184
     */
185
    private $breaks = [];
186
187
    /**
188
     * Collection of merged cell ranges.
189
     *
190
     * @var string[]
191
     */
192
    private $mergeCells = [];
193
194
    /**
195
     * Collection of protected cell ranges.
196
     *
197
     * @var array
198
     */
199
    private $protectedCells = [];
200
201
    /**
202
     * Autofilter Range and selection.
203
     *
204
     * @var AutoFilter
205
     */
206
    private $autoFilter;
207
208
    /**
209
     * Freeze pane.
210
     *
211
     * @var null|string
212
     */
213
    private $freezePane;
214
215
    /**
216
     * Default position of the right bottom pane.
217
     *
218
     * @var null|string
219
     */
220
    private $topLeftCell;
221
222
    /**
223
     * Show gridlines?
224
     *
225
     * @var bool
226
     */
227
    private $showGridlines = true;
228
229
    /**
230
     * Print gridlines?
231
     *
232
     * @var bool
233
     */
234
    private $printGridlines = false;
235
236
    /**
237
     * Show row and column headers?
238
     *
239
     * @var bool
240
     */
241
    private $showRowColHeaders = true;
242
243
    /**
244
     * Show summary below? (Row/Column outline).
245
     *
246
     * @var bool
247
     */
248
    private $showSummaryBelow = true;
249
250
    /**
251
     * Show summary right? (Row/Column outline).
252
     *
253
     * @var bool
254
     */
255
    private $showSummaryRight = true;
256
257
    /**
258
     * Collection of comments.
259
     *
260
     * @var Comment[]
261
     */
262
    private $comments = [];
263
264
    /**
265
     * Active cell. (Only one!).
266
     *
267
     * @var string
268
     */
269
    private $activeCell = 'A1';
270
271
    /**
272
     * Selected cells.
273
     *
274
     * @var string
275
     */
276
    private $selectedCells = 'A1';
277
278
    /**
279
     * Cached highest column.
280
     *
281
     * @var int
282
     */
283
    private $cachedHighestColumn = 1;
284
285
    /**
286
     * Cached highest row.
287
     *
288
     * @var int
289
     */
290
    private $cachedHighestRow = 1;
291
292
    /**
293
     * Right-to-left?
294
     *
295
     * @var bool
296
     */
297
    private $rightToLeft = false;
298
299
    /**
300
     * Hyperlinks. Indexed by cell coordinate, e.g. 'A1'.
301
     *
302
     * @var array
303
     */
304
    private $hyperlinkCollection = [];
305
306
    /**
307
     * Data validation objects. Indexed by cell coordinate, e.g. 'A1'.
308
     *
309
     * @var array
310
     */
311
    private $dataValidationCollection = [];
312
313
    /**
314
     * Tab color.
315
     *
316
     * @var null|Color
317
     */
318
    private $tabColor;
319
320
    /**
321
     * Dirty flag.
322
     *
323
     * @var bool
324
     */
325
    private $dirty = true;
326
327
    /**
328
     * Hash.
329
     *
330
     * @var string
331
     */
332
    private $hash;
333
334
    /**
335
     * CodeName.
336
     *
337
     * @var string
338
     */
339
    private $codeName;
340
341
    /**
342
     * Create a new worksheet.
343
     *
344
     * @param Spreadsheet $parent
345
     * @param string $pTitle
346
     */
347 4012
    public function __construct(?Spreadsheet $parent = null, $pTitle = 'Worksheet')
348
    {
349
        // Set parent and title
350 4012
        $this->parent = $parent;
351 4012
        $this->setTitle($pTitle, false);
352
        // setTitle can change $pTitle
353 4012
        $this->setCodeName($this->getTitle());
354 4012
        $this->setSheetState(self::SHEETSTATE_VISIBLE);
355
356 4012
        $this->cellCollection = CellsFactory::getInstance($this);
357
        // Set page setup
358 4012
        $this->pageSetup = new PageSetup();
359
        // Set page margins
360 4012
        $this->pageMargins = new PageMargins();
361
        // Set page header/footer
362 4012
        $this->headerFooter = new HeaderFooter();
363
        // Set sheet view
364 4012
        $this->sheetView = new SheetView();
365
        // Drawing collection
366 4012
        $this->drawingCollection = new ArrayObject();
367
        // Chart collection
368 4012
        $this->chartCollection = new ArrayObject();
369
        // Protection
370 4012
        $this->protection = new Protection();
371
        // Default row dimension
372 4012
        $this->defaultRowDimension = new RowDimension(null);
373
        // Default column dimension
374 4012
        $this->defaultColumnDimension = new ColumnDimension(null);
375 4012
        $this->autoFilter = new AutoFilter(null, $this);
376 4012
    }
377
378
    /**
379
     * Disconnect all cells from this Worksheet object,
380
     * typically so that the worksheet object can be unset.
381
     */
382 2418
    public function disconnectCells(): void
383
    {
384 2418
        if ($this->cellCollection !== null) {
385 2418
            $this->cellCollection->unsetWorksheetCells();
386
            // @phpstan-ignore-next-line
387 2418
            $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 2418
        $this->parent = null;
392 2418
    }
393
394
    /**
395
     * Code to execute when this worksheet is unset().
396
     */
397 33
    public function __destruct()
398
    {
399 33
        Calculation::getInstance($this->parent)->clearCalculationCacheForWorksheet($this->title);
400
401 33
        $this->disconnectCells();
402 33
    }
403
404
    /**
405
     * Return the cell collection.
406
     *
407
     * @return Cells
408
     */
409 3837
    public function getCellCollection()
410
    {
411 3837
        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 4012
    private static function checkSheetCodeName($pValue)
432
    {
433 4012
        $CharCount = Shared\StringHelper::countCharacters($pValue);
434 4012
        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 4012
            (str_replace(self::$invalidCharacters, '', $pValue) !== $pValue) ||
440 4012
            (Shared\StringHelper::substring($pValue, -1, 1) == '\'') ||
441 4012
            (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 4012
        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 4012
        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 4012
    private static function checkSheetTitle($pValue)
462
    {
463
        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ]
464 4012
        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 4012
        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 4012
        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->getCell($coordinate, false);
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 4012
    public function getParent()
790
    {
791 4012
        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 4012
    public function getTitle()
822
    {
823 4012
        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 4012
    public function setTitle($title, $updateFormulaCellReferences = true, $validate = true)
841
    {
842
        // Is this a 'rename' or not?
843 4012
        if ($this->getTitle() == $title) {
844 98
            return $this;
845
        }
846
847
        // Old title
848 4012
        $oldTitle = $this->getTitle();
849
850 4012
        if ($validate) {
851
            // Syntax check
852 4012
            self::checkSheetTitle($title);
853
854 4012
            if ($this->parent) {
855
                // Is there already such sheet name?
856 3974
                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 4012
        $this->title = $title;
883 4012
        $this->dirty = true;
884
885 4012
        if ($this->parent && $this->parent->getCalculationEngine()) {
886
            // New title
887 3974
            $newTitle = $this->getTitle();
888 3974
            $this->parent->getCalculationEngine()
889 3974
                ->renameCalculationCacheForWorksheet($oldTitle, $newTitle);
890 3974
            if ($updateFormulaCellReferences) {
891 251
                ReferenceHelper::getInstance()->updateNamedFormulas($this->parent, $oldTitle, $newTitle);
892
            }
893
        }
894
895 4012
        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 4012
    public function setSheetState($value)
916
    {
917 4012
        $this->sheetState = $value;
918
919 4012
        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 1743
    public function setCellValue($pCoordinate, $pValue)
1112
    {
1113 1743
        $this->getCell($pCoordinate)->setValue($pValue);
1114
1115 1743
        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 $pCoordinate Coordinate of the cell, eg: 'A1'
1172
     * @param bool $createIfNotExists Flag indicating whether a new cell should be created if it doesn't
1173
     *                                       already exist, or a null should be returned instead
1174
     *
1175
     * @return null|Cell Cell that was found/created or null
1176
     */
1177 3833
    public function getCell($pCoordinate, $createIfNotExists = true)
1178
    {
1179
        // Uppercase coordinate
1180 3833
        $pCoordinateUpper = strtoupper($pCoordinate);
1181
1182
        // Check cell collection
1183 3833
        if ($this->cellCollection->has($pCoordinateUpper)) {
1184 3751
            return $this->cellCollection->get($pCoordinateUpper);
1185
        }
1186
1187
        // Worksheet reference?
1188 3819
        if (strpos($pCoordinate, '!') !== false) {
1189 1
            $worksheetReference = self::extractSheetTitle($pCoordinate, true);
1190
1191 1
            return $this->parent->getSheetByName($worksheetReference[0])
1192 1
                ->getCell(strtoupper($worksheetReference[1]), $createIfNotExists);
1193
        }
1194
1195
        // Named range?
1196
        if (
1197 3819
            (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $pCoordinate, $matches)) &&
1198 3819
            (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/i', $pCoordinate, $matches))
1199
        ) {
1200 6
            $namedRange = $this->validateNamedRange($pCoordinate, true);
1201 6
            if ($namedRange !== null) {
1202 4
                $cellCoordinate = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!');
1203 4
                $cellCoordinate = str_replace('$', '', $cellCoordinate);
1204
1205 4
                return $namedRange->getWorksheet()->getCell($cellCoordinate, $createIfNotExists);
1206
            }
1207
        }
1208
1209 3819
        if (Coordinate::coordinateIsRange($pCoordinate)) {
1210 1
            throw new Exception('Cell coordinate can not be a range of cells.');
1211 3819
        } elseif (strpos($pCoordinate, '$') !== false) {
1212
            throw new Exception('Cell coordinate must not be absolute.');
1213
        }
1214
1215
        // Create new cell object, if required
1216 3819
        return $createIfNotExists ? $this->createNewCell($pCoordinateUpper) : null;
1217
    }
1218
1219
    /**
1220
     * Get cell at a specific coordinate by using numeric cell coordinates.
1221
     *
1222
     * @param int $columnIndex Numeric column coordinate of the cell
1223
     * @param int $row Numeric row coordinate of the cell
1224
     * @param bool $createIfNotExists Flag indicating whether a new cell should be created if it doesn't
1225
     *                                       already exist, or a null should be returned instead
1226
     *
1227
     * @return null|Cell Cell that was found/created or null
1228
     */
1229 260
    public function getCellByColumnAndRow($columnIndex, $row, $createIfNotExists = true)
1230
    {
1231 260
        $columnLetter = Coordinate::stringFromColumnIndex($columnIndex);
1232 260
        $coordinate = $columnLetter . $row;
1233
1234 260
        if ($this->cellCollection->has($coordinate)) {
1235 255
            return $this->cellCollection->get($coordinate);
1236
        }
1237
1238
        // Create new cell object, if required
1239 86
        return $createIfNotExists ? $this->createNewCell($coordinate) : null;
1240
    }
1241
1242
    /**
1243
     * Create a new cell at the specified coordinate.
1244
     *
1245
     * @param string $pCoordinate Coordinate of the cell
1246
     *
1247
     * @return Cell Cell that was created
1248
     */
1249 3837
    private function createNewCell($pCoordinate)
1250
    {
1251 3837
        $cell = new Cell(null, DataType::TYPE_NULL, $this);
1252 3837
        $this->cellCollection->add($pCoordinate, $cell);
1253 3837
        $this->cellCollectionIsSorted = false;
1254
1255
        // Coordinates
1256 3837
        $aCoordinates = Coordinate::coordinateFromString($pCoordinate);
1257 3837
        $aIndexes = Coordinate::indexesFromString($pCoordinate);
1258 3837
        if ($this->cachedHighestColumn < $aIndexes[0]) {
1259 2028
            $this->cachedHighestColumn = $aIndexes[0];
1260
        }
1261 3837
        if ($aIndexes[1] > $this->cachedHighestRow) {
1262 2635
            $this->cachedHighestRow = $aIndexes[1];
1263
        }
1264
1265
        // Cell needs appropriate xfIndex from dimensions records
1266
        //    but don't create dimension records if they don't already exist
1267 3837
        $rowDimension = $this->getRowDimension($aCoordinates[1], false);
1268 3837
        $columnDimension = $this->getColumnDimension($aCoordinates[0], false);
1269
1270 3837
        if ($rowDimension !== null && $rowDimension->getXfIndex() > 0) {
1271
            // then there is a row dimension with explicit style, assign it to the cell
1272 3
            $cell->setXfIndex($rowDimension->getXfIndex());
1273 3837
        } elseif ($columnDimension !== null && $columnDimension->getXfIndex() > 0) {
1274
            // then there is a column dimension, assign it to the cell
1275 2
            $cell->setXfIndex($columnDimension->getXfIndex());
1276
        }
1277
1278 3837
        return $cell;
1279
    }
1280
1281
    /**
1282
     * Does the cell at a specific coordinate exist?
1283
     *
1284
     * @param string $pCoordinate Coordinate of the cell eg: 'A1'
1285
     *
1286
     * @return bool
1287
     */
1288 3615
    public function cellExists($pCoordinate)
1289
    {
1290
        // Worksheet reference?
1291 3615
        if (strpos($pCoordinate, '!') !== false) {
1292
            $worksheetReference = self::extractSheetTitle($pCoordinate, true);
1293
1294
            return $this->parent->getSheetByName($worksheetReference[0])->cellExists(strtoupper($worksheetReference[1]));
1295
        }
1296
1297
        // Named range?
1298
        if (
1299 3615
            (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $pCoordinate, $matches)) &&
1300 3615
            (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/i', $pCoordinate, $matches))
1301
        ) {
1302 4
            $namedRange = $this->validateNamedRange($pCoordinate, true);
1303 4
            if ($namedRange !== null) {
1304 2
                $cellCoordinate = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!');
1305 2
                $cellCoordinate = str_replace('$', '', $cellCoordinate);
1306
1307 2
                return $namedRange->getWorksheet()->cellExists($cellCoordinate);
1308
            }
1309
        }
1310
1311
        // Uppercase coordinate
1312 3615
        $pCoordinate = strtoupper($pCoordinate);
1313
1314 3615
        if (Coordinate::coordinateIsRange($pCoordinate)) {
1315 1
            throw new Exception('Cell coordinate can not be a range of cells.');
1316 3614
        } elseif (strpos($pCoordinate, '$') !== false) {
1317
            throw new Exception('Cell coordinate must not be absolute.');
1318
        }
1319
1320
        // Cell exists?
1321 3614
        return $this->cellCollection->has($pCoordinate);
1322
    }
1323
1324
    /**
1325
     * Cell at a specific coordinate by using numeric cell coordinates exists?
1326
     *
1327
     * @param int $columnIndex Numeric column coordinate of the cell
1328
     * @param int $row Numeric row coordinate of the cell
1329
     *
1330
     * @return bool
1331
     */
1332 171
    public function cellExistsByColumnAndRow($columnIndex, $row)
1333
    {
1334 171
        return $this->cellExists(Coordinate::stringFromColumnIndex($columnIndex) . $row);
1335
    }
1336
1337
    /**
1338
     * Get row dimension at a specific row.
1339
     *
1340
     * @param int $pRow Numeric index of the row
1341
     * @param bool $create
1342
     *
1343
     * @return null|RowDimension
1344
     */
1345 3840
    public function getRowDimension($pRow, $create = true)
1346
    {
1347
        // Found
1348 3840
        $found = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $found is dead and can be removed.
Loading history...
1349
1350
        // Get row dimension
1351 3840
        if (!isset($this->rowDimensions[$pRow])) {
1352 3840
            if (!$create) {
1353 3817
                return null;
1354
            }
1355 192
            $this->rowDimensions[$pRow] = new RowDimension($pRow);
1356
1357 192
            $this->cachedHighestRow = max($this->cachedHighestRow, $pRow);
1358
        }
1359
1360 192
        return $this->rowDimensions[$pRow];
1361
    }
1362
1363
    /**
1364
     * Get column dimension at a specific column.
1365
     *
1366
     * @param string $pColumn String index of the column eg: 'A'
1367
     * @param bool $create
1368
     *
1369
     * @return null|ColumnDimension
1370
     */
1371 3840
    public function getColumnDimension($pColumn, $create = true)
1372
    {
1373
        // Uppercase coordinate
1374 3840
        $pColumn = strtoupper($pColumn);
1375
1376
        // Fetch dimensions
1377 3840
        if (!isset($this->columnDimensions[$pColumn])) {
1378 3840
            if (!$create) {
1379 3823
                return null;
1380
            }
1381 122
            $this->columnDimensions[$pColumn] = new ColumnDimension($pColumn);
1382
1383 122
            $columnIndex = Coordinate::columnIndexFromString($pColumn);
1384 122
            if ($this->cachedHighestColumn < $columnIndex) {
1385 62
                $this->cachedHighestColumn = $columnIndex;
1386
            }
1387
        }
1388
1389 122
        return $this->columnDimensions[$pColumn];
1390
    }
1391
1392
    /**
1393
     * Get column dimension at a specific column by using numeric cell coordinates.
1394
     *
1395
     * @param int $columnIndex Numeric column coordinate of the cell
1396
     *
1397
     * @return null|ColumnDimension
1398
     */
1399 34
    public function getColumnDimensionByColumn($columnIndex)
1400
    {
1401 34
        return $this->getColumnDimension(Coordinate::stringFromColumnIndex($columnIndex));
1402
    }
1403
1404
    /**
1405
     * Get styles.
1406
     *
1407
     * @return Style[]
1408
     */
1409
    public function getStyles()
1410
    {
1411
        return $this->styles;
1412
    }
1413
1414
    /**
1415
     * Get style for cell.
1416
     *
1417
     * @param string $pCellCoordinate Cell coordinate (or range) to get style for, eg: 'A1'
1418
     *
1419
     * @return Style
1420
     */
1421 3599
    public function getStyle($pCellCoordinate)
1422
    {
1423
        // set this sheet as active
1424 3599
        $this->parent->setActiveSheetIndex($this->parent->getIndex($this));
1425
1426
        // set cell coordinate as active
1427 3599
        $this->setSelectedCells($pCellCoordinate);
1428
1429 3599
        return $this->parent->getCellXfSupervisor();
1430
    }
1431
1432
    /**
1433
     * Get conditional styles for a cell.
1434
     *
1435
     * @param string $pCoordinate eg: 'A1'
1436
     *
1437
     * @return Conditional[]
1438
     */
1439 13
    public function getConditionalStyles($pCoordinate)
1440
    {
1441 13
        $pCoordinate = strtoupper($pCoordinate);
1442 13
        if (!isset($this->conditionalStylesCollection[$pCoordinate])) {
1443 2
            $this->conditionalStylesCollection[$pCoordinate] = [];
1444
        }
1445
1446 13
        return $this->conditionalStylesCollection[$pCoordinate];
1447
    }
1448
1449
    /**
1450
     * Do conditional styles exist for this cell?
1451
     *
1452
     * @param string $pCoordinate eg: 'A1'
1453
     *
1454
     * @return bool
1455
     */
1456 18
    public function conditionalStylesExists($pCoordinate)
1457
    {
1458 18
        return isset($this->conditionalStylesCollection[strtoupper($pCoordinate)]);
1459
    }
1460
1461
    /**
1462
     * Removes conditional styles for a cell.
1463
     *
1464
     * @param string $pCoordinate eg: 'A1'
1465
     *
1466
     * @return $this
1467
     */
1468 25
    public function removeConditionalStyles($pCoordinate)
1469
    {
1470 25
        unset($this->conditionalStylesCollection[strtoupper($pCoordinate)]);
1471
1472 25
        return $this;
1473
    }
1474
1475
    /**
1476
     * Get collection of conditional styles.
1477
     *
1478
     * @return array
1479
     */
1480 146
    public function getConditionalStylesCollection()
1481
    {
1482 146
        return $this->conditionalStylesCollection;
1483
    }
1484
1485
    /**
1486
     * Set conditional styles.
1487
     *
1488
     * @param string $pCoordinate eg: 'A1'
1489
     * @param Conditional[] $pValue
1490
     *
1491
     * @return $this
1492
     */
1493 13
    public function setConditionalStyles($pCoordinate, $pValue)
1494
    {
1495 13
        $this->conditionalStylesCollection[strtoupper($pCoordinate)] = $pValue;
1496
1497 13
        return $this;
1498
    }
1499
1500
    /**
1501
     * Get style for cell by using numeric cell coordinates.
1502
     *
1503
     * @param int $columnIndex1 Numeric column coordinate of the cell
1504
     * @param int $row1 Numeric row coordinate of the cell
1505
     * @param null|int $columnIndex2 Numeric column coordinate of the range cell
1506
     * @param null|int $row2 Numeric row coordinate of the range cell
1507
     *
1508
     * @return Style
1509
     */
1510
    public function getStyleByColumnAndRow($columnIndex1, $row1, $columnIndex2 = null, $row2 = null)
1511
    {
1512
        if ($columnIndex2 !== null && $row2 !== null) {
1513
            $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;
1514
1515
            return $this->getStyle($cellRange);
1516
        }
1517
1518
        return $this->getStyle(Coordinate::stringFromColumnIndex($columnIndex1) . $row1);
1519
    }
1520
1521
    /**
1522
     * Duplicate cell style to a range of cells.
1523
     *
1524
     * Please note that this will overwrite existing cell styles for cells in range!
1525
     *
1526
     * @param Style $pCellStyle Cell style to duplicate
1527
     * @param string $pRange Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
1528
     *
1529
     * @return $this
1530
     */
1531 2
    public function duplicateStyle(Style $pCellStyle, $pRange)
1532
    {
1533
        // Add the style to the workbook if necessary
1534 2
        $workbook = $this->parent;
1535 2
        if ($existingStyle = $this->parent->getCellXfByHashCode($pCellStyle->getHashCode())) {
1536
            // there is already such cell Xf in our collection
1537 1
            $xfIndex = $existingStyle->getIndex();
1538
        } else {
1539
            // we don't have such a cell Xf, need to add
1540 2
            $workbook->addCellXf($pCellStyle);
1541 2
            $xfIndex = $pCellStyle->getIndex();
1542
        }
1543
1544
        // Calculate range outer borders
1545 2
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($pRange . ':' . $pRange);
1546
1547
        // Make sure we can loop upwards on rows and columns
1548 2
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
1549
            $tmp = $rangeStart;
1550
            $rangeStart = $rangeEnd;
1551
            $rangeEnd = $tmp;
1552
        }
1553
1554
        // Loop through cells and apply styles
1555 2
        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
1556 2
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
1557 2
                $this->getCell(Coordinate::stringFromColumnIndex($col) . $row)->setXfIndex($xfIndex);
1558
            }
1559
        }
1560
1561 2
        return $this;
1562
    }
1563
1564
    /**
1565
     * Duplicate conditional style to a range of cells.
1566
     *
1567
     * Please note that this will overwrite existing cell styles for cells in range!
1568
     *
1569
     * @param Conditional[] $pCellStyle Cell style to duplicate
1570
     * @param string $pRange Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
1571
     *
1572
     * @return $this
1573
     */
1574 2
    public function duplicateConditionalStyle(array $pCellStyle, $pRange = '')
1575
    {
1576 2
        foreach ($pCellStyle as $cellStyle) {
1577 2
            if (!($cellStyle instanceof Conditional)) {
1578
                throw new Exception('Style is not a conditional style');
1579
            }
1580
        }
1581
1582
        // Calculate range outer borders
1583 2
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($pRange . ':' . $pRange);
1584
1585
        // Make sure we can loop upwards on rows and columns
1586 2
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
1587
            $tmp = $rangeStart;
1588
            $rangeStart = $rangeEnd;
1589
            $rangeEnd = $tmp;
1590
        }
1591
1592
        // Loop through cells and apply styles
1593 2
        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
1594 2
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
1595 2
                $this->setConditionalStyles(Coordinate::stringFromColumnIndex($col) . $row, $pCellStyle);
1596
            }
1597
        }
1598
1599 2
        return $this;
1600
    }
1601
1602
    /**
1603
     * Set break on a cell.
1604
     *
1605
     * @param string $pCoordinate Cell coordinate (e.g. A1)
1606
     * @param int $pBreak Break type (type of Worksheet::BREAK_*)
1607
     *
1608
     * @return $this
1609
     */
1610 4
    public function setBreak($pCoordinate, $pBreak)
1611
    {
1612
        // Uppercase coordinate
1613 4
        $pCoordinate = strtoupper($pCoordinate);
1614
1615 4
        if ($pCoordinate != '') {
1616 4
            if ($pBreak == self::BREAK_NONE) {
1617
                if (isset($this->breaks[$pCoordinate])) {
1618
                    unset($this->breaks[$pCoordinate]);
1619
                }
1620
            } else {
1621 4
                $this->breaks[$pCoordinate] = $pBreak;
1622
            }
1623
        } else {
1624
            throw new Exception('No cell coordinate specified.');
1625
        }
1626
1627 4
        return $this;
1628
    }
1629
1630
    /**
1631
     * Set break on a cell by using numeric cell coordinates.
1632
     *
1633
     * @param int $columnIndex Numeric column coordinate of the cell
1634
     * @param int $row Numeric row coordinate of the cell
1635
     * @param int $break Break type (type of Worksheet::BREAK_*)
1636
     *
1637
     * @return $this
1638
     */
1639
    public function setBreakByColumnAndRow($columnIndex, $row, $break)
1640
    {
1641
        return $this->setBreak(Coordinate::stringFromColumnIndex($columnIndex) . $row, $break);
1642
    }
1643
1644
    /**
1645
     * Get breaks.
1646
     *
1647
     * @return int[]
1648
     */
1649 320
    public function getBreaks()
1650
    {
1651 320
        return $this->breaks;
1652
    }
1653
1654
    /**
1655
     * Set merge on a cell range.
1656
     *
1657
     * @param string $pRange Cell range (e.g. A1:E1)
1658
     *
1659
     * @return $this
1660
     */
1661 62
    public function mergeCells($pRange)
1662
    {
1663
        // Uppercase coordinate
1664 62
        $pRange = strtoupper($pRange);
1665
1666 62
        if (strpos($pRange, ':') !== false) {
1667 62
            $this->mergeCells[$pRange] = $pRange;
1668
1669
            // make sure cells are created
1670
1671
            // get the cells in the range
1672 62
            $aReferences = Coordinate::extractAllCellReferencesInRange($pRange);
1673
1674
            // create upper left cell if it does not already exist
1675 62
            $upperLeft = $aReferences[0];
1676 62
            if (!$this->cellExists($upperLeft)) {
1677 24
                $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL);
1678
            }
1679
1680
            // Blank out the rest of the cells in the range (if they exist)
1681 62
            $count = count($aReferences);
1682 62
            for ($i = 1; $i < $count; ++$i) {
1683 62
                if ($this->cellExists($aReferences[$i])) {
1684 22
                    $this->getCell($aReferences[$i])->setValueExplicit(null, DataType::TYPE_NULL);
1685
                }
1686
            }
1687
        } else {
1688
            throw new Exception('Merge must be set on a range of cells.');
1689
        }
1690
1691 62
        return $this;
1692
    }
1693
1694
    /**
1695
     * Set merge on a cell range by using numeric cell coordinates.
1696
     *
1697
     * @param int $columnIndex1 Numeric column coordinate of the first cell
1698
     * @param int $row1 Numeric row coordinate of the first cell
1699
     * @param int $columnIndex2 Numeric column coordinate of the last cell
1700
     * @param int $row2 Numeric row coordinate of the last cell
1701
     *
1702
     * @return $this
1703
     */
1704
    public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
1705
    {
1706
        $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;
1707
1708
        return $this->mergeCells($cellRange);
1709
    }
1710
1711
    /**
1712
     * Remove merge on a cell range.
1713
     *
1714
     * @param string $pRange Cell range (e.g. A1:E1)
1715
     *
1716
     * @return $this
1717
     */
1718 16
    public function unmergeCells($pRange)
1719
    {
1720
        // Uppercase coordinate
1721 16
        $pRange = strtoupper($pRange);
1722
1723 16
        if (strpos($pRange, ':') !== false) {
1724 16
            if (isset($this->mergeCells[$pRange])) {
1725 16
                unset($this->mergeCells[$pRange]);
1726
            } else {
1727 16
                throw new Exception('Cell range ' . $pRange . ' not known as merged.');
1728
            }
1729
        } else {
1730
            throw new Exception('Merge can only be removed from a range of cells.');
1731
        }
1732
1733 16
        return $this;
1734
    }
1735
1736
    /**
1737
     * Remove merge on a cell range by using numeric cell coordinates.
1738
     *
1739
     * @param int $columnIndex1 Numeric column coordinate of the first cell
1740
     * @param int $row1 Numeric row coordinate of the first cell
1741
     * @param int $columnIndex2 Numeric column coordinate of the last cell
1742
     * @param int $row2 Numeric row coordinate of the last cell
1743
     *
1744
     * @return $this
1745
     */
1746
    public function unmergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
1747
    {
1748
        $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;
1749
1750
        return $this->unmergeCells($cellRange);
1751
    }
1752
1753
    /**
1754
     * Get merge cells array.
1755
     *
1756
     * @return string[]
1757
     */
1758 330
    public function getMergeCells()
1759
    {
1760 330
        return $this->mergeCells;
1761
    }
1762
1763
    /**
1764
     * Set merge cells array for the entire sheet. Use instead mergeCells() to merge
1765
     * a single cell range.
1766
     *
1767
     * @param string[] $pValue
1768
     *
1769
     * @return $this
1770
     */
1771 32
    public function setMergeCells(array $pValue)
1772
    {
1773 32
        $this->mergeCells = $pValue;
1774
1775 32
        return $this;
1776
    }
1777
1778
    /**
1779
     * Set protection on a cell range.
1780
     *
1781
     * @param string $pRange Cell (e.g. A1) or cell range (e.g. A1:E1)
1782
     * @param string $pPassword Password to unlock the protection
1783
     * @param bool $pAlreadyHashed If the password has already been hashed, set this to true
1784
     *
1785
     * @return $this
1786
     */
1787 16
    public function protectCells($pRange, $pPassword, $pAlreadyHashed = false)
1788
    {
1789
        // Uppercase coordinate
1790 16
        $pRange = strtoupper($pRange);
1791
1792 16
        if (!$pAlreadyHashed) {
1793 16
            $pPassword = Shared\PasswordHasher::hashPassword($pPassword);
1794
        }
1795 16
        $this->protectedCells[$pRange] = $pPassword;
1796
1797 16
        return $this;
1798
    }
1799
1800
    /**
1801
     * Set protection on a cell range by using numeric cell coordinates.
1802
     *
1803
     * @param int $columnIndex1 Numeric column coordinate of the first cell
1804
     * @param int $row1 Numeric row coordinate of the first cell
1805
     * @param int $columnIndex2 Numeric column coordinate of the last cell
1806
     * @param int $row2 Numeric row coordinate of the last cell
1807
     * @param string $password Password to unlock the protection
1808
     * @param bool $alreadyHashed If the password has already been hashed, set this to true
1809
     *
1810
     * @return $this
1811
     */
1812
    public function protectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $password, $alreadyHashed = false)
1813
    {
1814
        $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;
1815
1816
        return $this->protectCells($cellRange, $password, $alreadyHashed);
1817
    }
1818
1819
    /**
1820
     * Remove protection on a cell range.
1821
     *
1822
     * @param string $pRange Cell (e.g. A1) or cell range (e.g. A1:E1)
1823
     *
1824
     * @return $this
1825
     */
1826 16
    public function unprotectCells($pRange)
1827
    {
1828
        // Uppercase coordinate
1829 16
        $pRange = strtoupper($pRange);
1830
1831 16
        if (isset($this->protectedCells[$pRange])) {
1832 16
            unset($this->protectedCells[$pRange]);
1833
        } else {
1834
            throw new Exception('Cell range ' . $pRange . ' not known as protected.');
1835
        }
1836
1837 16
        return $this;
1838
    }
1839
1840
    /**
1841
     * Remove protection on a cell range by using numeric cell coordinates.
1842
     *
1843
     * @param int $columnIndex1 Numeric column coordinate of the first cell
1844
     * @param int $row1 Numeric row coordinate of the first cell
1845
     * @param int $columnIndex2 Numeric column coordinate of the last cell
1846
     * @param int $row2 Numeric row coordinate of the last cell
1847
     *
1848
     * @return $this
1849
     */
1850
    public function unprotectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
1851
    {
1852
        $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2;
1853
1854
        return $this->unprotectCells($cellRange);
1855
    }
1856
1857
    /**
1858
     * Get protected cells.
1859
     *
1860
     * @return array[]
1861
     */
1862 166
    public function getProtectedCells()
1863
    {
1864 166
        return $this->protectedCells;
1865
    }
1866
1867
    /**
1868
     * Get Autofilter.
1869
     *
1870
     * @return AutoFilter
1871
     */
1872 168
    public function getAutoFilter()
1873
    {
1874 168
        return $this->autoFilter;
1875
    }
1876
1877
    /**
1878
     * Set AutoFilter.
1879
     *
1880
     * @param AutoFilter|string $pValue
1881
     *            A simple string containing a Cell range like 'A1:E10' is permitted for backward compatibility
1882
     *
1883
     * @return $this
1884
     */
1885 4
    public function setAutoFilter($pValue)
1886
    {
1887 4
        if (is_string($pValue)) {
1888 4
            $this->autoFilter->setRange($pValue);
1889
        } elseif (is_object($pValue) && ($pValue instanceof AutoFilter)) {
1890
            $this->autoFilter = $pValue;
1891
        }
1892
1893 4
        return $this;
1894
    }
1895
1896
    /**
1897
     * Set Autofilter Range by using numeric cell coordinates.
1898
     *
1899
     * @param int $columnIndex1 Numeric column coordinate of the first cell
1900
     * @param int $row1 Numeric row coordinate of the first cell
1901
     * @param int $columnIndex2 Numeric column coordinate of the second cell
1902
     * @param int $row2 Numeric row coordinate of the second cell
1903
     *
1904
     * @return $this
1905
     */
1906
    public function setAutoFilterByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
1907
    {
1908
        return $this->setAutoFilter(
1909
            Coordinate::stringFromColumnIndex($columnIndex1) . $row1
1910
            . ':' .
1911
            Coordinate::stringFromColumnIndex($columnIndex2) . $row2
1912
        );
1913
    }
1914
1915
    /**
1916
     * Remove autofilter.
1917
     *
1918
     * @return $this
1919
     */
1920
    public function removeAutoFilter()
1921
    {
1922
        $this->autoFilter->setRange(null);
1923
1924
        return $this;
1925
    }
1926
1927
    /**
1928
     * Get Freeze Pane.
1929
     *
1930
     * @return string
1931
     */
1932 166
    public function getFreezePane()
1933
    {
1934 166
        return $this->freezePane;
1935
    }
1936
1937
    /**
1938
     * Freeze Pane.
1939
     *
1940
     * Examples:
1941
     *
1942
     *     - A2 will freeze the rows above cell A2 (i.e row 1)
1943
     *     - B1 will freeze the columns to the left of cell B1 (i.e column A)
1944
     *     - B2 will freeze the rows above and to the left of cell B2 (i.e row 1 and column A)
1945
     *
1946
     * @param null|string $cell Position of the split
1947
     * @param null|string $topLeftCell default position of the right bottom pane
1948
     *
1949
     * @return $this
1950
     */
1951 15
    public function freezePane($cell, $topLeftCell = null)
1952
    {
1953 15
        if (is_string($cell) && Coordinate::coordinateIsRange($cell)) {
1954
            throw new Exception('Freeze pane can not be set on a range of cells.');
1955
        }
1956
1957 15
        if ($cell !== null && $topLeftCell === null) {
1958 7
            $coordinate = Coordinate::coordinateFromString($cell);
1959 7
            $topLeftCell = $coordinate[0] . $coordinate[1];
1960
        }
1961
1962 15
        $this->freezePane = $cell;
1963 15
        $this->topLeftCell = $topLeftCell;
1964
1965 15
        return $this;
1966
    }
1967
1968
    /**
1969
     * Freeze Pane by using numeric cell coordinates.
1970
     *
1971
     * @param int $columnIndex Numeric column coordinate of the cell
1972
     * @param int $row Numeric row coordinate of the cell
1973
     *
1974
     * @return $this
1975
     */
1976
    public function freezePaneByColumnAndRow($columnIndex, $row)
1977
    {
1978
        return $this->freezePane(Coordinate::stringFromColumnIndex($columnIndex) . $row);
1979
    }
1980
1981
    /**
1982
     * Unfreeze Pane.
1983
     *
1984
     * @return $this
1985
     */
1986
    public function unfreezePane()
1987
    {
1988
        return $this->freezePane(null);
1989
    }
1990
1991
    /**
1992
     * Get the default position of the right bottom pane.
1993
     *
1994
     * @return null|string
1995
     */
1996 10
    public function getTopLeftCell()
1997
    {
1998 10
        return $this->topLeftCell;
1999
    }
2000
2001
    /**
2002
     * Insert a new row, updating all possible related data.
2003
     *
2004
     * @param int $pBefore Insert before this one
2005
     * @param int $pNumRows Number of rows to insert
2006
     *
2007
     * @return $this
2008
     */
2009 18
    public function insertNewRowBefore($pBefore, $pNumRows = 1)
2010
    {
2011 18
        if ($pBefore >= 1) {
2012 18
            $objReferenceHelper = ReferenceHelper::getInstance();
2013 18
            $objReferenceHelper->insertNewBefore('A' . $pBefore, 0, $pNumRows, $this);
2014
        } else {
2015
            throw new Exception('Rows can only be inserted before at least row 1.');
2016
        }
2017
2018 18
        return $this;
2019
    }
2020
2021
    /**
2022
     * Insert a new column, updating all possible related data.
2023
     *
2024
     * @param string $pBefore Insert before this one, eg: 'A'
2025
     * @param int $pNumCols Number of columns to insert
2026
     *
2027
     * @return $this
2028
     */
2029 16
    public function insertNewColumnBefore($pBefore, $pNumCols = 1)
2030
    {
2031 16
        if (!is_numeric($pBefore)) {
2032 16
            $objReferenceHelper = ReferenceHelper::getInstance();
2033 16
            $objReferenceHelper->insertNewBefore($pBefore . '1', $pNumCols, 0, $this);
2034
        } else {
2035
            throw new Exception('Column references should not be numeric.');
2036
        }
2037
2038 16
        return $this;
2039
    }
2040
2041
    /**
2042
     * Insert a new column, updating all possible related data.
2043
     *
2044
     * @param int $beforeColumnIndex Insert before this one (numeric column coordinate of the cell)
2045
     * @param int $pNumCols Number of columns to insert
2046
     *
2047
     * @return $this
2048
     */
2049
    public function insertNewColumnBeforeByIndex($beforeColumnIndex, $pNumCols = 1)
2050
    {
2051
        if ($beforeColumnIndex >= 1) {
2052
            return $this->insertNewColumnBefore(Coordinate::stringFromColumnIndex($beforeColumnIndex), $pNumCols);
2053
        }
2054
2055
        throw new Exception('Columns can only be inserted before at least column A (1).');
2056
    }
2057
2058
    /**
2059
     * Delete a row, updating all possible related data.
2060
     *
2061
     * @param int $pRow Remove starting with this one
2062
     * @param int $pNumRows Number of rows to remove
2063
     *
2064
     * @return $this
2065
     */
2066 28
    public function removeRow($pRow, $pNumRows = 1)
2067
    {
2068 28
        if ($pRow < 1) {
2069
            throw new Exception('Rows to be deleted should at least start from row 1.');
2070
        }
2071
2072 28
        $highestRow = $this->getHighestDataRow();
2073 28
        $removedRowsCounter = 0;
2074
2075 28
        for ($r = 0; $r < $pNumRows; ++$r) {
2076 28
            if ($pRow + $r <= $highestRow) {
2077 27
                $this->getCellCollection()->removeRow($pRow + $r);
2078 27
                ++$removedRowsCounter;
2079
            }
2080
        }
2081
2082 28
        $objReferenceHelper = ReferenceHelper::getInstance();
2083 28
        $objReferenceHelper->insertNewBefore('A' . ($pRow + $pNumRows), 0, -$pNumRows, $this);
2084 28
        for ($r = 0; $r < $removedRowsCounter; ++$r) {
2085 27
            $this->getCellCollection()->removeRow($highestRow);
2086 27
            --$highestRow;
2087
        }
2088
2089 28
        return $this;
2090
    }
2091
2092
    /**
2093
     * Remove a column, updating all possible related data.
2094
     *
2095
     * @param string $pColumn Remove starting with this one, eg: 'A'
2096
     * @param int $pNumCols Number of columns to remove
2097
     *
2098
     * @return $this
2099
     */
2100 21
    public function removeColumn($pColumn, $pNumCols = 1)
2101
    {
2102 21
        if (is_numeric($pColumn)) {
2103
            throw new Exception('Column references should not be numeric.');
2104
        }
2105
2106 21
        $highestColumn = $this->getHighestDataColumn();
2107 21
        $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
2108 21
        $pColumnIndex = Coordinate::columnIndexFromString($pColumn);
2109
2110 21
        if ($pColumnIndex > $highestColumnIndex) {
2111 1
            return $this;
2112
        }
2113
2114 20
        $pColumn = Coordinate::stringFromColumnIndex($pColumnIndex + $pNumCols);
2115 20
        $objReferenceHelper = ReferenceHelper::getInstance();
2116 20
        $objReferenceHelper->insertNewBefore($pColumn . '1', -$pNumCols, 0, $this);
2117
2118 20
        $maxPossibleColumnsToBeRemoved = $highestColumnIndex - $pColumnIndex + 1;
2119
2120 20
        for ($c = 0, $n = min($maxPossibleColumnsToBeRemoved, $pNumCols); $c < $n; ++$c) {
2121 20
            $this->getCellCollection()->removeColumn($highestColumn);
2122 20
            $highestColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($highestColumn) - 1);
2123
        }
2124
2125 20
        $this->garbageCollect();
2126
2127 20
        return $this;
2128
    }
2129
2130
    /**
2131
     * Remove a column, updating all possible related data.
2132
     *
2133
     * @param int $columnIndex Remove starting with this one (numeric column coordinate of the cell)
2134
     * @param int $numColumns Number of columns to remove
2135
     *
2136
     * @return $this
2137
     */
2138
    public function removeColumnByIndex($columnIndex, $numColumns = 1)
2139
    {
2140
        if ($columnIndex >= 1) {
2141
            return $this->removeColumn(Coordinate::stringFromColumnIndex($columnIndex), $numColumns);
2142
        }
2143
2144
        throw new Exception('Columns to be deleted should at least start from column A (1)');
2145
    }
2146
2147
    /**
2148
     * Show gridlines?
2149
     *
2150
     * @return bool
2151
     */
2152 304
    public function getShowGridlines()
2153
    {
2154 304
        return $this->showGridlines;
2155
    }
2156
2157
    /**
2158
     * Set show gridlines.
2159
     *
2160
     * @param bool $pValue Show gridlines (true/false)
2161
     *
2162
     * @return $this
2163
     */
2164 107
    public function setShowGridlines($pValue)
2165
    {
2166 107
        $this->showGridlines = $pValue;
2167
2168 107
        return $this;
2169
    }
2170
2171
    /**
2172
     * Print gridlines?
2173
     *
2174
     * @return bool
2175
     */
2176 307
    public function getPrintGridlines()
2177
    {
2178 307
        return $this->printGridlines;
2179
    }
2180
2181
    /**
2182
     * Set print gridlines.
2183
     *
2184
     * @param bool $pValue Print gridlines (true/false)
2185
     *
2186
     * @return $this
2187
     */
2188 38
    public function setPrintGridlines($pValue)
2189
    {
2190 38
        $this->printGridlines = $pValue;
2191
2192 38
        return $this;
2193
    }
2194
2195
    /**
2196
     * Show row and column headers?
2197
     *
2198
     * @return bool
2199
     */
2200 146
    public function getShowRowColHeaders()
2201
    {
2202 146
        return $this->showRowColHeaders;
2203
    }
2204
2205
    /**
2206
     * Set show row and column headers.
2207
     *
2208
     * @param bool $pValue Show row and column headers (true/false)
2209
     *
2210
     * @return $this
2211
     */
2212 99
    public function setShowRowColHeaders($pValue)
2213
    {
2214 99
        $this->showRowColHeaders = $pValue;
2215
2216 99
        return $this;
2217
    }
2218
2219
    /**
2220
     * Show summary below? (Row/Column outlining).
2221
     *
2222
     * @return bool
2223
     */
2224 146
    public function getShowSummaryBelow()
2225
    {
2226 146
        return $this->showSummaryBelow;
2227
    }
2228
2229
    /**
2230
     * Set show summary below.
2231
     *
2232
     * @param bool $pValue Show summary below (true/false)
2233
     *
2234
     * @return $this
2235
     */
2236 100
    public function setShowSummaryBelow($pValue)
2237
    {
2238 100
        $this->showSummaryBelow = $pValue;
2239
2240 100
        return $this;
2241
    }
2242
2243
    /**
2244
     * Show summary right? (Row/Column outlining).
2245
     *
2246
     * @return bool
2247
     */
2248 146
    public function getShowSummaryRight()
2249
    {
2250 146
        return $this->showSummaryRight;
2251
    }
2252
2253
    /**
2254
     * Set show summary right.
2255
     *
2256
     * @param bool $pValue Show summary right (true/false)
2257
     *
2258
     * @return $this
2259
     */
2260 100
    public function setShowSummaryRight($pValue)
2261
    {
2262 100
        $this->showSummaryRight = $pValue;
2263
2264 100
        return $this;
2265
    }
2266
2267
    /**
2268
     * Get comments.
2269
     *
2270
     * @return Comment[]
2271
     */
2272 307
    public function getComments()
2273
    {
2274 307
        return $this->comments;
2275
    }
2276
2277
    /**
2278
     * Set comments array for the entire sheet.
2279
     *
2280
     * @param Comment[] $pValue
2281
     *
2282
     * @return $this
2283
     */
2284 32
    public function setComments(array $pValue)
2285
    {
2286 32
        $this->comments = $pValue;
2287
2288 32
        return $this;
2289
    }
2290
2291
    /**
2292
     * Get comment for cell.
2293
     *
2294
     * @param string $pCellCoordinate Cell coordinate to get comment for, eg: 'A1'
2295
     *
2296
     * @return Comment
2297
     */
2298 55
    public function getComment($pCellCoordinate)
2299
    {
2300
        // Uppercase coordinate
2301 55
        $pCellCoordinate = strtoupper($pCellCoordinate);
2302
2303 55
        if (Coordinate::coordinateIsRange($pCellCoordinate)) {
2304
            throw new Exception('Cell coordinate string can not be a range of cells.');
2305 55
        } elseif (strpos($pCellCoordinate, '$') !== false) {
2306
            throw new Exception('Cell coordinate string must not be absolute.');
2307 55
        } elseif ($pCellCoordinate == '') {
2308
            throw new Exception('Cell coordinate can not be zero-length string.');
2309
        }
2310
2311
        // Check if we already have a comment for this cell.
2312 55
        if (isset($this->comments[$pCellCoordinate])) {
2313 31
            return $this->comments[$pCellCoordinate];
2314
        }
2315
2316
        // If not, create a new comment.
2317 55
        $newComment = new Comment();
2318 55
        $this->comments[$pCellCoordinate] = $newComment;
2319
2320 55
        return $newComment;
2321
    }
2322
2323
    /**
2324
     * Get comment for cell by using numeric cell coordinates.
2325
     *
2326
     * @param int $columnIndex Numeric column coordinate of the cell
2327
     * @param int $row Numeric row coordinate of the cell
2328
     *
2329
     * @return Comment
2330
     */
2331 3
    public function getCommentByColumnAndRow($columnIndex, $row)
2332
    {
2333 3
        return $this->getComment(Coordinate::stringFromColumnIndex($columnIndex) . $row);
2334
    }
2335
2336
    /**
2337
     * Get active cell.
2338
     *
2339
     * @return string Example: 'A1'
2340
     */
2341 3500
    public function getActiveCell()
2342
    {
2343 3500
        return $this->activeCell;
2344
    }
2345
2346
    /**
2347
     * Get selected cells.
2348
     *
2349
     * @return string
2350
     */
2351 3663
    public function getSelectedCells()
2352
    {
2353 3663
        return $this->selectedCells;
2354
    }
2355
2356
    /**
2357
     * Selected cell.
2358
     *
2359
     * @param string $pCoordinate Cell (i.e. A1)
2360
     *
2361
     * @return $this
2362
     */
2363 8
    public function setSelectedCell($pCoordinate)
2364
    {
2365 8
        return $this->setSelectedCells($pCoordinate);
2366
    }
2367
2368
    /**
2369
     * Select a range of cells.
2370
     *
2371
     * @param string $pCoordinate Cell range, examples: 'A1', 'B2:G5', 'A:C', '3:6'
2372
     *
2373
     * @return $this
2374
     */
2375 3695
    public function setSelectedCells($pCoordinate)
2376
    {
2377
        // Uppercase coordinate
2378 3695
        $pCoordinate = strtoupper($pCoordinate);
2379
2380
        // Convert 'A' to 'A:A'
2381 3695
        $pCoordinate = preg_replace('/^([A-Z]+)$/', '${1}:${1}', $pCoordinate);
2382
2383
        // Convert '1' to '1:1'
2384 3695
        $pCoordinate = preg_replace('/^(\d+)$/', '${1}:${1}', $pCoordinate);
2385
2386
        // Convert 'A:C' to 'A1:C1048576'
2387 3695
        $pCoordinate = preg_replace('/^([A-Z]+):([A-Z]+)$/', '${1}1:${2}1048576', $pCoordinate);
2388
2389
        // Convert '1:3' to 'A1:XFD3'
2390 3695
        $pCoordinate = preg_replace('/^(\d+):(\d+)$/', 'A${1}:XFD${2}', $pCoordinate);
2391
2392 3695
        if (Coordinate::coordinateIsRange($pCoordinate)) {
2393 114
            [$first] = Coordinate::splitRange($pCoordinate);
2394 114
            $this->activeCell = $first[0];
2395
        } else {
2396 3671
            $this->activeCell = $pCoordinate;
2397
        }
2398 3695
        $this->selectedCells = $pCoordinate;
2399
2400 3695
        return $this;
2401
    }
2402
2403
    /**
2404
     * Selected cell by using numeric cell coordinates.
2405
     *
2406
     * @param int $columnIndex Numeric column coordinate of the cell
2407
     * @param int $row Numeric row coordinate of the cell
2408
     *
2409
     * @return $this
2410
     */
2411 19
    public function setSelectedCellByColumnAndRow($columnIndex, $row)
2412
    {
2413 19
        return $this->setSelectedCells(Coordinate::stringFromColumnIndex($columnIndex) . $row);
2414
    }
2415
2416
    /**
2417
     * Get right-to-left.
2418
     *
2419
     * @return bool
2420
     */
2421 146
    public function getRightToLeft()
2422
    {
2423 146
        return $this->rightToLeft;
2424
    }
2425
2426
    /**
2427
     * Set right-to-left.
2428
     *
2429
     * @param bool $value Right-to-left true/false
2430
     *
2431
     * @return $this
2432
     */
2433 39
    public function setRightToLeft($value)
2434
    {
2435 39
        $this->rightToLeft = $value;
2436
2437 39
        return $this;
2438
    }
2439
2440
    /**
2441
     * Fill worksheet from values in array.
2442
     *
2443
     * @param array $source Source array
2444
     * @param mixed $nullValue Value in source array that stands for blank cell
2445
     * @param string $startCell Insert array starting from this cell address as the top left coordinate
2446
     * @param bool $strictNullComparison Apply strict comparison when testing for null values in the array
2447
     *
2448
     * @return $this
2449
     */
2450 149
    public function fromArray(array $source, $nullValue = null, $startCell = 'A1', $strictNullComparison = false)
2451
    {
2452
        //    Convert a 1-D array to 2-D (for ease of looping)
2453 149
        if (!is_array(end($source))) {
2454 20
            $source = [$source];
2455
        }
2456
2457
        // start coordinate
2458 149
        [$startColumn, $startRow] = Coordinate::coordinateFromString($startCell);
2459
2460
        // Loop through $source
2461 149
        foreach ($source as $rowData) {
2462 149
            $currentColumn = $startColumn;
2463 149
            foreach ($rowData as $cellValue) {
2464 149
                if ($strictNullComparison) {
2465 76
                    if ($cellValue !== $nullValue) {
2466
                        // Set cell value
2467 76
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
2468
                    }
2469
                } else {
2470 73
                    if ($cellValue != $nullValue) {
2471
                        // Set cell value
2472 70
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
2473
                    }
2474
                }
2475 149
                ++$currentColumn;
2476
            }
2477 149
            ++$startRow;
2478
        }
2479
2480 149
        return $this;
2481
    }
2482
2483
    /**
2484
     * Create array from a range of cells.
2485
     *
2486
     * @param string $pRange Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
2487
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
2488
     * @param bool $calculateFormulas Should formulas be calculated?
2489
     * @param bool $formatData Should formatting be applied to cell values?
2490
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
2491
     *                               True - Return rows and columns indexed by their actual row and column IDs
2492
     *
2493
     * @return array
2494
     */
2495 51
    public function rangeToArray($pRange, $nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
2496
    {
2497
        // Returnvalue
2498 51
        $returnValue = [];
2499
        //    Identify the range that we need to extract from the worksheet
2500 51
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($pRange);
2501 51
        $minCol = Coordinate::stringFromColumnIndex($rangeStart[0]);
2502 51
        $minRow = $rangeStart[1];
2503 51
        $maxCol = Coordinate::stringFromColumnIndex($rangeEnd[0]);
2504 51
        $maxRow = $rangeEnd[1];
2505
2506 51
        ++$maxCol;
2507
        // Loop through rows
2508 51
        $r = -1;
2509 51
        for ($row = $minRow; $row <= $maxRow; ++$row) {
2510 51
            $rRef = $returnCellRef ? $row : ++$r;
2511 51
            $c = -1;
2512
            // Loop through columns in the current row
2513 51
            for ($col = $minCol; $col != $maxCol; ++$col) {
2514 51
                $cRef = $returnCellRef ? $col : ++$c;
2515
                //    Using getCell() will create a new cell if it doesn't already exist. We don't want that to happen
2516
                //        so we test and retrieve directly against cellCollection
2517 51
                if ($this->cellCollection->has($col . $row)) {
2518
                    // Cell exists
2519 51
                    $cell = $this->cellCollection->get($col . $row);
2520 51
                    if ($cell->getValue() !== null) {
2521 51
                        if ($cell->getValue() instanceof RichText) {
2522 2
                            $returnValue[$rRef][$cRef] = $cell->getValue()->getPlainText();
2523
                        } else {
2524 51
                            if ($calculateFormulas) {
2525 50
                                $returnValue[$rRef][$cRef] = $cell->getCalculatedValue();
2526
                            } else {
2527 2
                                $returnValue[$rRef][$cRef] = $cell->getValue();
2528
                            }
2529
                        }
2530
2531 51
                        if ($formatData) {
2532 50
                            $style = $this->parent->getCellXfByIndex($cell->getXfIndex());
2533 50
                            $returnValue[$rRef][$cRef] = NumberFormat::toFormattedString(
2534 50
                                $returnValue[$rRef][$cRef],
2535 51
                                ($style && $style->getNumberFormat()) ? $style->getNumberFormat()->getFormatCode() : NumberFormat::FORMAT_GENERAL
2536
                            );
2537
                        }
2538
                    } else {
2539
                        // Cell holds a NULL
2540 51
                        $returnValue[$rRef][$cRef] = $nullValue;
2541
                    }
2542
                } else {
2543
                    // Cell doesn't exist
2544 13
                    $returnValue[$rRef][$cRef] = $nullValue;
2545
                }
2546
            }
2547
        }
2548
2549
        // Return
2550 51
        return $returnValue;
2551
    }
2552
2553 12
    private function validateNamedRange(string $definedName, bool $returnNullIfInvalid = false): ?DefinedName
2554
    {
2555 12
        $namedRange = DefinedName::resolveName($definedName, $this);
2556 12
        if ($namedRange === null) {
2557 5
            if ($returnNullIfInvalid) {
2558 4
                return null;
2559
            }
2560
2561 1
            throw new Exception('Named Range ' . $definedName . ' does not exist.');
2562
        }
2563
2564 7
        if ($namedRange->isFormula()) {
2565
            if ($returnNullIfInvalid) {
2566
                return null;
2567
            }
2568
2569
            throw new Exception('Defined Named ' . $definedName . ' is a formula, not a range or cell.');
2570
        }
2571
2572 7
        if ($namedRange->getLocalOnly() && $this->getHashCode() !== $namedRange->getWorksheet()->getHashCode()) {
2573
            if ($returnNullIfInvalid) {
2574
                return null;
2575
            }
2576
2577
            throw new Exception(
2578
                'Named range ' . $definedName . ' is not accessible from within sheet ' . $this->getTitle()
2579
            );
2580
        }
2581
2582 7
        return $namedRange;
2583
    }
2584
2585
    /**
2586
     * Create array from a range of cells.
2587
     *
2588
     * @param string $definedName The Named Range that should be returned
2589
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
2590
     * @param bool $calculateFormulas Should formulas be calculated?
2591
     * @param bool $formatData Should formatting be applied to cell values?
2592
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
2593
     *                                True - Return rows and columns indexed by their actual row and column IDs
2594
     *
2595
     * @return array
2596
     */
2597 2
    public function namedRangeToArray(string $definedName, $nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
2598
    {
2599 2
        $namedRange = $this->validateNamedRange($definedName);
2600 1
        $workSheet = $namedRange->getWorksheet();
2601 1
        $cellRange = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!');
2602 1
        $cellRange = str_replace('$', '', $cellRange);
2603
2604 1
        return $workSheet->rangeToArray($cellRange, $nullValue, $calculateFormulas, $formatData, $returnCellRef);
2605
    }
2606
2607
    /**
2608
     * Create array from worksheet.
2609
     *
2610
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
2611
     * @param bool $calculateFormulas Should formulas be calculated?
2612
     * @param bool $formatData Should formatting be applied to cell values?
2613
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
2614
     *                               True - Return rows and columns indexed by their actual row and column IDs
2615
     *
2616
     * @return array
2617
     */
2618 27
    public function toArray($nullValue = null, $calculateFormulas = true, $formatData = true, $returnCellRef = false)
2619
    {
2620
        // Garbage collect...
2621 27
        $this->garbageCollect();
2622
2623
        //    Identify the range that we need to extract from the worksheet
2624 27
        $maxCol = $this->getHighestColumn();
2625 27
        $maxRow = $this->getHighestRow();
2626
2627
        // Return
2628 27
        return $this->rangeToArray('A1:' . $maxCol . $maxRow, $nullValue, $calculateFormulas, $formatData, $returnCellRef);
2629
    }
2630
2631
    /**
2632
     * Get row iterator.
2633
     *
2634
     * @param int $startRow The row number at which to start iterating
2635
     * @param int $endRow The row number at which to stop iterating
2636
     *
2637
     * @return RowIterator
2638
     */
2639 13
    public function getRowIterator($startRow = 1, $endRow = null)
2640
    {
2641 13
        return new RowIterator($this, $startRow, $endRow);
2642
    }
2643
2644
    /**
2645
     * Get column iterator.
2646
     *
2647
     * @param string $startColumn The column address at which to start iterating
2648
     * @param string $endColumn The column address at which to stop iterating
2649
     *
2650
     * @return ColumnIterator
2651
     */
2652 3
    public function getColumnIterator($startColumn = 'A', $endColumn = null)
2653
    {
2654 3
        return new ColumnIterator($this, $startColumn, $endColumn);
2655
    }
2656
2657
    /**
2658
     * Run PhpSpreadsheet garbage collector.
2659
     *
2660
     * @return $this
2661
     */
2662 342
    public function garbageCollect()
2663
    {
2664
        // Flush cache
2665 342
        $this->cellCollection->get('A1');
2666
2667
        // Lookup highest column and highest row if cells are cleaned
2668 342
        $colRow = $this->cellCollection->getHighestRowAndColumn();
2669 342
        $highestRow = $colRow['row'];
2670 342
        $highestColumn = Coordinate::columnIndexFromString($colRow['column']);
2671
2672
        // Loop through column dimensions
2673 342
        foreach ($this->columnDimensions as $dimension) {
2674 50
            $highestColumn = max($highestColumn, Coordinate::columnIndexFromString($dimension->getColumnIndex()));
2675
        }
2676
2677
        // Loop through row dimensions
2678 342
        foreach ($this->rowDimensions as $dimension) {
2679 60
            $highestRow = max($highestRow, $dimension->getRowIndex());
2680
        }
2681
2682
        // Cache values
2683 342
        if ($highestColumn < 1) {
2684
            $this->cachedHighestColumn = 1;
2685
        } else {
2686 342
            $this->cachedHighestColumn = $highestColumn;
2687
        }
2688 342
        $this->cachedHighestRow = $highestRow;
2689
2690
        // Return
2691 342
        return $this;
2692
    }
2693
2694
    /**
2695
     * Get hash code.
2696
     *
2697
     * @return string Hash code
2698
     */
2699 3720
    public function getHashCode()
2700
    {
2701 3720
        if ($this->dirty) {
2702 3720
            $this->hash = md5($this->title . $this->autoFilter . ($this->protection->isProtectionEnabled() ? 't' : 'f') . __CLASS__);
2703 3720
            $this->dirty = false;
2704
        }
2705
2706 3720
        return $this->hash;
2707
    }
2708
2709
    /**
2710
     * Extract worksheet title from range.
2711
     *
2712
     * Example: extractSheetTitle("testSheet!A1") ==> 'A1'
2713
     * Example: extractSheetTitle("'testSheet 1'!A1", true) ==> ['testSheet 1', 'A1'];
2714
     *
2715
     * @param string $pRange Range to extract title from
2716
     * @param bool $returnRange Return range? (see example)
2717
     *
2718
     * @return mixed
2719
     */
2720 455
    public static function extractSheetTitle($pRange, $returnRange = false)
2721
    {
2722
        // Sheet title included?
2723 455
        if (($sep = strrpos($pRange, '!')) === false) {
2724 65
            return $returnRange ? ['', $pRange] : '';
2725
        }
2726
2727 419
        if ($returnRange) {
2728 419
            return [substr($pRange, 0, $sep), substr($pRange, $sep + 1)];
2729
        }
2730
2731 7
        return substr($pRange, $sep + 1);
2732
    }
2733
2734
    /**
2735
     * Get hyperlink.
2736
     *
2737
     * @param string $pCellCoordinate Cell coordinate to get hyperlink for, eg: 'A1'
2738
     *
2739
     * @return Hyperlink
2740
     */
2741 36
    public function getHyperlink($pCellCoordinate)
2742
    {
2743
        // return hyperlink if we already have one
2744 36
        if (isset($this->hyperlinkCollection[$pCellCoordinate])) {
2745 22
            return $this->hyperlinkCollection[$pCellCoordinate];
2746
        }
2747
2748
        // else create hyperlink
2749 36
        $this->hyperlinkCollection[$pCellCoordinate] = new Hyperlink();
2750
2751 36
        return $this->hyperlinkCollection[$pCellCoordinate];
2752
    }
2753
2754
    /**
2755
     * Set hyperlink.
2756
     *
2757
     * @param string $pCellCoordinate Cell coordinate to insert hyperlink, eg: 'A1'
2758
     *
2759
     * @return $this
2760
     */
2761 17
    public function setHyperlink($pCellCoordinate, ?Hyperlink $pHyperlink = null)
2762
    {
2763 17
        if ($pHyperlink === null) {
2764 17
            unset($this->hyperlinkCollection[$pCellCoordinate]);
2765
        } else {
2766 17
            $this->hyperlinkCollection[$pCellCoordinate] = $pHyperlink;
2767
        }
2768
2769 17
        return $this;
2770
    }
2771
2772
    /**
2773
     * Hyperlink at a specific coordinate exists?
2774
     *
2775
     * @param string $pCoordinate eg: 'A1'
2776
     *
2777
     * @return bool
2778
     */
2779 163
    public function hyperlinkExists($pCoordinate)
2780
    {
2781 163
        return isset($this->hyperlinkCollection[$pCoordinate]);
2782
    }
2783
2784
    /**
2785
     * Get collection of hyperlinks.
2786
     *
2787
     * @return Hyperlink[]
2788
     */
2789 166
    public function getHyperlinkCollection()
2790
    {
2791 166
        return $this->hyperlinkCollection;
2792
    }
2793
2794
    /**
2795
     * Get data validation.
2796
     *
2797
     * @param string $pCellCoordinate Cell coordinate to get data validation for, eg: 'A1'
2798
     *
2799
     * @return DataValidation
2800
     */
2801 5
    public function getDataValidation($pCellCoordinate)
2802
    {
2803
        // return data validation if we already have one
2804 5
        if (isset($this->dataValidationCollection[$pCellCoordinate])) {
2805 2
            return $this->dataValidationCollection[$pCellCoordinate];
2806
        }
2807
2808
        // else create data validation
2809 5
        $this->dataValidationCollection[$pCellCoordinate] = new DataValidation();
2810
2811 5
        return $this->dataValidationCollection[$pCellCoordinate];
2812
    }
2813
2814
    /**
2815
     * Set data validation.
2816
     *
2817
     * @param string $pCellCoordinate Cell coordinate to insert data validation, eg: 'A1'
2818
     *
2819
     * @return $this
2820
     */
2821
    public function setDataValidation($pCellCoordinate, ?DataValidation $pDataValidation = null)
2822
    {
2823
        if ($pDataValidation === null) {
2824
            unset($this->dataValidationCollection[$pCellCoordinate]);
2825
        } else {
2826
            $this->dataValidationCollection[$pCellCoordinate] = $pDataValidation;
2827
        }
2828
2829
        return $this;
2830
    }
2831
2832
    /**
2833
     * Data validation at a specific coordinate exists?
2834
     *
2835
     * @param string $pCoordinate eg: 'A1'
2836
     *
2837
     * @return bool
2838
     */
2839 4
    public function dataValidationExists($pCoordinate)
2840
    {
2841 4
        return isset($this->dataValidationCollection[$pCoordinate]);
2842
    }
2843
2844
    /**
2845
     * Get collection of data validations.
2846
     *
2847
     * @return DataValidation[]
2848
     */
2849 166
    public function getDataValidationCollection()
2850
    {
2851 166
        return $this->dataValidationCollection;
2852
    }
2853
2854
    /**
2855
     * Accepts a range, returning it as a range that falls within the current highest row and column of the worksheet.
2856
     *
2857
     * @param string $range
2858
     *
2859
     * @return string Adjusted range value
2860
     */
2861 1
    public function shrinkRangeToFit($range)
2862
    {
2863 1
        $maxCol = $this->getHighestColumn();
2864 1
        $maxRow = $this->getHighestRow();
2865 1
        $maxCol = Coordinate::columnIndexFromString($maxCol);
2866
2867 1
        $rangeBlocks = explode(' ', $range);
2868 1
        foreach ($rangeBlocks as &$rangeSet) {
2869 1
            $rangeBoundaries = Coordinate::getRangeBoundaries($rangeSet);
2870
2871 1
            if (Coordinate::columnIndexFromString($rangeBoundaries[0][0]) > $maxCol) {
2872
                $rangeBoundaries[0][0] = Coordinate::stringFromColumnIndex($maxCol);
2873
            }
2874 1
            if ($rangeBoundaries[0][1] > $maxRow) {
2875
                $rangeBoundaries[0][1] = $maxRow;
2876
            }
2877 1
            if (Coordinate::columnIndexFromString($rangeBoundaries[1][0]) > $maxCol) {
2878
                $rangeBoundaries[1][0] = Coordinate::stringFromColumnIndex($maxCol);
2879
            }
2880 1
            if ($rangeBoundaries[1][1] > $maxRow) {
2881
                $rangeBoundaries[1][1] = $maxRow;
2882
            }
2883 1
            $rangeSet = $rangeBoundaries[0][0] . $rangeBoundaries[0][1] . ':' . $rangeBoundaries[1][0] . $rangeBoundaries[1][1];
2884
        }
2885 1
        unset($rangeSet);
2886
2887 1
        return implode(' ', $rangeBlocks);
2888
    }
2889
2890
    /**
2891
     * Get tab color.
2892
     *
2893
     * @return Color
2894
     */
2895 16
    public function getTabColor()
2896
    {
2897 16
        if ($this->tabColor === null) {
2898 16
            $this->tabColor = new Color();
2899
        }
2900
2901 16
        return $this->tabColor;
2902
    }
2903
2904
    /**
2905
     * Reset tab color.
2906
     *
2907
     * @return $this
2908
     */
2909
    public function resetTabColor()
2910
    {
2911
        $this->tabColor = null;
2912
2913
        return $this;
2914
    }
2915
2916
    /**
2917
     * Tab color set?
2918
     *
2919
     * @return bool
2920
     */
2921 155
    public function isTabColorSet()
2922
    {
2923 155
        return $this->tabColor !== null;
2924
    }
2925
2926
    /**
2927
     * Copy worksheet (!= clone!).
2928
     *
2929
     * @return static
2930
     */
2931
    public function copy()
2932
    {
2933
        return clone $this;
2934
    }
2935
2936
    /**
2937
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
2938
     */
2939 5
    public function __clone()
2940
    {
2941
        // @phpstan-ignore-next-line
2942 5
        foreach ($this as $key => $val) {
2943 5
            if ($key == 'parent') {
2944 5
                continue;
2945
            }
2946
2947 5
            if (is_object($val) || (is_array($val))) {
2948 5
                if ($key == 'cellCollection') {
2949 5
                    $newCollection = $this->cellCollection->cloneCellCollection($this);
2950 5
                    $this->cellCollection = $newCollection;
2951 5
                } elseif ($key == 'drawingCollection') {
2952 5
                    $currentCollection = $this->drawingCollection;
2953 5
                    $this->drawingCollection = new ArrayObject();
2954 5
                    foreach ($currentCollection as $item) {
2955 3
                        if (is_object($item)) {
2956 3
                            $newDrawing = clone $item;
2957 3
                            $newDrawing->setWorksheet($this);
2958
                        }
2959
                    }
2960 5
                } elseif (($key == 'autoFilter') && ($this->autoFilter instanceof AutoFilter)) {
2961 5
                    $newAutoFilter = clone $this->autoFilter;
2962 5
                    $this->autoFilter = $newAutoFilter;
2963 5
                    $this->autoFilter->setParent($this);
2964
                } else {
2965 5
                    $this->{$key} = unserialize(serialize($val));
2966
                }
2967
            }
2968
        }
2969 5
    }
2970
2971
    /**
2972
     * Define the code name of the sheet.
2973
     *
2974
     * @param string $pValue Same rule as Title minus space not allowed (but, like Excel, change
2975
     *                       silently space to underscore)
2976
     * @param bool $validate False to skip validation of new title. WARNING: This should only be set
2977
     *                       at parse time (by Readers), where titles can be assumed to be valid.
2978
     *
2979
     * @return $this
2980
     */
2981 4012
    public function setCodeName($pValue, $validate = true)
2982
    {
2983
        // Is this a 'rename' or not?
2984 4012
        if ($this->getCodeName() == $pValue) {
2985
            return $this;
2986
        }
2987
2988 4012
        if ($validate) {
2989 4012
            $pValue = str_replace(' ', '_', $pValue); //Excel does this automatically without flinching, we are doing the same
2990
2991
            // Syntax check
2992
            // throw an exception if not valid
2993 4012
            self::checkSheetCodeName($pValue);
2994
2995
            // We use the same code that setTitle to find a valid codeName else not using a space (Excel don't like) but a '_'
2996
2997 4012
            if ($this->getParent()) {
2998
                // Is there already such sheet name?
2999 3974
                if ($this->getParent()->sheetCodeNameExists($pValue)) {
3000
                    // Use name, but append with lowest possible integer
3001
3002 92
                    if (Shared\StringHelper::countCharacters($pValue) > 29) {
3003
                        $pValue = Shared\StringHelper::substring($pValue, 0, 29);
3004
                    }
3005 92
                    $i = 1;
3006 92
                    while ($this->getParent()->sheetCodeNameExists($pValue . '_' . $i)) {
3007 27
                        ++$i;
3008 27
                        if ($i == 10) {
3009
                            if (Shared\StringHelper::countCharacters($pValue) > 28) {
3010
                                $pValue = Shared\StringHelper::substring($pValue, 0, 28);
3011
                            }
3012 27
                        } elseif ($i == 100) {
3013
                            if (Shared\StringHelper::countCharacters($pValue) > 27) {
3014
                                $pValue = Shared\StringHelper::substring($pValue, 0, 27);
3015
                            }
3016
                        }
3017
                    }
3018
3019 92
                    $pValue .= '_' . $i; // ok, we have a valid name
3020
                }
3021
            }
3022
        }
3023
3024 4012
        $this->codeName = $pValue;
3025
3026 4012
        return $this;
3027
    }
3028
3029
    /**
3030
     * Return the code name of the sheet.
3031
     *
3032
     * @return null|string
3033
     */
3034 4012
    public function getCodeName()
3035
    {
3036 4012
        return $this->codeName;
3037
    }
3038
3039
    /**
3040
     * Sheet has a code name ?
3041
     *
3042
     * @return bool
3043
     */
3044 1
    public function hasCodeName()
3045
    {
3046 1
        return $this->codeName !== null;
3047
    }
3048
}
3049