Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
 
3
namespace PhpOffice\PhpSpreadsheet\Worksheet;
4
 
5
use ArrayObject;
6
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
7
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
8
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
9
use PhpOffice\PhpSpreadsheet\Cell\Cell;
10
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
11
use PhpOffice\PhpSpreadsheet\Cell\CellRange;
12
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
13
use PhpOffice\PhpSpreadsheet\Cell\DataType;
14
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
15
use PhpOffice\PhpSpreadsheet\Cell\Hyperlink;
16
use PhpOffice\PhpSpreadsheet\Cell\IValueBinder;
17
use PhpOffice\PhpSpreadsheet\Chart\Chart;
18
use PhpOffice\PhpSpreadsheet\Collection\Cells;
19
use PhpOffice\PhpSpreadsheet\Collection\CellsFactory;
20
use PhpOffice\PhpSpreadsheet\Comment;
21
use PhpOffice\PhpSpreadsheet\DefinedName;
22
use PhpOffice\PhpSpreadsheet\Exception;
23
use PhpOffice\PhpSpreadsheet\IComparable;
24
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
25
use PhpOffice\PhpSpreadsheet\RichText\RichText;
26
use PhpOffice\PhpSpreadsheet\Shared;
27
use PhpOffice\PhpSpreadsheet\Spreadsheet;
28
use PhpOffice\PhpSpreadsheet\Style\Alignment;
29
use PhpOffice\PhpSpreadsheet\Style\Color;
30
use PhpOffice\PhpSpreadsheet\Style\Conditional;
31
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
32
use PhpOffice\PhpSpreadsheet\Style\Style;
33
 
34
class Worksheet implements IComparable
35
{
36
    // Break types
37
    public const BREAK_NONE = 0;
38
    public const BREAK_ROW = 1;
39
    public const BREAK_COLUMN = 2;
40
    // Maximum column for row break
41
    public const BREAK_ROW_MAX_COLUMN = 16383;
42
 
43
    // Sheet state
44
    public const SHEETSTATE_VISIBLE = 'visible';
45
    public const SHEETSTATE_HIDDEN = 'hidden';
46
    public const SHEETSTATE_VERYHIDDEN = 'veryHidden';
47
 
48
    public const MERGE_CELL_CONTENT_EMPTY = 'empty';
49
    public const MERGE_CELL_CONTENT_HIDE = 'hide';
50
    public const MERGE_CELL_CONTENT_MERGE = 'merge';
51
 
52
    protected const SHEET_NAME_REQUIRES_NO_QUOTES = '/^[_\p{L}][_\p{L}\p{N}]*$/mui';
53
 
54
    /**
55
     * Maximum 31 characters allowed for sheet title.
56
     *
57
     * @var int
58
     */
59
    const SHEET_TITLE_MAXIMUM_LENGTH = 31;
60
 
61
    /**
62
     * Invalid characters in sheet title.
63
     *
64
     * @var array
65
     */
66
    private static $invalidCharacters = ['*', ':', '/', '\\', '?', '[', ']'];
67
 
68
    /**
69
     * Parent spreadsheet.
70
     *
71
     * @var ?Spreadsheet
72
     */
73
    private $parent;
74
 
75
    /**
76
     * Collection of cells.
77
     *
78
     * @var Cells
79
     */
80
    private $cellCollection;
81
 
82
    /**
83
     * Collection of row dimensions.
84
     *
85
     * @var RowDimension[]
86
     */
87
    private $rowDimensions = [];
88
 
89
    /**
90
     * Default row dimension.
91
     *
92
     * @var RowDimension
93
     */
94
    private $defaultRowDimension;
95
 
96
    /**
97
     * Collection of column dimensions.
98
     *
99
     * @var ColumnDimension[]
100
     */
101
    private $columnDimensions = [];
102
 
103
    /**
104
     * Default column dimension.
105
     *
106
     * @var ColumnDimension
107
     */
108
    private $defaultColumnDimension;
109
 
110
    /**
111
     * Collection of drawings.
112
     *
113
     * @var ArrayObject<int, BaseDrawing>
114
     */
115
    private $drawingCollection;
116
 
117
    /**
118
     * Collection of Chart objects.
119
     *
120
     * @var ArrayObject<int, Chart>
121
     */
122
    private $chartCollection;
123
 
124
    /**
125
     * Collection of Table objects.
126
     *
127
     * @var ArrayObject<int, Table>
128
     */
129
    private $tableCollection;
130
 
131
    /**
132
     * Worksheet title.
133
     *
134
     * @var string
135
     */
136
    private $title;
137
 
138
    /**
139
     * Sheet state.
140
     *
141
     * @var string
142
     */
143
    private $sheetState;
144
 
145
    /**
146
     * Page setup.
147
     *
148
     * @var PageSetup
149
     */
150
    private $pageSetup;
151
 
152
    /**
153
     * Page margins.
154
     *
155
     * @var PageMargins
156
     */
157
    private $pageMargins;
158
 
159
    /**
160
     * Page header/footer.
161
     *
162
     * @var HeaderFooter
163
     */
164
    private $headerFooter;
165
 
166
    /**
167
     * Sheet view.
168
     *
169
     * @var SheetView
170
     */
171
    private $sheetView;
172
 
173
    /**
174
     * Protection.
175
     *
176
     * @var Protection
177
     */
178
    private $protection;
179
 
180
    /**
181
     * Collection of styles.
182
     *
183
     * @var Style[]
184
     */
185
    private $styles = [];
186
 
187
    /**
188
     * Conditional styles. Indexed by cell coordinate, e.g. 'A1'.
189
     *
190
     * @var array
191
     */
192
    private $conditionalStylesCollection = [];
193
 
194
    /**
195
     * Collection of row breaks.
196
     *
197
     * @var PageBreak[]
198
     */
199
    private $rowBreaks = [];
200
 
201
    /**
202
     * Collection of column breaks.
203
     *
204
     * @var PageBreak[]
205
     */
206
    private $columnBreaks = [];
207
 
208
    /**
209
     * Collection of merged cell ranges.
210
     *
211
     * @var string[]
212
     */
213
    private $mergeCells = [];
214
 
215
    /**
216
     * Collection of protected cell ranges.
217
     *
218
     * @var string[]
219
     */
220
    private $protectedCells = [];
221
 
222
    /**
223
     * Autofilter Range and selection.
224
     *
225
     * @var AutoFilter
226
     */
227
    private $autoFilter;
228
 
229
    /**
230
     * Freeze pane.
231
     *
232
     * @var null|string
233
     */
234
    private $freezePane;
235
 
236
    /**
237
     * Default position of the right bottom pane.
238
     *
239
     * @var null|string
240
     */
241
    private $topLeftCell;
242
 
243
    /**
244
     * Show gridlines?
245
     *
246
     * @var bool
247
     */
248
    private $showGridlines = true;
249
 
250
    /**
251
     * Print gridlines?
252
     *
253
     * @var bool
254
     */
255
    private $printGridlines = false;
256
 
257
    /**
258
     * Show row and column headers?
259
     *
260
     * @var bool
261
     */
262
    private $showRowColHeaders = true;
263
 
264
    /**
265
     * Show summary below? (Row/Column outline).
266
     *
267
     * @var bool
268
     */
269
    private $showSummaryBelow = true;
270
 
271
    /**
272
     * Show summary right? (Row/Column outline).
273
     *
274
     * @var bool
275
     */
276
    private $showSummaryRight = true;
277
 
278
    /**
279
     * Collection of comments.
280
     *
281
     * @var Comment[]
282
     */
283
    private $comments = [];
284
 
285
    /**
286
     * Active cell. (Only one!).
287
     *
288
     * @var string
289
     */
290
    private $activeCell = 'A1';
291
 
292
    /**
293
     * Selected cells.
294
     *
295
     * @var string
296
     */
297
    private $selectedCells = 'A1';
298
 
299
    /**
300
     * Cached highest column.
301
     *
302
     * @var int
303
     */
304
    private $cachedHighestColumn = 1;
305
 
306
    /**
307
     * Cached highest row.
308
     *
309
     * @var int
310
     */
311
    private $cachedHighestRow = 1;
312
 
313
    /**
314
     * Right-to-left?
315
     *
316
     * @var bool
317
     */
318
    private $rightToLeft = false;
319
 
320
    /**
321
     * Hyperlinks. Indexed by cell coordinate, e.g. 'A1'.
322
     *
323
     * @var array
324
     */
325
    private $hyperlinkCollection = [];
326
 
327
    /**
328
     * Data validation objects. Indexed by cell coordinate, e.g. 'A1'.
329
     *
330
     * @var array
331
     */
332
    private $dataValidationCollection = [];
333
 
334
    /**
335
     * Tab color.
336
     *
337
     * @var null|Color
338
     */
339
    private $tabColor;
340
 
341
    /**
342
     * Dirty flag.
343
     *
344
     * @var bool
345
     */
346
    private $dirty = true;
347
 
348
    /**
349
     * Hash.
350
     *
351
     * @var string
352
     */
353
    private $hash;
354
 
355
    /**
356
     * CodeName.
357
     *
358
     * @var string
359
     */
360
    private $codeName;
361
 
362
    /**
363
     * Create a new worksheet.
364
     *
365
     * @param string $title
366
     */
367
    public function __construct(?Spreadsheet $parent = null, $title = 'Worksheet')
368
    {
369
        // Set parent and title
370
        $this->parent = $parent;
371
        $this->setTitle($title, false);
372
        // setTitle can change $pTitle
373
        $this->setCodeName($this->getTitle());
374
        $this->setSheetState(self::SHEETSTATE_VISIBLE);
375
 
376
        $this->cellCollection = CellsFactory::getInstance($this);
377
        // Set page setup
378
        $this->pageSetup = new PageSetup();
379
        // Set page margins
380
        $this->pageMargins = new PageMargins();
381
        // Set page header/footer
382
        $this->headerFooter = new HeaderFooter();
383
        // Set sheet view
384
        $this->sheetView = new SheetView();
385
        // Drawing collection
386
        $this->drawingCollection = new ArrayObject();
387
        // Chart collection
388
        $this->chartCollection = new ArrayObject();
389
        // Protection
390
        $this->protection = new Protection();
391
        // Default row dimension
392
        $this->defaultRowDimension = new RowDimension(null);
393
        // Default column dimension
394
        $this->defaultColumnDimension = new ColumnDimension(null);
395
        // AutoFilter
396
        $this->autoFilter = new AutoFilter('', $this);
397
        // Table collection
398
        $this->tableCollection = new ArrayObject();
399
    }
400
 
401
    /**
402
     * Disconnect all cells from this Worksheet object,
403
     * typically so that the worksheet object can be unset.
404
     */
405
    public function disconnectCells(): void
406
    {
407
        if ($this->cellCollection !== null) {
408
            $this->cellCollection->unsetWorksheetCells();
409
            // @phpstan-ignore-next-line
410
            $this->cellCollection = null;
411
        }
412
        //    detach ourself from the workbook, so that it can then delete this worksheet successfully
413
        $this->parent = null;
414
    }
415
 
416
    /**
417
     * Code to execute when this worksheet is unset().
418
     */
419
    public function __destruct()
420
    {
421
        Calculation::getInstance($this->parent)->clearCalculationCacheForWorksheet($this->title);
422
 
423
        $this->disconnectCells();
424
        $this->rowDimensions = [];
425
    }
426
 
427
    /**
428
     * Return the cell collection.
429
     *
430
     * @return Cells
431
     */
432
    public function getCellCollection()
433
    {
434
        return $this->cellCollection;
435
    }
436
 
437
    /**
438
     * Get array of invalid characters for sheet title.
439
     *
440
     * @return array
441
     */
442
    public static function getInvalidCharacters()
443
    {
444
        return self::$invalidCharacters;
445
    }
446
 
447
    /**
448
     * Check sheet code name for valid Excel syntax.
449
     *
450
     * @param string $sheetCodeName The string to check
451
     *
452
     * @return string The valid string
453
     */
454
    private static function checkSheetCodeName($sheetCodeName)
455
    {
456
        $charCount = Shared\StringHelper::countCharacters($sheetCodeName);
457
        if ($charCount == 0) {
458
            throw new Exception('Sheet code name cannot be empty.');
459
        }
460
        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ] and  first and last characters cannot be a "'"
461
        if (
462
            (str_replace(self::$invalidCharacters, '', $sheetCodeName) !== $sheetCodeName) ||
463
            (Shared\StringHelper::substring($sheetCodeName, -1, 1) == '\'') ||
464
            (Shared\StringHelper::substring($sheetCodeName, 0, 1) == '\'')
465
        ) {
466
            throw new Exception('Invalid character found in sheet code name');
467
        }
468
 
469
        // Enforce maximum characters allowed for sheet title
470
        if ($charCount > self::SHEET_TITLE_MAXIMUM_LENGTH) {
471
            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet code name.');
472
        }
473
 
474
        return $sheetCodeName;
475
    }
476
 
477
    /**
478
     * Check sheet title for valid Excel syntax.
479
     *
480
     * @param string $sheetTitle The string to check
481
     *
482
     * @return string The valid string
483
     */
484
    private static function checkSheetTitle($sheetTitle)
485
    {
486
        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ]
487
        if (str_replace(self::$invalidCharacters, '', $sheetTitle) !== $sheetTitle) {
488
            throw new Exception('Invalid character found in sheet title');
489
        }
490
 
491
        // Enforce maximum characters allowed for sheet title
492
        if (Shared\StringHelper::countCharacters($sheetTitle) > self::SHEET_TITLE_MAXIMUM_LENGTH) {
493
            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet title.');
494
        }
495
 
496
        return $sheetTitle;
497
    }
498
 
499
    /**
500
     * Get a sorted list of all cell coordinates currently held in the collection by row and column.
501
     *
502
     * @param bool $sorted Also sort the cell collection?
503
     *
504
     * @return string[]
505
     */
506
    public function getCoordinates($sorted = true)
507
    {
508
        if ($this->cellCollection == null) {
509
            return [];
510
        }
511
 
512
        if ($sorted) {
513
            return $this->cellCollection->getSortedCoordinates();
514
        }
515
 
516
        return $this->cellCollection->getCoordinates();
517
    }
518
 
519
    /**
520
     * Get collection of row dimensions.
521
     *
522
     * @return RowDimension[]
523
     */
524
    public function getRowDimensions()
525
    {
526
        return $this->rowDimensions;
527
    }
528
 
529
    /**
530
     * Get default row dimension.
531
     *
532
     * @return RowDimension
533
     */
534
    public function getDefaultRowDimension()
535
    {
536
        return $this->defaultRowDimension;
537
    }
538
 
539
    /**
540
     * Get collection of column dimensions.
541
     *
542
     * @return ColumnDimension[]
543
     */
544
    public function getColumnDimensions()
545
    {
546
        /** @var callable */
547
        $callable = [self::class, 'columnDimensionCompare'];
548
        uasort($this->columnDimensions, $callable);
549
 
550
        return $this->columnDimensions;
551
    }
552
 
553
    private static function columnDimensionCompare(ColumnDimension $a, ColumnDimension $b): int
554
    {
555
        return $a->getColumnNumeric() - $b->getColumnNumeric();
556
    }
557
 
558
    /**
559
     * Get default column dimension.
560
     *
561
     * @return ColumnDimension
562
     */
563
    public function getDefaultColumnDimension()
564
    {
565
        return $this->defaultColumnDimension;
566
    }
567
 
568
    /**
569
     * Get collection of drawings.
570
     *
571
     * @return ArrayObject<int, BaseDrawing>
572
     */
573
    public function getDrawingCollection()
574
    {
575
        return $this->drawingCollection;
576
    }
577
 
578
    /**
579
     * Get collection of charts.
580
     *
581
     * @return ArrayObject<int, Chart>
582
     */
583
    public function getChartCollection()
584
    {
585
        return $this->chartCollection;
586
    }
587
 
588
    /**
589
     * Add chart.
590
     *
591
     * @param null|int $chartIndex Index where chart should go (0,1,..., or null for last)
592
     *
593
     * @return Chart
594
     */
595
    public function addChart(Chart $chart, $chartIndex = null)
596
    {
597
        $chart->setWorksheet($this);
598
        if ($chartIndex === null) {
599
            $this->chartCollection[] = $chart;
600
        } else {
601
            // Insert the chart at the requested index
602
            // @phpstan-ignore-next-line
603
            array_splice(/** @scrutinizer ignore-type */ $this->chartCollection, $chartIndex, 0, [$chart]);
604
        }
605
 
606
        return $chart;
607
    }
608
 
609
    /**
610
     * Return the count of charts on this worksheet.
611
     *
612
     * @return int The number of charts
613
     */
614
    public function getChartCount()
615
    {
616
        return count($this->chartCollection);
617
    }
618
 
619
    /**
620
     * Get a chart by its index position.
621
     *
622
     * @param ?string $index Chart index position
623
     *
624
     * @return Chart|false
625
     */
626
    public function getChartByIndex($index)
627
    {
628
        $chartCount = count($this->chartCollection);
629
        if ($chartCount == 0) {
630
            return false;
631
        }
632
        if ($index === null) {
633
            $index = --$chartCount;
634
        }
635
        if (!isset($this->chartCollection[$index])) {
636
            return false;
637
        }
638
 
639
        return $this->chartCollection[$index];
640
    }
641
 
642
    /**
643
     * Return an array of the names of charts on this worksheet.
644
     *
645
     * @return string[] The names of charts
646
     */
647
    public function getChartNames()
648
    {
649
        $chartNames = [];
650
        foreach ($this->chartCollection as $chart) {
651
            $chartNames[] = $chart->getName();
652
        }
653
 
654
        return $chartNames;
655
    }
656
 
657
    /**
658
     * Get a chart by name.
659
     *
660
     * @param string $chartName Chart name
661
     *
662
     * @return Chart|false
663
     */
664
    public function getChartByName($chartName)
665
    {
666
        foreach ($this->chartCollection as $index => $chart) {
667
            if ($chart->getName() == $chartName) {
668
                return $chart;
669
            }
670
        }
671
 
672
        return false;
673
    }
674
 
675
    /**
676
     * Refresh column dimensions.
677
     *
678
     * @return $this
679
     */
680
    public function refreshColumnDimensions()
681
    {
682
        $newColumnDimensions = [];
683
        foreach ($this->getColumnDimensions() as $objColumnDimension) {
684
            $newColumnDimensions[$objColumnDimension->getColumnIndex()] = $objColumnDimension;
685
        }
686
 
687
        $this->columnDimensions = $newColumnDimensions;
688
 
689
        return $this;
690
    }
691
 
692
    /**
693
     * Refresh row dimensions.
694
     *
695
     * @return $this
696
     */
697
    public function refreshRowDimensions()
698
    {
699
        $newRowDimensions = [];
700
        foreach ($this->getRowDimensions() as $objRowDimension) {
701
            $newRowDimensions[$objRowDimension->getRowIndex()] = $objRowDimension;
702
        }
703
 
704
        $this->rowDimensions = $newRowDimensions;
705
 
706
        return $this;
707
    }
708
 
709
    /**
710
     * Calculate worksheet dimension.
711
     *
712
     * @return string String containing the dimension of this worksheet
713
     */
714
    public function calculateWorksheetDimension()
715
    {
716
        // Return
717
        return 'A1:' . $this->getHighestColumn() . $this->getHighestRow();
718
    }
719
 
720
    /**
721
     * Calculate worksheet data dimension.
722
     *
723
     * @return string String containing the dimension of this worksheet that actually contain data
724
     */
725
    public function calculateWorksheetDataDimension()
726
    {
727
        // Return
728
        return 'A1:' . $this->getHighestDataColumn() . $this->getHighestDataRow();
729
    }
730
 
731
    /**
732
     * Calculate widths for auto-size columns.
733
     *
734
     * @return $this
735
     */
736
    public function calculateColumnWidths()
737
    {
738
        // initialize $autoSizes array
739
        $autoSizes = [];
740
        foreach ($this->getColumnDimensions() as $colDimension) {
741
            if ($colDimension->getAutoSize()) {
742
                $autoSizes[$colDimension->getColumnIndex()] = -1;
743
            }
744
        }
745
 
746
        // There is only something to do if there are some auto-size columns
747
        if (!empty($autoSizes)) {
748
            // build list of cells references that participate in a merge
749
            $isMergeCell = [];
750
            foreach ($this->getMergeCells() as $cells) {
751
                foreach (Coordinate::extractAllCellReferencesInRange($cells) as $cellReference) {
752
                    $isMergeCell[$cellReference] = true;
753
                }
754
            }
755
 
756
            $autoFilterIndentRanges = (new AutoFit($this))->getAutoFilterIndentRanges();
757
 
758
            // loop through all cells in the worksheet
759
            foreach ($this->getCoordinates(false) as $coordinate) {
760
                $cell = $this->getCellOrNull($coordinate);
761
 
762
                if ($cell !== null && isset($autoSizes[$this->cellCollection->getCurrentColumn()])) {
763
                    //Determine if cell is in merge range
764
                    $isMerged = isset($isMergeCell[$this->cellCollection->getCurrentCoordinate()]);
765
 
766
                    //By default merged cells should be ignored
767
                    $isMergedButProceed = false;
768
 
769
                    //The only exception is if it's a merge range value cell of a 'vertical' range (1 column wide)
770
                    if ($isMerged && $cell->isMergeRangeValueCell()) {
771
                        $range = (string) $cell->getMergeRange();
772
                        $rangeBoundaries = Coordinate::rangeDimension($range);
773
                        if ($rangeBoundaries[0] === 1) {
774
                            $isMergedButProceed = true;
775
                        }
776
                    }
777
 
778
                    // Determine width if cell is not part of a merge or does and is a value cell of 1-column wide range
779
                    if (!$isMerged || $isMergedButProceed) {
780
                        // Determine if we need to make an adjustment for the first row in an AutoFilter range that
781
                        //    has a column filter dropdown
782
                        $filterAdjustment = false;
783
                        if (!empty($autoFilterIndentRanges)) {
784
                            foreach ($autoFilterIndentRanges as $autoFilterFirstRowRange) {
785
                                if ($cell->isInRange($autoFilterFirstRowRange)) {
786
                                    $filterAdjustment = true;
787
 
788
                                    break;
789
                                }
790
                            }
791
                        }
792
 
793
                        $indentAdjustment = $cell->getStyle()->getAlignment()->getIndent();
794
                        $indentAdjustment += (int) ($cell->getStyle()->getAlignment()->getHorizontal() === Alignment::HORIZONTAL_CENTER);
795
 
796
                        // Calculated value
797
                        // To formatted string
798
                        $cellValue = NumberFormat::toFormattedString(
799
                            $cell->getCalculatedValue(),
800
                            (string) $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())
801
                                ->getNumberFormat()->getFormatCode()
802
                        );
803
 
804
                        if ($cellValue !== null && $cellValue !== '') {
805
                            $autoSizes[$this->cellCollection->getCurrentColumn()] = max(
806
                                $autoSizes[$this->cellCollection->getCurrentColumn()],
807
                                round(
808
                                    Shared\Font::calculateColumnWidth(
809
                                        $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont(),
810
                                        $cellValue,
811
                                        (int) $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())
812
                                            ->getAlignment()->getTextRotation(),
813
                                        $this->getParentOrThrow()->getDefaultStyle()->getFont(),
814
                                        $filterAdjustment,
815
                                        $indentAdjustment
816
                                    ),
817
                                    3
818
                                )
819
                            );
820
                        }
821
                    }
822
                }
823
            }
824
 
825
            // adjust column widths
826
            foreach ($autoSizes as $columnIndex => $width) {
827
                if ($width == -1) {
828
                    $width = $this->getDefaultColumnDimension()->getWidth();
829
                }
830
                $this->getColumnDimension($columnIndex)->setWidth($width);
831
            }
832
        }
833
 
834
        return $this;
835
    }
836
 
837
    /**
838
     * Get parent or null.
839
     */
840
    public function getParent(): ?Spreadsheet
841
    {
842
        return $this->parent;
843
    }
844
 
845
    /**
846
     * Get parent, throw exception if null.
847
     */
848
    public function getParentOrThrow(): Spreadsheet
849
    {
850
        if ($this->parent !== null) {
851
            return $this->parent;
852
        }
853
 
854
        throw new Exception('Sheet does not have a parent.');
855
    }
856
 
857
    /**
858
     * Re-bind parent.
859
     *
860
     * @return $this
861
     */
862
    public function rebindParent(Spreadsheet $parent)
863
    {
864
        if ($this->parent !== null) {
865
            $definedNames = $this->parent->getDefinedNames();
866
            foreach ($definedNames as $definedName) {
867
                $parent->addDefinedName($definedName);
868
            }
869
 
870
            $this->parent->removeSheetByIndex(
871
                $this->parent->getIndex($this)
872
            );
873
        }
874
        $this->parent = $parent;
875
 
876
        return $this;
877
    }
878
 
879
    /**
880
     * Get title.
881
     *
882
     * @return string
883
     */
884
    public function getTitle()
885
    {
886
        return $this->title;
887
    }
888
 
889
    /**
890
     * Set title.
891
     *
892
     * @param string $title String containing the dimension of this worksheet
893
     * @param bool $updateFormulaCellReferences Flag indicating whether cell references in formulae should
894
     *            be updated to reflect the new sheet name.
895
     *          This should be left as the default true, unless you are
896
     *          certain that no formula cells on any worksheet contain
897
     *          references to this worksheet
898
     * @param bool $validate False to skip validation of new title. WARNING: This should only be set
899
     *                       at parse time (by Readers), where titles can be assumed to be valid.
900
     *
901
     * @return $this
902
     */
903
    public function setTitle($title, $updateFormulaCellReferences = true, $validate = true)
904
    {
905
        // Is this a 'rename' or not?
906
        if ($this->getTitle() == $title) {
907
            return $this;
908
        }
909
 
910
        // Old title
911
        $oldTitle = $this->getTitle();
912
 
913
        if ($validate) {
914
            // Syntax check
915
            self::checkSheetTitle($title);
916
 
917
            if ($this->parent) {
918
                // Is there already such sheet name?
919
                if ($this->parent->sheetNameExists($title)) {
920
                    // Use name, but append with lowest possible integer
921
 
922
                    if (Shared\StringHelper::countCharacters($title) > 29) {
923
                        $title = Shared\StringHelper::substring($title, 0, 29);
924
                    }
925
                    $i = 1;
926
                    while ($this->parent->sheetNameExists($title . ' ' . $i)) {
927
                        ++$i;
928
                        if ($i == 10) {
929
                            if (Shared\StringHelper::countCharacters($title) > 28) {
930
                                $title = Shared\StringHelper::substring($title, 0, 28);
931
                            }
932
                        } elseif ($i == 100) {
933
                            if (Shared\StringHelper::countCharacters($title) > 27) {
934
                                $title = Shared\StringHelper::substring($title, 0, 27);
935
                            }
936
                        }
937
                    }
938
 
939
                    $title .= " $i";
940
                }
941
            }
942
        }
943
 
944
        // Set title
945
        $this->title = $title;
946
        $this->dirty = true;
947
 
948
        if ($this->parent && $this->parent->getCalculationEngine()) {
949
            // New title
950
            $newTitle = $this->getTitle();
951
            $this->parent->getCalculationEngine()
952
                ->renameCalculationCacheForWorksheet($oldTitle, $newTitle);
953
            if ($updateFormulaCellReferences) {
954
                ReferenceHelper::getInstance()->updateNamedFormulae($this->parent, $oldTitle, $newTitle);
955
            }
956
        }
957
 
958
        return $this;
959
    }
960
 
961
    /**
962
     * Get sheet state.
963
     *
964
     * @return string Sheet state (visible, hidden, veryHidden)
965
     */
966
    public function getSheetState()
967
    {
968
        return $this->sheetState;
969
    }
970
 
971
    /**
972
     * Set sheet state.
973
     *
974
     * @param string $value Sheet state (visible, hidden, veryHidden)
975
     *
976
     * @return $this
977
     */
978
    public function setSheetState($value)
979
    {
980
        $this->sheetState = $value;
981
 
982
        return $this;
983
    }
984
 
985
    /**
986
     * Get page setup.
987
     *
988
     * @return PageSetup
989
     */
990
    public function getPageSetup()
991
    {
992
        return $this->pageSetup;
993
    }
994
 
995
    /**
996
     * Set page setup.
997
     *
998
     * @return $this
999
     */
1000
    public function setPageSetup(PageSetup $pageSetup)
1001
    {
1002
        $this->pageSetup = $pageSetup;
1003
 
1004
        return $this;
1005
    }
1006
 
1007
    /**
1008
     * Get page margins.
1009
     *
1010
     * @return PageMargins
1011
     */
1012
    public function getPageMargins()
1013
    {
1014
        return $this->pageMargins;
1015
    }
1016
 
1017
    /**
1018
     * Set page margins.
1019
     *
1020
     * @return $this
1021
     */
1022
    public function setPageMargins(PageMargins $pageMargins)
1023
    {
1024
        $this->pageMargins = $pageMargins;
1025
 
1026
        return $this;
1027
    }
1028
 
1029
    /**
1030
     * Get page header/footer.
1031
     *
1032
     * @return HeaderFooter
1033
     */
1034
    public function getHeaderFooter()
1035
    {
1036
        return $this->headerFooter;
1037
    }
1038
 
1039
    /**
1040
     * Set page header/footer.
1041
     *
1042
     * @return $this
1043
     */
1044
    public function setHeaderFooter(HeaderFooter $headerFooter)
1045
    {
1046
        $this->headerFooter = $headerFooter;
1047
 
1048
        return $this;
1049
    }
1050
 
1051
    /**
1052
     * Get sheet view.
1053
     *
1054
     * @return SheetView
1055
     */
1056
    public function getSheetView()
1057
    {
1058
        return $this->sheetView;
1059
    }
1060
 
1061
    /**
1062
     * Set sheet view.
1063
     *
1064
     * @return $this
1065
     */
1066
    public function setSheetView(SheetView $sheetView)
1067
    {
1068
        $this->sheetView = $sheetView;
1069
 
1070
        return $this;
1071
    }
1072
 
1073
    /**
1074
     * Get Protection.
1075
     *
1076
     * @return Protection
1077
     */
1078
    public function getProtection()
1079
    {
1080
        return $this->protection;
1081
    }
1082
 
1083
    /**
1084
     * Set Protection.
1085
     *
1086
     * @return $this
1087
     */
1088
    public function setProtection(Protection $protection)
1089
    {
1090
        $this->protection = $protection;
1091
        $this->dirty = true;
1092
 
1093
        return $this;
1094
    }
1095
 
1096
    /**
1097
     * Get highest worksheet column.
1098
     *
1099
     * @param null|int|string $row Return the data highest column for the specified row,
1100
     *                                     or the highest column of any row if no row number is passed
1101
     *
1102
     * @return string Highest column name
1103
     */
1104
    public function getHighestColumn($row = null)
1105
    {
1106
        if ($row === null) {
1107
            return Coordinate::stringFromColumnIndex($this->cachedHighestColumn);
1108
        }
1109
 
1110
        return $this->getHighestDataColumn($row);
1111
    }
1112
 
1113
    /**
1114
     * Get highest worksheet column that contains data.
1115
     *
1116
     * @param null|int|string $row Return the highest data column for the specified row,
1117
     *                                     or the highest data column of any row if no row number is passed
1118
     *
1119
     * @return string Highest column name that contains data
1120
     */
1121
    public function getHighestDataColumn($row = null)
1122
    {
1123
        return $this->cellCollection->getHighestColumn($row);
1124
    }
1125
 
1126
    /**
1127
     * Get highest worksheet row.
1128
     *
1129
     * @param null|string $column Return the highest data row for the specified column,
1130
     *                                     or the highest row of any column if no column letter is passed
1131
     *
1132
     * @return int Highest row number
1133
     */
1134
    public function getHighestRow($column = null)
1135
    {
1136
        if ($column === null) {
1137
            return $this->cachedHighestRow;
1138
        }
1139
 
1140
        return $this->getHighestDataRow($column);
1141
    }
1142
 
1143
    /**
1144
     * Get highest worksheet row that contains data.
1145
     *
1146
     * @param null|string $column Return the highest data row for the specified column,
1147
     *                                     or the highest data row of any column if no column letter is passed
1148
     *
1149
     * @return int Highest row number that contains data
1150
     */
1151
    public function getHighestDataRow($column = null)
1152
    {
1153
        return $this->cellCollection->getHighestRow($column);
1154
    }
1155
 
1156
    /**
1157
     * Get highest worksheet column and highest row that have cell records.
1158
     *
1159
     * @return array Highest column name and highest row number
1160
     */
1161
    public function getHighestRowAndColumn()
1162
    {
1163
        return $this->cellCollection->getHighestRowAndColumn();
1164
    }
1165
 
1166
    /**
1167
     * Set a cell value.
1168
     *
1169
     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1170
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1171
     * @param mixed $value Value for the cell
1172
     * @param null|IValueBinder $binder Value Binder to override the currently set Value Binder
1173
     *
1174
     * @return $this
1175
     */
1176
    public function setCellValue($coordinate, $value, ?IValueBinder $binder = null)
1177
    {
1178
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
1179
        $this->getCell($cellAddress)->setValue($value, $binder);
1180
 
1181
        return $this;
1182
    }
1183
 
1184
    /**
1185
     * Set a cell value by using numeric cell coordinates.
1186
     *
1187
     * @deprecated 1.23.0
1188
     *      Use the setCellValue() method with a cell address such as 'C5' instead;,
1189
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1190
     * @see Worksheet::setCellValue()
1191
     *
1192
     * @param int $columnIndex Numeric column coordinate of the cell
1193
     * @param int $row Numeric row coordinate of the cell
1194
     * @param mixed $value Value of the cell
1195
     * @param null|IValueBinder $binder Value Binder to override the currently set Value Binder
1196
     *
1197
     * @return $this
1198
     */
1199
    public function setCellValueByColumnAndRow($columnIndex, $row, $value, ?IValueBinder $binder = null)
1200
    {
1201
        $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row)->setValue($value, $binder);
1202
 
1203
        return $this;
1204
    }
1205
 
1206
    /**
1207
     * Set a cell value.
1208
     *
1209
     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1210
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1211
     * @param mixed $value Value of the cell
1212
     * @param string $dataType Explicit data type, see DataType::TYPE_*
1213
     *        Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this
1214
     *             method, then it is your responsibility as an end-user developer to validate that the value and
1215
     *             the datatype match.
1216
     *       If you do mismatch value and datatpe, then the value you enter may be changed to match the datatype
1217
     *          that you specify.
1218
     *
1219
     * @see DataType
1220
     *
1221
     * @return $this
1222
     */
1223
    public function setCellValueExplicit($coordinate, $value, $dataType)
1224
    {
1225
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
1226
        $this->getCell($cellAddress)->setValueExplicit($value, $dataType);
1227
 
1228
        return $this;
1229
    }
1230
 
1231
    /**
1232
     * Set a cell value by using numeric cell coordinates.
1233
     *
1234
     * @deprecated 1.23.0
1235
     *      Use the setCellValueExplicit() method with a cell address such as 'C5' instead;,
1236
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1237
     * @see Worksheet::setCellValueExplicit()
1238
     *
1239
     * @param int $columnIndex Numeric column coordinate of the cell
1240
     * @param int $row Numeric row coordinate of the cell
1241
     * @param mixed $value Value of the cell
1242
     * @param string $dataType Explicit data type, see DataType::TYPE_*
1243
     *        Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this
1244
     *             method, then it is your responsibility as an end-user developer to validate that the value and
1245
     *             the datatype match.
1246
     *       If you do mismatch value and datatpe, then the value you enter may be changed to match the datatype
1247
     *          that you specify.
1248
     *
1249
     * @see DataType
1250
     *
1251
     * @return $this
1252
     */
1253
    public function setCellValueExplicitByColumnAndRow($columnIndex, $row, $value, $dataType)
1254
    {
1255
        $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row)->setValueExplicit($value, $dataType);
1256
 
1257
        return $this;
1258
    }
1259
 
1260
    /**
1261
     * Get cell at a specific coordinate.
1262
     *
1263
     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1264
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1265
     *
1266
     * @return Cell Cell that was found or created
1267
     *              WARNING: Because the cell collection can be cached to reduce memory, it only allows one
1268
     *              "active" cell at a time in memory. If you assign that cell to a variable, then select
1269
     *              another cell using getCell() or any of its variants, the newly selected cell becomes
1270
     *              the "active" cell, and any previous assignment becomes a disconnected reference because
1271
     *              the active cell has changed.
1272
     */
1273
    public function getCell($coordinate): Cell
1274
    {
1275
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
1276
 
1277
        // Shortcut for increased performance for the vast majority of simple cases
1278
        if ($this->cellCollection->has($cellAddress)) {
1279
            /** @var Cell $cell */
1280
            $cell = $this->cellCollection->get($cellAddress);
1281
 
1282
            return $cell;
1283
        }
1284
 
1285
        /** @var Worksheet $sheet */
1286
        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
1287
        $cell = $sheet->cellCollection->get($finalCoordinate);
1288
 
1289
        return $cell ?? $sheet->createNewCell($finalCoordinate);
1290
    }
1291
 
1292
    /**
1293
     * Get the correct Worksheet and coordinate from a coordinate that may
1294
     * contains reference to another sheet or a named range.
1295
     *
1296
     * @return array{0: Worksheet, 1: string}
1297
     */
1298
    private function getWorksheetAndCoordinate(string $coordinate): array
1299
    {
1300
        $sheet = null;
1301
        $finalCoordinate = null;
1302
 
1303
        // Worksheet reference?
1304
        if (strpos($coordinate, '!') !== false) {
1305
            $worksheetReference = self::extractSheetTitle($coordinate, true);
1306
 
1307
            $sheet = $this->getParentOrThrow()->getSheetByName($worksheetReference[0]);
1308
            $finalCoordinate = strtoupper($worksheetReference[1]);
1309
 
1310
            if ($sheet === null) {
1311
                throw new Exception('Sheet not found for name: ' . $worksheetReference[0]);
1312
            }
1313
        } elseif (
1314
            !preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $coordinate) &&
1315
            preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/iu', $coordinate)
1316
        ) {
1317
            // Named range?
1318
            $namedRange = $this->validateNamedRange($coordinate, true);
1319
            if ($namedRange !== null) {
1320
                $sheet = $namedRange->getWorksheet();
1321
                if ($sheet === null) {
1322
                    throw new Exception('Sheet not found for named range: ' . $namedRange->getName());
1323
                }
1324
 
1325
                /** @phpstan-ignore-next-line */
1326
                $cellCoordinate = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!');
1327
                $finalCoordinate = str_replace('$', '', $cellCoordinate);
1328
            }
1329
        }
1330
 
1331
        if ($sheet === null || $finalCoordinate === null) {
1332
            $sheet = $this;
1333
            $finalCoordinate = strtoupper($coordinate);
1334
        }
1335
 
1336
        if (Coordinate::coordinateIsRange($finalCoordinate)) {
1337
            throw new Exception('Cell coordinate string can not be a range of cells.');
1338
        } elseif (strpos($finalCoordinate, '$') !== false) {
1339
            throw new Exception('Cell coordinate must not be absolute.');
1340
        }
1341
 
1342
        return [$sheet, $finalCoordinate];
1343
    }
1344
 
1345
    /**
1346
     * Get an existing cell at a specific coordinate, or null.
1347
     *
1348
     * @param string $coordinate Coordinate of the cell, eg: 'A1'
1349
     *
1350
     * @return null|Cell Cell that was found or null
1351
     */
1352
    private function getCellOrNull($coordinate): ?Cell
1353
    {
1354
        // Check cell collection
1355
        if ($this->cellCollection->has($coordinate)) {
1356
            return $this->cellCollection->get($coordinate);
1357
        }
1358
 
1359
        return null;
1360
    }
1361
 
1362
    /**
1363
     * Get cell at a specific coordinate by using numeric cell coordinates.
1364
     *
1365
     * @deprecated 1.23.0
1366
     *      Use the getCell() method with a cell address such as 'C5' instead;,
1367
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1368
     * @see Worksheet::getCell()
1369
     *
1370
     * @param int $columnIndex Numeric column coordinate of the cell
1371
     * @param int $row Numeric row coordinate of the cell
1372
     *
1373
     * @return Cell Cell that was found/created or null
1374
     *              WARNING: Because the cell collection can be cached to reduce memory, it only allows one
1375
     *              "active" cell at a time in memory. If you assign that cell to a variable, then select
1376
     *              another cell using getCell() or any of its variants, the newly selected cell becomes
1377
     *              the "active" cell, and any previous assignment becomes a disconnected reference because
1378
     *              the active cell has changed.
1379
     */
1380
    public function getCellByColumnAndRow($columnIndex, $row): Cell
1381
    {
1382
        return $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row);
1383
    }
1384
 
1385
    /**
1386
     * Create a new cell at the specified coordinate.
1387
     *
1388
     * @param string $coordinate Coordinate of the cell
1389
     *
1390
     * @return Cell Cell that was created
1391
     *              WARNING: Because the cell collection can be cached to reduce memory, it only allows one
1392
     *              "active" cell at a time in memory. If you assign that cell to a variable, then select
1393
     *              another cell using getCell() or any of its variants, the newly selected cell becomes
1394
     *              the "active" cell, and any previous assignment becomes a disconnected reference because
1395
     *              the active cell has changed.
1396
     */
1397
    public function createNewCell($coordinate): Cell
1398
    {
1399
        [$column, $row, $columnString] = Coordinate::indexesFromString($coordinate);
1400
        $cell = new Cell(null, DataType::TYPE_NULL, $this);
1401
        $this->cellCollection->add($coordinate, $cell);
1402
 
1403
        // Coordinates
1404
        if ($column > $this->cachedHighestColumn) {
1405
            $this->cachedHighestColumn = $column;
1406
        }
1407
        if ($row > $this->cachedHighestRow) {
1408
            $this->cachedHighestRow = $row;
1409
        }
1410
 
1411
        // Cell needs appropriate xfIndex from dimensions records
1412
        //    but don't create dimension records if they don't already exist
1413
        $rowDimension = $this->rowDimensions[$row] ?? null;
1414
        $columnDimension = $this->columnDimensions[$columnString] ?? null;
1415
 
1416
        if ($rowDimension !== null) {
1417
            $rowXf = (int) $rowDimension->getXfIndex();
1418
            if ($rowXf > 0) {
1419
                // then there is a row dimension with explicit style, assign it to the cell
1420
                $cell->setXfIndex($rowXf);
1421
            }
1422
        } elseif ($columnDimension !== null) {
1423
            $colXf = (int) $columnDimension->getXfIndex();
1424
            if ($colXf > 0) {
1425
                // then there is a column dimension, assign it to the cell
1426
                $cell->setXfIndex($colXf);
1427
            }
1428
        }
1429
 
1430
        return $cell;
1431
    }
1432
 
1433
    /**
1434
     * Does the cell at a specific coordinate exist?
1435
     *
1436
     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1437
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1438
     */
1439
    public function cellExists($coordinate): bool
1440
    {
1441
        $cellAddress = Validations::validateCellAddress($coordinate);
1442
        /** @var Worksheet $sheet */
1443
        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
1444
 
1445
        return $sheet->cellCollection->has($finalCoordinate);
1446
    }
1447
 
1448
    /**
1449
     * Cell at a specific coordinate by using numeric cell coordinates exists?
1450
     *
1451
     * @deprecated 1.23.0
1452
     *      Use the cellExists() method with a cell address such as 'C5' instead;,
1453
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1454
     * @see Worksheet::cellExists()
1455
     *
1456
     * @param int $columnIndex Numeric column coordinate of the cell
1457
     * @param int $row Numeric row coordinate of the cell
1458
     */
1459
    public function cellExistsByColumnAndRow($columnIndex, $row): bool
1460
    {
1461
        return $this->cellExists(Coordinate::stringFromColumnIndex($columnIndex) . $row);
1462
    }
1463
 
1464
    /**
1465
     * Get row dimension at a specific row.
1466
     *
1467
     * @param int $row Numeric index of the row
1468
     */
1469
    public function getRowDimension(int $row): RowDimension
1470
    {
1471
        // Get row dimension
1472
        if (!isset($this->rowDimensions[$row])) {
1473
            $this->rowDimensions[$row] = new RowDimension($row);
1474
 
1475
            $this->cachedHighestRow = max($this->cachedHighestRow, $row);
1476
        }
1477
 
1478
        return $this->rowDimensions[$row];
1479
    }
1480
 
1481
    public function rowDimensionExists(int $row): bool
1482
    {
1483
        return isset($this->rowDimensions[$row]);
1484
    }
1485
 
1486
    /**
1487
     * Get column dimension at a specific column.
1488
     *
1489
     * @param string $column String index of the column eg: 'A'
1490
     */
1491
    public function getColumnDimension(string $column): ColumnDimension
1492
    {
1493
        // Uppercase coordinate
1494
        $column = strtoupper($column);
1495
 
1496
        // Fetch dimensions
1497
        if (!isset($this->columnDimensions[$column])) {
1498
            $this->columnDimensions[$column] = new ColumnDimension($column);
1499
 
1500
            $columnIndex = Coordinate::columnIndexFromString($column);
1501
            if ($this->cachedHighestColumn < $columnIndex) {
1502
                $this->cachedHighestColumn = $columnIndex;
1503
            }
1504
        }
1505
 
1506
        return $this->columnDimensions[$column];
1507
    }
1508
 
1509
    /**
1510
     * Get column dimension at a specific column by using numeric cell coordinates.
1511
     *
1512
     * @param int $columnIndex Numeric column coordinate of the cell
1513
     */
1514
    public function getColumnDimensionByColumn(int $columnIndex): ColumnDimension
1515
    {
1516
        return $this->getColumnDimension(Coordinate::stringFromColumnIndex($columnIndex));
1517
    }
1518
 
1519
    /**
1520
     * Get styles.
1521
     *
1522
     * @return Style[]
1523
     */
1524
    public function getStyles()
1525
    {
1526
        return $this->styles;
1527
    }
1528
 
1529
    /**
1530
     * Get style for cell.
1531
     *
1532
     * @param AddressRange|array<int>|CellAddress|int|string $cellCoordinate
1533
     *              A simple string containing a cell address like 'A1' or a cell range like 'A1:E10'
1534
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1535
     *              or a CellAddress or AddressRange object.
1536
     */
1537
    public function getStyle($cellCoordinate): Style
1538
    {
1539
        $cellCoordinate = Validations::validateCellOrCellRange($cellCoordinate);
1540
 
1541
        // set this sheet as active
1542
        $this->getParentOrThrow()->setActiveSheetIndex($this->getParentOrThrow()->getIndex($this));
1543
 
1544
        // set cell coordinate as active
1545
        $this->setSelectedCells($cellCoordinate);
1546
 
1547
        return $this->getParentOrThrow()->getCellXfSupervisor();
1548
    }
1549
 
1550
    /**
1551
     * Get style for cell by using numeric cell coordinates.
1552
     *
1553
     * @deprecated 1.23.0
1554
     *      Use the getStyle() method with a cell address range such as 'C5:F8' instead;,
1555
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1556
     *          or an AddressRange object.
1557
     * @see Worksheet::getStyle()
1558
     *
1559
     * @param int $columnIndex1 Numeric column coordinate of the cell
1560
     * @param int $row1 Numeric row coordinate of the cell
1561
     * @param null|int $columnIndex2 Numeric column coordinate of the range cell
1562
     * @param null|int $row2 Numeric row coordinate of the range cell
1563
     *
1564
     * @return Style
1565
     */
1566
    public function getStyleByColumnAndRow($columnIndex1, $row1, $columnIndex2 = null, $row2 = null)
1567
    {
1568
        if ($columnIndex2 !== null && $row2 !== null) {
1569
            $cellRange = new CellRange(
1570
                CellAddress::fromColumnAndRow($columnIndex1, $row1),
1571
                CellAddress::fromColumnAndRow($columnIndex2, $row2)
1572
            );
1573
 
1574
            return $this->getStyle($cellRange);
1575
        }
1576
 
1577
        return $this->getStyle(CellAddress::fromColumnAndRow($columnIndex1, $row1));
1578
    }
1579
 
1580
    /**
1581
     * Get conditional styles for a cell.
1582
     *
1583
     * @param string $coordinate eg: 'A1' or 'A1:A3'.
1584
     *          If a single cell is referenced, then the array of conditional styles will be returned if the cell is
1585
     *               included in a conditional style range.
1586
     *          If a range of cells is specified, then the styles will only be returned if the range matches the entire
1587
     *               range of the conditional.
1588
     *
1589
     * @return Conditional[]
1590
     */
1591
    public function getConditionalStyles(string $coordinate): array
1592
    {
1593
        $coordinate = strtoupper($coordinate);
1594
        if (strpos($coordinate, ':') !== false) {
1595
            return $this->conditionalStylesCollection[$coordinate] ?? [];
1596
        }
1597
 
1598
        $cell = $this->getCell($coordinate);
1599
        foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
1600
            if ($cell->isInRange($conditionalRange)) {
1601
                return $this->conditionalStylesCollection[$conditionalRange];
1602
            }
1603
        }
1604
 
1605
        return [];
1606
    }
1607
 
1608
    public function getConditionalRange(string $coordinate): ?string
1609
    {
1610
        $coordinate = strtoupper($coordinate);
1611
        $cell = $this->getCell($coordinate);
1612
        foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
1613
            if ($cell->isInRange($conditionalRange)) {
1614
                return $conditionalRange;
1615
            }
1616
        }
1617
 
1618
        return null;
1619
    }
1620
 
1621
    /**
1622
     * Do conditional styles exist for this cell?
1623
     *
1624
     * @param string $coordinate eg: 'A1' or 'A1:A3'.
1625
     *          If a single cell is specified, then this method will return true if that cell is included in a
1626
     *               conditional style range.
1627
     *          If a range of cells is specified, then true will only be returned if the range matches the entire
1628
     *               range of the conditional.
1629
     */
1630
    public function conditionalStylesExists($coordinate): bool
1631
    {
1632
        $coordinate = strtoupper($coordinate);
1633
        if (strpos($coordinate, ':') !== false) {
1634
            return isset($this->conditionalStylesCollection[$coordinate]);
1635
        }
1636
 
1637
        $cell = $this->getCell($coordinate);
1638
        foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
1639
            if ($cell->isInRange($conditionalRange)) {
1640
                return true;
1641
            }
1642
        }
1643
 
1644
        return false;
1645
    }
1646
 
1647
    /**
1648
     * Removes conditional styles for a cell.
1649
     *
1650
     * @param string $coordinate eg: 'A1'
1651
     *
1652
     * @return $this
1653
     */
1654
    public function removeConditionalStyles($coordinate)
1655
    {
1656
        unset($this->conditionalStylesCollection[strtoupper($coordinate)]);
1657
 
1658
        return $this;
1659
    }
1660
 
1661
    /**
1662
     * Get collection of conditional styles.
1663
     *
1664
     * @return array
1665
     */
1666
    public function getConditionalStylesCollection()
1667
    {
1668
        return $this->conditionalStylesCollection;
1669
    }
1670
 
1671
    /**
1672
     * Set conditional styles.
1673
     *
1674
     * @param string $coordinate eg: 'A1'
1675
     * @param Conditional[] $styles
1676
     *
1677
     * @return $this
1678
     */
1679
    public function setConditionalStyles($coordinate, $styles)
1680
    {
1681
        $this->conditionalStylesCollection[strtoupper($coordinate)] = $styles;
1682
 
1683
        return $this;
1684
    }
1685
 
1686
    /**
1687
     * Duplicate cell style to a range of cells.
1688
     *
1689
     * Please note that this will overwrite existing cell styles for cells in range!
1690
     *
1691
     * @param Style $style Cell style to duplicate
1692
     * @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
1693
     *
1694
     * @return $this
1695
     */
1696
    public function duplicateStyle(Style $style, $range)
1697
    {
1698
        // Add the style to the workbook if necessary
1699
        $workbook = $this->getParentOrThrow();
1700
        if ($existingStyle = $workbook->getCellXfByHashCode($style->getHashCode())) {
1701
            // there is already such cell Xf in our collection
1702
            $xfIndex = $existingStyle->getIndex();
1703
        } else {
1704
            // we don't have such a cell Xf, need to add
1705
            $workbook->addCellXf($style);
1706
            $xfIndex = $style->getIndex();
1707
        }
1708
 
1709
        // Calculate range outer borders
1710
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
1711
 
1712
        // Make sure we can loop upwards on rows and columns
1713
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
1714
            $tmp = $rangeStart;
1715
            $rangeStart = $rangeEnd;
1716
            $rangeEnd = $tmp;
1717
        }
1718
 
1719
        // Loop through cells and apply styles
1720
        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
1721
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
1722
                $this->getCell(Coordinate::stringFromColumnIndex($col) . $row)->setXfIndex($xfIndex);
1723
            }
1724
        }
1725
 
1726
        return $this;
1727
    }
1728
 
1729
    /**
1730
     * Duplicate conditional style to a range of cells.
1731
     *
1732
     * Please note that this will overwrite existing cell styles for cells in range!
1733
     *
1734
     * @param Conditional[] $styles Cell style to duplicate
1735
     * @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
1736
     *
1737
     * @return $this
1738
     */
1739
    public function duplicateConditionalStyle(array $styles, $range = '')
1740
    {
1741
        foreach ($styles as $cellStyle) {
1742
            if (!($cellStyle instanceof Conditional)) {
1743
                throw new Exception('Style is not a conditional style');
1744
            }
1745
        }
1746
 
1747
        // Calculate range outer borders
1748
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
1749
 
1750
        // Make sure we can loop upwards on rows and columns
1751
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
1752
            $tmp = $rangeStart;
1753
            $rangeStart = $rangeEnd;
1754
            $rangeEnd = $tmp;
1755
        }
1756
 
1757
        // Loop through cells and apply styles
1758
        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
1759
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
1760
                $this->setConditionalStyles(Coordinate::stringFromColumnIndex($col) . $row, $styles);
1761
            }
1762
        }
1763
 
1764
        return $this;
1765
    }
1766
 
1767
    /**
1768
     * Set break on a cell.
1769
     *
1770
     * @param array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1771
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1772
     * @param int $break Break type (type of Worksheet::BREAK_*)
1773
     *
1774
     * @return $this
1775
     */
1776
    public function setBreak($coordinate, $break, int $max = -1)
1777
    {
1778
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
1779
 
1780
        if ($break === self::BREAK_NONE) {
1781
            unset($this->rowBreaks[$cellAddress], $this->columnBreaks[$cellAddress]);
1782
        } elseif ($break === self::BREAK_ROW) {
1783
            $this->rowBreaks[$cellAddress] = new PageBreak($break, $cellAddress, $max);
1784
        } elseif ($break === self::BREAK_COLUMN) {
1785
            $this->columnBreaks[$cellAddress] = new PageBreak($break, $cellAddress, $max);
1786
        }
1787
 
1788
        return $this;
1789
    }
1790
 
1791
    /**
1792
     * Set break on a cell by using numeric cell coordinates.
1793
     *
1794
     * @deprecated 1.23.0
1795
     *      Use the setBreak() method with a cell address such as 'C5' instead;,
1796
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1797
     * @see Worksheet::setBreak()
1798
     *
1799
     * @param int $columnIndex Numeric column coordinate of the cell
1800
     * @param int $row Numeric row coordinate of the cell
1801
     * @param int $break Break type (type of Worksheet::BREAK_*)
1802
     *
1803
     * @return $this
1804
     */
1805
    public function setBreakByColumnAndRow($columnIndex, $row, $break)
1806
    {
1807
        return $this->setBreak(Coordinate::stringFromColumnIndex($columnIndex) . $row, $break);
1808
    }
1809
 
1810
    /**
1811
     * Get breaks.
1812
     *
1813
     * @return int[]
1814
     */
1815
    public function getBreaks()
1816
    {
1817
        $breaks = [];
1818
        /** @var callable */
1819
        $compareFunction = [self::class, 'compareRowBreaks'];
1820
        uksort($this->rowBreaks, $compareFunction);
1821
        foreach ($this->rowBreaks as $break) {
1822
            $breaks[$break->getCoordinate()] = self::BREAK_ROW;
1823
        }
1824
        /** @var callable */
1825
        $compareFunction = [self::class, 'compareColumnBreaks'];
1826
        uksort($this->columnBreaks, $compareFunction);
1827
        foreach ($this->columnBreaks as $break) {
1828
            $breaks[$break->getCoordinate()] = self::BREAK_COLUMN;
1829
        }
1830
 
1831
        return $breaks;
1832
    }
1833
 
1834
    /**
1835
     * Get row breaks.
1836
     *
1837
     * @return PageBreak[]
1838
     */
1839
    public function getRowBreaks()
1840
    {
1841
        /** @var callable */
1842
        $compareFunction = [self::class, 'compareRowBreaks'];
1843
        uksort($this->rowBreaks, $compareFunction);
1844
 
1845
        return $this->rowBreaks;
1846
    }
1847
 
1848
    protected static function compareRowBreaks(string $coordinate1, string $coordinate2): int
1849
    {
1850
        $row1 = Coordinate::indexesFromString($coordinate1)[1];
1851
        $row2 = Coordinate::indexesFromString($coordinate2)[1];
1852
 
1853
        return $row1 - $row2;
1854
    }
1855
 
1856
    protected static function compareColumnBreaks(string $coordinate1, string $coordinate2): int
1857
    {
1858
        $column1 = Coordinate::indexesFromString($coordinate1)[0];
1859
        $column2 = Coordinate::indexesFromString($coordinate2)[0];
1860
 
1861
        return $column1 - $column2;
1862
    }
1863
 
1864
    /**
1865
     * Get column breaks.
1866
     *
1867
     * @return PageBreak[]
1868
     */
1869
    public function getColumnBreaks()
1870
    {
1871
        /** @var callable */
1872
        $compareFunction = [self::class, 'compareColumnBreaks'];
1873
        uksort($this->columnBreaks, $compareFunction);
1874
 
1875
        return $this->columnBreaks;
1876
    }
1877
 
1878
    /**
1879
     * Set merge on a cell range.
1880
     *
1881
     * @param AddressRange|array<int>|string $range A simple string containing a Cell range like 'A1:E10'
1882
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1883
     *              or an AddressRange.
1884
     * @param string $behaviour How the merged cells should behave.
1885
     *               Possible values are:
1886
     *                   MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells
1887
     *                   MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells
1888
     *                   MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell
1889
     *
1890
     * @return $this
1891
     */
1892
    public function mergeCells($range, $behaviour = self::MERGE_CELL_CONTENT_EMPTY)
1893
    {
1894
        $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range));
1895
 
1896
        if (strpos($range, ':') === false) {
1897
            $range .= ":{$range}";
1898
        }
1899
 
1900
        if (preg_match('/^([A-Z]+)(\\d+):([A-Z]+)(\\d+)$/', $range, $matches) !== 1) {
1901
            throw new Exception('Merge must be on a valid range of cells.');
1902
        }
1903
 
1904
        $this->mergeCells[$range] = $range;
1905
        $firstRow = (int) $matches[2];
1906
        $lastRow = (int) $matches[4];
1907
        $firstColumn = $matches[1];
1908
        $lastColumn = $matches[3];
1909
        $firstColumnIndex = Coordinate::columnIndexFromString($firstColumn);
1910
        $lastColumnIndex = Coordinate::columnIndexFromString($lastColumn);
1911
        $numberRows = $lastRow - $firstRow;
1912
        $numberColumns = $lastColumnIndex - $firstColumnIndex;
1913
 
1914
        if ($numberRows === 1 && $numberColumns === 1) {
1915
            return $this;
1916
        }
1917
 
1918
        // create upper left cell if it does not already exist
1919
        $upperLeft = "{$firstColumn}{$firstRow}";
1920
        if (!$this->cellExists($upperLeft)) {
1921
            $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL);
1922
        }
1923
 
1924
        if ($behaviour !== self::MERGE_CELL_CONTENT_HIDE) {
1925
            // Blank out the rest of the cells in the range (if they exist)
1926
            if ($numberRows > $numberColumns) {
1927
                $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft, $behaviour);
1928
            } else {
1929
                $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft, $behaviour);
1930
            }
1931
        }
1932
 
1933
        return $this;
1934
    }
1935
 
1936
    private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
1937
    {
1938
        $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
1939
            ? [$this->getCell($upperLeft)->getFormattedValue()]
1940
            : [];
1941
 
1942
        foreach ($this->getColumnIterator($firstColumn, $lastColumn) as $column) {
1943
            $iterator = $column->getCellIterator($firstRow);
1944
            $iterator->setIterateOnlyExistingCells(true);
1945
            foreach ($iterator as $cell) {
1946
                if ($cell !== null) {
1947
                    $row = $cell->getRow();
1948
                    if ($row > $lastRow) {
1949
                        break;
1950
                    }
1951
                    $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue);
1952
                }
1953
            }
1954
        }
1955
 
1956
        if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
1957
            $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
1958
        }
1959
    }
1960
 
1961
    private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
1962
    {
1963
        $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
1964
            ? [$this->getCell($upperLeft)->getFormattedValue()]
1965
            : [];
1966
 
1967
        foreach ($this->getRowIterator($firstRow, $lastRow) as $row) {
1968
            $iterator = $row->getCellIterator($firstColumn);
1969
            $iterator->setIterateOnlyExistingCells(true);
1970
            foreach ($iterator as $cell) {
1971
                if ($cell !== null) {
1972
                    $column = $cell->getColumn();
1973
                    $columnIndex = Coordinate::columnIndexFromString($column);
1974
                    if ($columnIndex > $lastColumnIndex) {
1975
                        break;
1976
                    }
1977
                    $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue);
1978
                }
1979
            }
1980
        }
1981
 
1982
        if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
1983
            $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
1984
        }
1985
    }
1986
 
1987
    public function mergeCellBehaviour(Cell $cell, string $upperLeft, string $behaviour, array $leftCellValue): array
1988
    {
1989
        if ($cell->getCoordinate() !== $upperLeft) {
1990
            Calculation::getInstance($cell->getWorksheet()->getParentOrThrow())->flushInstance();
1991
            if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
1992
                $cellValue = $cell->getFormattedValue();
1993
                if ($cellValue !== '') {
1994
                    $leftCellValue[] = $cellValue;
1995
                }
1996
            }
1997
            $cell->setValueExplicit(null, DataType::TYPE_NULL);
1998
        }
1999
 
2000
        return $leftCellValue;
2001
    }
2002
 
2003
    /**
2004
     * Set merge on a cell range by using numeric cell coordinates.
2005
     *
2006
     * @deprecated 1.23.0
2007
     *      Use the mergeCells() method with a cell address range such as 'C5:F8' instead;,
2008
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2009
     *          or an AddressRange object.
2010
     * @see Worksheet::mergeCells()
2011
     *
2012
     * @param int $columnIndex1 Numeric column coordinate of the first cell
2013
     * @param int $row1 Numeric row coordinate of the first cell
2014
     * @param int $columnIndex2 Numeric column coordinate of the last cell
2015
     * @param int $row2 Numeric row coordinate of the last cell
2016
     * @param string $behaviour How the merged cells should behave.
2017
     *               Possible values are:
2018
     *                   MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells
2019
     *                   MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells
2020
     *                   MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell
2021
     *
2022
     * @return $this
2023
     */
2024
    public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $behaviour = self::MERGE_CELL_CONTENT_EMPTY)
2025
    {
2026
        $cellRange = new CellRange(
2027
            CellAddress::fromColumnAndRow($columnIndex1, $row1),
2028
            CellAddress::fromColumnAndRow($columnIndex2, $row2)
2029
        );
2030
 
2031
        return $this->mergeCells($cellRange, $behaviour);
2032
    }
2033
 
2034
    /**
2035
     * Remove merge on a cell range.
2036
     *
2037
     * @param AddressRange|array<int>|string $range A simple string containing a Cell range like 'A1:E10'
2038
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2039
     *              or an AddressRange.
2040
     *
2041
     * @return $this
2042
     */
2043
    public function unmergeCells($range)
2044
    {
2045
        $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range));
2046
 
2047
        if (strpos($range, ':') !== false) {
2048
            if (isset($this->mergeCells[$range])) {
2049
                unset($this->mergeCells[$range]);
2050
            } else {
2051
                throw new Exception('Cell range ' . $range . ' not known as merged.');
2052
            }
2053
        } else {
2054
            throw new Exception('Merge can only be removed from a range of cells.');
2055
        }
2056
 
2057
        return $this;
2058
    }
2059
 
2060
    /**
2061
     * Remove merge on a cell range by using numeric cell coordinates.
2062
     *
2063
     * @deprecated 1.23.0
2064
     *      Use the unmergeCells() method with a cell address range such as 'C5:F8' instead;,
2065
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2066
     *          or an AddressRange object.
2067
     * @see Worksheet::unmergeCells()
2068
     *
2069
     * @param int $columnIndex1 Numeric column coordinate of the first cell
2070
     * @param int $row1 Numeric row coordinate of the first cell
2071
     * @param int $columnIndex2 Numeric column coordinate of the last cell
2072
     * @param int $row2 Numeric row coordinate of the last cell
2073
     *
2074
     * @return $this
2075
     */
2076
    public function unmergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
2077
    {
2078
        $cellRange = new CellRange(
2079
            CellAddress::fromColumnAndRow($columnIndex1, $row1),
2080
            CellAddress::fromColumnAndRow($columnIndex2, $row2)
2081
        );
2082
 
2083
        return $this->unmergeCells($cellRange);
2084
    }
2085
 
2086
    /**
2087
     * Get merge cells array.
2088
     *
2089
     * @return string[]
2090
     */
2091
    public function getMergeCells()
2092
    {
2093
        return $this->mergeCells;
2094
    }
2095
 
2096
    /**
2097
     * Set merge cells array for the entire sheet. Use instead mergeCells() to merge
2098
     * a single cell range.
2099
     *
2100
     * @param string[] $mergeCells
2101
     *
2102
     * @return $this
2103
     */
2104
    public function setMergeCells(array $mergeCells)
2105
    {
2106
        $this->mergeCells = $mergeCells;
2107
 
2108
        return $this;
2109
    }
2110
 
2111
    /**
2112
     * Set protection on a cell or cell range.
2113
     *
2114
     * @param AddressRange|array<int>|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10'
2115
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2116
     *              or a CellAddress or AddressRange object.
2117
     * @param string $password Password to unlock the protection
2118
     * @param bool $alreadyHashed If the password has already been hashed, set this to true
2119
     *
2120
     * @return $this
2121
     */
2122
    public function protectCells($range, $password, $alreadyHashed = false)
2123
    {
2124
        $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
2125
 
2126
        if (!$alreadyHashed) {
2127
            $password = Shared\PasswordHasher::hashPassword($password);
2128
        }
2129
        $this->protectedCells[$range] = $password;
2130
 
2131
        return $this;
2132
    }
2133
 
2134
    /**
2135
     * Set protection on a cell range by using numeric cell coordinates.
2136
     *
2137
     * @deprecated 1.23.0
2138
     *      Use the protectCells() method with a cell address range such as 'C5:F8' instead;,
2139
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2140
     *          or an AddressRange object.
2141
     * @see Worksheet::protectCells()
2142
     *
2143
     * @param int $columnIndex1 Numeric column coordinate of the first cell
2144
     * @param int $row1 Numeric row coordinate of the first cell
2145
     * @param int $columnIndex2 Numeric column coordinate of the last cell
2146
     * @param int $row2 Numeric row coordinate of the last cell
2147
     * @param string $password Password to unlock the protection
2148
     * @param bool $alreadyHashed If the password has already been hashed, set this to true
2149
     *
2150
     * @return $this
2151
     */
2152
    public function protectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $password, $alreadyHashed = false)
2153
    {
2154
        $cellRange = new CellRange(
2155
            CellAddress::fromColumnAndRow($columnIndex1, $row1),
2156
            CellAddress::fromColumnAndRow($columnIndex2, $row2)
2157
        );
2158
 
2159
        return $this->protectCells($cellRange, $password, $alreadyHashed);
2160
    }
2161
 
2162
    /**
2163
     * Remove protection on a cell or cell range.
2164
     *
2165
     * @param AddressRange|array<int>|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10'
2166
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2167
     *              or a CellAddress or AddressRange object.
2168
     *
2169
     * @return $this
2170
     */
2171
    public function unprotectCells($range)
2172
    {
2173
        $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
2174
 
2175
        if (isset($this->protectedCells[$range])) {
2176
            unset($this->protectedCells[$range]);
2177
        } else {
2178
            throw new Exception('Cell range ' . $range . ' not known as protected.');
2179
        }
2180
 
2181
        return $this;
2182
    }
2183
 
2184
    /**
2185
     * Remove protection on a cell range by using numeric cell coordinates.
2186
     *
2187
     * @deprecated 1.23.0
2188
     *      Use the unprotectCells() method with a cell address range such as 'C5:F8' instead;,
2189
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2190
     *          or an AddressRange object.
2191
     * @see Worksheet::unprotectCells()
2192
     *
2193
     * @param int $columnIndex1 Numeric column coordinate of the first cell
2194
     * @param int $row1 Numeric row coordinate of the first cell
2195
     * @param int $columnIndex2 Numeric column coordinate of the last cell
2196
     * @param int $row2 Numeric row coordinate of the last cell
2197
     *
2198
     * @return $this
2199
     */
2200
    public function unprotectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
2201
    {
2202
        $cellRange = new CellRange(
2203
            CellAddress::fromColumnAndRow($columnIndex1, $row1),
2204
            CellAddress::fromColumnAndRow($columnIndex2, $row2)
2205
        );
2206
 
2207
        return $this->unprotectCells($cellRange);
2208
    }
2209
 
2210
    /**
2211
     * Get protected cells.
2212
     *
2213
     * @return string[]
2214
     */
2215
    public function getProtectedCells()
2216
    {
2217
        return $this->protectedCells;
2218
    }
2219
 
2220
    /**
2221
     * Get Autofilter.
2222
     *
2223
     * @return AutoFilter
2224
     */
2225
    public function getAutoFilter()
2226
    {
2227
        return $this->autoFilter;
2228
    }
2229
 
2230
    /**
2231
     * Set AutoFilter.
2232
     *
2233
     * @param AddressRange|array<int>|AutoFilter|string $autoFilterOrRange
2234
     *            A simple string containing a Cell range like 'A1:E10' is permitted for backward compatibility
2235
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2236
     *              or an AddressRange.
2237
     *
2238
     * @return $this
2239
     */
2240
    public function setAutoFilter($autoFilterOrRange)
2241
    {
2242
        if (is_object($autoFilterOrRange) && ($autoFilterOrRange instanceof AutoFilter)) {
2243
            $this->autoFilter = $autoFilterOrRange;
2244
        } else {
2245
            $cellRange = Functions::trimSheetFromCellReference(Validations::validateCellRange($autoFilterOrRange));
2246
 
2247
            $this->autoFilter->setRange($cellRange);
2248
        }
2249
 
2250
        return $this;
2251
    }
2252
 
2253
    /**
2254
     * Set Autofilter Range by using numeric cell coordinates.
2255
     *
2256
     * @deprecated 1.23.0
2257
     *      Use the setAutoFilter() method with a cell address range such as 'C5:F8' instead;,
2258
     *          or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2259
     *          or an AddressRange object or AutoFilter object.
2260
     * @see Worksheet::setAutoFilter()
2261
     *
2262
     * @param int $columnIndex1 Numeric column coordinate of the first cell
2263
     * @param int $row1 Numeric row coordinate of the first cell
2264
     * @param int $columnIndex2 Numeric column coordinate of the second cell
2265
     * @param int $row2 Numeric row coordinate of the second cell
2266
     *
2267
     * @return $this
2268
     */
2269
    public function setAutoFilterByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2)
2270
    {
2271
        $cellRange = new CellRange(
2272
            CellAddress::fromColumnAndRow($columnIndex1, $row1),
2273
            CellAddress::fromColumnAndRow($columnIndex2, $row2)
2274
        );
2275
 
2276
        return $this->setAutoFilter($cellRange);
2277
    }
2278
 
2279
    /**
2280
     * Remove autofilter.
2281
     */
2282
    public function removeAutoFilter(): self
2283
    {
2284
        $this->autoFilter->setRange('');
2285
 
2286
        return $this;
2287
    }
2288
 
2289
    /**
2290
     * Get collection of Tables.
2291
     *
2292
     * @return ArrayObject<int, Table>
2293
     */
2294
    public function getTableCollection()
2295
    {
2296
        return $this->tableCollection;
2297
    }
2298
 
2299
    /**
2300
     * Add Table.
2301
     *
2302
     * @return $this
2303
     */
2304
    public function addTable(Table $table): self
2305
    {
2306
        $table->setWorksheet($this);
2307
        $this->tableCollection[] = $table;
2308
 
2309
        return $this;
2310
    }
2311
 
2312
    /**
2313
     * @return string[] array of Table names
2314
     */
2315
    public function getTableNames(): array
2316
    {
2317
        $tableNames = [];
2318
 
2319
        foreach ($this->tableCollection as $table) {
2320
            /** @var Table $table */
2321
            $tableNames[] = $table->getName();
2322
        }
2323
 
2324
        return $tableNames;
2325
    }
2326
 
2327
    /** @var null|Table */
2328
    private static $scrutinizerNullTable;
2329
 
2330
    /** @var null|int */
2331
    private static $scrutinizerNullInt;
2332
 
2333
    /**
2334
     * @param string $name the table name to search
2335
     *
2336
     * @return null|Table The table from the tables collection, or null if not found
2337
     */
2338
    public function getTableByName(string $name): ?Table
2339
    {
2340
        $tableIndex = $this->getTableIndexByName($name);
2341
 
2342
        return ($tableIndex === null) ? self::$scrutinizerNullTable : $this->tableCollection[$tableIndex];
2343
    }
2344
 
2345
    /**
2346
     * @param string $name the table name to search
2347
     *
2348
     * @return null|int The index of the located table in the tables collection, or null if not found
2349
     */
2350
    protected function getTableIndexByName(string $name): ?int
2351
    {
2352
        $name = Shared\StringHelper::strToUpper($name);
2353
        foreach ($this->tableCollection as $index => $table) {
2354
            /** @var Table $table */
2355
            if (Shared\StringHelper::strToUpper($table->getName()) === $name) {
2356
                return $index;
2357
            }
2358
        }
2359
 
2360
        return self::$scrutinizerNullInt;
2361
    }
2362
 
2363
    /**
2364
     * Remove Table by name.
2365
     *
2366
     * @param string $name Table name
2367
     *
2368
     * @return $this
2369
     */
2370
    public function removeTableByName(string $name): self
2371
    {
2372
        $tableIndex = $this->getTableIndexByName($name);
2373
 
2374
        if ($tableIndex !== null) {
2375
            unset($this->tableCollection[$tableIndex]);
2376
        }
2377
 
2378
        return $this;
2379
    }
2380
 
2381
    /**
2382
     * Remove collection of Tables.
2383
     */
2384
    public function removeTableCollection(): self
2385
    {
2386
        $this->tableCollection = new ArrayObject();
2387
 
2388
        return $this;
2389
    }
2390
 
2391
    /**
2392
     * Get Freeze Pane.
2393
     *
2394
     * @return null|string
2395
     */
2396
    public function getFreezePane()
2397
    {
2398
        return $this->freezePane;
2399
    }
2400
 
2401
    /**
2402
     * Freeze Pane.
2403
     *
2404
     * Examples:
2405
     *
2406
     *     - A2 will freeze the rows above cell A2 (i.e row 1)
2407
     *     - B1 will freeze the columns to the left of cell B1 (i.e column A)
2408
     *     - B2 will freeze the rows above and to the left of cell B2 (i.e row 1 and column A)
2409
     *
2410
     * @param null|array<int>|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
2411
     *            or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2412
     *        Passing a null value for this argument will clear any existing freeze pane for this worksheet.
2413
     * @param null|array<int>|CellAddress|string $topLeftCell default position of the right bottom pane
2414
     *            Coordinate of the cell as a string, eg: 'C5'; or as an array of [$columnIndex, $row] (e.g. [3, 5]),
2415
     *            or a CellAddress object.
2416
     *
2417
     * @return $this
2418
     */
2419
    public function freezePane($coordinate, $topLeftCell = null)
2420
    {
2421
        $cellAddress = ($coordinate !== null)
2422
            ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate))
2423
            : null;
2424
        if ($cellAddress !== null && Coordinate::coordinateIsRange($cellAddress)) {
2425
            throw new Exception('Freeze pane can not be set on a range of cells.');
2426
        }
2427
        $topLeftCell = ($topLeftCell !== null)
2428
            ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($topLeftCell))
2429
            : null;
2430
 
2431
        if ($cellAddress !== null && $topLeftCell === null) {
2432
            $coordinate = Coordinate::coordinateFromString($cellAddress);
2433
            $topLeftCell = $coordinate[0] . $coordinate[1];
2434
        }
2435
 
2436
        $this->freezePane = $cellAddress;
2437
        $this->topLeftCell = $topLeftCell;
2438
 
2439
        return $this;
2440
    }
2441
 
2442
    public function setTopLeftCell(string $topLeftCell): self
2443
    {
2444
        $this->topLeftCell = $topLeftCell;
2445
 
2446
        return $this;
2447
    }
2448
 
2449
    /**
2450
     * Freeze Pane by using numeric cell coordinates.
2451
     *
2452
     * @deprecated 1.23.0
2453
     *      Use the freezePane() method with a cell address such as 'C5' instead;,
2454
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2455
     * @see Worksheet::freezePane()
2456
     *
2457
     * @param int $columnIndex Numeric column coordinate of the cell
2458
     * @param int $row Numeric row coordinate of the cell
2459
     *
2460
     * @return $this
2461
     */
2462
    public function freezePaneByColumnAndRow($columnIndex, $row)
2463
    {
2464
        return $this->freezePane(Coordinate::stringFromColumnIndex($columnIndex) . $row);
2465
    }
2466
 
2467
    /**
2468
     * Unfreeze Pane.
2469
     *
2470
     * @return $this
2471
     */
2472
    public function unfreezePane()
2473
    {
2474
        return $this->freezePane(null);
2475
    }
2476
 
2477
    /**
2478
     * Get the default position of the right bottom pane.
2479
     *
2480
     * @return null|string
2481
     */
2482
    public function getTopLeftCell()
2483
    {
2484
        return $this->topLeftCell;
2485
    }
2486
 
2487
    /**
2488
     * Insert a new row, updating all possible related data.
2489
     *
2490
     * @param int $before Insert before this row number
2491
     * @param int $numberOfRows Number of new rows to insert
2492
     *
2493
     * @return $this
2494
     */
2495
    public function insertNewRowBefore(int $before, int $numberOfRows = 1)
2496
    {
2497
        if ($before >= 1) {
2498
            $objReferenceHelper = ReferenceHelper::getInstance();
2499
            $objReferenceHelper->insertNewBefore('A' . $before, 0, $numberOfRows, $this);
2500
        } else {
2501
            throw new Exception('Rows can only be inserted before at least row 1.');
2502
        }
2503
 
2504
        return $this;
2505
    }
2506
 
2507
    /**
2508
     * Insert a new column, updating all possible related data.
2509
     *
2510
     * @param string $before Insert before this column Name, eg: 'A'
2511
     * @param int $numberOfColumns Number of new columns to insert
2512
     *
2513
     * @return $this
2514
     */
2515
    public function insertNewColumnBefore(string $before, int $numberOfColumns = 1)
2516
    {
2517
        if (!is_numeric($before)) {
2518
            $objReferenceHelper = ReferenceHelper::getInstance();
2519
            $objReferenceHelper->insertNewBefore($before . '1', $numberOfColumns, 0, $this);
2520
        } else {
2521
            throw new Exception('Column references should not be numeric.');
2522
        }
2523
 
2524
        return $this;
2525
    }
2526
 
2527
    /**
2528
     * Insert a new column, updating all possible related data.
2529
     *
2530
     * @param int $beforeColumnIndex Insert before this column ID (numeric column coordinate of the cell)
2531
     * @param int $numberOfColumns Number of new columns to insert
2532
     *
2533
     * @return $this
2534
     */
2535
    public function insertNewColumnBeforeByIndex(int $beforeColumnIndex, int $numberOfColumns = 1)
2536
    {
2537
        if ($beforeColumnIndex >= 1) {
2538
            return $this->insertNewColumnBefore(Coordinate::stringFromColumnIndex($beforeColumnIndex), $numberOfColumns);
2539
        }
2540
 
2541
        throw new Exception('Columns can only be inserted before at least column A (1).');
2542
    }
2543
 
2544
    /**
2545
     * Delete a row, updating all possible related data.
2546
     *
2547
     * @param int $row Remove rows, starting with this row number
2548
     * @param int $numberOfRows Number of rows to remove
2549
     *
2550
     * @return $this
2551
     */
2552
    public function removeRow(int $row, int $numberOfRows = 1)
2553
    {
2554
        if ($row < 1) {
2555
            throw new Exception('Rows to be deleted should at least start from row 1.');
2556
        }
2557
 
2558
        $holdRowDimensions = $this->removeRowDimensions($row, $numberOfRows);
2559
        $highestRow = $this->getHighestDataRow();
2560
        $removedRowsCounter = 0;
2561
 
2562
        for ($r = 0; $r < $numberOfRows; ++$r) {
2563
            if ($row + $r <= $highestRow) {
2564
                $this->getCellCollection()->removeRow($row + $r);
2565
                ++$removedRowsCounter;
2566
            }
2567
        }
2568
 
2569
        $objReferenceHelper = ReferenceHelper::getInstance();
2570
        $objReferenceHelper->insertNewBefore('A' . ($row + $numberOfRows), 0, -$numberOfRows, $this);
2571
        for ($r = 0; $r < $removedRowsCounter; ++$r) {
2572
            $this->getCellCollection()->removeRow($highestRow);
2573
            --$highestRow;
2574
        }
2575
 
2576
        $this->rowDimensions = $holdRowDimensions;
2577
 
2578
        return $this;
2579
    }
2580
 
2581
    private function removeRowDimensions(int $row, int $numberOfRows): array
2582
    {
2583
        $highRow = $row + $numberOfRows - 1;
2584
        $holdRowDimensions = [];
2585
        foreach ($this->rowDimensions as $rowDimension) {
2586
            $num = $rowDimension->getRowIndex();
2587
            if ($num < $row) {
2588
                $holdRowDimensions[$num] = $rowDimension;
2589
            } elseif ($num > $highRow) {
2590
                $num -= $numberOfRows;
2591
                $cloneDimension = clone $rowDimension;
2592
                $cloneDimension->setRowIndex(/** @scrutinizer ignore-type */ $num);
2593
                $holdRowDimensions[$num] = $cloneDimension;
2594
            }
2595
        }
2596
 
2597
        return $holdRowDimensions;
2598
    }
2599
 
2600
    /**
2601
     * Remove a column, updating all possible related data.
2602
     *
2603
     * @param string $column Remove columns starting with this column name, eg: 'A'
2604
     * @param int $numberOfColumns Number of columns to remove
2605
     *
2606
     * @return $this
2607
     */
2608
    public function removeColumn(string $column, int $numberOfColumns = 1)
2609
    {
2610
        if (is_numeric($column)) {
2611
            throw new Exception('Column references should not be numeric.');
2612
        }
2613
 
2614
        $highestColumn = $this->getHighestDataColumn();
2615
        $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
2616
        $pColumnIndex = Coordinate::columnIndexFromString($column);
2617
 
2618
        $holdColumnDimensions = $this->removeColumnDimensions($pColumnIndex, $numberOfColumns);
2619
 
2620
        $column = Coordinate::stringFromColumnIndex($pColumnIndex + $numberOfColumns);
2621
        $objReferenceHelper = ReferenceHelper::getInstance();
2622
        $objReferenceHelper->insertNewBefore($column . '1', -$numberOfColumns, 0, $this);
2623
 
2624
        $this->columnDimensions = $holdColumnDimensions;
2625
 
2626
        if ($pColumnIndex > $highestColumnIndex) {
2627
            return $this;
2628
        }
2629
 
2630
        $maxPossibleColumnsToBeRemoved = $highestColumnIndex - $pColumnIndex + 1;
2631
 
2632
        for ($c = 0, $n = min($maxPossibleColumnsToBeRemoved, $numberOfColumns); $c < $n; ++$c) {
2633
            $this->getCellCollection()->removeColumn($highestColumn);
2634
            $highestColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($highestColumn) - 1);
2635
        }
2636
 
2637
        $this->garbageCollect();
2638
 
2639
        return $this;
2640
    }
2641
 
2642
    private function removeColumnDimensions(int $pColumnIndex, int $numberOfColumns): array
2643
    {
2644
        $highCol = $pColumnIndex + $numberOfColumns - 1;
2645
        $holdColumnDimensions = [];
2646
        foreach ($this->columnDimensions as $columnDimension) {
2647
            $num = $columnDimension->getColumnNumeric();
2648
            if ($num < $pColumnIndex) {
2649
                $str = $columnDimension->getColumnIndex();
2650
                $holdColumnDimensions[$str] = $columnDimension;
2651
            } elseif ($num > $highCol) {
2652
                $cloneDimension = clone $columnDimension;
2653
                $cloneDimension->setColumnNumeric($num - $numberOfColumns);
2654
                $str = $cloneDimension->getColumnIndex();
2655
                $holdColumnDimensions[$str] = $cloneDimension;
2656
            }
2657
        }
2658
 
2659
        return $holdColumnDimensions;
2660
    }
2661
 
2662
    /**
2663
     * Remove a column, updating all possible related data.
2664
     *
2665
     * @param int $columnIndex Remove starting with this column Index (numeric column coordinate)
2666
     * @param int $numColumns Number of columns to remove
2667
     *
2668
     * @return $this
2669
     */
2670
    public function removeColumnByIndex(int $columnIndex, int $numColumns = 1)
2671
    {
2672
        if ($columnIndex >= 1) {
2673
            return $this->removeColumn(Coordinate::stringFromColumnIndex($columnIndex), $numColumns);
2674
        }
2675
 
2676
        throw new Exception('Columns to be deleted should at least start from column A (1)');
2677
    }
2678
 
2679
    /**
2680
     * Show gridlines?
2681
     */
2682
    public function getShowGridlines(): bool
2683
    {
2684
        return $this->showGridlines;
2685
    }
2686
 
2687
    /**
2688
     * Set show gridlines.
2689
     *
2690
     * @param bool $showGridLines Show gridlines (true/false)
2691
     *
2692
     * @return $this
2693
     */
2694
    public function setShowGridlines(bool $showGridLines): self
2695
    {
2696
        $this->showGridlines = $showGridLines;
2697
 
2698
        return $this;
2699
    }
2700
 
2701
    /**
2702
     * Print gridlines?
2703
     */
2704
    public function getPrintGridlines(): bool
2705
    {
2706
        return $this->printGridlines;
2707
    }
2708
 
2709
    /**
2710
     * Set print gridlines.
2711
     *
2712
     * @param bool $printGridLines Print gridlines (true/false)
2713
     *
2714
     * @return $this
2715
     */
2716
    public function setPrintGridlines(bool $printGridLines): self
2717
    {
2718
        $this->printGridlines = $printGridLines;
2719
 
2720
        return $this;
2721
    }
2722
 
2723
    /**
2724
     * Show row and column headers?
2725
     */
2726
    public function getShowRowColHeaders(): bool
2727
    {
2728
        return $this->showRowColHeaders;
2729
    }
2730
 
2731
    /**
2732
     * Set show row and column headers.
2733
     *
2734
     * @param bool $showRowColHeaders Show row and column headers (true/false)
2735
     *
2736
     * @return $this
2737
     */
2738
    public function setShowRowColHeaders(bool $showRowColHeaders): self
2739
    {
2740
        $this->showRowColHeaders = $showRowColHeaders;
2741
 
2742
        return $this;
2743
    }
2744
 
2745
    /**
2746
     * Show summary below? (Row/Column outlining).
2747
     */
2748
    public function getShowSummaryBelow(): bool
2749
    {
2750
        return $this->showSummaryBelow;
2751
    }
2752
 
2753
    /**
2754
     * Set show summary below.
2755
     *
2756
     * @param bool $showSummaryBelow Show summary below (true/false)
2757
     *
2758
     * @return $this
2759
     */
2760
    public function setShowSummaryBelow(bool $showSummaryBelow): self
2761
    {
2762
        $this->showSummaryBelow = $showSummaryBelow;
2763
 
2764
        return $this;
2765
    }
2766
 
2767
    /**
2768
     * Show summary right? (Row/Column outlining).
2769
     */
2770
    public function getShowSummaryRight(): bool
2771
    {
2772
        return $this->showSummaryRight;
2773
    }
2774
 
2775
    /**
2776
     * Set show summary right.
2777
     *
2778
     * @param bool $showSummaryRight Show summary right (true/false)
2779
     *
2780
     * @return $this
2781
     */
2782
    public function setShowSummaryRight(bool $showSummaryRight): self
2783
    {
2784
        $this->showSummaryRight = $showSummaryRight;
2785
 
2786
        return $this;
2787
    }
2788
 
2789
    /**
2790
     * Get comments.
2791
     *
2792
     * @return Comment[]
2793
     */
2794
    public function getComments()
2795
    {
2796
        return $this->comments;
2797
    }
2798
 
2799
    /**
2800
     * Set comments array for the entire sheet.
2801
     *
2802
     * @param Comment[] $comments
2803
     *
2804
     * @return $this
2805
     */
2806
    public function setComments(array $comments): self
2807
    {
2808
        $this->comments = $comments;
2809
 
2810
        return $this;
2811
    }
2812
 
2813
    /**
2814
     * Remove comment from cell.
2815
     *
2816
     * @param array<int>|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
2817
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2818
     *
2819
     * @return $this
2820
     */
2821
    public function removeComment($cellCoordinate): self
2822
    {
2823
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
2824
 
2825
        if (Coordinate::coordinateIsRange($cellAddress)) {
2826
            throw new Exception('Cell coordinate string can not be a range of cells.');
2827
        } elseif (strpos($cellAddress, '$') !== false) {
2828
            throw new Exception('Cell coordinate string must not be absolute.');
2829
        } elseif ($cellAddress == '') {
2830
            throw new Exception('Cell coordinate can not be zero-length string.');
2831
        }
2832
        // Check if we have a comment for this cell and delete it
2833
        if (isset($this->comments[$cellAddress])) {
2834
            unset($this->comments[$cellAddress]);
2835
        }
2836
 
2837
        return $this;
2838
    }
2839
 
2840
    /**
2841
     * Get comment for cell.
2842
     *
2843
     * @param array<int>|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
2844
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2845
     */
2846
    public function getComment($cellCoordinate): Comment
2847
    {
2848
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
2849
 
2850
        if (Coordinate::coordinateIsRange($cellAddress)) {
2851
            throw new Exception('Cell coordinate string can not be a range of cells.');
2852
        } elseif (strpos($cellAddress, '$') !== false) {
2853
            throw new Exception('Cell coordinate string must not be absolute.');
2854
        } elseif ($cellAddress == '') {
2855
            throw new Exception('Cell coordinate can not be zero-length string.');
2856
        }
2857
 
2858
        // Check if we already have a comment for this cell.
2859
        if (isset($this->comments[$cellAddress])) {
2860
            return $this->comments[$cellAddress];
2861
        }
2862
 
2863
        // If not, create a new comment.
2864
        $newComment = new Comment();
2865
        $this->comments[$cellAddress] = $newComment;
2866
 
2867
        return $newComment;
2868
    }
2869
 
2870
    /**
2871
     * Get comment for cell by using numeric cell coordinates.
2872
     *
2873
     * @deprecated 1.23.0
2874
     *      Use the getComment() method with a cell address such as 'C5' instead;,
2875
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2876
     * @see Worksheet::getComment()
2877
     *
2878
     * @param int $columnIndex Numeric column coordinate of the cell
2879
     * @param int $row Numeric row coordinate of the cell
2880
     */
2881
    public function getCommentByColumnAndRow($columnIndex, $row): Comment
2882
    {
2883
        return $this->getComment(Coordinate::stringFromColumnIndex($columnIndex) . $row);
2884
    }
2885
 
2886
    /**
2887
     * Get active cell.
2888
     *
2889
     * @return string Example: 'A1'
2890
     */
2891
    public function getActiveCell()
2892
    {
2893
        return $this->activeCell;
2894
    }
2895
 
2896
    /**
2897
     * Get selected cells.
2898
     *
2899
     * @return string
2900
     */
2901
    public function getSelectedCells()
2902
    {
2903
        return $this->selectedCells;
2904
    }
2905
 
2906
    /**
2907
     * Selected cell.
2908
     *
2909
     * @param string $coordinate Cell (i.e. A1)
2910
     *
2911
     * @return $this
2912
     */
2913
    public function setSelectedCell($coordinate)
2914
    {
2915
        return $this->setSelectedCells($coordinate);
2916
    }
2917
 
2918
    /**
2919
     * Select a range of cells.
2920
     *
2921
     * @param AddressRange|array<int>|CellAddress|int|string $coordinate A simple string containing a Cell range like 'A1:E10'
2922
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2923
     *              or a CellAddress or AddressRange object.
2924
     *
2925
     * @return $this
2926
     */
2927
    public function setSelectedCells($coordinate)
2928
    {
2929
        if (is_string($coordinate)) {
2930
            $coordinate = Validations::definedNameToCoordinate($coordinate, $this);
2931
        }
2932
        $coordinate = Validations::validateCellOrCellRange($coordinate);
2933
 
2934
        if (Coordinate::coordinateIsRange($coordinate)) {
2935
            [$first] = Coordinate::splitRange($coordinate);
2936
            $this->activeCell = $first[0];
2937
        } else {
2938
            $this->activeCell = $coordinate;
2939
        }
2940
        $this->selectedCells = $coordinate;
2941
 
2942
        return $this;
2943
    }
2944
 
2945
    /**
2946
     * Selected cell by using numeric cell coordinates.
2947
     *
2948
     * @deprecated 1.23.0
2949
     *      Use the setSelectedCells() method with a cell address such as 'C5' instead;,
2950
     *          or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2951
     * @see Worksheet::setSelectedCells()
2952
     *
2953
     * @param int $columnIndex Numeric column coordinate of the cell
2954
     * @param int $row Numeric row coordinate of the cell
2955
     *
2956
     * @return $this
2957
     */
2958
    public function setSelectedCellByColumnAndRow($columnIndex, $row)
2959
    {
2960
        return $this->setSelectedCells(Coordinate::stringFromColumnIndex($columnIndex) . $row);
2961
    }
2962
 
2963
    /**
2964
     * Get right-to-left.
2965
     *
2966
     * @return bool
2967
     */
2968
    public function getRightToLeft()
2969
    {
2970
        return $this->rightToLeft;
2971
    }
2972
 
2973
    /**
2974
     * Set right-to-left.
2975
     *
2976
     * @param bool $value Right-to-left true/false
2977
     *
2978
     * @return $this
2979
     */
2980
    public function setRightToLeft($value)
2981
    {
2982
        $this->rightToLeft = $value;
2983
 
2984
        return $this;
2985
    }
2986
 
2987
    /**
2988
     * Fill worksheet from values in array.
2989
     *
2990
     * @param array $source Source array
2991
     * @param mixed $nullValue Value in source array that stands for blank cell
2992
     * @param string $startCell Insert array starting from this cell address as the top left coordinate
2993
     * @param bool $strictNullComparison Apply strict comparison when testing for null values in the array
2994
     *
2995
     * @return $this
2996
     */
2997
    public function fromArray(array $source, $nullValue = null, $startCell = 'A1', $strictNullComparison = false)
2998
    {
2999
        //    Convert a 1-D array to 2-D (for ease of looping)
3000
        if (!is_array(end($source))) {
3001
            $source = [$source];
3002
        }
3003
 
3004
        // start coordinate
3005
        [$startColumn, $startRow] = Coordinate::coordinateFromString($startCell);
3006
 
3007
        // Loop through $source
3008
        foreach ($source as $rowData) {
3009
            $currentColumn = $startColumn;
3010
            foreach ($rowData as $cellValue) {
3011
                if ($strictNullComparison) {
3012
                    if ($cellValue !== $nullValue) {
3013
                        // Set cell value
3014
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
3015
                    }
3016
                } else {
3017
                    if ($cellValue != $nullValue) {
3018
                        // Set cell value
3019
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
3020
                    }
3021
                }
3022
                ++$currentColumn;
3023
            }
3024
            ++$startRow;
3025
        }
3026
 
3027
        return $this;
3028
    }
3029
 
3030
    /**
3031
     * @param mixed $nullValue
3032
     *
3033
     * @throws Exception
3034
     * @throws \PhpOffice\PhpSpreadsheet\Calculation\Exception
3035
     *
3036
     * @return mixed
3037
     */
3038
    protected function cellToArray(Cell $cell, bool $calculateFormulas, bool $formatData, $nullValue)
3039
    {
3040
        $returnValue = $nullValue;
3041
 
3042
        if ($cell->getValue() !== null) {
3043
            if ($cell->getValue() instanceof RichText) {
3044
                $returnValue = $cell->getValue()->getPlainText();
3045
            } else {
3046
                $returnValue = ($calculateFormulas) ? $cell->getCalculatedValue() : $cell->getValue();
3047
            }
3048
 
3049
            if ($formatData) {
3050
                $style = $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex());
3051
                $returnValue = NumberFormat::toFormattedString(
3052
                    $returnValue,
3053
                    $style->getNumberFormat()->getFormatCode() ?? NumberFormat::FORMAT_GENERAL
3054
                );
3055
            }
3056
        }
3057
 
3058
        return $returnValue;
3059
    }
3060
 
3061
    /**
3062
     * Create array from a range of cells.
3063
     *
3064
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
3065
     * @param bool $calculateFormulas Should formulas be calculated?
3066
     * @param bool $formatData Should formatting be applied to cell values?
3067
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3068
     *                             True - Return rows and columns indexed by their actual row and column IDs
3069
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3070
     *                            True - Don't return values for rows/columns that are defined as hidden.
3071
     */
3072
    public function rangeToArray(
3073
        string $range,
3074
        $nullValue = null,
3075
        bool $calculateFormulas = true,
3076
        bool $formatData = true,
3077
        bool $returnCellRef = false,
3078
        bool $ignoreHidden = false
3079
    ): array {
3080
        $range = Validations::validateCellOrCellRange($range);
3081
 
3082
        $returnValue = [];
3083
        //    Identify the range that we need to extract from the worksheet
3084
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range);
3085
        $minCol = Coordinate::stringFromColumnIndex($rangeStart[0]);
3086
        $minRow = $rangeStart[1];
3087
        $maxCol = Coordinate::stringFromColumnIndex($rangeEnd[0]);
3088
        $maxRow = $rangeEnd[1];
3089
 
3090
        ++$maxCol;
3091
        // Loop through rows
3092
        $r = -1;
3093
        for ($row = $minRow; $row <= $maxRow; ++$row) {
3094
            if (($ignoreHidden === true) && ($this->getRowDimension($row)->getVisible() === false)) {
3095
                continue;
3096
            }
3097
            $rowRef = $returnCellRef ? $row : ++$r;
3098
            $c = -1;
3099
            // Loop through columns in the current row
3100
            for ($col = $minCol; $col !== $maxCol; ++$col) {
3101
                if (($ignoreHidden === true) && ($this->getColumnDimension($col)->getVisible() === false)) {
3102
                    continue;
3103
                }
3104
                $columnRef = $returnCellRef ? $col : ++$c;
3105
                //    Using getCell() will create a new cell if it doesn't already exist. We don't want that to happen
3106
                //        so we test and retrieve directly against cellCollection
3107
                $cell = $this->cellCollection->get("{$col}{$row}");
3108
                $returnValue[$rowRef][$columnRef] = $nullValue;
3109
                if ($cell !== null) {
3110
                    $returnValue[$rowRef][$columnRef] = $this->cellToArray($cell, $calculateFormulas, $formatData, $nullValue);
3111
                }
3112
            }
3113
        }
3114
 
3115
        // Return
3116
        return $returnValue;
3117
    }
3118
 
3119
    private function validateNamedRange(string $definedName, bool $returnNullIfInvalid = false): ?DefinedName
3120
    {
3121
        $namedRange = DefinedName::resolveName($definedName, $this);
3122
        if ($namedRange === null) {
3123
            if ($returnNullIfInvalid) {
3124
                return null;
3125
            }
3126
 
3127
            throw new Exception('Named Range ' . $definedName . ' does not exist.');
3128
        }
3129
 
3130
        if ($namedRange->isFormula()) {
3131
            if ($returnNullIfInvalid) {
3132
                return null;
3133
            }
3134
 
3135
            throw new Exception('Defined Named ' . $definedName . ' is a formula, not a range or cell.');
3136
        }
3137
 
3138
        if ($namedRange->getLocalOnly()) {
3139
            $worksheet = $namedRange->getWorksheet();
3140
            if ($worksheet === null || $this->getHashCode() !== $worksheet->getHashCode()) {
3141
                if ($returnNullIfInvalid) {
3142
                    return null;
3143
                }
3144
 
3145
                throw new Exception(
3146
                    'Named range ' . $definedName . ' is not accessible from within sheet ' . $this->getTitle()
3147
                );
3148
            }
3149
        }
3150
 
3151
        return $namedRange;
3152
    }
3153
 
3154
    /**
3155
     * Create array from a range of cells.
3156
     *
3157
     * @param string $definedName The Named Range that should be returned
3158
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
3159
     * @param bool $calculateFormulas Should formulas be calculated?
3160
     * @param bool $formatData Should formatting be applied to cell values?
3161
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3162
     *                             True - Return rows and columns indexed by their actual row and column IDs
3163
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3164
     *                            True - Don't return values for rows/columns that are defined as hidden.
3165
     */
3166
    public function namedRangeToArray(
3167
        string $definedName,
3168
        $nullValue = null,
3169
        bool $calculateFormulas = true,
3170
        bool $formatData = true,
3171
        bool $returnCellRef = false,
3172
        bool $ignoreHidden = false
3173
    ): array {
3174
        $retVal = [];
3175
        $namedRange = $this->validateNamedRange($definedName);
3176
        if ($namedRange !== null) {
3177
            $cellRange = ltrim(substr($namedRange->getValue(), (int) strrpos($namedRange->getValue(), '!')), '!');
3178
            $cellRange = str_replace('$', '', $cellRange);
3179
            $workSheet = $namedRange->getWorksheet();
3180
            if ($workSheet !== null) {
3181
                $retVal = $workSheet->rangeToArray($cellRange, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden);
3182
            }
3183
        }
3184
 
3185
        return $retVal;
3186
    }
3187
 
3188
    /**
3189
     * Create array from worksheet.
3190
     *
3191
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
3192
     * @param bool $calculateFormulas Should formulas be calculated?
3193
     * @param bool $formatData Should formatting be applied to cell values?
3194
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3195
     *                             True - Return rows and columns indexed by their actual row and column IDs
3196
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3197
     *                            True - Don't return values for rows/columns that are defined as hidden.
3198
     */
3199
    public function toArray(
3200
        $nullValue = null,
3201
        bool $calculateFormulas = true,
3202
        bool $formatData = true,
3203
        bool $returnCellRef = false,
3204
        bool $ignoreHidden = false
3205
    ): array {
3206
        // Garbage collect...
3207
        $this->garbageCollect();
3208
 
3209
        //    Identify the range that we need to extract from the worksheet
3210
        $maxCol = $this->getHighestColumn();
3211
        $maxRow = $this->getHighestRow();
3212
 
3213
        // Return
3214
        return $this->rangeToArray("A1:{$maxCol}{$maxRow}", $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden);
3215
    }
3216
 
3217
    /**
3218
     * Get row iterator.
3219
     *
3220
     * @param int $startRow The row number at which to start iterating
3221
     * @param int $endRow The row number at which to stop iterating
3222
     *
3223
     * @return RowIterator
3224
     */
3225
    public function getRowIterator($startRow = 1, $endRow = null)
3226
    {
3227
        return new RowIterator($this, $startRow, $endRow);
3228
    }
3229
 
3230
    /**
3231
     * Get column iterator.
3232
     *
3233
     * @param string $startColumn The column address at which to start iterating
3234
     * @param string $endColumn The column address at which to stop iterating
3235
     *
3236
     * @return ColumnIterator
3237
     */
3238
    public function getColumnIterator($startColumn = 'A', $endColumn = null)
3239
    {
3240
        return new ColumnIterator($this, $startColumn, $endColumn);
3241
    }
3242
 
3243
    /**
3244
     * Run PhpSpreadsheet garbage collector.
3245
     *
3246
     * @return $this
3247
     */
3248
    public function garbageCollect()
3249
    {
3250
        // Flush cache
3251
        $this->cellCollection->get('A1');
3252
 
3253
        // Lookup highest column and highest row if cells are cleaned
3254
        $colRow = $this->cellCollection->getHighestRowAndColumn();
3255
        $highestRow = $colRow['row'];
3256
        $highestColumn = Coordinate::columnIndexFromString($colRow['column']);
3257
 
3258
        // Loop through column dimensions
3259
        foreach ($this->columnDimensions as $dimension) {
3260
            $highestColumn = max($highestColumn, Coordinate::columnIndexFromString($dimension->getColumnIndex()));
3261
        }
3262
 
3263
        // Loop through row dimensions
3264
        foreach ($this->rowDimensions as $dimension) {
3265
            $highestRow = max($highestRow, $dimension->getRowIndex());
3266
        }
3267
 
3268
        // Cache values
3269
        if ($highestColumn < 1) {
3270
            $this->cachedHighestColumn = 1;
3271
        } else {
3272
            $this->cachedHighestColumn = $highestColumn;
3273
        }
3274
        $this->cachedHighestRow = $highestRow;
3275
 
3276
        // Return
3277
        return $this;
3278
    }
3279
 
3280
    /**
3281
     * Get hash code.
3282
     *
3283
     * @return string Hash code
3284
     */
3285
    public function getHashCode()
3286
    {
3287
        if ($this->dirty) {
3288
            $this->hash = md5($this->title . $this->autoFilter . ($this->protection->isProtectionEnabled() ? 't' : 'f') . __CLASS__);
3289
            $this->dirty = false;
3290
        }
3291
 
3292
        return $this->hash;
3293
    }
3294
 
3295
    /**
3296
     * Extract worksheet title from range.
3297
     *
3298
     * Example: extractSheetTitle("testSheet!A1") ==> 'A1'
3299
     * Example: extractSheetTitle("testSheet!A1:C3") ==> 'A1:C3'
3300
     * Example: extractSheetTitle("'testSheet 1'!A1", true) ==> ['testSheet 1', 'A1'];
3301
     * Example: extractSheetTitle("'testSheet 1'!A1:C3", true) ==> ['testSheet 1', 'A1:C3'];
3302
     * Example: extractSheetTitle("A1", true) ==> ['', 'A1'];
3303
     * Example: extractSheetTitle("A1:C3", true) ==> ['', 'A1:C3']
3304
     *
3305
     * @param string $range Range to extract title from
3306
     * @param bool $returnRange Return range? (see example)
3307
     *
3308
     * @return mixed
3309
     */
3310
    public static function extractSheetTitle($range, $returnRange = false)
3311
    {
3312
        if (empty($range)) {
3313
            return $returnRange ? [null, null] : null;
3314
        }
3315
 
3316
        // Sheet title included?
3317
        if (($sep = strrpos($range, '!')) === false) {
3318
            return $returnRange ? ['', $range] : '';
3319
        }
3320
 
3321
        if ($returnRange) {
3322
            return [substr($range, 0, $sep), substr($range, $sep + 1)];
3323
        }
3324
 
3325
        return substr($range, $sep + 1);
3326
    }
3327
 
3328
    /**
3329
     * Get hyperlink.
3330
     *
3331
     * @param string $cellCoordinate Cell coordinate to get hyperlink for, eg: 'A1'
3332
     *
3333
     * @return Hyperlink
3334
     */
3335
    public function getHyperlink($cellCoordinate)
3336
    {
3337
        // return hyperlink if we already have one
3338
        if (isset($this->hyperlinkCollection[$cellCoordinate])) {
3339
            return $this->hyperlinkCollection[$cellCoordinate];
3340
        }
3341
 
3342
        // else create hyperlink
3343
        $this->hyperlinkCollection[$cellCoordinate] = new Hyperlink();
3344
 
3345
        return $this->hyperlinkCollection[$cellCoordinate];
3346
    }
3347
 
3348
    /**
3349
     * Set hyperlink.
3350
     *
3351
     * @param string $cellCoordinate Cell coordinate to insert hyperlink, eg: 'A1'
3352
     *
3353
     * @return $this
3354
     */
3355
    public function setHyperlink($cellCoordinate, ?Hyperlink $hyperlink = null)
3356
    {
3357
        if ($hyperlink === null) {
3358
            unset($this->hyperlinkCollection[$cellCoordinate]);
3359
        } else {
3360
            $this->hyperlinkCollection[$cellCoordinate] = $hyperlink;
3361
        }
3362
 
3363
        return $this;
3364
    }
3365
 
3366
    /**
3367
     * Hyperlink at a specific coordinate exists?
3368
     *
3369
     * @param string $coordinate eg: 'A1'
3370
     *
3371
     * @return bool
3372
     */
3373
    public function hyperlinkExists($coordinate)
3374
    {
3375
        return isset($this->hyperlinkCollection[$coordinate]);
3376
    }
3377
 
3378
    /**
3379
     * Get collection of hyperlinks.
3380
     *
3381
     * @return Hyperlink[]
3382
     */
3383
    public function getHyperlinkCollection()
3384
    {
3385
        return $this->hyperlinkCollection;
3386
    }
3387
 
3388
    /**
3389
     * Get data validation.
3390
     *
3391
     * @param string $cellCoordinate Cell coordinate to get data validation for, eg: 'A1'
3392
     *
3393
     * @return DataValidation
3394
     */
3395
    public function getDataValidation($cellCoordinate)
3396
    {
3397
        // return data validation if we already have one
3398
        if (isset($this->dataValidationCollection[$cellCoordinate])) {
3399
            return $this->dataValidationCollection[$cellCoordinate];
3400
        }
3401
 
3402
        // else create data validation
3403
        $this->dataValidationCollection[$cellCoordinate] = new DataValidation();
3404
 
3405
        return $this->dataValidationCollection[$cellCoordinate];
3406
    }
3407
 
3408
    /**
3409
     * Set data validation.
3410
     *
3411
     * @param string $cellCoordinate Cell coordinate to insert data validation, eg: 'A1'
3412
     *
3413
     * @return $this
3414
     */
3415
    public function setDataValidation($cellCoordinate, ?DataValidation $dataValidation = null)
3416
    {
3417
        if ($dataValidation === null) {
3418
            unset($this->dataValidationCollection[$cellCoordinate]);
3419
        } else {
3420
            $this->dataValidationCollection[$cellCoordinate] = $dataValidation;
3421
        }
3422
 
3423
        return $this;
3424
    }
3425
 
3426
    /**
3427
     * Data validation at a specific coordinate exists?
3428
     *
3429
     * @param string $coordinate eg: 'A1'
3430
     *
3431
     * @return bool
3432
     */
3433
    public function dataValidationExists($coordinate)
3434
    {
3435
        return isset($this->dataValidationCollection[$coordinate]);
3436
    }
3437
 
3438
    /**
3439
     * Get collection of data validations.
3440
     *
3441
     * @return DataValidation[]
3442
     */
3443
    public function getDataValidationCollection()
3444
    {
3445
        return $this->dataValidationCollection;
3446
    }
3447
 
3448
    /**
3449
     * Accepts a range, returning it as a range that falls within the current highest row and column of the worksheet.
3450
     *
3451
     * @param string $range
3452
     *
3453
     * @return string Adjusted range value
3454
     */
3455
    public function shrinkRangeToFit($range)
3456
    {
3457
        $maxCol = $this->getHighestColumn();
3458
        $maxRow = $this->getHighestRow();
3459
        $maxCol = Coordinate::columnIndexFromString($maxCol);
3460
 
3461
        $rangeBlocks = explode(' ', $range);
3462
        foreach ($rangeBlocks as &$rangeSet) {
3463
            $rangeBoundaries = Coordinate::getRangeBoundaries($rangeSet);
3464
 
3465
            if (Coordinate::columnIndexFromString($rangeBoundaries[0][0]) > $maxCol) {
3466
                $rangeBoundaries[0][0] = Coordinate::stringFromColumnIndex($maxCol);
3467
            }
3468
            if ($rangeBoundaries[0][1] > $maxRow) {
3469
                $rangeBoundaries[0][1] = $maxRow;
3470
            }
3471
            if (Coordinate::columnIndexFromString($rangeBoundaries[1][0]) > $maxCol) {
3472
                $rangeBoundaries[1][0] = Coordinate::stringFromColumnIndex($maxCol);
3473
            }
3474
            if ($rangeBoundaries[1][1] > $maxRow) {
3475
                $rangeBoundaries[1][1] = $maxRow;
3476
            }
3477
            $rangeSet = $rangeBoundaries[0][0] . $rangeBoundaries[0][1] . ':' . $rangeBoundaries[1][0] . $rangeBoundaries[1][1];
3478
        }
3479
        unset($rangeSet);
3480
 
3481
        return implode(' ', $rangeBlocks);
3482
    }
3483
 
3484
    /**
3485
     * Get tab color.
3486
     *
3487
     * @return Color
3488
     */
3489
    public function getTabColor()
3490
    {
3491
        if ($this->tabColor === null) {
3492
            $this->tabColor = new Color();
3493
        }
3494
 
3495
        return $this->tabColor;
3496
    }
3497
 
3498
    /**
3499
     * Reset tab color.
3500
     *
3501
     * @return $this
3502
     */
3503
    public function resetTabColor()
3504
    {
3505
        $this->tabColor = null;
3506
 
3507
        return $this;
3508
    }
3509
 
3510
    /**
3511
     * Tab color set?
3512
     *
3513
     * @return bool
3514
     */
3515
    public function isTabColorSet()
3516
    {
3517
        return $this->tabColor !== null;
3518
    }
3519
 
3520
    /**
3521
     * Copy worksheet (!= clone!).
3522
     *
3523
     * @return static
3524
     */
3525
    public function copy()
3526
    {
3527
        return clone $this;
3528
    }
3529
 
3530
    /**
3531
     * Returns a boolean true if the specified row contains no cells. By default, this means that no cell records
3532
     *          exist in the collection for this row. false will be returned otherwise.
3533
     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
3534
     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
3535
     *                  cells, then the row will be considered empty.
3536
     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
3537
     *                  string value cells, then the row will be considered empty.
3538
     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3539
     *                  If the only cells in the collection are null value or empty string value cells, then the row
3540
     *                  will be considered empty.
3541
     *
3542
     * @param int $definitionOfEmptyFlags
3543
     *              Possible Flag Values are:
3544
     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
3545
     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3546
     */
3547
    public function isEmptyRow(int $rowId, int $definitionOfEmptyFlags = 0): bool
3548
    {
3549
        try {
3550
            $iterator = new RowIterator($this, $rowId, $rowId);
3551
            $iterator->seek($rowId);
3552
            $row = $iterator->current();
3553
        } catch (Exception $e) {
3554
            return true;
3555
        }
3556
 
3557
        return $row->isEmpty($definitionOfEmptyFlags);
3558
    }
3559
 
3560
    /**
3561
     * Returns a boolean true if the specified column contains no cells. By default, this means that no cell records
3562
     *          exist in the collection for this column. false will be returned otherwise.
3563
     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
3564
     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
3565
     *                  cells, then the column will be considered empty.
3566
     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
3567
     *                  string value cells, then the column will be considered empty.
3568
     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3569
     *                  If the only cells in the collection are null value or empty string value cells, then the column
3570
     *                  will be considered empty.
3571
     *
3572
     * @param int $definitionOfEmptyFlags
3573
     *              Possible Flag Values are:
3574
     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
3575
     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3576
     */
3577
    public function isEmptyColumn(string $columnId, int $definitionOfEmptyFlags = 0): bool
3578
    {
3579
        try {
3580
            $iterator = new ColumnIterator($this, $columnId, $columnId);
3581
            $iterator->seek($columnId);
3582
            $column = $iterator->current();
3583
        } catch (Exception $e) {
3584
            return true;
3585
        }
3586
 
3587
        return $column->isEmpty($definitionOfEmptyFlags);
3588
    }
3589
 
3590
    /**
3591
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
3592
     */
3593
    public function __clone()
3594
    {
3595
        // @phpstan-ignore-next-line
3596
        foreach ($this as $key => $val) {
3597
            if ($key == 'parent') {
3598
                continue;
3599
            }
3600
 
3601
            if (is_object($val) || (is_array($val))) {
3602
                if ($key == 'cellCollection') {
3603
                    $newCollection = $this->cellCollection->cloneCellCollection($this);
3604
                    $this->cellCollection = $newCollection;
3605
                } elseif ($key == 'drawingCollection') {
3606
                    $currentCollection = $this->drawingCollection;
3607
                    $this->drawingCollection = new ArrayObject();
3608
                    foreach ($currentCollection as $item) {
3609
                        if (is_object($item)) {
3610
                            $newDrawing = clone $item;
3611
                            $newDrawing->setWorksheet($this);
3612
                        }
3613
                    }
3614
                } elseif (($key == 'autoFilter') && ($this->autoFilter instanceof AutoFilter)) {
3615
                    $newAutoFilter = clone $this->autoFilter;
3616
                    $this->autoFilter = $newAutoFilter;
3617
                    $this->autoFilter->setParent($this);
3618
                } else {
3619
                    $this->{$key} = unserialize(serialize($val));
3620
                }
3621
            }
3622
        }
3623
    }
3624
 
3625
    /**
3626
     * Define the code name of the sheet.
3627
     *
3628
     * @param string $codeName Same rule as Title minus space not allowed (but, like Excel, change
3629
     *                       silently space to underscore)
3630
     * @param bool $validate False to skip validation of new title. WARNING: This should only be set
3631
     *                       at parse time (by Readers), where titles can be assumed to be valid.
3632
     *
3633
     * @return $this
3634
     */
3635
    public function setCodeName($codeName, $validate = true)
3636
    {
3637
        // Is this a 'rename' or not?
3638
        if ($this->getCodeName() == $codeName) {
3639
            return $this;
3640
        }
3641
 
3642
        if ($validate) {
3643
            $codeName = str_replace(' ', '_', $codeName); //Excel does this automatically without flinching, we are doing the same
3644
 
3645
            // Syntax check
3646
            // throw an exception if not valid
3647
            self::checkSheetCodeName($codeName);
3648
 
3649
            // We use the same code that setTitle to find a valid codeName else not using a space (Excel don't like) but a '_'
3650
 
3651
            if ($this->parent !== null) {
3652
                // Is there already such sheet name?
3653
                if ($this->parent->sheetCodeNameExists($codeName)) {
3654
                    // Use name, but append with lowest possible integer
3655
 
3656
                    if (Shared\StringHelper::countCharacters($codeName) > 29) {
3657
                        $codeName = Shared\StringHelper::substring($codeName, 0, 29);
3658
                    }
3659
                    $i = 1;
3660
                    while ($this->getParentOrThrow()->sheetCodeNameExists($codeName . '_' . $i)) {
3661
                        ++$i;
3662
                        if ($i == 10) {
3663
                            if (Shared\StringHelper::countCharacters($codeName) > 28) {
3664
                                $codeName = Shared\StringHelper::substring($codeName, 0, 28);
3665
                            }
3666
                        } elseif ($i == 100) {
3667
                            if (Shared\StringHelper::countCharacters($codeName) > 27) {
3668
                                $codeName = Shared\StringHelper::substring($codeName, 0, 27);
3669
                            }
3670
                        }
3671
                    }
3672
 
3673
                    $codeName .= '_' . $i; // ok, we have a valid name
3674
                }
3675
            }
3676
        }
3677
 
3678
        $this->codeName = $codeName;
3679
 
3680
        return $this;
3681
    }
3682
 
3683
    /**
3684
     * Return the code name of the sheet.
3685
     *
3686
     * @return null|string
3687
     */
3688
    public function getCodeName()
3689
    {
3690
        return $this->codeName;
3691
    }
3692
 
3693
    /**
3694
     * Sheet has a code name ?
3695
     *
3696
     * @return bool
3697
     */
3698
    public function hasCodeName()
3699
    {
3700
        return $this->codeName !== null;
3701
    }
3702
 
3703
    public static function nameRequiresQuotes(string $sheetName): bool
3704
    {
3705
        return preg_match(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName) !== 1;
3706
    }
3707
}