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

Worksheet::calculateColumnWidths()   C

Complexity

Conditions 16
Paths 6

Size

Total Lines 71
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 16.747

Importance

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

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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