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

Worksheet::calculateColumnWidths()   C

Complexity

Conditions 16
Paths 6

Size

Total Lines 71
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 16.747

Importance

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

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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