Passed
Push — master ( 1e8ff9...2e39ec )
by Adrien
07:24
created

Worksheet::getRowDimension()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

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

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

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