• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPOffice / PhpSpreadsheet / 25975969039

16 May 2026 11:38PM UTC coverage: 97.066% (+0.003%) from 97.063%
25975969039

Pull #4830

github

web-flow
Merge aeb3775f9 into 7cc6e4e58
Pull Request #4830: Add optional XMLReader streaming mode for XLSX cell data parsing

123 of 127 new or added lines in 1 file covered. (96.85%)

47 existing lines in 3 files now uncovered.

48070 of 49523 relevant lines covered (97.07%)

386.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.96
/src/PhpSpreadsheet/Worksheet/Worksheet.php
1
<?php
2

3
namespace PhpOffice\PhpSpreadsheet\Worksheet;
4

5
use ArrayObject;
6
use Composer\Pcre\Preg;
7
use Generator;
8
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
9
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
10
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
11
use PhpOffice\PhpSpreadsheet\Cell\Cell;
12
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
13
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
14
use PhpOffice\PhpSpreadsheet\Cell\DataType;
15
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
16
use PhpOffice\PhpSpreadsheet\Cell\Hyperlink;
17
use PhpOffice\PhpSpreadsheet\Cell\IValueBinder;
18
use PhpOffice\PhpSpreadsheet\Chart\Chart;
19
use PhpOffice\PhpSpreadsheet\Collection\Cells;
20
use PhpOffice\PhpSpreadsheet\Collection\CellsFactory;
21
use PhpOffice\PhpSpreadsheet\Comment;
22
use PhpOffice\PhpSpreadsheet\DefinedName;
23
use PhpOffice\PhpSpreadsheet\Exception;
24
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
25
use PhpOffice\PhpSpreadsheet\RichText\RichText;
26
use PhpOffice\PhpSpreadsheet\Shared;
27
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
28
use PhpOffice\PhpSpreadsheet\Spreadsheet;
29
use PhpOffice\PhpSpreadsheet\Style\Alignment;
30
use PhpOffice\PhpSpreadsheet\Style\Color;
31
use PhpOffice\PhpSpreadsheet\Style\Conditional;
32
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
33
use PhpOffice\PhpSpreadsheet\Style\Protection as StyleProtection;
34
use PhpOffice\PhpSpreadsheet\Style\Style;
35

36
class Worksheet
37
{
38
    // Break types
39
    public const BREAK_NONE = 0;
40
    public const BREAK_ROW = 1;
41
    public const BREAK_COLUMN = 2;
42
    // Maximum column for row break
43
    public const BREAK_ROW_MAX_COLUMN = 16383;
44

45
    // Sheet state
46
    public const SHEETSTATE_VISIBLE = 'visible';
47
    public const SHEETSTATE_HIDDEN = 'hidden';
48
    public const SHEETSTATE_VERYHIDDEN = 'veryHidden';
49

50
    public const MERGE_CELL_CONTENT_EMPTY = 'empty';
51
    public const MERGE_CELL_CONTENT_HIDE = 'hide';
52
    public const MERGE_CELL_CONTENT_MERGE = 'merge';
53

54
    public const FUNCTION_LIKE_GROUPBY = '/\b(groupby|_xleta)\b/i'; // weird new syntax
55

56
    protected const SHEET_NAME_REQUIRES_NO_QUOTES = '/^[_\p{L}][_\p{L}\p{N}]*$/mui';
57

58
    /**
59
     * Maximum 31 characters allowed for sheet title.
60
     *
61
     * @var int
62
     */
63
    const SHEET_TITLE_MAXIMUM_LENGTH = 31;
64

65
    /**
66
     * Invalid characters in sheet title.
67
     */
68
    private const INVALID_CHARACTERS = ['*', ':', '/', '\\', '?', '[', ']'];
69

70
    /**
71
     * Parent spreadsheet.
72
     */
73
    private ?Spreadsheet $parent = null;
74

75
    /**
76
     * Collection of cells.
77
     */
78
    private Cells $cellCollection;
79

80
    /**
81
     * Collection of row dimensions.
82
     *
83
     * @var RowDimension[]
84
     */
85
    private array $rowDimensions = [];
86

87
    /**
88
     * Default row dimension.
89
     */
90
    private RowDimension $defaultRowDimension;
91

92
    /**
93
     * Collection of column dimensions.
94
     *
95
     * @var ColumnDimension[]
96
     */
97
    private array $columnDimensions = [];
98

99
    /**
100
     * Default column dimension.
101
     */
102
    private ColumnDimension $defaultColumnDimension;
103

104
    /**
105
     * Collection of drawings.
106
     *
107
     * @var ArrayObject<int, BaseDrawing>
108
     */
109
    private ArrayObject $drawingCollection;
110

111
    /**
112
     * Collection of drawings.
113
     *
114
     * @var ArrayObject<int, BaseDrawing>
115
     */
116
    private ArrayObject $inCellDrawingCollection;
117

118
    /**
119
     * Collection of Chart objects.
120
     *
121
     * @var ArrayObject<int, Chart>
122
     */
123
    private ArrayObject $chartCollection;
124

125
    /**
126
     * Collection of Table objects.
127
     *
128
     * @var ArrayObject<int, Table>
129
     */
130
    private ArrayObject $tableCollection;
131

132
    /**
133
     * Worksheet title.
134
     */
135
    private string $title = '';
136

137
    /**
138
     * Sheet state.
139
     */
140
    private string $sheetState;
141

142
    /**
143
     * Page setup.
144
     */
145
    private PageSetup $pageSetup;
146

147
    /**
148
     * Page margins.
149
     */
150
    private PageMargins $pageMargins;
151

152
    /**
153
     * Page header/footer.
154
     */
155
    private HeaderFooter $headerFooter;
156

157
    /**
158
     * Sheet view.
159
     */
160
    private SheetView $sheetView;
161

162
    /**
163
     * Protection.
164
     */
165
    private Protection $protection;
166

167
    /**
168
     * Conditional styles. Indexed by cell coordinate, e.g. 'A1'.
169
     *
170
     * @var Conditional[][]
171
     */
172
    private array $conditionalStylesCollection = [];
173

174
    /**
175
     * Collection of row breaks.
176
     *
177
     * @var PageBreak[]
178
     */
179
    private array $rowBreaks = [];
180

181
    /**
182
     * Collection of column breaks.
183
     *
184
     * @var PageBreak[]
185
     */
186
    private array $columnBreaks = [];
187

188
    /**
189
     * Collection of merged cell ranges.
190
     *
191
     * @var string[]
192
     */
193
    private array $mergeCells = [];
194

195
    /**
196
     * Collection of protected cell ranges.
197
     *
198
     * @var ProtectedRange[]
199
     */
200
    private array $protectedCells = [];
201

202
    /**
203
     * Autofilter Range and selection.
204
     */
205
    private AutoFilter $autoFilter;
206

207
    /**
208
     * Freeze pane.
209
     */
210
    private ?string $freezePane = null;
211

212
    /**
213
     * Default position of the right bottom pane.
214
     */
215
    private ?string $topLeftCell = null;
216

217
    private string $paneTopLeftCell = '';
218

219
    private string $activePane = '';
220

221
    private int $xSplit = 0;
222

223
    private int $ySplit = 0;
224

225
    private string $paneState = '';
226

227
    /**
228
     * Properties of the 4 panes.
229
     *
230
     * @var (null|Pane)[]
231
     */
232
    private array $panes = [
233
        'bottomRight' => null,
234
        'bottomLeft' => null,
235
        'topRight' => null,
236
        'topLeft' => null,
237
    ];
238

239
    /**
240
     * Show gridlines?
241
     */
242
    private bool $showGridlines = true;
243

244
    /**
245
     * Print gridlines?
246
     */
247
    private bool $printGridlines = false;
248

249
    /**
250
     * Show row and column headers?
251
     */
252
    private bool $showRowColHeaders = true;
253

254
    /**
255
     * Show summary below? (Row/Column outline).
256
     */
257
    private bool $showSummaryBelow = true;
258

259
    /**
260
     * Show summary right? (Row/Column outline).
261
     */
262
    private bool $showSummaryRight = true;
263

264
    /**
265
     * Collection of comments.
266
     *
267
     * @var Comment[]
268
     */
269
    private array $comments = [];
270

271
    /**
272
     * Active cell. (Only one!).
273
     */
274
    private string $activeCell = 'A1';
275

276
    /**
277
     * Selected cells.
278
     */
279
    private string $selectedCells = 'A1';
280

281
    /**
282
     * Cached highest column.
283
     */
284
    private int $cachedHighestColumn = 1;
285

286
    /**
287
     * Cached highest row.
288
     */
289
    private int $cachedHighestRow = 1;
290

291
    /**
292
     * Right-to-left?
293
     */
294
    private bool $rightToLeft = false;
295

296
    /**
297
     * Hyperlinks. Indexed by cell coordinate, e.g. 'A1'.
298
     *
299
     * @var Hyperlink[]
300
     */
301
    private array $hyperlinkCollection = [];
302

303
    /**
304
     * Data validation objects. Indexed by cell coordinate, e.g. 'A1'.
305
     * Index can include ranges, and multiple cells/ranges.
306
     *
307
     * @var DataValidation[]
308
     */
309
    private array $dataValidationCollection = [];
310

311
    /**
312
     * Tab color.
313
     */
314
    private ?Color $tabColor = null;
315

316
    /**
317
     * CodeName.
318
     */
319
    private ?string $codeName = null;
320

321
    /**
322
     * Create a new worksheet.
323
     */
324
    public function __construct(?Spreadsheet $parent = null, string $title = 'Worksheet')
11,444✔
325
    {
326
        // Set parent and title
327
        $this->parent = $parent;
11,444✔
328
        $this->setTitle($title, false);
11,444✔
329
        // setTitle can change $pTitle
330
        $this->setCodeName($this->getTitle());
11,444✔
331
        $this->setSheetState(self::SHEETSTATE_VISIBLE);
11,444✔
332

333
        $this->cellCollection = CellsFactory::getInstance($this);
11,444✔
334
        // Set page setup
335
        $this->pageSetup = new PageSetup();
11,444✔
336
        // Set page margins
337
        $this->pageMargins = new PageMargins();
11,444✔
338
        // Set page header/footer
339
        $this->headerFooter = new HeaderFooter();
11,444✔
340
        // Set sheet view
341
        $this->sheetView = new SheetView();
11,444✔
342
        // Drawing collection
343
        $this->drawingCollection = new ArrayObject();
11,444✔
344
        // In Cell Drawing collection
345
        $this->inCellDrawingCollection = new ArrayObject();
11,444✔
346
        // Chart collection
347
        $this->chartCollection = new ArrayObject();
11,444✔
348
        // Protection
349
        $this->protection = new Protection();
11,444✔
350
        // Default row dimension
351
        $this->defaultRowDimension = new RowDimension(null);
11,444✔
352
        // Default column dimension
353
        $this->defaultColumnDimension = new ColumnDimension(null);
11,444✔
354
        // AutoFilter
355
        $this->autoFilter = new AutoFilter('', $this);
11,444✔
356
        // Table collection
357
        $this->tableCollection = new ArrayObject();
11,444✔
358
    }
359

360
    /**
361
     * Disconnect all cells from this Worksheet object,
362
     * typically so that the worksheet object can be unset.
363
     */
364
    public function disconnectCells(): void
10,460✔
365
    {
366
        if (isset($this->cellCollection)) { //* @phpstan-ignore-line
10,460✔
367
            $this->cellCollection->unsetWorksheetCells();
10,460✔
368
            unset($this->cellCollection);
10,460✔
369
        }
370
        //    detach ourself from the workbook, so that it can then delete this worksheet successfully
371
        $this->parent = null;
10,460✔
372
    }
373

374
    /**
375
     * Code to execute when this worksheet is unset().
376
     */
377
    public function __destruct()
128✔
378
    {
379
        Calculation::getInstanceOrNull($this->parent)
128✔
380
            ?->clearCalculationCacheForWorksheet($this->title);
128✔
381

382
        $this->disconnectCells();
128✔
383
        unset($this->rowDimensions, $this->columnDimensions, $this->tableCollection, $this->drawingCollection, $this->inCellDrawingCollection, $this->chartCollection, $this->autoFilter);
128✔
384
    }
385

386
    /**
387
     * Return the cell collection.
388
     */
389
    public function getCellCollection(): Cells
10,996✔
390
    {
391
        return $this->cellCollection;
10,996✔
392
    }
393

394
    /**
395
     * Get array of invalid characters for sheet title.
396
     *
397
     * @return string[]
398
     */
399
    public static function getInvalidCharacters(): array
1✔
400
    {
401
        return self::INVALID_CHARACTERS;
1✔
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
    private static function checkSheetCodeName(string $sheetCodeName): string
11,444✔
412
    {
413
        $charCount = StringHelper::countCharacters($sheetCodeName);
11,444✔
414
        if ($charCount == 0) {
11,444✔
415
            throw new Exception('Sheet code name cannot be empty.');
1✔
416
        }
417
        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ] and  first and last characters cannot be a "'"
418
        if (
419
            (str_replace(self::INVALID_CHARACTERS, '', $sheetCodeName) !== $sheetCodeName)
11,444✔
420
            || (StringHelper::substring($sheetCodeName, -1, 1) == '\'')
11,444✔
421
            || (StringHelper::substring($sheetCodeName, 0, 1) == '\'')
11,444✔
422
        ) {
423
            throw new Exception('Invalid character found in sheet code name');
1✔
424
        }
425

426
        // Enforce maximum characters allowed for sheet title
427
        if ($charCount > self::SHEET_TITLE_MAXIMUM_LENGTH) {
11,444✔
428
            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet code name.');
1✔
429
        }
430

431
        return $sheetCodeName;
11,444✔
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
    private static function checkSheetTitle(string $sheetTitle): string
11,444✔
442
    {
443
        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ]
444
        if (str_replace(self::INVALID_CHARACTERS, '', $sheetTitle) !== $sheetTitle) {
11,444✔
445
            throw new Exception('Invalid character found in sheet title');
2✔
446
        }
447

448
        // Enforce maximum characters allowed for sheet title
449
        if (StringHelper::countCharacters($sheetTitle) > self::SHEET_TITLE_MAXIMUM_LENGTH) {
11,444✔
450
            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet title.');
4✔
451
        }
452

453
        return $sheetTitle;
11,444✔
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
    public function getCoordinates(bool $sorted = true): array
1,652✔
464
    {
465
        if (!isset($this->cellCollection)) { //* @phpstan-ignore-line
1,652✔
466
            return [];
1✔
467
        }
468

469
        if ($sorted) {
1,652✔
470
            return $this->cellCollection->getSortedCoordinates();
626✔
471
        }
472

473
        return $this->cellCollection->getCoordinates();
1,505✔
474
    }
475

476
    /**
477
     * Get collection of row dimensions.
478
     *
479
     * @return RowDimension[]
480
     */
481
    public function getRowDimensions(): array
1,353✔
482
    {
483
        return $this->rowDimensions;
1,353✔
484
    }
485

486
    /**
487
     * Get default row dimension.
488
     */
489
    public function getDefaultRowDimension(): RowDimension
1,324✔
490
    {
491
        return $this->defaultRowDimension;
1,324✔
492
    }
493

494
    /**
495
     * Get collection of column dimensions.
496
     *
497
     * @return ColumnDimension[]
498
     */
499
    public function getColumnDimensions(): array
1,359✔
500
    {
501
        /** @var callable $callable */
502
        $callable = [self::class, 'columnDimensionCompare'];
1,359✔
503
        uasort($this->columnDimensions, $callable);
1,359✔
504

505
        return $this->columnDimensions;
1,359✔
506
    }
507

508
    private static function columnDimensionCompare(ColumnDimension $a, ColumnDimension $b): int
115✔
509
    {
510
        return $a->getColumnNumeric() - $b->getColumnNumeric();
115✔
511
    }
512

513
    /**
514
     * Get default column dimension.
515
     */
516
    public function getDefaultColumnDimension(): ColumnDimension
656✔
517
    {
518
        return $this->defaultColumnDimension;
656✔
519
    }
520

521
    /**
522
     * Get collection of drawings.
523
     *
524
     * @return ArrayObject<int, BaseDrawing>
525
     */
526
    public function getDrawingCollection(): ArrayObject
1,320✔
527
    {
528
        return $this->drawingCollection;
1,320✔
529
    }
530

531
    /**
532
     * Get collection of drawings.
533
     *
534
     * @return ArrayObject<int, BaseDrawing>
535
     */
536
    public function getInCellDrawingCollection(): ArrayObject
446✔
537
    {
538
        return $this->inCellDrawingCollection;
446✔
539
    }
540

541
    /**
542
     * Get collection of charts.
543
     *
544
     * @return ArrayObject<int, Chart>
545
     */
546
    public function getChartCollection(): ArrayObject
101✔
547
    {
548
        return $this->chartCollection;
101✔
549
    }
550

551
    public function addChart(Chart $chart): Chart
107✔
552
    {
553
        $chart->setWorksheet($this);
107✔
554
        $this->chartCollection[] = $chart;
107✔
555

556
        return $chart;
107✔
557
    }
558

559
    /**
560
     * Return the count of charts on this worksheet.
561
     *
562
     * @return int The number of charts
563
     */
564
    public function getChartCount(): int
84✔
565
    {
566
        return count($this->chartCollection);
84✔
567
    }
568

569
    /**
570
     * Get a chart by its index position.
571
     *
572
     * @param ?string $index Chart index position
573
     *
574
     * @return Chart|false
575
     */
576
    public function getChartByIndex(?string $index)
79✔
577
    {
578
        $chartCount = count($this->chartCollection);
79✔
579
        if ($chartCount == 0) {
79✔
580
            return false;
×
581
        }
582
        if ($index === null) {
79✔
583
            $index = --$chartCount;
×
584
        }
585
        if (!isset($this->chartCollection[$index])) {
79✔
586
            return false;
×
587
        }
588

589
        return $this->chartCollection[$index];
79✔
590
    }
591

592
    /**
593
     * Return an array of the names of charts on this worksheet.
594
     *
595
     * @return string[] The names of charts
596
     */
597
    public function getChartNames(): array
5✔
598
    {
599
        $chartNames = [];
5✔
600
        foreach ($this->chartCollection as $chart) {
5✔
601
            $chartNames[] = $chart->getName();
5✔
602
        }
603

604
        return $chartNames;
5✔
605
    }
606

607
    /**
608
     * Get a chart by name.
609
     *
610
     * @param string $chartName Chart name
611
     *
612
     * @return Chart|false
613
     */
614
    public function getChartByName(string $chartName)
6✔
615
    {
616
        foreach ($this->chartCollection as $index => $chart) {
6✔
617
            if ($chart->getName() == $chartName) {
6✔
618
                return $chart;
6✔
619
            }
620
        }
621

622
        return false;
1✔
623
    }
624

625
    public function getChartByNameOrThrow(string $chartName): Chart
6✔
626
    {
627
        $chart = $this->getChartByName($chartName);
6✔
628
        if ($chart !== false) {
6✔
629
            return $chart;
6✔
630
        }
631

632
        throw new Exception("Sheet does not have a chart named $chartName.");
1✔
633
    }
634

635
    /**
636
     * Refresh column dimensions.
637
     *
638
     * @return $this
639
     */
640
    public function refreshColumnDimensions(): static
25✔
641
    {
642
        $newColumnDimensions = [];
25✔
643
        foreach ($this->getColumnDimensions() as $objColumnDimension) {
25✔
644
            $newColumnDimensions[$objColumnDimension->getColumnIndex()] = $objColumnDimension;
25✔
645
        }
646

647
        $this->columnDimensions = $newColumnDimensions;
25✔
648

649
        return $this;
25✔
650
    }
651

652
    /**
653
     * Refresh row dimensions.
654
     *
655
     * @return $this
656
     */
657
    public function refreshRowDimensions(): static
9✔
658
    {
659
        $newRowDimensions = [];
9✔
660
        foreach ($this->getRowDimensions() as $objRowDimension) {
9✔
661
            $newRowDimensions[$objRowDimension->getRowIndex()] = $objRowDimension;
9✔
662
        }
663

664
        $this->rowDimensions = $newRowDimensions;
9✔
665

666
        return $this;
9✔
667
    }
668

669
    /**
670
     * Calculate worksheet dimension.
671
     *
672
     * @return string String containing the dimension of this worksheet
673
     */
674
    public function calculateWorksheetDimension(): string
531✔
675
    {
676
        // Return
677
        return 'A1:' . $this->getHighestColumn() . $this->getHighestRow();
531✔
678
    }
679

680
    /**
681
     * Calculate worksheet data dimension.
682
     *
683
     * @return string String containing the dimension of this worksheet that actually contain data
684
     */
685
    public function calculateWorksheetDataDimension(): string
611✔
686
    {
687
        // Return
688
        return 'A1:' . $this->getHighestDataColumn() . $this->getHighestDataRow();
611✔
689
    }
690

691
    /**
692
     * Calculate widths for auto-size columns.
693
     *
694
     * @return $this
695
     */
696
    public function calculateColumnWidths(): static
879✔
697
    {
698
        $activeSheet = $this->getParent()?->getActiveSheetIndex();
879✔
699
        $selectedCells = $this->selectedCells;
879✔
700
        // initialize $autoSizes array
701
        $autoSizes = [];
879✔
702
        foreach ($this->getColumnDimensions() as $colDimension) {
879✔
703
            if ($colDimension->getAutoSize()) {
183✔
704
                $autoSizes[$colDimension->getColumnIndex()] = -1;
67✔
705
            }
706
        }
707

708
        // There is only something to do if there are some auto-size columns
709
        if (!empty($autoSizes)) {
879✔
710
            $holdActivePane = $this->activePane;
67✔
711
            // build list of cells references that participate in a merge
712
            $isMergeCell = [];
67✔
713
            foreach ($this->getMergeCells() as $cells) {
67✔
714
                foreach (Coordinate::extractAllCellReferencesInRange($cells) as $cellReference) {
16✔
715
                    $isMergeCell[$cellReference] = true;
16✔
716
                }
717
            }
718

719
            $autoFilterIndentRanges = (new AutoFit($this))->getAutoFilterIndentRanges();
67✔
720

721
            // loop through all cells in the worksheet
722
            foreach ($this->getCoordinates(false) as $coordinate) {
67✔
723
                $cell = $this->getCellOrNull($coordinate);
67✔
724

725
                if ($cell !== null && isset($autoSizes[$this->cellCollection->getCurrentColumn()])) {
67✔
726
                    //Determine if cell is in merge range
727
                    $isMerged = isset($isMergeCell[$this->cellCollection->getCurrentCoordinate()]);
67✔
728

729
                    //By default merged cells should be ignored
730
                    $isMergedButProceed = false;
67✔
731

732
                    //The only exception is if it's a merge range value cell of a 'vertical' range (1 column wide)
733
                    if ($isMerged && $cell->isMergeRangeValueCell()) {
67✔
734
                        $range = (string) $cell->getMergeRange();
×
735
                        $rangeBoundaries = Coordinate::rangeDimension($range);
×
736
                        if ($rangeBoundaries[0] === 1) {
×
737
                            $isMergedButProceed = true;
×
738
                        }
739
                    }
740

741
                    // Determine width if cell is not part of a merge or does and is a value cell of 1-column wide range
742
                    if (!$isMerged || $isMergedButProceed) {
67✔
743
                        // Determine if we need to make an adjustment for the first row in an AutoFilter range that
744
                        //    has a column filter dropdown
745
                        $filterAdjustment = false;
67✔
746
                        if (!empty($autoFilterIndentRanges)) {
67✔
747
                            foreach ($autoFilterIndentRanges as $autoFilterFirstRowRange) {
4✔
748
                                /** @var string $autoFilterFirstRowRange */
749
                                if ($cell->isInRange($autoFilterFirstRowRange)) {
4✔
750
                                    $filterAdjustment = true;
4✔
751

752
                                    break;
4✔
753
                                }
754
                            }
755
                        }
756

757
                        $indentAdjustment = $cell->getStyle()->getAlignment()->getIndent();
67✔
758
                        $indentAdjustment += (int) ($cell->getStyle()->getAlignment()->getHorizontal() === Alignment::HORIZONTAL_CENTER);
67✔
759

760
                        // Calculated value
761
                        // To formatted string
762
                        $cellValue = NumberFormat::toFormattedString(
67✔
763
                            $cell->getCalculatedValueString(),
67✔
764
                            (string) $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())
67✔
765
                                ->getNumberFormat()->getFormatCode(true)
67✔
766
                        );
67✔
767

768
                        if ($cellValue !== '') {
67✔
769
                            $autoSizes[$this->cellCollection->getCurrentColumn()] = max(
67✔
770
                                $autoSizes[$this->cellCollection->getCurrentColumn()],
67✔
771
                                round(
67✔
772
                                    Shared\Font::calculateColumnWidth(
67✔
773
                                        $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont(),
67✔
774
                                        $cellValue,
67✔
775
                                        (int) $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())
67✔
776
                                            ->getAlignment()->getTextRotation(),
67✔
777
                                        $this->getParentOrThrow()->getDefaultStyle()->getFont(),
67✔
778
                                        $filterAdjustment,
67✔
779
                                        $indentAdjustment
67✔
780
                                    ),
67✔
781
                                    3
67✔
782
                                )
67✔
783
                            );
67✔
784
                        }
785
                    }
786
                }
787
            }
788

789
            // adjust column widths
790
            foreach ($autoSizes as $columnIndex => $width) {
67✔
791
                if ($width == -1) {
67✔
792
                    $width = $this->getDefaultColumnDimension()->getWidth();
×
793
                }
794
                $this->getColumnDimension($columnIndex)->setWidth($width);
67✔
795
            }
796
            $this->activePane = $holdActivePane;
67✔
797
        }
798
        if ($activeSheet !== null && $activeSheet >= 0) {
879✔
799
            // Not sure what PhpStan doesn't like about next stmt
800
            $this->getParent()?->setActiveSheetIndex($activeSheet); // @phpstan-ignore-line
879✔
801
        }
802
        $this->setSelectedCells($selectedCells);
879✔
803

804
        return $this;
879✔
805
    }
806

807
    /**
808
     * Get parent or null.
809
     */
810
    public function getParent(): ?Spreadsheet
11,005✔
811
    {
812
        return $this->parent;
11,005✔
813
    }
814

815
    /**
816
     * Get parent, throw exception if null.
817
     */
818
    public function getParentOrThrow(): Spreadsheet
11,077✔
819
    {
820
        if ($this->parent !== null) {
11,077✔
821
            return $this->parent;
11,076✔
822
        }
823

824
        throw new Exception('Sheet does not have a parent.');
1✔
825
    }
826

827
    /**
828
     * Re-bind parent.
829
     *
830
     * @return $this
831
     */
832
    public function rebindParent(Spreadsheet $parent): static
54✔
833
    {
834
        if ($this->parent !== null) {
54✔
835
            $definedNames = $this->parent->getDefinedNames();
4✔
836
            foreach ($definedNames as $definedName) {
4✔
UNCOV
837
                $parent->addDefinedName($definedName);
×
838
            }
839

840
            $this->parent->removeSheetByIndex(
4✔
841
                $this->parent->getIndex($this)
4✔
842
            );
4✔
843
        }
844
        $this->parent = $parent;
54✔
845

846
        return $this;
54✔
847
    }
848

849
    public function setParent(Spreadsheet $parent): self
12✔
850
    {
851
        $this->parent = $parent;
12✔
852

853
        return $this;
12✔
854
    }
855

856
    /**
857
     * Get title.
858
     */
859
    public function getTitle(): string
11,444✔
860
    {
861
        return $this->title;
11,444✔
862
    }
863

864
    /**
865
     * Set title.
866
     *
867
     * @param string $title String containing the dimension of this worksheet
868
     * @param bool $updateFormulaCellReferences Flag indicating whether cell references in formulae should
869
     *            be updated to reflect the new sheet name.
870
     *          This should be left as the default true, unless you are
871
     *          certain that no formula cells on any worksheet contain
872
     *          references to this worksheet
873
     * @param bool $validate False to skip validation of new title. WARNING: This should only be set
874
     *                       at parse time (by Readers), where titles can be assumed to be valid.
875
     *
876
     * @return $this
877
     */
878
    public function setTitle(string $title, bool $updateFormulaCellReferences = true, bool $validate = true): static
11,444✔
879
    {
880
        // Is this a 'rename' or not?
881
        if ($this->getTitle() == $title) {
11,444✔
882
            return $this;
338✔
883
        }
884

885
        // Old title
886
        $oldTitle = $this->getTitle();
11,444✔
887

888
        if ($validate) {
11,444✔
889
            // Syntax check
890
            self::checkSheetTitle($title);
11,444✔
891

892
            if ($this->parent && $this->parent->getIndex($this, true) >= 0) {
11,444✔
893
                // Is there already such sheet name?
894
                if ($this->parent->sheetNameExists($title)) {
848✔
895
                    // Use name, but append with lowest possible integer
896

897
                    if (StringHelper::countCharacters($title) > 29) {
2✔
UNCOV
898
                        $title = StringHelper::substring($title, 0, 29);
×
899
                    }
900
                    $i = 1;
2✔
901
                    while ($this->parent->sheetNameExists($title . ' ' . $i)) {
2✔
902
                        ++$i;
1✔
903
                        if ($i == 10) {
1✔
904
                            if (StringHelper::countCharacters($title) > 28) {
×
UNCOV
905
                                $title = StringHelper::substring($title, 0, 28);
×
906
                            }
907
                        } elseif ($i == 100) {
1✔
908
                            if (StringHelper::countCharacters($title) > 27) {
×
UNCOV
909
                                $title = StringHelper::substring($title, 0, 27);
×
910
                            }
911
                        }
912
                    }
913

914
                    $title .= " $i";
2✔
915
                }
916
            }
917
        }
918

919
        // Set title
920
        $this->title = $title;
11,444✔
921

922
        if ($this->parent && $this->parent->getIndex($this, true) >= 0) {
11,444✔
923
            // New title
924
            $newTitle = $this->getTitle();
1,515✔
925
            $this->parent->getCalculationEngine()
1,515✔
926
                ->renameCalculationCacheForWorksheet($oldTitle, $newTitle);
1,515✔
927
            if ($updateFormulaCellReferences) {
1,515✔
928
                ReferenceHelper::getInstance()->updateNamedFormulae($this->parent, $oldTitle, $newTitle);
848✔
929
            }
930
        }
931

932
        return $this;
11,444✔
933
    }
934

935
    /**
936
     * Get sheet state.
937
     *
938
     * @return string Sheet state (visible, hidden, veryHidden)
939
     */
940
    public function getSheetState(): string
585✔
941
    {
942
        return $this->sheetState;
585✔
943
    }
944

945
    /**
946
     * Set sheet state.
947
     *
948
     * @param string $value Sheet state (visible, hidden, veryHidden)
949
     *
950
     * @return $this
951
     */
952
    public function setSheetState(string $value): static
11,444✔
953
    {
954
        $this->sheetState = $value;
11,444✔
955

956
        return $this;
11,444✔
957
    }
958

959
    /**
960
     * Get page setup.
961
     */
962
    public function getPageSetup(): PageSetup
1,778✔
963
    {
964
        return $this->pageSetup;
1,778✔
965
    }
966

967
    /**
968
     * Set page setup.
969
     *
970
     * @return $this
971
     */
972
    public function setPageSetup(PageSetup $pageSetup): static
1✔
973
    {
974
        $this->pageSetup = $pageSetup;
1✔
975

976
        return $this;
1✔
977
    }
978

979
    /**
980
     * Get page margins.
981
     */
982
    public function getPageMargins(): PageMargins
1,767✔
983
    {
984
        return $this->pageMargins;
1,767✔
985
    }
986

987
    /**
988
     * Set page margins.
989
     *
990
     * @return $this
991
     */
992
    public function setPageMargins(PageMargins $pageMargins): static
1✔
993
    {
994
        $this->pageMargins = $pageMargins;
1✔
995

996
        return $this;
1✔
997
    }
998

999
    /**
1000
     * Get page header/footer.
1001
     */
1002
    public function getHeaderFooter(): HeaderFooter
649✔
1003
    {
1004
        return $this->headerFooter;
649✔
1005
    }
1006

1007
    /**
1008
     * Set page header/footer.
1009
     *
1010
     * @return $this
1011
     */
1012
    public function setHeaderFooter(HeaderFooter $headerFooter): static
1✔
1013
    {
1014
        $this->headerFooter = $headerFooter;
1✔
1015

1016
        return $this;
1✔
1017
    }
1018

1019
    /**
1020
     * Get sheet view.
1021
     */
1022
    public function getSheetView(): SheetView
680✔
1023
    {
1024
        return $this->sheetView;
680✔
1025
    }
1026

1027
    /**
1028
     * Set sheet view.
1029
     *
1030
     * @return $this
1031
     */
1032
    public function setSheetView(SheetView $sheetView): static
1✔
1033
    {
1034
        $this->sheetView = $sheetView;
1✔
1035

1036
        return $this;
1✔
1037
    }
1038

1039
    /**
1040
     * Get Protection.
1041
     */
1042
    public function getProtection(): Protection
699✔
1043
    {
1044
        return $this->protection;
699✔
1045
    }
1046

1047
    /**
1048
     * Set Protection.
1049
     *
1050
     * @return $this
1051
     */
1052
    public function setProtection(Protection $protection): static
1✔
1053
    {
1054
        $this->protection = $protection;
1✔
1055

1056
        return $this;
1✔
1057
    }
1058

1059
    /**
1060
     * Get highest worksheet column.
1061
     *
1062
     * @param null|int|string $row Return the data highest column for the specified row,
1063
     *                                     or the highest column of any row if no row number is passed
1064
     *
1065
     * @return string Highest column name
1066
     */
1067
    public function getHighestColumn($row = null): string
1,674✔
1068
    {
1069
        if ($row === null) {
1,674✔
1070
            return Coordinate::stringFromColumnIndex($this->cachedHighestColumn);
1,673✔
1071
        }
1072

1073
        return $this->getHighestDataColumn($row);
1✔
1074
    }
1075

1076
    /**
1077
     * Get highest worksheet column that contains data.
1078
     *
1079
     * @param null|int|string $row Return the highest data column for the specified row,
1080
     *                                     or the highest data column of any row if no row number is passed
1081
     *
1082
     * @return string Highest column name that contains data
1083
     */
1084
    public function getHighestDataColumn($row = null): string
917✔
1085
    {
1086
        return $this->cellCollection->getHighestColumn($row);
917✔
1087
    }
1088

1089
    /**
1090
     * Get highest worksheet row.
1091
     *
1092
     * @param null|string $column Return the highest data row for the specified column,
1093
     *                                     or the highest row of any column if no column letter is passed
1094
     *
1095
     * @return int Highest row number
1096
     */
1097
    public function getHighestRow(?string $column = null): int
1,108✔
1098
    {
1099
        if ($column === null) {
1,108✔
1100
            return $this->cachedHighestRow;
1,107✔
1101
        }
1102

1103
        return $this->getHighestDataRow($column);
1✔
1104
    }
1105

1106
    /**
1107
     * Get highest worksheet row that contains data.
1108
     *
1109
     * @param null|string $column Return the highest data row for the specified column,
1110
     *                                     or the highest data row of any column if no column letter is passed
1111
     *
1112
     * @return int Highest row number that contains data
1113
     */
1114
    public function getHighestDataRow(?string $column = null): int
821✔
1115
    {
1116
        return $this->cellCollection->getHighestRow($column);
821✔
1117
    }
1118

1119
    /**
1120
     * Get highest worksheet column and highest row that have cell records.
1121
     *
1122
     * @return array{row: int, column: string} Highest column name and highest row number
1123
     */
1124
    public function getHighestRowAndColumn(): array
1✔
1125
    {
1126
        return $this->cellCollection->getHighestRowAndColumn();
1✔
1127
    }
1128

1129
    /**
1130
     * Set a cell value.
1131
     *
1132
     * @param array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1133
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1134
     * @param mixed $value Value for the cell
1135
     * @param null|IValueBinder $binder Value Binder to override the currently set Value Binder
1136
     *
1137
     * @return $this
1138
     */
1139
    public function setCellValue(CellAddress|string|array $coordinate, mixed $value, ?IValueBinder $binder = null): static
5,118✔
1140
    {
1141
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
5,118✔
1142
        $this->getCell($cellAddress)->setValue($value, $binder);
5,118✔
1143

1144
        return $this;
5,112✔
1145
    }
1146

1147
    /**
1148
     * Set a cell value.
1149
     *
1150
     * @param array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1151
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1152
     * @param mixed $value Value of the cell
1153
     * @param string $dataType Explicit data type, see DataType::TYPE_*
1154
     *        Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this
1155
     *             method, then it is your responsibility as an end-user developer to validate that the value and
1156
     *             the datatype match.
1157
     *       If you do mismatch value and datatpe, then the value you enter may be changed to match the datatype
1158
     *          that you specify.
1159
     *
1160
     * @see DataType
1161
     *
1162
     * @return $this
1163
     */
1164
    public function setCellValueExplicit(CellAddress|string|array $coordinate, mixed $value, string $dataType): static
124✔
1165
    {
1166
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
124✔
1167
        $this->getCell($cellAddress)->setValueExplicit($value, $dataType);
124✔
1168

1169
        return $this;
124✔
1170
    }
1171

1172
    /**
1173
     * Get cell at a specific coordinate.
1174
     *
1175
     * @param array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1176
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1177
     *
1178
     * @return Cell Cell that was found or created
1179
     *              WARNING: Because the cell collection can be cached to reduce memory, it only allows one
1180
     *              "active" cell at a time in memory. If you assign that cell to a variable, then select
1181
     *              another cell using getCell() or any of its variants, the newly selected cell becomes
1182
     *              the "active" cell, and any previous assignment becomes a disconnected reference because
1183
     *              the active cell has changed.
1184
     */
1185
    public function getCell(CellAddress|string|array $coordinate): Cell
10,936✔
1186
    {
1187
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
10,936✔
1188

1189
        // Shortcut for increased performance for the vast majority of simple cases
1190
        if ($this->cellCollection->has($cellAddress)) {
10,936✔
1191
            /** @var Cell $cell */
1192
            $cell = $this->cellCollection->get($cellAddress);
10,907✔
1193

1194
            return $cell;
10,907✔
1195
        }
1196

1197
        /** @var Worksheet $sheet */
1198
        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
10,936✔
1199
        $cell = $sheet->getCellCollection()->get($finalCoordinate);
10,936✔
1200

1201
        return $cell ?? $sheet->createNewCell($finalCoordinate);
10,936✔
1202
    }
1203

1204
    /**
1205
     * Get the correct Worksheet and coordinate from a coordinate that may
1206
     * contains reference to another sheet or a named range.
1207
     *
1208
     * @return array{0: Worksheet, 1: string}
1209
     */
1210
    private function getWorksheetAndCoordinate(string $coordinate): array
10,960✔
1211
    {
1212
        $sheet = null;
10,960✔
1213
        $finalCoordinate = null;
10,960✔
1214

1215
        // Worksheet reference?
1216
        if (str_contains($coordinate, '!')) {
10,960✔
UNCOV
1217
            $worksheetReference = self::extractSheetTitle($coordinate, true, true);
×
1218

1219
            $sheet = $this->getParentOrThrow()->getSheetByName($worksheetReference[0]);
×
UNCOV
1220
            $finalCoordinate = strtoupper($worksheetReference[1]);
×
1221

1222
            if ($sheet === null) {
×
UNCOV
1223
                throw new Exception('Sheet not found for name: ' . $worksheetReference[0]);
×
1224
            }
1225
        } elseif (
1226
            !Preg::isMatch('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $coordinate)
10,960✔
1227
            && Preg::isMatch('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/iu', $coordinate)
10,960✔
1228
        ) {
1229
            // Named range?
1230
            $namedRange = $this->validateNamedRange($coordinate, true);
17✔
1231
            if ($namedRange !== null) {
17✔
1232
                $sheet = $namedRange->getWorksheet();
12✔
1233
                if ($sheet === null) {
12✔
UNCOV
1234
                    throw new Exception('Sheet not found for named range: ' . $namedRange->getName());
×
1235
                }
1236

1237
                /** @phpstan-ignore-next-line */
1238
                $cellCoordinate = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!');
12✔
1239
                $finalCoordinate = str_replace('$', '', $cellCoordinate);
12✔
1240
            }
1241
        }
1242

1243
        if ($sheet === null || $finalCoordinate === null) {
10,960✔
1244
            $sheet = $this;
10,960✔
1245
            $finalCoordinate = strtoupper($coordinate);
10,960✔
1246
        }
1247

1248
        if (Coordinate::coordinateIsRange($finalCoordinate)) {
10,960✔
1249
            throw new Exception('Cell coordinate string can not be a range of cells.');
2✔
1250
        }
1251
        $finalCoordinate = str_replace('$', '', $finalCoordinate);
10,960✔
1252

1253
        return [$sheet, $finalCoordinate];
10,960✔
1254
    }
1255

1256
    /**
1257
     * Get an existing cell at a specific coordinate, or null.
1258
     *
1259
     * @param string $coordinate Coordinate of the cell, eg: 'A1'
1260
     *
1261
     * @return null|Cell Cell that was found or null
1262
     */
1263
    private function getCellOrNull(string $coordinate): ?Cell
67✔
1264
    {
1265
        // Check cell collection
1266
        if ($this->cellCollection->has($coordinate)) {
67✔
1267
            return $this->cellCollection->get($coordinate);
67✔
1268
        }
1269

UNCOV
1270
        return null;
×
1271
    }
1272

1273
    /**
1274
     * Create a new cell at the specified coordinate.
1275
     *
1276
     * @param string $coordinate Coordinate of the cell
1277
     *
1278
     * @return Cell Cell that was created
1279
     *              WARNING: Because the cell collection can be cached to reduce memory, it only allows one
1280
     *              "active" cell at a time in memory. If you assign that cell to a variable, then select
1281
     *              another cell using getCell() or any of its variants, the newly selected cell becomes
1282
     *              the "active" cell, and any previous assignment becomes a disconnected reference because
1283
     *              the active cell has changed.
1284
     */
1285
    public function createNewCell(string $coordinate): Cell
10,936✔
1286
    {
1287
        [$column, $row, $columnString] = Coordinate::indexesFromString($coordinate);
10,936✔
1288
        $cell = new Cell(null, DataType::TYPE_NULL, $this);
10,936✔
1289
        $this->cellCollection->add($coordinate, $cell);
10,936✔
1290

1291
        // Coordinates
1292
        if ($column > $this->cachedHighestColumn) {
10,936✔
1293
            $this->cachedHighestColumn = $column;
7,514✔
1294
        }
1295
        if ($row > $this->cachedHighestRow) {
10,936✔
1296
            $this->cachedHighestRow = $row;
8,957✔
1297
        }
1298

1299
        // Cell needs appropriate xfIndex from dimensions records
1300
        //    but don't create dimension records if they don't already exist
1301
        $rowDimension = $this->rowDimensions[$row] ?? null;
10,936✔
1302
        $columnDimension = $this->columnDimensions[$columnString] ?? null;
10,936✔
1303

1304
        $xfSet = false;
10,936✔
1305
        if ($rowDimension !== null) {
10,936✔
1306
            $rowXf = (int) $rowDimension->getXfIndex();
420✔
1307
            if ($rowXf > 0) {
420✔
1308
                // then there is a row dimension with explicit style, assign it to the cell
1309
                $cell->setXfIndex($rowXf);
203✔
1310
                $xfSet = true;
203✔
1311
            }
1312
        }
1313
        if (!$xfSet && $columnDimension !== null) {
10,936✔
1314
            $colXf = (int) $columnDimension->getXfIndex();
602✔
1315
            if ($colXf > 0) {
602✔
1316
                // then there is a column dimension, assign it to the cell
1317
                $cell->setXfIndex($colXf);
229✔
1318
            }
1319
        }
1320

1321
        return $cell;
10,936✔
1322
    }
1323

1324
    /**
1325
     * Does the cell at a specific coordinate exist?
1326
     *
1327
     * @param array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1328
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1329
     */
1330
    public function cellExists(CellAddress|string|array $coordinate): bool
10,872✔
1331
    {
1332
        $cellAddress = Validations::validateCellAddress($coordinate);
10,872✔
1333
        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
10,872✔
1334

1335
        return $sheet->getCellCollection()->has($finalCoordinate);
10,872✔
1336
    }
1337

1338
    /**
1339
     * Get row dimension at a specific row.
1340
     *
1341
     * @param int $row Numeric index of the row
1342
     */
1343
    public function getRowDimension(int $row): RowDimension
627✔
1344
    {
1345
        // Get row dimension
1346
        if (!isset($this->rowDimensions[$row])) {
627✔
1347
            $this->rowDimensions[$row] = new RowDimension($row);
627✔
1348

1349
            $this->cachedHighestRow = max($this->cachedHighestRow, $row);
627✔
1350
        }
1351

1352
        return $this->rowDimensions[$row];
627✔
1353
    }
1354

1355
    public function getRowStyle(int $row): ?Style
1✔
1356
    {
1357
        return $this->parent?->getCellXfByIndexOrNull(
1✔
1358
            ($this->rowDimensions[$row] ?? null)?->getXfIndex()
1✔
1359
        );
1✔
1360
    }
1361

1362
    public function rowDimensionExists(int $row): bool
705✔
1363
    {
1364
        return isset($this->rowDimensions[$row]);
705✔
1365
    }
1366

1367
    public function columnDimensionExists(string $column): bool
107✔
1368
    {
1369
        return isset($this->columnDimensions[$column]);
107✔
1370
    }
1371

1372
    /**
1373
     * Get column dimension at a specific column.
1374
     *
1375
     * @param string $column String index of the column eg: 'A'
1376
     */
1377
    public function getColumnDimension(string $column): ColumnDimension
706✔
1378
    {
1379
        // Uppercase coordinate
1380
        $column = strtoupper($column);
706✔
1381

1382
        // Fetch dimensions
1383
        if (!isset($this->columnDimensions[$column])) {
706✔
1384
            $this->columnDimensions[$column] = new ColumnDimension($column);
706✔
1385

1386
            $columnIndex = Coordinate::columnIndexFromString($column);
706✔
1387
            if ($this->cachedHighestColumn < $columnIndex) {
706✔
1388
                $this->cachedHighestColumn = $columnIndex;
488✔
1389
            }
1390
        }
1391

1392
        return $this->columnDimensions[$column];
706✔
1393
    }
1394

1395
    /**
1396
     * Get column dimension at a specific column by using numeric cell coordinates.
1397
     *
1398
     * @param int $columnIndex Numeric column coordinate of the cell
1399
     */
1400
    public function getColumnDimensionByColumn(int $columnIndex): ColumnDimension
136✔
1401
    {
1402
        return $this->getColumnDimension(Coordinate::stringFromColumnIndex($columnIndex));
136✔
1403
    }
1404

1405
    public function getColumnStyle(string $column): ?Style
1✔
1406
    {
1407
        return $this->parent?->getCellXfByIndexOrNull(
1✔
1408
            ($this->columnDimensions[$column] ?? null)?->getXfIndex()
1✔
1409
        );
1✔
1410
    }
1411

1412
    /**
1413
     * Get style for cell.
1414
     *
1415
     * @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
1416
     *              A simple string containing a cell address like 'A1' or a cell range like 'A1:E10'
1417
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1418
     *              or a CellAddress or AddressRange object.
1419
     */
1420
    public function getStyle(AddressRange|CellAddress|int|string|array $cellCoordinate): Style
10,899✔
1421
    {
1422
        if (is_string($cellCoordinate)) {
10,899✔
1423
            $cellCoordinate = Validations::definedNameToCoordinate($cellCoordinate, $this);
10,897✔
1424
        }
1425
        $cellCoordinate = Validations::validateCellOrCellRange($cellCoordinate);
10,899✔
1426
        $cellCoordinate = str_replace('$', '', $cellCoordinate);
10,899✔
1427

1428
        // set this sheet as active
1429
        $this->getParentOrThrow()->setActiveSheetIndex($this->getParentOrThrow()->getIndex($this));
10,899✔
1430

1431
        // set cell coordinate as active
1432
        $this->setSelectedCells($cellCoordinate);
10,899✔
1433

1434
        return $this->getParentOrThrow()->getCellXfSupervisor();
10,899✔
1435
    }
1436

1437
    /**
1438
     * Get table styles set for the for given cell.
1439
     *
1440
     * @param Cell $cell
1441
     *              The Cell for which the tables are retrieved
1442
     *
1443
     * @return Table[]
1444
     */
1445
    public function getTablesWithStylesForCell(Cell $cell): array
7✔
1446
    {
1447
        $retVal = [];
7✔
1448

1449
        foreach ($this->tableCollection as $table) {
7✔
1450
            $dxfsTableStyle = $table->getStyle()->getTableDxfsStyle();
7✔
1451
            if ($dxfsTableStyle !== null) {
7✔
1452
                if ($dxfsTableStyle->getHeaderRowStyle() !== null || $dxfsTableStyle->getFirstRowStripeStyle() !== null || $dxfsTableStyle->getSecondRowStripeStyle() !== null) {
4✔
1453
                    $range = $table->getRange();
4✔
1454
                    if ($cell->isInRange($range)) {
4✔
1455
                        $retVal[] = $table;
4✔
1456
                    }
1457
                }
1458
            }
1459
        }
1460

1461
        return $retVal;
7✔
1462
    }
1463

1464
    /**
1465
     * Get tables without styles set for the for given cell.
1466
     *
1467
     * @param Cell $cell
1468
     *              The Cell for which the tables are retrieved
1469
     *
1470
     * @return Table[]
1471
     */
1472
    public function getTablesWithoutStylesForCell(Cell $cell): array
6✔
1473
    {
1474
        $retVal = [];
6✔
1475

1476
        foreach ($this->tableCollection as $table) {
6✔
1477
            $range = $table->getRange();
6✔
1478
            if ($cell->isInRange($range)) {
6✔
1479
                $dxfsTableStyle = $table->getStyle()->getTableDxfsStyle();
6✔
1480
                if ($dxfsTableStyle === null || ($dxfsTableStyle->getHeaderRowStyle() === null && $dxfsTableStyle->getFirstRowStripeStyle() === null && $dxfsTableStyle->getSecondRowStripeStyle() === null)) {
6✔
1481
                    $retVal[] = $table;
2✔
1482
                }
1483
            }
1484
        }
1485

1486
        return $retVal;
6✔
1487
    }
1488

1489
    /**
1490
     * Get conditional styles for a cell.
1491
     *
1492
     * @param string $coordinate eg: 'A1' or 'A1:A3'.
1493
     *          If a single cell is referenced, then the array of conditional styles will be returned if the cell is
1494
     *               included in a conditional style range.
1495
     *          If a range of cells is specified, then the styles will only be returned if the range matches the entire
1496
     *               range of the conditional.
1497
     * @param bool $firstOnly default true, return all matching
1498
     *          conditionals ordered by priority if false, first only if true
1499
     *
1500
     * @return Conditional[]
1501
     */
1502
    public function getConditionalStyles(string $coordinate, bool $firstOnly = true): array
844✔
1503
    {
1504
        $coordinate = strtoupper($coordinate);
844✔
1505
        if (Preg::isMatch('/[: ,]/', $coordinate)) {
844✔
1506
            return $this->conditionalStylesCollection[$coordinate] ?? [];
49✔
1507
        }
1508

1509
        $conditionalStyles = [];
820✔
1510
        foreach ($this->conditionalStylesCollection as $keyStylesOrig => $conditionalRange) {
820✔
1511
            $keyStyles = Coordinate::resolveUnionAndIntersection($keyStylesOrig);
225✔
1512
            $keyParts = explode(',', $keyStyles);
225✔
1513
            foreach ($keyParts as $keyPart) {
225✔
1514
                if ($keyPart === $coordinate) {
225✔
1515
                    if ($firstOnly) {
14✔
1516
                        return $conditionalRange;
14✔
1517
                    }
UNCOV
1518
                    $conditionalStyles[$keyStylesOrig] = $conditionalRange;
×
1519

UNCOV
1520
                    break;
×
1521
                } elseif (str_contains($keyPart, ':')) {
220✔
1522
                    if (Coordinate::coordinateIsInsideRange($keyPart, $coordinate)) {
215✔
1523
                        if ($firstOnly) {
201✔
1524
                            return $conditionalRange;
200✔
1525
                        }
1526
                        $conditionalStyles[$keyStylesOrig] = $conditionalRange;
1✔
1527

1528
                        break;
1✔
1529
                    }
1530
                }
1531
            }
1532
        }
1533
        $outArray = [];
643✔
1534
        foreach ($conditionalStyles as $conditionalArray) {
643✔
1535
            foreach ($conditionalArray as $conditional) {
1✔
1536
                $outArray[] = $conditional;
1✔
1537
            }
1538
        }
1539
        usort($outArray, [self::class, 'comparePriority']);
643✔
1540

1541
        return $outArray;
643✔
1542
    }
1543

1544
    private static function comparePriority(Conditional $condA, Conditional $condB): int
1✔
1545
    {
1546
        $a = $condA->getPriority();
1✔
1547
        $b = $condB->getPriority();
1✔
1548
        if ($a === $b) {
1✔
UNCOV
1549
            return 0;
×
1550
        }
1551
        if ($a === 0) {
1✔
UNCOV
1552
            return 1;
×
1553
        }
1554
        if ($b === 0) {
1✔
UNCOV
1555
            return -1;
×
1556
        }
1557

1558
        return ($a < $b) ? -1 : 1;
1✔
1559
    }
1560

1561
    public function getConditionalRange(string $coordinate): ?string
192✔
1562
    {
1563
        $coordinate = strtoupper($coordinate);
192✔
1564
        $cell = $this->getCell($coordinate);
192✔
1565
        foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
192✔
1566
            $cellBlocks = explode(',', Coordinate::resolveUnionAndIntersection($conditionalRange));
192✔
1567
            foreach ($cellBlocks as $cellBlock) {
192✔
1568
                if ($cell->isInRange($cellBlock)) {
192✔
1569
                    return $conditionalRange;
191✔
1570
                }
1571
            }
1572
        }
1573

1574
        return null;
10✔
1575
    }
1576

1577
    /**
1578
     * Do conditional styles exist for this cell?
1579
     *
1580
     * @param string $coordinate eg: 'A1' or 'A1:A3'.
1581
     *          If a single cell is specified, then this method will return true if that cell is included in a
1582
     *               conditional style range.
1583
     *          If a range of cells is specified, then true will only be returned if the range matches the entire
1584
     *               range of the conditional.
1585
     */
1586
    public function conditionalStylesExists(string $coordinate): bool
22✔
1587
    {
1588
        return !empty($this->getConditionalStyles($coordinate));
22✔
1589
    }
1590

1591
    /**
1592
     * Removes conditional styles for a cell.
1593
     *
1594
     * @param string $coordinate eg: 'A1'
1595
     *
1596
     * @return $this
1597
     */
1598
    public function removeConditionalStyles(string $coordinate): static
55✔
1599
    {
1600
        unset($this->conditionalStylesCollection[strtoupper($coordinate)]);
55✔
1601

1602
        return $this;
55✔
1603
    }
1604

1605
    /**
1606
     * Get collection of conditional styles.
1607
     *
1608
     * @return Conditional[][]
1609
     */
1610
    public function getConditionalStylesCollection(): array
1,461✔
1611
    {
1612
        return $this->conditionalStylesCollection;
1,461✔
1613
    }
1614

1615
    /**
1616
     * Set conditional styles.
1617
     *
1618
     * @param string $coordinate eg: 'A1'
1619
     * @param Conditional[] $styles
1620
     *
1621
     * @return $this
1622
     */
1623
    public function setConditionalStyles(string $coordinate, array $styles): static
348✔
1624
    {
1625
        $this->conditionalStylesCollection[strtoupper($coordinate)] = $styles;
348✔
1626

1627
        return $this;
348✔
1628
    }
1629

1630
    /**
1631
     * Duplicate cell style to a range of cells.
1632
     *
1633
     * Please note that this will overwrite existing cell styles for cells in range!
1634
     *
1635
     * @param Style $style Cell style to duplicate
1636
     * @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
1637
     *
1638
     * @return $this
1639
     */
1640
    public function duplicateStyle(Style $style, string $range): static
2✔
1641
    {
1642
        // Add the style to the workbook if necessary
1643
        $workbook = $this->getParentOrThrow();
2✔
1644
        if ($existingStyle = $workbook->getCellXfByHashCode($style->getHashCode())) {
2✔
1645
            // there is already such cell Xf in our collection
1646
            $xfIndex = $existingStyle->getIndex();
1✔
1647
        } else {
1648
            // we don't have such a cell Xf, need to add
1649
            $workbook->addCellXf($style);
2✔
1650
            $xfIndex = $style->getIndex();
2✔
1651
        }
1652

1653
        // Calculate range outer borders
1654
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
2✔
1655

1656
        // Make sure we can loop upwards on rows and columns
1657
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
2✔
1658
            $tmp = $rangeStart;
×
1659
            $rangeStart = $rangeEnd;
×
UNCOV
1660
            $rangeEnd = $tmp;
×
1661
        }
1662

1663
        // Loop through cells and apply styles
1664
        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
2✔
1665
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
2✔
1666
                $this->getCell(Coordinate::stringFromColumnIndex($col) . $row)->setXfIndex($xfIndex);
2✔
1667
            }
1668
        }
1669

1670
        return $this;
2✔
1671
    }
1672

1673
    /**
1674
     * Duplicate conditional style to a range of cells.
1675
     *
1676
     * Please note that this will overwrite existing cell styles for cells in range!
1677
     *
1678
     * @param Conditional[] $styles Cell style to duplicate
1679
     * @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
1680
     *
1681
     * @return $this
1682
     */
1683
    public function duplicateConditionalStyle(array $styles, string $range = ''): static
18✔
1684
    {
1685
        foreach ($styles as $cellStyle) {
18✔
1686
            if (!($cellStyle instanceof Conditional)) { // @phpstan-ignore-line
18✔
UNCOV
1687
                throw new Exception('Style is not a conditional style');
×
1688
            }
1689
        }
1690

1691
        // Calculate range outer borders
1692
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
18✔
1693

1694
        // Make sure we can loop upwards on rows and columns
1695
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
18✔
1696
            $tmp = $rangeStart;
×
1697
            $rangeStart = $rangeEnd;
×
UNCOV
1698
            $rangeEnd = $tmp;
×
1699
        }
1700

1701
        // Loop through cells and apply styles
1702
        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
18✔
1703
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
18✔
1704
                $this->setConditionalStyles(Coordinate::stringFromColumnIndex($col) . $row, $styles);
18✔
1705
            }
1706
        }
1707

1708
        return $this;
18✔
1709
    }
1710

1711
    /**
1712
     * Set break on a cell.
1713
     *
1714
     * @param array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1715
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1716
     * @param int $break Break type (type of Worksheet::BREAK_*)
1717
     *
1718
     * @return $this
1719
     */
1720
    public function setBreak(CellAddress|string|array $coordinate, int $break, int $max = -1): static
33✔
1721
    {
1722
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
33✔
1723

1724
        if ($break === self::BREAK_NONE) {
33✔
1725
            unset($this->rowBreaks[$cellAddress], $this->columnBreaks[$cellAddress]);
7✔
1726
        } elseif ($break === self::BREAK_ROW) {
33✔
1727
            $this->rowBreaks[$cellAddress] = new PageBreak($break, $cellAddress, $max);
23✔
1728
        } elseif ($break === self::BREAK_COLUMN) {
19✔
1729
            $this->columnBreaks[$cellAddress] = new PageBreak($break, $cellAddress, $max);
19✔
1730
        }
1731

1732
        return $this;
33✔
1733
    }
1734

1735
    /**
1736
     * Get breaks.
1737
     *
1738
     * @return int[]
1739
     */
1740
    public function getBreaks(): array
728✔
1741
    {
1742
        $breaks = [];
728✔
1743
        /** @var callable $compareFunction */
1744
        $compareFunction = [self::class, 'compareRowBreaks'];
728✔
1745
        uksort($this->rowBreaks, $compareFunction);
728✔
1746
        foreach ($this->rowBreaks as $break) {
728✔
1747
            $breaks[$break->getCoordinate()] = self::BREAK_ROW;
10✔
1748
        }
1749
        /** @var callable $compareFunction */
1750
        $compareFunction = [self::class, 'compareColumnBreaks'];
728✔
1751
        uksort($this->columnBreaks, $compareFunction);
728✔
1752
        foreach ($this->columnBreaks as $break) {
728✔
1753
            $breaks[$break->getCoordinate()] = self::BREAK_COLUMN;
8✔
1754
        }
1755

1756
        return $breaks;
728✔
1757
    }
1758

1759
    /**
1760
     * Get row breaks.
1761
     *
1762
     * @return PageBreak[]
1763
     */
1764
    public function getRowBreaks(): array
593✔
1765
    {
1766
        /** @var callable $compareFunction */
1767
        $compareFunction = [self::class, 'compareRowBreaks'];
593✔
1768
        uksort($this->rowBreaks, $compareFunction);
593✔
1769

1770
        return $this->rowBreaks;
593✔
1771
    }
1772

1773
    protected static function compareRowBreaks(string $coordinate1, string $coordinate2): int
9✔
1774
    {
1775
        $row1 = Coordinate::indexesFromString($coordinate1)[1];
9✔
1776
        $row2 = Coordinate::indexesFromString($coordinate2)[1];
9✔
1777

1778
        return $row1 - $row2;
9✔
1779
    }
1780

1781
    protected static function compareColumnBreaks(string $coordinate1, string $coordinate2): int
5✔
1782
    {
1783
        $column1 = Coordinate::indexesFromString($coordinate1)[0];
5✔
1784
        $column2 = Coordinate::indexesFromString($coordinate2)[0];
5✔
1785

1786
        return $column1 - $column2;
5✔
1787
    }
1788

1789
    /**
1790
     * Get column breaks.
1791
     *
1792
     * @return PageBreak[]
1793
     */
1794
    public function getColumnBreaks(): array
592✔
1795
    {
1796
        /** @var callable $compareFunction */
1797
        $compareFunction = [self::class, 'compareColumnBreaks'];
592✔
1798
        uksort($this->columnBreaks, $compareFunction);
592✔
1799

1800
        return $this->columnBreaks;
592✔
1801
    }
1802

1803
    /**
1804
     * Set merge on a cell range.
1805
     *
1806
     * @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'
1807
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1808
     *              or an AddressRange.
1809
     * @param string $behaviour How the merged cells should behave.
1810
     *               Possible values are:
1811
     *                   MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells
1812
     *                   MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells
1813
     *                   MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell
1814
     *
1815
     * @return $this
1816
     */
1817
    public function mergeCells(AddressRange|string|array $range, string $behaviour = self::MERGE_CELL_CONTENT_EMPTY): static
181✔
1818
    {
1819
        $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range));
181✔
1820

1821
        if (!str_contains($range, ':')) {
180✔
1822
            $range .= ":{$range}";
1✔
1823
        }
1824

1825
        if (!Preg::isMatch('/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/', $range, $matches)) {
180✔
1826
            throw new Exception('Merge must be on a valid range of cells.');
1✔
1827
        }
1828

1829
        $this->mergeCells[$range] = $range;
179✔
1830
        $firstRow = (int) $matches[2];
179✔
1831
        $lastRow = (int) $matches[4];
179✔
1832
        $firstColumn = $matches[1];
179✔
1833
        $lastColumn = $matches[3];
179✔
1834
        $firstColumnIndex = Coordinate::columnIndexFromString($firstColumn);
179✔
1835
        $lastColumnIndex = Coordinate::columnIndexFromString($lastColumn);
179✔
1836
        $numberRows = $lastRow - $firstRow;
179✔
1837
        $numberColumns = $lastColumnIndex - $firstColumnIndex;
179✔
1838

1839
        if ($numberRows === 1 && $numberColumns === 1) {
179✔
1840
            return $this;
36✔
1841
        }
1842

1843
        // create upper left cell if it does not already exist
1844
        $upperLeft = "{$firstColumn}{$firstRow}";
172✔
1845
        if (!$this->cellExists($upperLeft)) {
172✔
1846
            $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL);
37✔
1847
        }
1848

1849
        if ($behaviour !== self::MERGE_CELL_CONTENT_HIDE) {
172✔
1850
            // Blank out the rest of the cells in the range (if they exist)
1851
            if ($numberRows > $numberColumns) {
61✔
1852
                $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft, $behaviour);
20✔
1853
            } else {
1854
                $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft, $behaviour);
41✔
1855
            }
1856
        }
1857

1858
        return $this;
172✔
1859
    }
1860

1861
    private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
20✔
1862
    {
1863
        $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
20✔
1864
            ? [$this->getCell($upperLeft)->getFormattedValue()]
1✔
1865
            : [];
19✔
1866

1867
        foreach ($this->getColumnIterator($firstColumn, $lastColumn) as $column) {
20✔
1868
            $iterator = $column->getCellIterator($firstRow);
20✔
1869
            $iterator->setIterateOnlyExistingCells(true);
20✔
1870
            foreach ($iterator as $cell) {
20✔
1871
                $row = $cell->getRow();
20✔
1872
                if ($row > $lastRow) {
20✔
1873
                    break;
8✔
1874
                }
1875
                $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue);
20✔
1876
            }
1877
        }
1878

1879
        if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
20✔
1880
            /** @var string[] $leftCellValue */
1881
            $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
1✔
1882
        }
1883
    }
1884

1885
    private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
41✔
1886
    {
1887
        $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
41✔
1888
            ? [$this->getCell($upperLeft)->getFormattedValue()]
4✔
1889
            : [];
37✔
1890

1891
        foreach ($this->getRowIterator($firstRow, $lastRow) as $row) {
41✔
1892
            $iterator = $row->getCellIterator($firstColumn);
41✔
1893
            $iterator->setIterateOnlyExistingCells(true);
41✔
1894
            foreach ($iterator as $cell) {
41✔
1895
                $column = $cell->getColumn();
41✔
1896
                $columnIndex = Coordinate::columnIndexFromString($column);
41✔
1897
                if ($columnIndex > $lastColumnIndex) {
41✔
1898
                    break;
9✔
1899
                }
1900
                $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue);
41✔
1901
            }
1902
        }
1903

1904
        if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
41✔
1905
            /** @var string[] $leftCellValue */
1906
            $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
4✔
1907
        }
1908
    }
1909

1910
    /**
1911
     * @param mixed[] $leftCellValue
1912
     *
1913
     * @return mixed[]
1914
     */
1915
    public function mergeCellBehaviour(Cell $cell, string $upperLeft, string $behaviour, array $leftCellValue): array
61✔
1916
    {
1917
        if ($cell->getCoordinate() !== $upperLeft) {
61✔
1918
            Calculation::getInstance($cell->getWorksheet()->getParentOrThrow())->flushInstance();
25✔
1919
            if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
25✔
1920
                $cellValue = $cell->getFormattedValue();
5✔
1921
                if ($cellValue !== '') {
5✔
1922
                    $leftCellValue[] = $cellValue;
5✔
1923
                }
1924
            }
1925
            $cell->setValueExplicit(null, DataType::TYPE_NULL);
25✔
1926
        }
1927

1928
        return $leftCellValue;
61✔
1929
    }
1930

1931
    /**
1932
     * Remove merge on a cell range.
1933
     *
1934
     * @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'
1935
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1936
     *              or an AddressRange.
1937
     *
1938
     * @return $this
1939
     */
1940
    public function unmergeCells(AddressRange|string|array $range): static
23✔
1941
    {
1942
        $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range));
23✔
1943

1944
        if (str_contains($range, ':')) {
23✔
1945
            if (isset($this->mergeCells[$range])) {
22✔
1946
                unset($this->mergeCells[$range]);
22✔
1947
            } else {
UNCOV
1948
                throw new Exception('Cell range ' . $range . ' not known as merged.');
×
1949
            }
1950
        } else {
1951
            throw new Exception('Merge can only be removed from a range of cells.');
1✔
1952
        }
1953

1954
        return $this;
22✔
1955
    }
1956

1957
    /**
1958
     * Get merge cells array.
1959
     *
1960
     * @return string[]
1961
     */
1962
    public function getMergeCells(): array
1,363✔
1963
    {
1964
        return $this->mergeCells;
1,363✔
1965
    }
1966

1967
    /**
1968
     * Set merge cells array for the entire sheet. Use instead mergeCells() to merge
1969
     * a single cell range.
1970
     *
1971
     * @param string[] $mergeCells
1972
     *
1973
     * @return $this
1974
     */
1975
    public function setMergeCells(array $mergeCells): static
127✔
1976
    {
1977
        $this->mergeCells = $mergeCells;
127✔
1978

1979
        return $this;
127✔
1980
    }
1981

1982
    /**
1983
     * Set protection on a cell or cell range.
1984
     *
1985
     * @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'
1986
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1987
     *              or a CellAddress or AddressRange object.
1988
     * @param string $password Password to unlock the protection
1989
     * @param bool $alreadyHashed If the password has already been hashed, set this to true
1990
     *
1991
     * @return $this
1992
     */
1993
    public function protectCells(AddressRange|CellAddress|int|string|array $range, string $password = '', bool $alreadyHashed = false, string $name = '', string $securityDescriptor = ''): static
28✔
1994
    {
1995
        $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
28✔
1996

1997
        if (!$alreadyHashed && $password !== '') {
28✔
1998
            $password = Shared\PasswordHasher::hashPassword($password);
24✔
1999
        }
2000
        $this->protectedCells[$range] = new ProtectedRange($range, $password, $name, $securityDescriptor);
28✔
2001

2002
        return $this;
28✔
2003
    }
2004

2005
    /**
2006
     * Remove protection on a cell or cell range.
2007
     *
2008
     * @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'
2009
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2010
     *              or a CellAddress or AddressRange object.
2011
     *
2012
     * @return $this
2013
     */
2014
    public function unprotectCells(AddressRange|CellAddress|int|string|array $range): static
22✔
2015
    {
2016
        $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
22✔
2017

2018
        if (isset($this->protectedCells[$range])) {
22✔
2019
            unset($this->protectedCells[$range]);
21✔
2020
        } else {
2021
            throw new Exception('Cell range ' . $range . ' not known as protected.');
1✔
2022
        }
2023

2024
        return $this;
21✔
2025
    }
2026

2027
    /**
2028
     * Get protected cells.
2029
     *
2030
     * @return ProtectedRange[]
2031
     */
2032
    public function getProtectedCellRanges(): array
703✔
2033
    {
2034
        return $this->protectedCells;
703✔
2035
    }
2036

2037
    /**
2038
     * Get Autofilter.
2039
     */
2040
    public function getAutoFilter(): AutoFilter
921✔
2041
    {
2042
        return $this->autoFilter;
921✔
2043
    }
2044

2045
    /**
2046
     * Set AutoFilter.
2047
     *
2048
     * @param AddressRange<CellAddress>|AddressRange<int>|AddressRange<string>|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|AutoFilter|string $autoFilterOrRange
2049
     *            A simple string containing a Cell range like 'A1:E10' is permitted for backward compatibility
2050
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2051
     *              or an AddressRange.
2052
     *
2053
     * @return $this
2054
     */
2055
    public function setAutoFilter(AddressRange|string|array|AutoFilter $autoFilterOrRange): static
21✔
2056
    {
2057
        if (is_object($autoFilterOrRange) && ($autoFilterOrRange instanceof AutoFilter)) {
21✔
UNCOV
2058
            $this->autoFilter = $autoFilterOrRange;
×
2059
        } else {
2060
            $cellRange = Functions::trimSheetFromCellReference(Validations::validateCellRange($autoFilterOrRange));
21✔
2061

2062
            $this->autoFilter->setRange($cellRange);
21✔
2063
        }
2064

2065
        return $this;
21✔
2066
    }
2067

2068
    /**
2069
     * Remove autofilter.
2070
     */
2071
    public function removeAutoFilter(): self
1✔
2072
    {
2073
        $this->autoFilter->setRange('');
1✔
2074

2075
        return $this;
1✔
2076
    }
2077

2078
    /**
2079
     * Get collection of Tables.
2080
     *
2081
     * @return ArrayObject<int, Table>
2082
     */
2083
    public function getTableCollection(): ArrayObject
10,930✔
2084
    {
2085
        return $this->tableCollection;
10,930✔
2086
    }
2087

2088
    /**
2089
     * Add Table.
2090
     *
2091
     * @return $this
2092
     */
2093
    public function addTable(Table $table): self
105✔
2094
    {
2095
        $table->setWorksheet($this);
105✔
2096
        $this->tableCollection[] = $table;
105✔
2097

2098
        return $this;
105✔
2099
    }
2100

2101
    /**
2102
     * @return string[] array of Table names
2103
     */
2104
    public function getTableNames(): array
1✔
2105
    {
2106
        $tableNames = [];
1✔
2107

2108
        foreach ($this->tableCollection as $table) {
1✔
2109
            /** @var Table $table */
2110
            $tableNames[] = $table->getName();
1✔
2111
        }
2112

2113
        return $tableNames;
1✔
2114
    }
2115

2116
    /**
2117
     * @param string $name the table name to search
2118
     *
2119
     * @return null|Table The table from the tables collection, or null if not found
2120
     */
2121
    public function getTableByName(string $name): ?Table
97✔
2122
    {
2123
        $tableIndex = $this->getTableIndexByName($name);
97✔
2124

2125
        return ($tableIndex === null) ? null : $this->tableCollection[$tableIndex];
97✔
2126
    }
2127

2128
    /**
2129
     * @param string $name the table name to search
2130
     *
2131
     * @return null|int The index of the located table in the tables collection, or null if not found
2132
     */
2133
    protected function getTableIndexByName(string $name): ?int
98✔
2134
    {
2135
        $name = StringHelper::strToUpper($name);
98✔
2136
        foreach ($this->tableCollection as $index => $table) {
98✔
2137
            /** @var Table $table */
2138
            if (StringHelper::strToUpper($table->getName()) === $name) {
63✔
2139
                return $index;
62✔
2140
            }
2141
        }
2142

2143
        return null;
41✔
2144
    }
2145

2146
    /**
2147
     * Remove Table by name.
2148
     *
2149
     * @param string $name Table name
2150
     *
2151
     * @return $this
2152
     */
2153
    public function removeTableByName(string $name): self
1✔
2154
    {
2155
        $tableIndex = $this->getTableIndexByName($name);
1✔
2156

2157
        if ($tableIndex !== null) {
1✔
2158
            unset($this->tableCollection[$tableIndex]);
1✔
2159
        }
2160

2161
        return $this;
1✔
2162
    }
2163

2164
    /**
2165
     * Remove collection of Tables.
2166
     */
2167
    public function removeTableCollection(): self
1✔
2168
    {
2169
        $this->tableCollection = new ArrayObject();
1✔
2170

2171
        return $this;
1✔
2172
    }
2173

2174
    /**
2175
     * Get Freeze Pane.
2176
     */
2177
    public function getFreezePane(): ?string
317✔
2178
    {
2179
        return $this->freezePane;
317✔
2180
    }
2181

2182
    /**
2183
     * Freeze Pane.
2184
     *
2185
     * Examples:
2186
     *
2187
     *     - A2 will freeze the rows above cell A2 (i.e row 1)
2188
     *     - B1 will freeze the columns to the left of cell B1 (i.e column A)
2189
     *     - B2 will freeze the rows above and to the left of cell B2 (i.e row 1 and column A)
2190
     *
2191
     * @param null|array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
2192
     *            or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2193
     *        Passing a null value for this argument will clear any existing freeze pane for this worksheet.
2194
     * @param null|array{0: int, 1: int}|CellAddress|string $topLeftCell default position of the right bottom pane
2195
     *            Coordinate of the cell as a string, eg: 'C5'; or as an array of [$columnIndex, $row] (e.g. [3, 5]),
2196
     *            or a CellAddress object.
2197
     *
2198
     * @return $this
2199
     */
2200
    public function freezePane(null|CellAddress|string|array $coordinate, null|CellAddress|string|array $topLeftCell = null, bool $frozenSplit = false): static
50✔
2201
    {
2202
        $this->panes = [
50✔
2203
            'bottomRight' => null,
50✔
2204
            'bottomLeft' => null,
50✔
2205
            'topRight' => null,
50✔
2206
            'topLeft' => null,
50✔
2207
        ];
50✔
2208
        $cellAddress = ($coordinate !== null)
50✔
2209
            ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate))
50✔
2210
            : null;
1✔
2211
        if ($cellAddress !== null && Coordinate::coordinateIsRange($cellAddress)) {
50✔
2212
            throw new Exception('Freeze pane can not be set on a range of cells.');
1✔
2213
        }
2214
        $topLeftCell = ($topLeftCell !== null)
49✔
2215
            ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($topLeftCell))
37✔
2216
            : null;
37✔
2217

2218
        if ($cellAddress !== null && $topLeftCell === null) {
49✔
2219
            $coordinate = Coordinate::coordinateFromString($cellAddress);
37✔
2220
            $topLeftCell = $coordinate[0] . $coordinate[1];
37✔
2221
        }
2222

2223
        $topLeftCell = "$topLeftCell";
49✔
2224
        $this->paneTopLeftCell = $topLeftCell;
49✔
2225

2226
        $this->freezePane = $cellAddress;
49✔
2227
        $this->topLeftCell = $topLeftCell;
49✔
2228
        if ($cellAddress === null) {
49✔
2229
            $this->paneState = '';
1✔
2230
            $this->xSplit = $this->ySplit = 0;
1✔
2231
            $this->activePane = '';
1✔
2232
        } else {
2233
            $coordinates = Coordinate::indexesFromString($cellAddress);
49✔
2234
            $this->xSplit = $coordinates[0] - 1;
49✔
2235
            $this->ySplit = $coordinates[1] - 1;
49✔
2236
            if ($this->xSplit > 0 || $this->ySplit > 0) {
49✔
2237
                $this->paneState = $frozenSplit ? self::PANE_FROZENSPLIT : self::PANE_FROZEN;
48✔
2238
                $this->setSelectedCellsActivePane();
48✔
2239
            } else {
2240
                $this->paneState = '';
1✔
2241
                $this->freezePane = null;
1✔
2242
                $this->activePane = '';
1✔
2243
            }
2244
        }
2245

2246
        return $this;
49✔
2247
    }
2248

2249
    public function setTopLeftCell(string $topLeftCell): self
61✔
2250
    {
2251
        $this->topLeftCell = $topLeftCell;
61✔
2252

2253
        return $this;
61✔
2254
    }
2255

2256
    /**
2257
     * Unfreeze Pane.
2258
     *
2259
     * @return $this
2260
     */
2261
    public function unfreezePane(): static
1✔
2262
    {
2263
        return $this->freezePane(null);
1✔
2264
    }
2265

2266
    /**
2267
     * Get the default position of the right bottom pane.
2268
     */
2269
    public function getTopLeftCell(): ?string
525✔
2270
    {
2271
        return $this->topLeftCell;
525✔
2272
    }
2273

2274
    public function getPaneTopLeftCell(): string
11✔
2275
    {
2276
        return $this->paneTopLeftCell;
11✔
2277
    }
2278

2279
    public function setPaneTopLeftCell(string $paneTopLeftCell): self
26✔
2280
    {
2281
        $this->paneTopLeftCell = $paneTopLeftCell;
26✔
2282

2283
        return $this;
26✔
2284
    }
2285

2286
    public function usesPanes(): bool
513✔
2287
    {
2288
        return $this->xSplit > 0 || $this->ySplit > 0;
513✔
2289
    }
2290

2291
    public function getPane(string $position): ?Pane
2✔
2292
    {
2293
        return $this->panes[$position] ?? null;
2✔
2294
    }
2295

2296
    public function setPane(string $position, ?Pane $pane): self
47✔
2297
    {
2298
        if (array_key_exists($position, $this->panes)) {
47✔
2299
            $this->panes[$position] = $pane;
47✔
2300
        }
2301

2302
        return $this;
47✔
2303
    }
2304

2305
    /** @return (null|Pane)[] */
2306
    public function getPanes(): array
3✔
2307
    {
2308
        return $this->panes;
3✔
2309
    }
2310

2311
    public function getActivePane(): string
14✔
2312
    {
2313
        return $this->activePane;
14✔
2314
    }
2315

2316
    public function setActivePane(string $activePane): self
49✔
2317
    {
2318
        $this->activePane = array_key_exists($activePane, $this->panes) ? $activePane : '';
49✔
2319

2320
        return $this;
49✔
2321
    }
2322

2323
    public function getXSplit(): int
11✔
2324
    {
2325
        return $this->xSplit;
11✔
2326
    }
2327

2328
    public function setXSplit(int $xSplit): self
11✔
2329
    {
2330
        $this->xSplit = $xSplit;
11✔
2331
        if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
11✔
2332
            $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
1✔
2333
        }
2334

2335
        return $this;
11✔
2336
    }
2337

2338
    public function getYSplit(): int
11✔
2339
    {
2340
        return $this->ySplit;
11✔
2341
    }
2342

2343
    public function setYSplit(int $ySplit): self
26✔
2344
    {
2345
        $this->ySplit = $ySplit;
26✔
2346
        if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
26✔
2347
            $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
1✔
2348
        }
2349

2350
        return $this;
26✔
2351
    }
2352

2353
    public function getPaneState(): string
30✔
2354
    {
2355
        return $this->paneState;
30✔
2356
    }
2357

2358
    public const PANE_FROZEN = 'frozen';
2359
    public const PANE_FROZENSPLIT = 'frozenSplit';
2360
    public const PANE_SPLIT = 'split';
2361
    private const VALIDPANESTATE = [self::PANE_FROZEN, self::PANE_SPLIT, self::PANE_FROZENSPLIT];
2362
    private const VALIDFROZENSTATE = [self::PANE_FROZEN, self::PANE_FROZENSPLIT];
2363

2364
    public function setPaneState(string $paneState): self
26✔
2365
    {
2366
        $this->paneState = in_array($paneState, self::VALIDPANESTATE, true) ? $paneState : '';
26✔
2367
        if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
26✔
2368
            $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
25✔
2369
        } else {
2370
            $this->freezePane = null;
3✔
2371
        }
2372

2373
        return $this;
26✔
2374
    }
2375

2376
    /**
2377
     * Insert a new row, updating all possible related data.
2378
     *
2379
     * @param int $before Insert before this row number
2380
     * @param int $numberOfRows Number of new rows to insert
2381
     *
2382
     * @return $this
2383
     */
2384
    public function insertNewRowBefore(int $before, int $numberOfRows = 1): static
43✔
2385
    {
2386
        if ($before >= 1) {
43✔
2387
            $objReferenceHelper = ReferenceHelper::getInstance();
42✔
2388
            $objReferenceHelper->insertNewBefore('A' . $before, 0, $numberOfRows, $this);
42✔
2389
        } else {
2390
            throw new Exception('Rows can only be inserted before at least row 1.');
1✔
2391
        }
2392

2393
        return $this;
42✔
2394
    }
2395

2396
    /**
2397
     * Insert a new column, updating all possible related data.
2398
     *
2399
     * @param string $before Insert before this column Name, eg: 'A'
2400
     * @param int $numberOfColumns Number of new columns to insert
2401
     *
2402
     * @return $this
2403
     */
2404
    public function insertNewColumnBefore(string $before, int $numberOfColumns = 1): static
51✔
2405
    {
2406
        if (!is_numeric($before)) {
51✔
2407
            $objReferenceHelper = ReferenceHelper::getInstance();
50✔
2408
            $objReferenceHelper->insertNewBefore($before . '1', $numberOfColumns, 0, $this);
50✔
2409
        } else {
2410
            throw new Exception('Column references should not be numeric.');
1✔
2411
        }
2412

2413
        return $this;
50✔
2414
    }
2415

2416
    /**
2417
     * Insert a new column, updating all possible related data.
2418
     *
2419
     * @param int $beforeColumnIndex Insert before this column ID (numeric column coordinate of the cell)
2420
     * @param int $numberOfColumns Number of new columns to insert
2421
     *
2422
     * @return $this
2423
     */
2424
    public function insertNewColumnBeforeByIndex(int $beforeColumnIndex, int $numberOfColumns = 1): static
2✔
2425
    {
2426
        if ($beforeColumnIndex >= 1) {
2✔
2427
            return $this->insertNewColumnBefore(Coordinate::stringFromColumnIndex($beforeColumnIndex), $numberOfColumns);
1✔
2428
        }
2429

2430
        throw new Exception('Columns can only be inserted before at least column A (1).');
1✔
2431
    }
2432

2433
    /**
2434
     * Delete a row, updating all possible related data.
2435
     *
2436
     * @param int $row Remove rows, starting with this row number
2437
     * @param int $numberOfRows Number of rows to remove
2438
     *
2439
     * @return $this
2440
     */
2441
    public function removeRow(int $row, int $numberOfRows = 1): static
53✔
2442
    {
2443
        if ($row < 1) {
53✔
2444
            throw new Exception('Rows to be deleted should at least start from row 1.');
1✔
2445
        }
2446
        $startRow = $row;
52✔
2447
        $endRow = $startRow + $numberOfRows - 1;
52✔
2448
        $removeKeys = [];
52✔
2449
        $addKeys = [];
52✔
2450
        foreach ($this->mergeCells as $key => $value) {
52✔
2451
            if (
2452
                Preg::isMatch(
21✔
2453
                    '/^([a-z]{1,3})(\d+):([a-z]{1,3})(\d+)/i',
21✔
2454
                    $key,
21✔
2455
                    $matches
21✔
2456
                )
21✔
2457
            ) {
2458
                $startMergeInt = (int) $matches[2];
21✔
2459
                $endMergeInt = (int) $matches[4];
21✔
2460
                if ($startMergeInt >= $startRow) {
21✔
2461
                    if ($startMergeInt <= $endRow) {
21✔
2462
                        $removeKeys[] = $key;
3✔
2463
                    }
2464
                } elseif ($endMergeInt >= $startRow) {
1✔
2465
                    if ($endMergeInt <= $endRow) {
1✔
2466
                        $temp = $endMergeInt - 1;
1✔
2467
                        $removeKeys[] = $key;
1✔
2468
                        if ($temp !== $startMergeInt) {
1✔
2469
                            $temp3 = $matches[1] . $matches[2] . ':' . $matches[3] . $temp;
1✔
2470
                            $addKeys[] = $temp3;
1✔
2471
                        }
2472
                    }
2473
                }
2474
            }
2475
        }
2476
        foreach ($removeKeys as $key) {
52✔
2477
            unset($this->mergeCells[$key]);
3✔
2478
        }
2479
        foreach ($addKeys as $key) {
52✔
2480
            $this->mergeCells[$key] = $key;
1✔
2481
        }
2482

2483
        $holdRowDimensions = $this->removeRowDimensions($row, $numberOfRows);
52✔
2484
        $highestRow = $this->getHighestDataRow();
52✔
2485
        $removedRowsCounter = 0;
52✔
2486

2487
        for ($r = 0; $r < $numberOfRows; ++$r) {
52✔
2488
            if ($row + $r <= $highestRow) {
52✔
2489
                $this->cellCollection->removeRow($row + $r);
40✔
2490
                ++$removedRowsCounter;
40✔
2491
            }
2492
        }
2493

2494
        $objReferenceHelper = ReferenceHelper::getInstance();
52✔
2495
        $objReferenceHelper->insertNewBefore('A' . ($row + $numberOfRows), 0, -$numberOfRows, $this);
52✔
2496
        for ($r = 0; $r < $removedRowsCounter; ++$r) {
52✔
2497
            $this->cellCollection->removeRow($highestRow);
40✔
2498
            --$highestRow;
40✔
2499
        }
2500

2501
        $this->rowDimensions = $holdRowDimensions;
52✔
2502

2503
        return $this;
52✔
2504
    }
2505

2506
    /** @return RowDimension[] */
2507
    private function removeRowDimensions(int $row, int $numberOfRows): array
52✔
2508
    {
2509
        $highRow = $row + $numberOfRows - 1;
52✔
2510
        $holdRowDimensions = [];
52✔
2511
        foreach ($this->rowDimensions as $rowDimension) {
52✔
2512
            $num = $rowDimension->getRowIndex();
5✔
2513
            if ($num < $row) {
5✔
2514
                $holdRowDimensions[$num] = $rowDimension;
3✔
2515
            } elseif ($num > $highRow) {
5✔
2516
                $num -= $numberOfRows;
4✔
2517
                $cloneDimension = clone $rowDimension;
4✔
2518
                $cloneDimension->setRowIndex($num);
4✔
2519
                $holdRowDimensions[$num] = $cloneDimension;
4✔
2520
            }
2521
        }
2522

2523
        return $holdRowDimensions;
52✔
2524
    }
2525

2526
    /**
2527
     * Remove a column, updating all possible related data.
2528
     *
2529
     * @param string $column Remove columns starting with this column name, eg: 'A'
2530
     * @param int $numberOfColumns Number of columns to remove
2531
     *
2532
     * @return $this
2533
     */
2534
    public function removeColumn(string $column, int $numberOfColumns = 1): static
43✔
2535
    {
2536
        if (is_numeric($column)) {
43✔
2537
            throw new Exception('Column references should not be numeric.');
1✔
2538
        }
2539
        $startColumnInt = Coordinate::columnIndexFromString($column);
42✔
2540
        $endColumnInt = $startColumnInt + $numberOfColumns - 1;
42✔
2541
        $removeKeys = [];
42✔
2542
        $addKeys = [];
42✔
2543
        foreach ($this->mergeCells as $key => $value) {
42✔
2544
            if (
2545
                Preg::isMatch(
19✔
2546
                    '/^([a-z]{1,3})(\d+):([a-z]{1,3})(\d+)/i',
19✔
2547
                    $key,
19✔
2548
                    $matches
19✔
2549
                )
19✔
2550
            ) {
2551
                $startMergeInt = Coordinate::columnIndexFromString($matches[1]);
19✔
2552
                $endMergeInt = Coordinate::columnIndexFromString($matches[3]);
19✔
2553
                if ($startMergeInt >= $startColumnInt) {
19✔
2554
                    if ($startMergeInt <= $endColumnInt) {
2✔
2555
                        $removeKeys[] = $key;
2✔
2556
                    }
2557
                } elseif ($endMergeInt >= $startColumnInt) {
18✔
2558
                    if ($endMergeInt <= $endColumnInt) {
18✔
2559
                        $temp = Coordinate::columnIndexFromString($matches[3]) - 1;
1✔
2560
                        $temp2 = Coordinate::stringFromColumnIndex($temp);
1✔
2561
                        $removeKeys[] = $key;
1✔
2562
                        if ($temp2 !== $matches[1]) {
1✔
2563
                            $temp3 = $matches[1] . $matches[2] . ':' . $temp2 . $matches[4];
1✔
2564
                            $addKeys[] = $temp3;
1✔
2565
                        }
2566
                    }
2567
                }
2568
            }
2569
        }
2570
        foreach ($removeKeys as $key) {
42✔
2571
            unset($this->mergeCells[$key]);
2✔
2572
        }
2573
        foreach ($addKeys as $key) {
42✔
2574
            $this->mergeCells[$key] = $key;
1✔
2575
        }
2576

2577
        $highestColumn = $this->getHighestDataColumn();
42✔
2578
        $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
42✔
2579
        $pColumnIndex = Coordinate::columnIndexFromString($column);
42✔
2580

2581
        $holdColumnDimensions = $this->removeColumnDimensions($pColumnIndex, $numberOfColumns);
42✔
2582

2583
        $column = Coordinate::stringFromColumnIndex($pColumnIndex + $numberOfColumns);
42✔
2584
        $objReferenceHelper = ReferenceHelper::getInstance();
42✔
2585
        $objReferenceHelper->insertNewBefore($column . '1', -$numberOfColumns, 0, $this);
42✔
2586

2587
        $this->columnDimensions = $holdColumnDimensions;
42✔
2588

2589
        if ($pColumnIndex > $highestColumnIndex) {
42✔
2590
            return $this;
9✔
2591
        }
2592

2593
        $maxPossibleColumnsToBeRemoved = $highestColumnIndex - $pColumnIndex + 1;
33✔
2594

2595
        for ($c = 0, $n = min($maxPossibleColumnsToBeRemoved, $numberOfColumns); $c < $n; ++$c) {
33✔
2596
            $this->cellCollection->removeColumn($highestColumn);
33✔
2597
            $highestColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($highestColumn) - 1);
33✔
2598
        }
2599

2600
        $this->garbageCollect();
33✔
2601

2602
        return $this;
33✔
2603
    }
2604

2605
    /** @return ColumnDimension[] */
2606
    private function removeColumnDimensions(int $pColumnIndex, int $numberOfColumns): array
42✔
2607
    {
2608
        $highCol = $pColumnIndex + $numberOfColumns - 1;
42✔
2609
        $holdColumnDimensions = [];
42✔
2610
        foreach ($this->columnDimensions as $columnDimension) {
42✔
2611
            $num = $columnDimension->getColumnNumeric();
18✔
2612
            if ($num < $pColumnIndex) {
18✔
2613
                $str = $columnDimension->getColumnIndex();
18✔
2614
                $holdColumnDimensions[$str] = $columnDimension;
18✔
2615
            } elseif ($num > $highCol) {
18✔
2616
                $cloneDimension = clone $columnDimension;
18✔
2617
                $cloneDimension->setColumnNumeric($num - $numberOfColumns);
18✔
2618
                $str = $cloneDimension->getColumnIndex();
18✔
2619
                $holdColumnDimensions[$str] = $cloneDimension;
18✔
2620
            }
2621
        }
2622

2623
        return $holdColumnDimensions;
42✔
2624
    }
2625

2626
    /**
2627
     * Remove a column, updating all possible related data.
2628
     *
2629
     * @param int $columnIndex Remove starting with this column Index (numeric column coordinate)
2630
     * @param int $numColumns Number of columns to remove
2631
     *
2632
     * @return $this
2633
     */
2634
    public function removeColumnByIndex(int $columnIndex, int $numColumns = 1): static
3✔
2635
    {
2636
        if ($columnIndex >= 1) {
3✔
2637
            return $this->removeColumn(Coordinate::stringFromColumnIndex($columnIndex), $numColumns);
2✔
2638
        }
2639

2640
        throw new Exception('Columns to be deleted should at least start from column A (1)');
1✔
2641
    }
2642

2643
    /**
2644
     * Show gridlines?
2645
     */
2646
    public function getShowGridlines(): bool
1,156✔
2647
    {
2648
        return $this->showGridlines;
1,156✔
2649
    }
2650

2651
    /**
2652
     * Set show gridlines.
2653
     *
2654
     * @param bool $showGridLines Show gridlines (true/false)
2655
     *
2656
     * @return $this
2657
     */
2658
    public function setShowGridlines(bool $showGridLines): self
930✔
2659
    {
2660
        $this->showGridlines = $showGridLines;
930✔
2661

2662
        return $this;
930✔
2663
    }
2664

2665
    /**
2666
     * Print gridlines?
2667
     */
2668
    public function getPrintGridlines(): bool
1,179✔
2669
    {
2670
        return $this->printGridlines;
1,179✔
2671
    }
2672

2673
    /**
2674
     * Set print gridlines.
2675
     *
2676
     * @param bool $printGridLines Print gridlines (true/false)
2677
     *
2678
     * @return $this
2679
     */
2680
    public function setPrintGridlines(bool $printGridLines): self
608✔
2681
    {
2682
        $this->printGridlines = $printGridLines;
608✔
2683

2684
        return $this;
608✔
2685
    }
2686

2687
    /**
2688
     * Show row and column headers?
2689
     */
2690
    public function getShowRowColHeaders(): bool
589✔
2691
    {
2692
        return $this->showRowColHeaders;
589✔
2693
    }
2694

2695
    /**
2696
     * Set show row and column headers.
2697
     *
2698
     * @param bool $showRowColHeaders Show row and column headers (true/false)
2699
     *
2700
     * @return $this
2701
     */
2702
    public function setShowRowColHeaders(bool $showRowColHeaders): self
445✔
2703
    {
2704
        $this->showRowColHeaders = $showRowColHeaders;
445✔
2705

2706
        return $this;
445✔
2707
    }
2708

2709
    /**
2710
     * Show summary below? (Row/Column outlining).
2711
     */
2712
    public function getShowSummaryBelow(): bool
590✔
2713
    {
2714
        return $this->showSummaryBelow;
590✔
2715
    }
2716

2717
    /**
2718
     * Set show summary below.
2719
     *
2720
     * @param bool $showSummaryBelow Show summary below (true/false)
2721
     *
2722
     * @return $this
2723
     */
2724
    public function setShowSummaryBelow(bool $showSummaryBelow): self
444✔
2725
    {
2726
        $this->showSummaryBelow = $showSummaryBelow;
444✔
2727

2728
        return $this;
444✔
2729
    }
2730

2731
    /**
2732
     * Show summary right? (Row/Column outlining).
2733
     */
2734
    public function getShowSummaryRight(): bool
590✔
2735
    {
2736
        return $this->showSummaryRight;
590✔
2737
    }
2738

2739
    /**
2740
     * Set show summary right.
2741
     *
2742
     * @param bool $showSummaryRight Show summary right (true/false)
2743
     *
2744
     * @return $this
2745
     */
2746
    public function setShowSummaryRight(bool $showSummaryRight): self
444✔
2747
    {
2748
        $this->showSummaryRight = $showSummaryRight;
444✔
2749

2750
        return $this;
444✔
2751
    }
2752

2753
    /**
2754
     * Get comments.
2755
     *
2756
     * @return Comment[]
2757
     */
2758
    public function getComments(): array
1,232✔
2759
    {
2760
        return $this->comments;
1,232✔
2761
    }
2762

2763
    /**
2764
     * Set comments array for the entire sheet.
2765
     *
2766
     * @param Comment[] $comments
2767
     *
2768
     * @return $this
2769
     */
2770
    public function setComments(array $comments): self
127✔
2771
    {
2772
        $this->comments = $comments;
127✔
2773

2774
        return $this;
127✔
2775
    }
2776

2777
    /**
2778
     * Remove comment from cell.
2779
     *
2780
     * @param array{0: int, 1: int}|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
2781
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2782
     *
2783
     * @return $this
2784
     */
2785
    public function removeComment(CellAddress|string|array $cellCoordinate): self
58✔
2786
    {
2787
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
58✔
2788

2789
        if (Coordinate::coordinateIsRange($cellAddress)) {
58✔
2790
            throw new Exception('Cell coordinate string can not be a range of cells.');
1✔
2791
        } elseif (str_contains($cellAddress, '$')) {
57✔
2792
            throw new Exception('Cell coordinate string must not be absolute.');
1✔
2793
        } elseif ($cellAddress == '') {
56✔
2794
            throw new Exception('Cell coordinate can not be zero-length string.');
1✔
2795
        }
2796
        // Check if we have a comment for this cell and delete it
2797
        if (isset($this->comments[$cellAddress])) {
55✔
2798
            unset($this->comments[$cellAddress]);
3✔
2799
        }
2800

2801
        return $this;
55✔
2802
    }
2803

2804
    /**
2805
     * Get comment for cell.
2806
     *
2807
     * @param array{0: int, 1: int}|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
2808
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2809
     */
2810
    public function getComment(CellAddress|string|array $cellCoordinate, bool $attachNew = true): Comment
120✔
2811
    {
2812
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
120✔
2813

2814
        if (Coordinate::coordinateIsRange($cellAddress)) {
120✔
2815
            throw new Exception('Cell coordinate string can not be a range of cells.');
1✔
2816
        } elseif (str_contains($cellAddress, '$')) {
119✔
2817
            throw new Exception('Cell coordinate string must not be absolute.');
1✔
2818
        } elseif ($cellAddress == '') {
118✔
2819
            throw new Exception('Cell coordinate can not be zero-length string.');
1✔
2820
        }
2821

2822
        // Check if we already have a comment for this cell.
2823
        if (isset($this->comments[$cellAddress])) {
117✔
2824
            return $this->comments[$cellAddress];
83✔
2825
        }
2826

2827
        // If not, create a new comment.
2828
        $newComment = new Comment();
117✔
2829
        if ($attachNew) {
117✔
2830
            $this->comments[$cellAddress] = $newComment;
117✔
2831
        }
2832

2833
        return $newComment;
117✔
2834
    }
2835

2836
    /**
2837
     * Get active cell.
2838
     *
2839
     * @return string Example: 'A1'
2840
     */
2841
    public function getActiveCell(): string
10,965✔
2842
    {
2843
        return $this->activeCell;
10,965✔
2844
    }
2845

2846
    /**
2847
     * Get selected cells.
2848
     */
2849
    public function getSelectedCells(): string
11,019✔
2850
    {
2851
        return $this->selectedCells;
11,019✔
2852
    }
2853

2854
    /**
2855
     * Selected cell.
2856
     *
2857
     * @param string $coordinate Cell (i.e. A1)
2858
     *
2859
     * @return $this
2860
     */
2861
    public function setSelectedCell(string $coordinate): static
38✔
2862
    {
2863
        return $this->setSelectedCells($coordinate);
38✔
2864
    }
2865

2866
    /**
2867
     * Select a range of cells.
2868
     *
2869
     * @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'
2870
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2871
     *              or a CellAddress or AddressRange object.
2872
     *
2873
     * @return $this
2874
     */
2875
    public function setSelectedCells(AddressRange|CellAddress|int|string|array $coordinate): static
10,968✔
2876
    {
2877
        if (is_string($coordinate)) {
10,968✔
2878
            $coordinate = Validations::definedNameToCoordinate($coordinate, $this);
10,968✔
2879
        }
2880
        $coordinate = Validations::validateCellOrCellRange($coordinate);
10,968✔
2881

2882
        if (Coordinate::coordinateIsRange($coordinate)) {
10,968✔
2883
            [$first] = Coordinate::splitRange($coordinate);
541✔
2884
            $this->activeCell = $first[0];
541✔
2885
        } else {
2886
            $this->activeCell = $coordinate;
10,937✔
2887
        }
2888
        $this->selectedCells = $coordinate;
10,968✔
2889
        $this->setSelectedCellsActivePane();
10,968✔
2890

2891
        return $this;
10,968✔
2892
    }
2893

2894
    private function setSelectedCellsActivePane(): void
10,969✔
2895
    {
2896
        if (!empty($this->freezePane)) {
10,969✔
2897
            $coordinateC = Coordinate::indexesFromString($this->freezePane);
48✔
2898
            $coordinateT = Coordinate::indexesFromString($this->activeCell);
48✔
2899
            if ($coordinateC[0] === 1) {
48✔
2900
                $activePane = ($coordinateT[1] <= $coordinateC[1]) ? 'topLeft' : 'bottomLeft';
26✔
2901
            } elseif ($coordinateC[1] === 1) {
24✔
2902
                $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'topLeft' : 'topRight';
3✔
2903
            } elseif ($coordinateT[1] <= $coordinateC[1]) {
22✔
2904
                $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'topLeft' : 'topRight';
22✔
2905
            } else {
2906
                $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'bottomLeft' : 'bottomRight';
10✔
2907
            }
2908
            $this->setActivePane($activePane);
48✔
2909
            $this->panes[$activePane] = new Pane($activePane, $this->selectedCells, $this->activeCell);
48✔
2910
        }
2911
    }
2912

2913
    /**
2914
     * Get right-to-left.
2915
     */
2916
    public function getRightToLeft(): bool
1,182✔
2917
    {
2918
        return $this->rightToLeft;
1,182✔
2919
    }
2920

2921
    /**
2922
     * Set right-to-left.
2923
     *
2924
     * @param bool $value Right-to-left true/false
2925
     *
2926
     * @return $this
2927
     */
2928
    public function setRightToLeft(bool $value): static
168✔
2929
    {
2930
        $this->rightToLeft = $value;
168✔
2931

2932
        return $this;
168✔
2933
    }
2934

2935
    /**
2936
     * Fill worksheet from values in array.
2937
     *
2938
     * @param mixed[]|mixed[][] $source Source array
2939
     * @param mixed $nullValue Value in source array that stands for blank cell
2940
     * @param string $startCell Insert array starting from this cell address as the top left coordinate
2941
     * @param bool $strictNullComparison Apply strict comparison when testing for null values in the array
2942
     *
2943
     * @return $this
2944
     */
2945
    public function fromArray(array $source, mixed $nullValue = null, string $startCell = 'A1', bool $strictNullComparison = false): static
907✔
2946
    {
2947
        //    Convert a 1-D array to 2-D (for ease of looping)
2948
        if (!is_array(end($source))) {
907✔
2949
            $source = [$source];
49✔
2950
        }
2951
        /** @var mixed[][] $source */
2952

2953
        // start coordinate
2954
        [$startColumn, $startRow] = Coordinate::coordinateFromString($startCell);
907✔
2955
        $startRow = (int) $startRow;
907✔
2956

2957
        // Loop through $source
2958
        if ($strictNullComparison) {
907✔
2959
            foreach ($source as $rowData) {
410✔
2960
                /** @var string */
2961
                $currentColumn = $startColumn;
410✔
2962
                foreach ($rowData as $cellValue) {
410✔
2963
                    if ($cellValue !== $nullValue) {
410✔
2964
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
410✔
2965
                    }
2966
                    StringHelper::stringIncrement($currentColumn);
410✔
2967
                }
2968
                ++$startRow;
410✔
2969
            }
2970
        } else {
2971
            foreach ($source as $rowData) {
506✔
2972
                $currentColumn = $startColumn;
506✔
2973
                foreach ($rowData as $cellValue) {
506✔
2974
                    if ($cellValue != $nullValue) {
505✔
2975
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
499✔
2976
                    }
2977
                    StringHelper::stringIncrement($currentColumn);
505✔
2978
                }
2979
                ++$startRow;
506✔
2980
            }
2981
        }
2982

2983
        return $this;
907✔
2984
    }
2985

2986
    /**
2987
     * @param bool $calculateFormulas Whether to calculate cell's value if it is a formula.
2988
     * @param null|bool|float|int|RichText|string $nullValue value to use when null
2989
     * @param bool $formatData Whether to format data according to cell's style.
2990
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
2991
     * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
2992
     *
2993
     * @throws Exception
2994
     * @throws \PhpOffice\PhpSpreadsheet\Calculation\Exception
2995
     */
2996
    protected function cellToArray(Cell $cell, bool $calculateFormulas, bool $formatData, mixed $nullValue, bool $lessFloatPrecision = false, $oldCalculatedValue = false): mixed
201✔
2997
    {
2998
        $returnValue = $nullValue;
201✔
2999

3000
        if ($cell->getValue() !== null) {
201✔
3001
            if ($cell->getValue() instanceof RichText) {
201✔
3002
                $returnValue = $cell->getValue()->getPlainText();
4✔
3003
            } elseif ($calculateFormulas) {
201✔
3004
                $returnValue = $cell->getCalculatedValue();
172✔
3005
            } elseif ($oldCalculatedValue && ($cell->getDataType() === DataType::TYPE_FORMULA)) {
35✔
3006
                $returnValue = $cell->getOldCalculatedValue() ?? $cell->getValue();
2✔
3007
            } else {
3008
                $returnValue = $cell->getValue();
35✔
3009
            }
3010

3011
            if ($formatData) {
201✔
3012
                $style = $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex());
128✔
3013
                /** @var null|bool|float|int|RichText|string */
3014
                $returnValuex = $returnValue;
128✔
3015
                $returnValue = NumberFormat::toFormattedString(
128✔
3016
                    $returnValuex,
128✔
3017
                    $style->getNumberFormat()->getFormatCode() ?? NumberFormat::FORMAT_GENERAL,
128✔
3018
                    lessFloatPrecision: $lessFloatPrecision
128✔
3019
                );
128✔
3020
            }
3021
        }
3022

3023
        return $returnValue;
201✔
3024
    }
3025

3026
    /**
3027
     * Create array from a range of cells.
3028
     *
3029
     * @param null|bool|float|int|RichText|string $nullValue Value returned in the array entry if a cell doesn't exist
3030
     * @param bool $calculateFormulas Should formulas be calculated?
3031
     * @param bool $formatData Should formatting be applied to cell values?
3032
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3033
     *                             True - Return rows and columns indexed by their actual row and column IDs
3034
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3035
     *                            True - Don't return values for rows/columns that are defined as hidden.
3036
     * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3037
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3038
     * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3039
     *
3040
     * @return mixed[][]
3041
     */
3042
    public function rangeToArray(
166✔
3043
        string $range,
3044
        mixed $nullValue = null,
3045
        bool $calculateFormulas = true,
3046
        bool $formatData = true,
3047
        bool $returnCellRef = false,
3048
        bool $ignoreHidden = false,
3049
        bool $reduceArrays = false,
3050
        bool $lessFloatPrecision = false,
3051
        bool $oldCalculatedValue = false,
3052
    ): array {
3053
        $returnValue = [];
166✔
3054

3055
        // Loop through rows
3056
        foreach ($this->rangeToArrayYieldRows($range, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue) as $rowRef => $rowArray) {
166✔
3057
            /** @var int $rowRef */
3058
            $returnValue[$rowRef] = $rowArray;
166✔
3059
        }
3060

3061
        // Return
3062
        return $returnValue;
166✔
3063
    }
3064

3065
    /**
3066
     * Create array from a multiple ranges of cells. (such as A1:A3,A15,B17:C17).
3067
     *
3068
     * @param null|bool|float|int|RichText|string $nullValue Value returned in the array entry if a cell doesn't exist
3069
     * @param bool $calculateFormulas Should formulas be calculated?
3070
     * @param bool $formatData Should formatting be applied to cell values?
3071
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3072
     *                             True - Return rows and columns indexed by their actual row and column IDs
3073
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3074
     *                            True - Don't return values for rows/columns that are defined as hidden.
3075
     * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3076
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3077
     * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3078
     *
3079
     * @return mixed[][]
3080
     */
3081
    public function rangesToArray(
6✔
3082
        string $ranges,
3083
        mixed $nullValue = null,
3084
        bool $calculateFormulas = true,
3085
        bool $formatData = true,
3086
        bool $returnCellRef = false,
3087
        bool $ignoreHidden = false,
3088
        bool $reduceArrays = false,
3089
        bool $lessFloatPrecision = false,
3090
        bool $oldCalculatedValue = false,
3091
    ): array {
3092
        $returnValue = [];
6✔
3093

3094
        $parts = explode(',', $ranges);
6✔
3095
        foreach ($parts as $part) {
6✔
3096
            // Loop through rows
3097
            foreach ($this->rangeToArrayYieldRows($part, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue) as $rowRef => $rowArray) {
6✔
3098
                /** @var int $rowRef */
3099
                $returnValue[$rowRef] = $rowArray;
6✔
3100
            }
3101
        }
3102

3103
        // Return
3104
        return $returnValue;
6✔
3105
    }
3106

3107
    /**
3108
     * Create array from a range of cells, yielding each row in turn.
3109
     *
3110
     * @param null|bool|float|int|RichText|string $nullValue Value returned in the array entry if a cell doesn't exist
3111
     * @param bool $calculateFormulas Should formulas be calculated?
3112
     * @param bool $formatData Should formatting be applied to cell values?
3113
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3114
     *                             True - Return rows and columns indexed by their actual row and column IDs
3115
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3116
     *                            True - Don't return values for rows/columns that are defined as hidden.
3117
     * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3118
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3119
     * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3120
     *
3121
     * @return Generator<array<mixed>>
3122
     */
3123
    public function rangeToArrayYieldRows(
201✔
3124
        string $range,
3125
        mixed $nullValue = null,
3126
        bool $calculateFormulas = true,
3127
        bool $formatData = true,
3128
        bool $returnCellRef = false,
3129
        bool $ignoreHidden = false,
3130
        bool $reduceArrays = false,
3131
        bool $lessFloatPrecision = false,
3132
        bool $oldCalculatedValue = false,
3133
    ) {
3134
        $range = Validations::validateCellOrCellRange($range);
201✔
3135

3136
        //    Identify the range that we need to extract from the worksheet
3137
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range);
201✔
3138
        $minCol = Coordinate::stringFromColumnIndex($rangeStart[0]);
201✔
3139
        $minRow = $rangeStart[1];
201✔
3140
        $maxCol = Coordinate::stringFromColumnIndex($rangeEnd[0]);
201✔
3141
        $maxRow = $rangeEnd[1];
201✔
3142
        $minColInt = $rangeStart[0];
201✔
3143
        $maxColInt = $rangeEnd[0];
201✔
3144

3145
        StringHelper::stringIncrement($maxCol);
201✔
3146
        /** @var array<string, bool> */
3147
        $hiddenColumns = [];
201✔
3148
        $nullRow = $this->buildNullRow($nullValue, $minCol, $maxCol, $returnCellRef, $ignoreHidden, $hiddenColumns);
201✔
3149
        $hideColumns = !empty($hiddenColumns);
201✔
3150

3151
        $keys = $this->cellCollection->getSortedCoordinatesInt();
201✔
3152
        $keyIndex = 0;
201✔
3153
        $keysCount = count($keys);
201✔
3154
        // Loop through rows
3155
        for ($row = $minRow; $row <= $maxRow; ++$row) {
201✔
3156
            if (($ignoreHidden === true) && ($this->isRowVisible($row) === false)) {
201✔
3157
                continue;
4✔
3158
            }
3159
            $rowRef = $returnCellRef ? $row : ($row - $minRow);
201✔
3160
            $returnValue = $nullRow;
201✔
3161

3162
            $index = ($row - 1) * AddressRange::MAX_COLUMN_INT + 1;
201✔
3163
            $indexPlus = $index + AddressRange::MAX_COLUMN_INT - 1;
201✔
3164

3165
            // Binary search to quickly approach the correct index
3166
            $keyIndex = intdiv($keysCount, 2);
201✔
3167
            $boundLow = 0;
201✔
3168
            $boundHigh = $keysCount - 1;
201✔
3169
            while ($boundLow <= $boundHigh) {
201✔
3170
                $keyIndex = intdiv($boundLow + $boundHigh, 2);
201✔
3171
                if ($keys[$keyIndex] < $index) {
201✔
3172
                    $boundLow = $keyIndex + 1;
167✔
3173
                } elseif ($keys[$keyIndex] > $index) {
201✔
3174
                    $boundHigh = $keyIndex - 1;
183✔
3175
                } else {
3176
                    break;
193✔
3177
                }
3178
            }
3179

3180
            // Realign to the proper index value
3181
            while ($keyIndex > 0 && $keys[$keyIndex] > $index) {
201✔
3182
                --$keyIndex;
14✔
3183
            }
3184
            while ($keyIndex < $keysCount && $keys[$keyIndex] < $index) {
201✔
3185
                ++$keyIndex;
21✔
3186
            }
3187

3188
            while ($keyIndex < $keysCount && $keys[$keyIndex] <= $indexPlus) {
201✔
3189
                $key = $keys[$keyIndex];
201✔
3190
                $thisRow = intdiv($key - 1, AddressRange::MAX_COLUMN_INT) + 1;
201✔
3191
                $thisCol = ($key % AddressRange::MAX_COLUMN_INT) ?: AddressRange::MAX_COLUMN_INT;
201✔
3192
                if ($thisCol >= $minColInt && $thisCol <= $maxColInt) {
201✔
3193
                    $col = Coordinate::stringFromColumnIndex($thisCol);
201✔
3194
                    if ($hideColumns === false || !isset($hiddenColumns[$col])) {
201✔
3195
                        $columnRef = $returnCellRef ? $col : ($thisCol - $minColInt);
201✔
3196
                        $cell = $this->cellCollection->get("{$col}{$thisRow}");
201✔
3197
                        if ($cell !== null) {
201✔
3198
                            $value = $this->cellToArray($cell, $calculateFormulas, $formatData, $nullValue, lessFloatPrecision: $lessFloatPrecision, oldCalculatedValue: $oldCalculatedValue);
201✔
3199
                            if ($reduceArrays) {
201✔
3200
                                while (is_array($value)) {
21✔
3201
                                    $value = array_shift($value);
19✔
3202
                                }
3203
                            }
3204
                            if ($value !== $nullValue) {
201✔
3205
                                $returnValue[$columnRef] = $value;
201✔
3206
                            }
3207
                        }
3208
                    }
3209
                }
3210
                ++$keyIndex;
201✔
3211
            }
3212

3213
            yield $rowRef => $returnValue;
201✔
3214
        }
3215
    }
3216

3217
    /**
3218
     * Prepare a row data filled with null values to deduplicate the memory areas for empty rows.
3219
     *
3220
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
3221
     * @param string $minCol Start column of the range
3222
     * @param string $maxCol End column of the range
3223
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3224
     *                              True - Return rows and columns indexed by their actual row and column IDs
3225
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3226
     *                             True - Don't return values for rows/columns that are defined as hidden.
3227
     * @param array<string, bool> $hiddenColumns
3228
     *
3229
     * @return mixed[]
3230
     */
3231
    private function buildNullRow(
201✔
3232
        mixed $nullValue,
3233
        string $minCol,
3234
        string $maxCol,
3235
        bool $returnCellRef,
3236
        bool $ignoreHidden,
3237
        array &$hiddenColumns
3238
    ): array {
3239
        $nullRow = [];
201✔
3240
        $c = -1;
201✔
3241
        for ($col = $minCol; $col !== $maxCol; StringHelper::stringIncrement($col)) {
201✔
3242
            if ($ignoreHidden === true && $this->columnDimensionExists($col) && $this->getColumnDimension($col)->getVisible() === false) {
201✔
3243
                $hiddenColumns[$col] = true;
2✔
3244
            } else {
3245
                $columnRef = $returnCellRef ? $col : ++$c;
201✔
3246
                $nullRow[$columnRef] = $nullValue;
201✔
3247
            }
3248
        }
3249

3250
        return $nullRow;
201✔
3251
    }
3252

3253
    private function validateNamedRange(string $definedName, bool $returnNullIfInvalid = false): ?DefinedName
19✔
3254
    {
3255
        $namedRange = DefinedName::resolveName($definedName, $this);
19✔
3256
        if ($namedRange === null) {
19✔
3257
            if ($returnNullIfInvalid) {
6✔
3258
                return null;
5✔
3259
            }
3260

3261
            throw new Exception('Named Range ' . $definedName . ' does not exist.');
1✔
3262
        }
3263

3264
        if ($namedRange->isFormula()) {
13✔
3265
            if ($returnNullIfInvalid) {
×
UNCOV
3266
                return null;
×
3267
            }
3268

UNCOV
3269
            throw new Exception('Defined Named ' . $definedName . ' is a formula, not a range or cell.');
×
3270
        }
3271

3272
        if ($namedRange->getLocalOnly()) {
13✔
3273
            $worksheet = $namedRange->getWorksheet();
2✔
3274
            if ($worksheet === null || $this !== $worksheet) {
2✔
3275
                if ($returnNullIfInvalid) {
×
UNCOV
3276
                    return null;
×
3277
                }
3278

3279
                throw new Exception(
×
3280
                    'Named range ' . $definedName . ' is not accessible from within sheet ' . $this->getTitle()
×
UNCOV
3281
                );
×
3282
            }
3283
        }
3284

3285
        return $namedRange;
13✔
3286
    }
3287

3288
    /**
3289
     * Create array from a range of cells.
3290
     *
3291
     * @param string $definedName The Named Range that should be returned
3292
     * @param null|bool|float|int|RichText|string $nullValue Value returned in the array entry if a cell doesn't exist
3293
     * @param bool $calculateFormulas Should formulas be calculated?
3294
     * @param bool $formatData Should formatting be applied to cell values?
3295
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3296
     *                             True - Return rows and columns indexed by their actual row and column IDs
3297
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3298
     *                            True - Don't return values for rows/columns that are defined as hidden.
3299
     * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3300
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3301
     * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3302
     *
3303
     * @return mixed[][]
3304
     */
3305
    public function namedRangeToArray(
2✔
3306
        string $definedName,
3307
        mixed $nullValue = null,
3308
        bool $calculateFormulas = true,
3309
        bool $formatData = true,
3310
        bool $returnCellRef = false,
3311
        bool $ignoreHidden = false,
3312
        bool $reduceArrays = false,
3313
        bool $lessFloatPrecision = false,
3314
        bool $oldCalculatedValue = false,
3315
    ): array {
3316
        $retVal = [];
2✔
3317
        $namedRange = $this->validateNamedRange($definedName);
2✔
3318
        if ($namedRange !== null) {
1✔
3319
            $cellRange = ltrim(substr($namedRange->getValue(), (int) strrpos($namedRange->getValue(), '!')), '!');
1✔
3320
            $cellRange = str_replace('$', '', $cellRange);
1✔
3321
            $workSheet = $namedRange->getWorksheet();
1✔
3322
            if ($workSheet !== null) {
1✔
3323
                $retVal = $workSheet->rangeToArray($cellRange, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue);
1✔
3324
            }
3325
        }
3326

3327
        return $retVal;
1✔
3328
    }
3329

3330
    /**
3331
     * Create array from worksheet.
3332
     *
3333
     * @param null|bool|float|int|RichText|string $nullValue Value returned in the array entry if a cell doesn't exist
3334
     * @param bool $calculateFormulas Should formulas be calculated?
3335
     * @param bool $formatData Should formatting be applied to cell values?
3336
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3337
     *                             True - Return rows and columns indexed by their actual row and column IDs
3338
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3339
     *                            True - Don't return values for rows/columns that are defined as hidden.
3340
     * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3341
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3342
     * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3343
     *
3344
     * @return mixed[][]
3345
     */
3346
    public function toArray(
97✔
3347
        mixed $nullValue = null,
3348
        bool $calculateFormulas = true,
3349
        bool $formatData = true,
3350
        bool $returnCellRef = false,
3351
        bool $ignoreHidden = false,
3352
        bool $reduceArrays = false,
3353
        bool $lessFloatPrecision = false,
3354
        bool $oldCalculatedValue = false,
3355
    ): array {
3356
        // Garbage collect...
3357
        $this->garbageCollect();
97✔
3358
        $this->calculateArrays($calculateFormulas);
97✔
3359

3360
        //    Identify the range that we need to extract from the worksheet
3361
        $maxCol = $this->getHighestColumn();
97✔
3362
        $maxRow = $this->getHighestRow();
97✔
3363

3364
        // Return
3365
        return $this->rangeToArray("A1:{$maxCol}{$maxRow}", $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue);
97✔
3366
    }
3367

3368
    /**
3369
     * Get row iterator.
3370
     *
3371
     * @param int $startRow The row number at which to start iterating
3372
     * @param ?int $endRow The row number at which to stop iterating
3373
     */
3374
    public function getRowIterator(int $startRow = 1, ?int $endRow = null): RowIterator
117✔
3375
    {
3376
        return new RowIterator($this, $startRow, $endRow);
117✔
3377
    }
3378

3379
    /**
3380
     * Get column iterator.
3381
     *
3382
     * @param string $startColumn The column address at which to start iterating
3383
     * @param ?string $endColumn The column address at which to stop iterating
3384
     */
3385
    public function getColumnIterator(string $startColumn = 'A', ?string $endColumn = null): ColumnIterator
39✔
3386
    {
3387
        return new ColumnIterator($this, $startColumn, $endColumn);
39✔
3388
    }
3389

3390
    /**
3391
     * Run PhpSpreadsheet garbage collector.
3392
     *
3393
     * @return $this
3394
     */
3395
    public function garbageCollect(): static
1,328✔
3396
    {
3397
        // Flush cache
3398
        $this->cellCollection->get('A1');
1,328✔
3399

3400
        // Lookup highest column and highest row if cells are cleaned
3401
        $colRow = $this->cellCollection->getHighestRowAndColumn();
1,328✔
3402
        $highestRow = $colRow['row'];
1,328✔
3403
        $highestColumn = Coordinate::columnIndexFromString($colRow['column']);
1,328✔
3404

3405
        // Loop through column dimensions
3406
        foreach ($this->columnDimensions as $dimension) {
1,328✔
3407
            $highestColumn = max($highestColumn, Coordinate::columnIndexFromString($dimension->getColumnIndex()));
195✔
3408
        }
3409

3410
        // Loop through row dimensions
3411
        foreach ($this->rowDimensions as $dimension) {
1,328✔
3412
            $highestRow = max($highestRow, $dimension->getRowIndex());
124✔
3413
        }
3414

3415
        // Cache values
3416
        $this->cachedHighestColumn = max(1, $highestColumn);
1,328✔
3417
        /** @var int $highestRow */
3418
        $this->cachedHighestRow = $highestRow;
1,328✔
3419

3420
        // Return
3421
        return $this;
1,328✔
3422
    }
3423

3424
    /**
3425
     * @deprecated 5.2.0 Serves no useful purpose. No replacement.
3426
     *
3427
     * @codeCoverageIgnore
3428
     */
3429
    public function getHashInt(): int
3430
    {
3431
        return spl_object_id($this);
3432
    }
3433

3434
    /**
3435
     * Extract worksheet title from range.
3436
     *
3437
     * Example: extractSheetTitle("testSheet!A1") ==> 'A1'
3438
     * Example: extractSheetTitle("testSheet!A1:C3") ==> 'A1:C3'
3439
     * Example: extractSheetTitle("'testSheet 1'!A1", true) ==> ['testSheet 1', 'A1'];
3440
     * Example: extractSheetTitle("'testSheet 1'!A1:C3", true) ==> ['testSheet 1', 'A1:C3'];
3441
     * Example: extractSheetTitle("A1", true) ==> ['', 'A1'];
3442
     * Example: extractSheetTitle("A1:C3", true) ==> ['', 'A1:C3']
3443
     *
3444
     * @param ?string $range Range to extract title from
3445
     * @param bool $returnRange Return range? (see example)
3446
     *
3447
     * @return ($range is non-empty-string ? ($returnRange is true ? array{0: string, 1: string} : string) : ($returnRange is true ? array{0: null, 1: null} : null))
3448
     */
3449
    public static function extractSheetTitle(?string $range, bool $returnRange = false, bool $unapostrophize = false): array|null|string
11,199✔
3450
    {
3451
        if (empty($range)) {
11,199✔
3452
            return $returnRange ? [null, null] : null;
13✔
3453
        }
3454

3455
        // Sheet title included?
3456
        if (($sep = strrpos($range, '!')) === false) {
11,197✔
3457
            return $returnRange ? ['', $range] : '';
11,168✔
3458
        }
3459

3460
        if ($returnRange) {
1,494✔
3461
            $title = substr($range, 0, $sep);
1,494✔
3462
            if ($unapostrophize) {
1,494✔
3463
                $title = self::unApostrophizeTitle($title);
1,434✔
3464
            }
3465

3466
            return [$title, substr($range, $sep + 1)];
1,494✔
3467
        }
3468

3469
        return substr($range, $sep + 1);
7✔
3470
    }
3471

3472
    public static function unApostrophizeTitle(?string $title): string
1,448✔
3473
    {
3474
        $title ??= '';
1,448✔
3475
        if (str_starts_with($title, "'") && str_ends_with($title, "'")) {
1,448✔
3476
            $title = str_replace("''", "'", substr($title, 1, -1));
1,372✔
3477
        }
3478

3479
        return $title;
1,448✔
3480
    }
3481

3482
    /**
3483
     * Get hyperlink.
3484
     *
3485
     * @param string $cellCoordinate Cell coordinate to get hyperlink for, eg: 'A1'
3486
     */
3487
    public function getHyperlink(string $cellCoordinate): Hyperlink
113✔
3488
    {
3489
        $this->getCell($cellCoordinate)->setHadHyperlink(true);
113✔
3490
        // return hyperlink if we already have one
3491
        if (isset($this->hyperlinkCollection[$cellCoordinate])) {
113✔
3492
            return $this->hyperlinkCollection[$cellCoordinate];
61✔
3493
        }
3494

3495
        // else create hyperlink
3496
        $this->hyperlinkCollection[$cellCoordinate] = new Hyperlink();
113✔
3497

3498
        return $this->hyperlinkCollection[$cellCoordinate];
113✔
3499
    }
3500

3501
    /**
3502
     * Set hyperlink.
3503
     *
3504
     * @param string $cellCoordinate Cell coordinate to insert hyperlink, eg: 'A1'
3505
     *
3506
     * @return $this
3507
     */
3508
    public function setHyperlink(string $cellCoordinate, ?Hyperlink $hyperlink = null, bool $reset = true): static
592✔
3509
    {
3510
        if ($hyperlink === null) {
592✔
3511
            unset($this->hyperlinkCollection[$cellCoordinate]);
591✔
3512
            if ($reset) {
591✔
3513
                $this->getCell($cellCoordinate)
557✔
3514
                    ->setHadHyperlink(false);
557✔
3515
            }
3516
        } else {
3517
            $this->hyperlinkCollection[$cellCoordinate] = $hyperlink;
36✔
3518
            $this->getCell($cellCoordinate)->setHadHyperlink(true);
36✔
3519
        }
3520

3521
        return $this;
592✔
3522
    }
3523

3524
    /**
3525
     * Hyperlink at a specific coordinate exists?
3526
     *
3527
     * @param string $coordinate eg: 'A1'
3528
     */
3529
    public function hyperlinkExists(string $coordinate): bool
688✔
3530
    {
3531
        return isset($this->hyperlinkCollection[$coordinate]);
688✔
3532
    }
3533

3534
    /**
3535
     * Get collection of hyperlinks.
3536
     *
3537
     * @return Hyperlink[]
3538
     */
3539
    public function getHyperlinkCollection(): array
698✔
3540
    {
3541
        return $this->hyperlinkCollection;
698✔
3542
    }
3543

3544
    /**
3545
     * Get data validation.
3546
     *
3547
     * @param string $cellCoordinate Cell coordinate to get data validation for, eg: 'A1'
3548
     */
3549
    public function getDataValidation(string $cellCoordinate): DataValidation
37✔
3550
    {
3551
        // return data validation if we already have one
3552
        if (isset($this->dataValidationCollection[$cellCoordinate])) {
37✔
3553
            return $this->dataValidationCollection[$cellCoordinate];
28✔
3554
        }
3555

3556
        // or if cell is part of a data validation range
3557
        foreach ($this->dataValidationCollection as $key => $dataValidation) {
28✔
3558
            $keyParts = explode(' ', $key);
12✔
3559
            foreach ($keyParts as $keyPart) {
12✔
3560
                if ($keyPart === $cellCoordinate) {
12✔
3561
                    return $dataValidation;
1✔
3562
                }
3563
                if (str_contains($keyPart, ':')) {
12✔
3564
                    if (Coordinate::coordinateIsInsideRange($keyPart, $cellCoordinate)) {
9✔
3565
                        return $dataValidation;
9✔
3566
                    }
3567
                }
3568
            }
3569
        }
3570

3571
        // else create data validation
3572
        $dataValidation = new DataValidation();
20✔
3573
        $dataValidation->setSqref($cellCoordinate);
20✔
3574
        $this->dataValidationCollection[$cellCoordinate] = $dataValidation;
20✔
3575

3576
        return $dataValidation;
20✔
3577
    }
3578

3579
    /**
3580
     * Set data validation.
3581
     *
3582
     * @param string $cellCoordinate Cell coordinate to insert data validation, eg: 'A1'
3583
     *
3584
     * @return $this
3585
     */
3586
    public function setDataValidation(string $cellCoordinate, ?DataValidation $dataValidation = null): static
92✔
3587
    {
3588
        if ($dataValidation === null) {
92✔
3589
            unset($this->dataValidationCollection[$cellCoordinate]);
59✔
3590
        } else {
3591
            $dataValidation->setSqref($cellCoordinate);
40✔
3592
            $this->dataValidationCollection[$cellCoordinate] = $dataValidation;
40✔
3593
        }
3594

3595
        return $this;
92✔
3596
    }
3597

3598
    /**
3599
     * Data validation at a specific coordinate exists?
3600
     *
3601
     * @param string $coordinate eg: 'A1'
3602
     */
3603
    public function dataValidationExists(string $coordinate): bool
25✔
3604
    {
3605
        if (isset($this->dataValidationCollection[$coordinate])) {
25✔
3606
            return true;
23✔
3607
        }
3608
        foreach ($this->dataValidationCollection as $key => $dataValidation) {
8✔
3609
            $keyParts = explode(' ', $key);
7✔
3610
            foreach ($keyParts as $keyPart) {
7✔
3611
                if ($keyPart === $coordinate) {
7✔
3612
                    return true;
1✔
3613
                }
3614
                if (str_contains($keyPart, ':')) {
7✔
3615
                    if (Coordinate::coordinateIsInsideRange($keyPart, $coordinate)) {
2✔
3616
                        return true;
2✔
3617
                    }
3618
                }
3619
            }
3620
        }
3621

3622
        return false;
6✔
3623
    }
3624

3625
    /**
3626
     * Get collection of data validations.
3627
     *
3628
     * @return DataValidation[]
3629
     */
3630
    public function getDataValidationCollection(): array
699✔
3631
    {
3632
        $collectionCells = [];
699✔
3633
        $collectionRanges = [];
699✔
3634
        foreach ($this->dataValidationCollection as $key => $dataValidation) {
699✔
3635
            if (Preg::isMatch('/[: ]/', $key)) {
27✔
3636
                $collectionRanges[$key] = $dataValidation;
15✔
3637
            } else {
3638
                $collectionCells[$key] = $dataValidation;
22✔
3639
            }
3640
        }
3641

3642
        return array_merge($collectionCells, $collectionRanges);
699✔
3643
    }
3644

3645
    /**
3646
     * Accepts a range, returning it as a range that falls within the current highest row and column of the worksheet.
3647
     *
3648
     * @return string Adjusted range value
3649
     */
UNCOV
3650
    public function shrinkRangeToFit(string $range): string
×
3651
    {
3652
        $maxCol = $this->getHighestColumn();
×
3653
        $maxRow = $this->getHighestRow();
×
UNCOV
3654
        $maxCol = Coordinate::columnIndexFromString($maxCol);
×
3655

3656
        $rangeBlocks = explode(' ', $range);
×
3657
        foreach ($rangeBlocks as &$rangeSet) {
×
UNCOV
3658
            $rangeBoundaries = Coordinate::getRangeBoundaries($rangeSet);
×
3659

3660
            if (Coordinate::columnIndexFromString($rangeBoundaries[0][0]) > $maxCol) {
×
UNCOV
3661
                $rangeBoundaries[0][0] = Coordinate::stringFromColumnIndex($maxCol);
×
3662
            }
3663
            if ($rangeBoundaries[0][1] > $maxRow) {
×
UNCOV
3664
                $rangeBoundaries[0][1] = $maxRow;
×
3665
            }
3666
            if (Coordinate::columnIndexFromString($rangeBoundaries[1][0]) > $maxCol) {
×
UNCOV
3667
                $rangeBoundaries[1][0] = Coordinate::stringFromColumnIndex($maxCol);
×
3668
            }
3669
            if ($rangeBoundaries[1][1] > $maxRow) {
×
UNCOV
3670
                $rangeBoundaries[1][1] = $maxRow;
×
3671
            }
UNCOV
3672
            $rangeSet = $rangeBoundaries[0][0] . $rangeBoundaries[0][1] . ':' . $rangeBoundaries[1][0] . $rangeBoundaries[1][1];
×
3673
        }
UNCOV
3674
        unset($rangeSet);
×
3675

UNCOV
3676
        return implode(' ', $rangeBlocks);
×
3677
    }
3678

3679
    /**
3680
     * Get tab color.
3681
     */
3682
    public function getTabColor(): Color
23✔
3683
    {
3684
        if ($this->tabColor === null) {
23✔
3685
            $this->tabColor = new Color();
23✔
3686
        }
3687

3688
        return $this->tabColor;
23✔
3689
    }
3690

3691
    /**
3692
     * Reset tab color.
3693
     *
3694
     * @return $this
3695
     */
3696
    public function resetTabColor(): static
1✔
3697
    {
3698
        $this->tabColor = null;
1✔
3699

3700
        return $this;
1✔
3701
    }
3702

3703
    /**
3704
     * Tab color set?
3705
     */
3706
    public function isTabColorSet(): bool
591✔
3707
    {
3708
        return $this->tabColor !== null;
591✔
3709
    }
3710

3711
    /**
3712
     * Copy worksheet (!= clone!).
3713
     */
UNCOV
3714
    public function copy(): static
×
3715
    {
UNCOV
3716
        return clone $this;
×
3717
    }
3718

3719
    /**
3720
     * Returns a boolean true if the specified row contains no cells. By default, this means that no cell records
3721
     *          exist in the collection for this row. false will be returned otherwise.
3722
     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
3723
     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
3724
     *                  cells, then the row will be considered empty.
3725
     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
3726
     *                  string value cells, then the row will be considered empty.
3727
     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3728
     *                  If the only cells in the collection are null value or empty string value cells, then the row
3729
     *                  will be considered empty.
3730
     *
3731
     * @param int $definitionOfEmptyFlags
3732
     *              Possible Flag Values are:
3733
     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
3734
     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3735
     */
3736
    public function isEmptyRow(int $rowId, int $definitionOfEmptyFlags = 0): bool
9✔
3737
    {
3738
        try {
3739
            $iterator = new RowIterator($this, $rowId, $rowId);
9✔
3740
            $iterator->seek($rowId);
8✔
3741
            $row = $iterator->current();
8✔
3742
        } catch (Exception) {
1✔
3743
            return true;
1✔
3744
        }
3745

3746
        return $row->isEmpty($definitionOfEmptyFlags);
8✔
3747
    }
3748

3749
    /**
3750
     * Returns a boolean true if the specified column contains no cells. By default, this means that no cell records
3751
     *          exist in the collection for this column. false will be returned otherwise.
3752
     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
3753
     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
3754
     *                  cells, then the column will be considered empty.
3755
     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
3756
     *                  string value cells, then the column will be considered empty.
3757
     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3758
     *                  If the only cells in the collection are null value or empty string value cells, then the column
3759
     *                  will be considered empty.
3760
     *
3761
     * @param int $definitionOfEmptyFlags
3762
     *              Possible Flag Values are:
3763
     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
3764
     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3765
     */
3766
    public function isEmptyColumn(string $columnId, int $definitionOfEmptyFlags = 0): bool
9✔
3767
    {
3768
        try {
3769
            $iterator = new ColumnIterator($this, $columnId, $columnId);
9✔
3770
            $iterator->seek($columnId);
8✔
3771
            $column = $iterator->current();
8✔
3772
        } catch (Exception) {
1✔
3773
            return true;
1✔
3774
        }
3775

3776
        return $column->isEmpty($definitionOfEmptyFlags);
8✔
3777
    }
3778

3779
    /**
3780
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
3781
     */
3782
    public function __clone()
27✔
3783
    {
3784
        foreach (get_object_vars($this) as $key => $val) {
27✔
3785
            if ($key == 'parent') {
27✔
3786
                continue;
27✔
3787
            }
3788

3789
            if (is_object($val) || (is_array($val))) {
27✔
3790
                if ($key === 'cellCollection') {
27✔
3791
                    $newCollection = $this->cellCollection->cloneCellCollection($this);
27✔
3792
                    $this->cellCollection = $newCollection;
27✔
3793
                } elseif ($key === 'drawingCollection') {
27✔
3794
                    $currentCollection = $this->drawingCollection;
27✔
3795
                    $this->drawingCollection = new ArrayObject();
27✔
3796
                    foreach ($currentCollection as $item) {
27✔
3797
                        $newDrawing = clone $item;
4✔
3798
                        $newDrawing->setWorksheet($this);
4✔
3799
                    }
3800
                } elseif ($key === 'inCellDrawingCollection') {
27✔
3801
                    $currentCollection = $this->inCellDrawingCollection;
27✔
3802
                    $this->inCellDrawingCollection = new ArrayObject();
27✔
3803
                    foreach ($currentCollection as $item) {
27✔
3804
                        $newDrawing = clone $item;
1✔
3805
                        $newDrawing->setWorksheet($this);
1✔
3806
                    }
3807
                } elseif ($key === 'tableCollection') {
27✔
3808
                    $currentCollection = $this->tableCollection;
27✔
3809
                    $this->tableCollection = new ArrayObject();
27✔
3810
                    foreach ($currentCollection as $item) {
27✔
3811
                        $newTable = clone $item;
1✔
3812
                        $newTable->setName($item->getName() . 'clone');
1✔
3813
                        $this->addTable($newTable);
1✔
3814
                    }
3815
                } elseif ($key === 'chartCollection') {
27✔
3816
                    $currentCollection = $this->chartCollection;
27✔
3817
                    $this->chartCollection = new ArrayObject();
27✔
3818
                    foreach ($currentCollection as $item) {
27✔
3819
                        $newChart = clone $item;
5✔
3820
                        $this->addChart($newChart);
5✔
3821
                    }
3822
                } elseif ($key === 'autoFilter') {
27✔
3823
                    $newAutoFilter = clone $this->autoFilter;
27✔
3824
                    $this->autoFilter = $newAutoFilter;
27✔
3825
                    $this->autoFilter->setParent($this);
27✔
3826
                } else {
3827
                    $this->{$key} = unserialize(serialize($val));
27✔
3828
                }
3829
            }
3830
        }
3831
    }
3832

3833
    /**
3834
     * Define the code name of the sheet.
3835
     *
3836
     * @param string $codeName Same rule as Title minus space not allowed (but, like Excel, change
3837
     *                       silently space to underscore)
3838
     * @param bool $validate False to skip validation of new title. WARNING: This should only be set
3839
     *                       at parse time (by Readers), where titles can be assumed to be valid.
3840
     *
3841
     * @return $this
3842
     */
3843
    public function setCodeName(string $codeName, bool $validate = true): static
11,444✔
3844
    {
3845
        // Is this a 'rename' or not?
3846
        if ($this->getCodeName() == $codeName) {
11,444✔
UNCOV
3847
            return $this;
×
3848
        }
3849

3850
        if ($validate) {
11,444✔
3851
            $codeName = str_replace(' ', '_', $codeName); //Excel does this automatically without flinching, we are doing the same
11,444✔
3852

3853
            // Syntax check
3854
            // throw an exception if not valid
3855
            self::checkSheetCodeName($codeName);
11,444✔
3856

3857
            // We use the same code that setTitle to find a valid codeName else not using a space (Excel don't like) but a '_'
3858

3859
            if ($this->parent !== null) {
11,444✔
3860
                // Is there already such sheet name?
3861
                if ($this->parent->sheetCodeNameExists($codeName)) {
11,403✔
3862
                    // Use name, but append with lowest possible integer
3863

3864
                    if (StringHelper::countCharacters($codeName) > 29) {
737✔
UNCOV
3865
                        $codeName = StringHelper::substring($codeName, 0, 29);
×
3866
                    }
3867
                    $i = 1;
737✔
3868
                    while ($this->getParentOrThrow()->sheetCodeNameExists($codeName . '_' . $i)) {
737✔
3869
                        ++$i;
300✔
3870
                        if ($i == 10) {
300✔
3871
                            if (StringHelper::countCharacters($codeName) > 28) {
2✔
UNCOV
3872
                                $codeName = StringHelper::substring($codeName, 0, 28);
×
3873
                            }
3874
                        } elseif ($i == 100) {
300✔
3875
                            if (StringHelper::countCharacters($codeName) > 27) {
×
UNCOV
3876
                                $codeName = StringHelper::substring($codeName, 0, 27);
×
3877
                            }
3878
                        }
3879
                    }
3880

3881
                    $codeName .= '_' . $i; // ok, we have a valid name
737✔
3882
                }
3883
            }
3884
        }
3885

3886
        $this->codeName = $codeName;
11,444✔
3887

3888
        return $this;
11,444✔
3889
    }
3890

3891
    /**
3892
     * Return the code name of the sheet.
3893
     */
3894
    public function getCodeName(): ?string
11,444✔
3895
    {
3896
        return $this->codeName;
11,444✔
3897
    }
3898

3899
    /**
3900
     * Sheet has a code name ?
3901
     */
3902
    public function hasCodeName(): bool
2✔
3903
    {
3904
        return $this->codeName !== null;
2✔
3905
    }
3906

3907
    public static function nameRequiresQuotes(string $sheetName): bool
4✔
3908
    {
3909
        return !Preg::isMatch(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName);
4✔
3910
    }
3911

3912
    public function isRowVisible(int $row): bool
130✔
3913
    {
3914
        return !$this->rowDimensionExists($row) || $this->getRowDimension($row)->getVisible();
130✔
3915
    }
3916

3917
    /**
3918
     * Same as Cell->isLocked, but without creating cell if it doesn't exist.
3919
     */
3920
    public function isCellLocked(string $coordinate): bool
1✔
3921
    {
3922
        if ($this->getProtection()->getsheet() !== true) {
1✔
3923
            return false;
1✔
3924
        }
3925
        if ($this->cellExists($coordinate)) {
1✔
3926
            return $this->getCell($coordinate)->isLocked();
1✔
3927
        }
3928
        $spreadsheet = $this->parent;
1✔
3929
        $xfIndex = $this->getXfIndex($coordinate);
1✔
3930
        if ($spreadsheet === null || $xfIndex === null) {
1✔
3931
            return true;
1✔
3932
        }
3933

UNCOV
3934
        return $spreadsheet->getCellXfByIndex($xfIndex)->getProtection()->getLocked() !== StyleProtection::PROTECTION_UNPROTECTED;
×
3935
    }
3936

3937
    /**
3938
     * Same as Cell->isHiddenOnFormulaBar, but without creating cell if it doesn't exist.
3939
     */
3940
    public function isCellHiddenOnFormulaBar(string $coordinate): bool
1✔
3941
    {
3942
        if ($this->cellExists($coordinate)) {
1✔
3943
            return $this->getCell($coordinate)->isHiddenOnFormulaBar();
1✔
3944
        }
3945

3946
        // cell doesn't exist, therefore isn't a formula,
3947
        // therefore isn't hidden on formula bar.
3948
        return false;
1✔
3949
    }
3950

3951
    private function getXfIndex(string $coordinate): ?int
1✔
3952
    {
3953
        [$column, $row] = Coordinate::coordinateFromString($coordinate);
1✔
3954
        $row = (int) $row;
1✔
3955
        $xfIndex = null;
1✔
3956
        if ($this->rowDimensionExists($row)) {
1✔
UNCOV
3957
            $xfIndex = $this->getRowDimension($row)->getXfIndex();
×
3958
        }
3959
        if ($xfIndex === null && $this->ColumnDimensionExists($column)) {
1✔
UNCOV
3960
            $xfIndex = $this->getColumnDimension($column)->getXfIndex();
×
3961
        }
3962

3963
        return $xfIndex;
1✔
3964
    }
3965

3966
    private string $backgroundImage = '';
3967

3968
    private string $backgroundMime = '';
3969

3970
    private string $backgroundExtension = '';
3971

3972
    public function getBackgroundImage(): string
1,106✔
3973
    {
3974
        return $this->backgroundImage;
1,106✔
3975
    }
3976

3977
    public function getBackgroundMime(): string
444✔
3978
    {
3979
        return $this->backgroundMime;
444✔
3980
    }
3981

3982
    public function getBackgroundExtension(): string
444✔
3983
    {
3984
        return $this->backgroundExtension;
444✔
3985
    }
3986

3987
    /**
3988
     * Set background image.
3989
     * Used on read/write for Xlsx.
3990
     * Used on write for Html.
3991
     *
3992
     * @param string $backgroundImage Image represented as a string, e.g. results of file_get_contents
3993
     */
3994
    public function setBackgroundImage(string $backgroundImage): self
4✔
3995
    {
3996
        $imageArray = getimagesizefromstring($backgroundImage) ?: ['mime' => ''];
4✔
3997
        $mime = $imageArray['mime'];
4✔
3998
        if ($mime !== '') {
4✔
3999
            $extension = explode('/', $mime);
3✔
4000
            $extension = $extension[1];
3✔
4001
            $this->backgroundImage = $backgroundImage;
3✔
4002
            $this->backgroundMime = $mime;
3✔
4003
            $this->backgroundExtension = $extension;
3✔
4004
        }
4005

4006
        return $this;
4✔
4007
    }
4008

4009
    /**
4010
     * Copy cells, adjusting relative cell references in formulas.
4011
     * Acts similarly to Excel "fill handle" feature.
4012
     *
4013
     * @param string $fromCell Single source cell, e.g. C3
4014
     * @param string $toCells Single cell or cell range, e.g. C4 or C4:C10
4015
     * @param bool $copyStyle Copy styles as well as values, defaults to true
4016
     */
4017
    public function copyCells(string $fromCell, string $toCells, bool $copyStyle = true): void
1✔
4018
    {
4019
        $toArray = Coordinate::extractAllCellReferencesInRange($toCells);
1✔
4020
        $valueString = $this->getCell($fromCell)->getValueString();
1✔
4021
        /** @var mixed[][] */
4022
        $style = $this->getStyle($fromCell)->exportArray();
1✔
4023
        $fromIndexes = Coordinate::indexesFromString($fromCell);
1✔
4024
        $referenceHelper = ReferenceHelper::getInstance();
1✔
4025
        foreach ($toArray as $destination) {
1✔
4026
            if ($destination !== $fromCell) {
1✔
4027
                $toIndexes = Coordinate::indexesFromString($destination);
1✔
4028
                $this->getCell($destination)->setValue($referenceHelper->updateFormulaReferences($valueString, 'A1', $toIndexes[0] - $fromIndexes[0], $toIndexes[1] - $fromIndexes[1]));
1✔
4029
                if ($copyStyle) {
1✔
4030
                    $this->getCell($destination)->getStyle()->applyFromArray($style);
1✔
4031
                }
4032
            }
4033
        }
4034
    }
4035

4036
    public function calculateArrays(bool $preCalculateFormulas = true): void
1,299✔
4037
    {
4038
        if ($preCalculateFormulas && Calculation::getInstance($this->parent)->getInstanceArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY) {
1,299✔
4039
            $keys = $this->cellCollection->getCoordinates();
52✔
4040
            foreach ($keys as $key) {
52✔
4041
                if ($this->getCell($key)->getDataType() === DataType::TYPE_FORMULA) {
52✔
4042
                    if (!Preg::isMatch(self::FUNCTION_LIKE_GROUPBY, $this->getCell($key)->getValueString())) {
48✔
4043
                        $this->getCell($key)->getCalculatedValue();
47✔
4044
                    }
4045
                }
4046
            }
4047
        }
4048
    }
4049

4050
    public function isCellInSpillRange(string $coordinate): bool
2✔
4051
    {
4052
        if (Calculation::getInstance($this->parent)->getInstanceArrayReturnType() !== Calculation::RETURN_ARRAY_AS_ARRAY) {
2✔
4053
            return false;
1✔
4054
        }
4055
        $this->calculateArrays();
1✔
4056
        $keys = $this->cellCollection->getCoordinates();
1✔
4057
        foreach ($keys as $key) {
1✔
4058
            $attributes = $this->getCell($key)->getFormulaAttributes();
1✔
4059
            if (isset($attributes['ref'])) {
1✔
4060
                if (Coordinate::coordinateIsInsideRange($attributes['ref'], $coordinate)) {
1✔
4061
                    // false for first cell in range, true otherwise
4062
                    return $coordinate !== $key;
1✔
4063
                }
4064
            }
4065
        }
4066

4067
        return false;
1✔
4068
    }
4069

4070
    /** @param mixed[][] $styleArray */
4071
    public function applyStylesFromArray(string $coordinate, array $styleArray): bool
2✔
4072
    {
4073
        $spreadsheet = $this->parent;
2✔
4074
        if ($spreadsheet === null) {
2✔
4075
            return false;
1✔
4076
        }
4077
        $activeSheetIndex = $spreadsheet->getActiveSheetIndex();
1✔
4078
        $originalSelected = $this->selectedCells;
1✔
4079
        $this->getStyle($coordinate)->applyFromArray($styleArray);
1✔
4080
        $this->setSelectedCells($originalSelected);
1✔
4081
        if ($activeSheetIndex >= 0) {
1✔
4082
            $spreadsheet->setActiveSheetIndex($activeSheetIndex);
1✔
4083
        }
4084

4085
        return true;
1✔
4086
    }
4087

4088
    public function copyFormula(string $fromCell, string $toCell): void
1✔
4089
    {
4090
        $formula = $this->getCell($fromCell)->getValue();
1✔
4091
        $newFormula = $formula;
1✔
4092
        if (is_string($formula) && $this->getCell($fromCell)->getDataType() === DataType::TYPE_FORMULA) {
1✔
4093
            [$fromColInt, $fromRow] = Coordinate::indexesFromString($fromCell);
1✔
4094
            [$toColInt, $toRow] = Coordinate::indexesFromString($toCell);
1✔
4095
            $helper = ReferenceHelper::getInstance();
1✔
4096
            $newFormula = $helper->updateFormulaReferences(
1✔
4097
                $formula,
1✔
4098
                'A1',
1✔
4099
                $toColInt - $fromColInt,
1✔
4100
                $toRow - $fromRow
1✔
4101
            );
1✔
4102
        }
4103
        $this->setCellValue($toCell, $newFormula);
1✔
4104
    }
4105
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc