Worksheet::rangeToArray()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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