Passed
Pull Request — master (#4468)
by Owen
10:02
created

Worksheet::compareRowBreaks()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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