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

PHPOffice / PhpSpreadsheet / 28190270571

25 Jun 2026 06:02PM UTC coverage: 97.151%. Remained the same
28190270571

Pull #4926

github

web-flow
Merge a5e7ea2fb into e0b1e17f7
Pull Request #4926: Minor Changes to Worksheet

22 of 23 new or added lines in 7 files covered. (95.65%)

27 existing lines in 3 files now uncovered.

48357 of 49775 relevant lines covered (97.15%)

386.2 hits per line

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

96.59
/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,465✔
325
    {
326
        // Set parent and title
327
        $this->parent = $parent;
11,465✔
328
        $this->setTitle($title, false);
11,465✔
329
        // setTitle can change $pTitle
330
        $this->setCodeName($this->getTitle());
11,465✔
331
        $this->setSheetState(self::SHEETSTATE_VISIBLE);
11,465✔
332

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

360
    /**
361
     * Disconnect all cells from this Worksheet object,
362
     * typically so that the worksheet object can be unset.
363
     * The worksheet will be in an unusable state after
364
     * this method has completed.
365
     */
366
    public function disconnectCells(): void
10,476✔
367
    {
368
        // isset needed to avoid problems at destruct time
369
        if (isset($this->cellCollection)) { //* @phpstan-ignore-line
10,476✔
370
            $this->cellCollection->unsetWorksheetCells();
10,476✔
371
            unset($this->cellCollection);
10,476✔
372
        }
373
        //    detach ourself from the workbook, so that it can then delete this worksheet successfully
374
        $this->parent = null;
10,476✔
375
    }
376

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

385
        $this->disconnectCells();
130✔
386
        unset($this->rowDimensions, $this->columnDimensions, $this->tableCollection, $this->drawingCollection, $this->inCellDrawingCollection, $this->chartCollection, $this->autoFilter);
130✔
387
    }
388

389
    /**
390
     * Return the cell collection.
391
     */
392
    public function getCellCollection(): Cells
11,012✔
393
    {
394
        return $this->cellCollection;
11,012✔
395
    }
396

397
    /**
398
     * Get array of invalid characters for sheet title.
399
     *
400
     * @return string[]
401
     */
402
    public static function getInvalidCharacters(): array
1✔
403
    {
404
        return self::INVALID_CHARACTERS;
1✔
405
    }
406

407
    /**
408
     * Check sheet code name for valid Excel syntax.
409
     *
410
     * @param string $sheetCodeName The string to check
411
     *
412
     * @return string The valid string
413
     */
414
    private static function checkSheetCodeName(string $sheetCodeName): string
11,465✔
415
    {
416
        $charCount = StringHelper::countCharacters($sheetCodeName);
11,465✔
417
        if ($charCount == 0) {
11,465✔
418
            throw new Exception('Sheet code name cannot be empty.');
1✔
419
        }
420
        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ] and  first and last characters cannot be a "'"
421
        if (
422
            (str_replace(self::INVALID_CHARACTERS, '', $sheetCodeName) !== $sheetCodeName)
11,465✔
423
            || (StringHelper::substring($sheetCodeName, -1, 1) == '\'')
11,465✔
424
            || (StringHelper::substring($sheetCodeName, 0, 1) == '\'')
11,465✔
425
        ) {
426
            throw new Exception('Invalid character found in sheet code name');
1✔
427
        }
428

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

434
        return $sheetCodeName;
11,465✔
435
    }
436

437
    /**
438
     * Check sheet title for valid Excel syntax.
439
     *
440
     * @param string $sheetTitle The string to check
441
     *
442
     * @return string The valid string
443
     */
444
    private static function checkSheetTitle(string $sheetTitle): string
11,465✔
445
    {
446
        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ]
447
        if (str_replace(self::INVALID_CHARACTERS, '', $sheetTitle) !== $sheetTitle) {
11,465✔
448
            throw new Exception('Invalid character found in sheet title');
2✔
449
        }
450

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

456
        return $sheetTitle;
11,465✔
457
    }
458

459
    /**
460
     * Get a sorted list of all cell coordinates currently held in the collection by row and column.
461
     *
462
     * @param bool $sorted Also sort the cell collection?
463
     *
464
     * @return string[]
465
     */
466
    public function getCoordinates(bool $sorted = true): array
1,658✔
467
    {
468
        // isset needed to avoid problems at destruct time
469
        if (!isset($this->cellCollection)) { //* @phpstan-ignore-line
1,658✔
470
            return [];
1✔
471
        }
472

473
        if ($sorted) {
1,658✔
474
            return $this->cellCollection->getSortedCoordinates();
631✔
475
        }
476

477
        return $this->cellCollection->getCoordinates();
1,511✔
478
    }
479

480
    /**
481
     * Get collection of row dimensions.
482
     *
483
     * @return RowDimension[]
484
     */
485
    public function getRowDimensions(): array
1,359✔
486
    {
487
        return $this->rowDimensions;
1,359✔
488
    }
489

490
    /**
491
     * Get default row dimension.
492
     */
493
    public function getDefaultRowDimension(): RowDimension
1,330✔
494
    {
495
        return $this->defaultRowDimension;
1,330✔
496
    }
497

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

509
        return $this->columnDimensions;
1,365✔
510
    }
511

512
    private static function columnDimensionCompare(ColumnDimension $a, ColumnDimension $b): int
117✔
513
    {
514
        return $a->getColumnNumeric() - $b->getColumnNumeric();
117✔
515
    }
516

517
    /**
518
     * Get default column dimension.
519
     */
520
    public function getDefaultColumnDimension(): ColumnDimension
661✔
521
    {
522
        return $this->defaultColumnDimension;
661✔
523
    }
524

525
    /**
526
     * Get collection of drawings.
527
     *
528
     * @return ArrayObject<int, BaseDrawing>
529
     */
530
    public function getDrawingCollection(): ArrayObject
1,327✔
531
    {
532
        return $this->drawingCollection;
1,327✔
533
    }
534

535
    /**
536
     * Get collection of drawings.
537
     *
538
     * @return ArrayObject<int, BaseDrawing>
539
     */
540
    public function getInCellDrawingCollection(): ArrayObject
451✔
541
    {
542
        return $this->inCellDrawingCollection;
451✔
543
    }
544

545
    /**
546
     * Get collection of charts.
547
     *
548
     * @return ArrayObject<int, Chart>
549
     */
550
    public function getChartCollection(): ArrayObject
106✔
551
    {
552
        return $this->chartCollection;
106✔
553
    }
554

555
    public function addChart(Chart $chart): Chart
112✔
556
    {
557
        $chart->setWorksheet($this);
112✔
558
        $this->chartCollection[] = $chart;
112✔
559

560
        return $chart;
112✔
561
    }
562

563
    /**
564
     * Return the count of charts on this worksheet.
565
     *
566
     * @return int The number of charts
567
     */
568
    public function getChartCount(): int
87✔
569
    {
570
        return count($this->chartCollection);
87✔
571
    }
572

573
    /**
574
     * Get a chart by its index position.
575
     *
576
     * @param null|int|string $index Chart index position
577
     *
578
     * @return Chart|false
579
     */
580
    public function getChartByIndex(null|int|string $index)
1✔
581
    {
582
        $chartCount = count($this->chartCollection);
1✔
583
        if ($chartCount === 0 || (is_string($index) && $index !== (string) (int) $index)) {
1✔
584
            return false;
1✔
585
        }
586
        if ($index === null) {
1✔
587
            $index = --$chartCount;
1✔
588
        }
589
        if (!isset($this->chartCollection[$index])) {
1✔
590
            return false;
1✔
591
        }
592

593
        return $this->chartCollection[$index];
1✔
594
    }
595

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

608
        return $chartNames;
5✔
609
    }
610

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

626
        return false;
1✔
627
    }
628

629
    public function getChartByNameOrThrow(string $chartName): Chart
6✔
630
    {
631
        $chart = $this->getChartByName($chartName);
6✔
632
        if ($chart !== false) {
6✔
633
            return $chart;
6✔
634
        }
635

636
        throw new Exception("Sheet does not have a chart named $chartName.");
1✔
637
    }
638

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

651
        $this->columnDimensions = $newColumnDimensions;
25✔
652

653
        return $this;
25✔
654
    }
655

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

668
        $this->rowDimensions = $newRowDimensions;
9✔
669

670
        return $this;
9✔
671
    }
672

673
    /**
674
     * Calculate worksheet dimension.
675
     *
676
     * @return string String containing the dimension of this worksheet
677
     */
678
    public function calculateWorksheetDimension(): string
536✔
679
    {
680
        // Return
681
        return 'A1:' . $this->getHighestColumn() . $this->getHighestRow();
536✔
682
    }
683

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

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

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

723
            $autoFilterIndentRanges = (new AutoFit($this))->getAutoFilterIndentRanges();
67✔
724

725
            // loop through all cells in the worksheet
726
            foreach ($this->getCoordinates(false) as $coordinate) {
67✔
727
                $cell = $this->getCellOrNull($coordinate);
67✔
728

729
                if ($cell !== null && isset($autoSizes[$this->cellCollection->getCurrentColumn()])) {
67✔
730
                    //Determine if cell is in merge range
731
                    $isMerged = isset($isMergeCell[$this->cellCollection->getCurrentCoordinate()]);
67✔
732

733
                    //By default merged cells should be ignored
734
                    $isMergedButProceed = false;
67✔
735

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

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

756
                                    break;
4✔
757
                                }
758
                            }
759
                        }
760

761
                        $indentAdjustment = $cell->getStyle()->getAlignment()->getIndent();
67✔
762
                        $indentAdjustment += (int) ($cell->getStyle()->getAlignment()->getHorizontal() === Alignment::HORIZONTAL_CENTER);
67✔
763

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

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

793
            // adjust column widths
794
            foreach ($autoSizes as $columnIndex => $width) {
67✔
795
                if ($width == -1) {
67✔
796
                    $width = $this->getDefaultColumnDimension()->getWidth();
×
797
                }
798
                $this->getColumnDimension($columnIndex)->setWidth($width);
67✔
799
            }
800
            $this->activePane = $holdActivePane;
67✔
801
        }
802
        if ($activeSheet !== null && $activeSheet >= 0) {
882✔
803
            // Okay, I get it now - if $activeSheet is not null,
804
            // then $this->getParent() must also be non-null.
805
            $this->getParent()->setActiveSheetIndex($activeSheet);
882✔
806
        }
807
        $this->setSelectedCells($selectedCells);
882✔
808

809
        return $this;
882✔
810
    }
811

812
    /**
813
     * Get parent or null.
814
     */
815
    public function getParent(): ?Spreadsheet
11,021✔
816
    {
817
        return $this->parent;
11,021✔
818
    }
819

820
    /**
821
     * Get parent, throw exception if null.
822
     */
823
    public function getParentOrThrow(): Spreadsheet
11,093✔
824
    {
825
        if ($this->parent !== null) {
11,093✔
826
            return $this->parent;
11,092✔
827
        }
828

829
        throw new Exception('Sheet does not have a parent.');
1✔
830
    }
831

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

845
            $this->parent->removeSheetByIndex(
4✔
846
                $this->parent->getIndex($this)
4✔
847
            );
4✔
848
        }
849
        $this->parent = $parent;
54✔
850

851
        return $this;
54✔
852
    }
853

854
    public function setParent(Spreadsheet $parent): self
12✔
855
    {
856
        $this->parent = $parent;
12✔
857

858
        return $this;
12✔
859
    }
860

861
    /**
862
     * Get title.
863
     */
864
    public function getTitle(): string
11,465✔
865
    {
866
        return $this->title;
11,465✔
867
    }
868

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

890
        // Old title
891
        $oldTitle = $this->getTitle();
11,465✔
892

893
        if ($validate) {
11,465✔
894
            // Syntax check
895
            self::checkSheetTitle($title);
11,465✔
896

897
            if ($this->parent && $this->parent->getIndex($this, true) >= 0) {
11,465✔
898
                // Is there already such sheet name?
899
                if ($this->parent->sheetNameExists($title)) {
851✔
900
                    // Use name, but append with lowest possible integer
901

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

919
                    $title .= " $i";
2✔
920
                }
921
            }
922
        }
923

924
        // Set title
925
        $this->title = $title;
11,465✔
926

927
        if ($this->parent && $this->parent->getIndex($this, true) >= 0) {
11,465✔
928
            // New title
929
            $newTitle = $this->getTitle();
1,522✔
930
            $this->parent->getCalculationEngine()
1,522✔
931
                ->renameCalculationCacheForWorksheet($oldTitle, $newTitle);
1,522✔
932
            if ($updateFormulaCellReferences) {
1,522✔
933
                ReferenceHelper::getInstance()->updateNamedFormulae($this->parent, $oldTitle, $newTitle);
851✔
934
            }
935
        }
936

937
        return $this;
11,465✔
938
    }
939

940
    /**
941
     * Get sheet state.
942
     *
943
     * @return string Sheet state (visible, hidden, veryHidden)
944
     */
945
    public function getSheetState(): string
590✔
946
    {
947
        return $this->sheetState;
590✔
948
    }
949

950
    /**
951
     * Set sheet state.
952
     *
953
     * @param string $value Sheet state (visible, hidden, veryHidden)
954
     *
955
     * @return $this
956
     */
957
    public function setSheetState(string $value): static
11,465✔
958
    {
959
        $this->sheetState = $value;
11,465✔
960

961
        return $this;
11,465✔
962
    }
963

964
    /**
965
     * Get page setup.
966
     */
967
    public function getPageSetup(): PageSetup
1,786✔
968
    {
969
        return $this->pageSetup;
1,786✔
970
    }
971

972
    /**
973
     * Set page setup.
974
     *
975
     * @return $this
976
     */
977
    public function setPageSetup(PageSetup $pageSetup): static
1✔
978
    {
979
        $this->pageSetup = $pageSetup;
1✔
980

981
        return $this;
1✔
982
    }
983

984
    /**
985
     * Get page margins.
986
     */
987
    public function getPageMargins(): PageMargins
1,775✔
988
    {
989
        return $this->pageMargins;
1,775✔
990
    }
991

992
    /**
993
     * Set page margins.
994
     *
995
     * @return $this
996
     */
997
    public function setPageMargins(PageMargins $pageMargins): static
1✔
998
    {
999
        $this->pageMargins = $pageMargins;
1✔
1000

1001
        return $this;
1✔
1002
    }
1003

1004
    /**
1005
     * Get page header/footer.
1006
     */
1007
    public function getHeaderFooter(): HeaderFooter
654✔
1008
    {
1009
        return $this->headerFooter;
654✔
1010
    }
1011

1012
    /**
1013
     * Set page header/footer.
1014
     *
1015
     * @return $this
1016
     */
1017
    public function setHeaderFooter(HeaderFooter $headerFooter): static
1✔
1018
    {
1019
        $this->headerFooter = $headerFooter;
1✔
1020

1021
        return $this;
1✔
1022
    }
1023

1024
    /**
1025
     * Get sheet view.
1026
     */
1027
    public function getSheetView(): SheetView
686✔
1028
    {
1029
        return $this->sheetView;
686✔
1030
    }
1031

1032
    /**
1033
     * Set sheet view.
1034
     *
1035
     * @return $this
1036
     */
1037
    public function setSheetView(SheetView $sheetView): static
1✔
1038
    {
1039
        $this->sheetView = $sheetView;
1✔
1040

1041
        return $this;
1✔
1042
    }
1043

1044
    /**
1045
     * Get Protection.
1046
     */
1047
    public function getProtection(): Protection
704✔
1048
    {
1049
        return $this->protection;
704✔
1050
    }
1051

1052
    /**
1053
     * Set Protection.
1054
     *
1055
     * @return $this
1056
     */
1057
    public function setProtection(Protection $protection): static
1✔
1058
    {
1059
        $this->protection = $protection;
1✔
1060

1061
        return $this;
1✔
1062
    }
1063

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

1078
        return $this->getHighestDataColumn($row);
1✔
1079
    }
1080

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

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

1108
        return $this->getHighestDataRow($column);
1✔
1109
    }
1110

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

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

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

1149
        return $this;
5,114✔
1150
    }
1151

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

1174
        return $this;
124✔
1175
    }
1176

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

1194
        // Shortcut for increased performance for the vast majority of simple cases
1195
        if ($this->cellCollection->has($cellAddress)) {
10,952✔
1196
            /** @var Cell $cell */
1197
            $cell = $this->cellCollection->get($cellAddress);
10,923✔
1198

1199
            return $cell;
10,923✔
1200
        }
1201

1202
        /** @var Worksheet $sheet */
1203
        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
10,952✔
1204
        $cell = $sheet->getCellCollection()->get($finalCoordinate);
10,952✔
1205

1206
        return $cell ?? $sheet->createNewCell($finalCoordinate);
10,952✔
1207
    }
1208

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

1220
        // Worksheet reference?
1221
        if (str_contains($coordinate, '!')) {
10,976✔
1222
            $worksheetReference = self::extractSheetTitle($coordinate, true, true);
×
1223

1224
            $sheet = $this->getParentOrThrow()->getSheetByName($worksheetReference[0]);
×
1225
            $finalCoordinate = strtoupper($worksheetReference[1]);
×
1226

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

1242
                $cellCoordinate = ltrim(substr($namedRange->getValue(), (int) strrpos($namedRange->getValue(), '!')), '!');
12✔
1243
                $finalCoordinate = str_replace('$', '', $cellCoordinate);
12✔
1244
            }
1245
        }
1246

1247
        if ($sheet === null || $finalCoordinate === null) {
10,976✔
1248
            $sheet = $this;
10,976✔
1249
            $finalCoordinate = strtoupper($coordinate);
10,976✔
1250
        }
1251

1252
        if (Coordinate::coordinateIsRange($finalCoordinate)) {
10,976✔
1253
            throw new Exception('Cell coordinate string can not be a range of cells.');
2✔
1254
        }
1255
        $finalCoordinate = str_replace('$', '', $finalCoordinate);
10,976✔
1256

1257
        return [$sheet, $finalCoordinate];
10,976✔
1258
    }
1259

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

1274
        return null;
×
1275
    }
1276

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

1295
        // Coordinates
1296
        if ($column > $this->cachedHighestColumn) {
10,952✔
1297
            $this->cachedHighestColumn = $column;
7,527✔
1298
        }
1299
        if ($row > $this->cachedHighestRow) {
10,952✔
1300
            $this->cachedHighestRow = $row;
8,973✔
1301
        }
1302

1303
        // Cell needs appropriate xfIndex from dimensions records
1304
        //    but don't create dimension records if they don't already exist
1305
        $rowDimension = $this->rowDimensions[$row] ?? null;
10,952✔
1306
        $columnDimension = $this->columnDimensions[$columnString] ?? null;
10,952✔
1307

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

1325
        return $cell;
10,952✔
1326
    }
1327

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

1339
        return $sheet->getCellCollection()->has($finalCoordinate);
10,888✔
1340
    }
1341

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

1353
            $this->cachedHighestRow = max($this->cachedHighestRow, $row);
630✔
1354
        }
1355

1356
        return $this->rowDimensions[$row];
630✔
1357
    }
1358

1359
    public function getRowStyle(int $row): ?Style
1✔
1360
    {
1361
        return $this->parent?->getCellXfByIndexOrNull(
1✔
1362
            ($this->rowDimensions[$row] ?? null)?->getXfIndex()
1✔
1363
        );
1✔
1364
    }
1365

1366
    public function rowDimensionExists(int $row): bool
710✔
1367
    {
1368
        return isset($this->rowDimensions[$row]);
710✔
1369
    }
1370

1371
    public function columnDimensionExists(string $column): bool
107✔
1372
    {
1373
        return isset($this->columnDimensions[$column]);
107✔
1374
    }
1375

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

1386
        // Fetch dimensions
1387
        if (!isset($this->columnDimensions[$column])) {
711✔
1388
            $this->columnDimensions[$column] = new ColumnDimension($column);
711✔
1389

1390
            $columnIndex = Coordinate::columnIndexFromString($column);
711✔
1391
            if ($this->cachedHighestColumn < $columnIndex) {
711✔
1392
                $this->cachedHighestColumn = $columnIndex;
491✔
1393
            }
1394
        }
1395

1396
        return $this->columnDimensions[$column];
711✔
1397
    }
1398

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

1409
    public function getColumnStyle(string $column): ?Style
1✔
1410
    {
1411
        return $this->parent?->getCellXfByIndexOrNull(
1✔
1412
            ($this->columnDimensions[$column] ?? null)?->getXfIndex()
1✔
1413
        );
1✔
1414
    }
1415

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

1432
        // set this sheet as active
1433
        $this->getParentOrThrow()->setActiveSheetIndex($this->getParentOrThrow()->getIndex($this));
10,915✔
1434

1435
        // set cell coordinate as active
1436
        $this->setSelectedCells($cellCoordinate);
10,915✔
1437

1438
        return $this->getParentOrThrow()->getCellXfSupervisor();
10,915✔
1439
    }
1440

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

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

1465
        return $retVal;
7✔
1466
    }
1467

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

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

1490
        return $retVal;
6✔
1491
    }
1492

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

1513
        $conditionalStyles = [];
821✔
1514
        foreach ($this->conditionalStylesCollection as $keyStylesOrig => $conditionalRange) {
821✔
1515
            $keyStyles = Coordinate::resolveUnionAndIntersection($keyStylesOrig);
225✔
1516
            $keyParts = explode(',', $keyStyles);
225✔
1517
            foreach ($keyParts as $keyPart) {
225✔
1518
                if ($keyPart === $coordinate) {
225✔
1519
                    if ($firstOnly) {
14✔
1520
                        return $conditionalRange;
14✔
1521
                    }
1522
                    $conditionalStyles[$keyStylesOrig] = $conditionalRange;
×
1523

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

1532
                        break;
1✔
1533
                    }
1534
                }
1535
            }
1536
        }
1537
        $outArray = [];
644✔
1538
        foreach ($conditionalStyles as $conditionalArray) {
644✔
1539
            foreach ($conditionalArray as $conditional) {
1✔
1540
                $outArray[] = $conditional;
1✔
1541
            }
1542
        }
1543
        usort($outArray, [self::class, 'comparePriority']);
644✔
1544

1545
        return $outArray;
644✔
1546
    }
1547

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

1562
        return ($a < $b) ? -1 : 1;
1✔
1563
    }
1564

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

1578
        return null;
10✔
1579
    }
1580

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

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

1606
        return $this;
55✔
1607
    }
1608

1609
    /**
1610
     * Get collection of conditional styles.
1611
     *
1612
     * @return Conditional[][]
1613
     */
1614
    public function getConditionalStylesCollection(): array
1,467✔
1615
    {
1616
        return $this->conditionalStylesCollection;
1,467✔
1617
    }
1618

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

1631
        return $this;
348✔
1632
    }
1633

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

1657
        // Calculate range outer borders
1658
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
2✔
1659

1660
        // Make sure we can loop upwards on rows and columns
1661
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
2✔
1662
            $tmp = $rangeStart;
×
1663
            $rangeStart = $rangeEnd;
×
1664
            $rangeEnd = $tmp;
×
1665
        }
1666

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

1674
        return $this;
2✔
1675
    }
1676

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

1696
        // Calculate range outer borders
1697
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
18✔
1698

1699
        // Make sure we can loop upwards on rows and columns
1700
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
18✔
1701
            $tmp = $rangeStart;
×
1702
            $rangeStart = $rangeEnd;
×
UNCOV
1703
            $rangeEnd = $tmp;
×
1704
        }
1705

1706
        // Loop through cells and apply styles
1707
        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
18✔
1708
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
18✔
1709
                $this->setConditionalStyles(Coordinate::stringFromColumnIndex($col) . $row, $styles);
18✔
1710
            }
1711
        }
1712

1713
        return $this;
18✔
1714
    }
1715

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

1729
        if ($break === self::BREAK_NONE) {
33✔
1730
            unset($this->rowBreaks[$cellAddress], $this->columnBreaks[$cellAddress]);
7✔
1731
        } elseif ($break === self::BREAK_ROW) {
33✔
1732
            $this->rowBreaks[$cellAddress] = new PageBreak($break, $cellAddress, $max);
23✔
1733
        } elseif ($break === self::BREAK_COLUMN) {
19✔
1734
            $this->columnBreaks[$cellAddress] = new PageBreak($break, $cellAddress, $max);
19✔
1735
        }
1736

1737
        return $this;
33✔
1738
    }
1739

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

1761
        return $breaks;
729✔
1762
    }
1763

1764
    /**
1765
     * Get row breaks.
1766
     *
1767
     * @return PageBreak[]
1768
     */
1769
    public function getRowBreaks(): array
598✔
1770
    {
1771
        /** @var callable $compareFunction */
1772
        $compareFunction = [self::class, 'compareRowBreaks'];
598✔
1773
        uksort($this->rowBreaks, $compareFunction);
598✔
1774

1775
        return $this->rowBreaks;
598✔
1776
    }
1777

1778
    protected static function compareRowBreaks(string $coordinate1, string $coordinate2): int
9✔
1779
    {
1780
        $row1 = Coordinate::indexesFromString($coordinate1)[1];
9✔
1781
        $row2 = Coordinate::indexesFromString($coordinate2)[1];
9✔
1782

1783
        return $row1 - $row2;
9✔
1784
    }
1785

1786
    protected static function compareColumnBreaks(string $coordinate1, string $coordinate2): int
5✔
1787
    {
1788
        $column1 = Coordinate::indexesFromString($coordinate1)[0];
5✔
1789
        $column2 = Coordinate::indexesFromString($coordinate2)[0];
5✔
1790

1791
        return $column1 - $column2;
5✔
1792
    }
1793

1794
    /**
1795
     * Get column breaks.
1796
     *
1797
     * @return PageBreak[]
1798
     */
1799
    public function getColumnBreaks(): array
597✔
1800
    {
1801
        /** @var callable $compareFunction */
1802
        $compareFunction = [self::class, 'compareColumnBreaks'];
597✔
1803
        uksort($this->columnBreaks, $compareFunction);
597✔
1804

1805
        return $this->columnBreaks;
597✔
1806
    }
1807

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

1826
        if (!str_contains($range, ':')) {
183✔
1827
            $range .= ":{$range}";
1✔
1828
        }
1829

1830
        if (!Preg::isMatch('/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/', $range, $matches)) {
183✔
1831
            throw new Exception('Merge must be on a valid range of cells.');
1✔
1832
        }
1833

1834
        $this->mergeCells[$range] = $range;
182✔
1835
        $firstRow = (int) $matches[2];
182✔
1836
        $lastRow = (int) $matches[4];
182✔
1837
        $firstColumn = $matches[1];
182✔
1838
        $lastColumn = $matches[3];
182✔
1839
        $firstColumnIndex = Coordinate::columnIndexFromString($firstColumn);
182✔
1840
        $lastColumnIndex = Coordinate::columnIndexFromString($lastColumn);
182✔
1841
        $numberRows = $lastRow - $firstRow;
182✔
1842
        $numberColumns = $lastColumnIndex - $firstColumnIndex;
182✔
1843

1844
        if ($numberRows === 1 && $numberColumns === 1) {
182✔
1845
            return $this;
36✔
1846
        }
1847

1848
        // create upper left cell if it does not already exist
1849
        $upperLeft = "{$firstColumn}{$firstRow}";
175✔
1850
        if (!$this->cellExists($upperLeft)) {
175✔
1851
            $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL);
37✔
1852
        }
1853

1854
        if ($behaviour !== self::MERGE_CELL_CONTENT_HIDE) {
175✔
1855
            // Blank out the rest of the cells in the range (if they exist)
1856
            if ($numberRows > $numberColumns) {
61✔
1857
                $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft, $behaviour);
20✔
1858
            } else {
1859
                $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft, $behaviour);
41✔
1860
            }
1861
        }
1862

1863
        return $this;
175✔
1864
    }
1865

1866
    private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
20✔
1867
    {
1868
        $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
20✔
1869
            ? [$this->getCell($upperLeft)->getFormattedValue()]
1✔
1870
            : [];
19✔
1871

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

1884
        if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
20✔
1885
            /** @var string[] $leftCellValue */
1886
            $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
1✔
1887
        }
1888
    }
1889

1890
    private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
41✔
1891
    {
1892
        $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
41✔
1893
            ? [$this->getCell($upperLeft)->getFormattedValue()]
4✔
1894
            : [];
37✔
1895

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

1909
        if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
41✔
1910
            /** @var string[] $leftCellValue */
1911
            $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
4✔
1912
        }
1913
    }
1914

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

1933
        return $leftCellValue;
61✔
1934
    }
1935

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

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

1959
        return $this;
22✔
1960
    }
1961

1962
    /**
1963
     * Get merge cells array.
1964
     *
1965
     * @return string[]
1966
     */
1967
    public function getMergeCells(): array
1,369✔
1968
    {
1969
        return $this->mergeCells;
1,369✔
1970
    }
1971

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

1984
        return $this;
127✔
1985
    }
1986

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

2002
        if (!$alreadyHashed && $password !== '') {
28✔
2003
            $password = Shared\PasswordHasher::hashPassword($password);
24✔
2004
        }
2005
        $this->protectedCells[$range] = new ProtectedRange($range, $password, $name, $securityDescriptor);
28✔
2006

2007
        return $this;
28✔
2008
    }
2009

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

2023
        if (isset($this->protectedCells[$range])) {
22✔
2024
            unset($this->protectedCells[$range]);
21✔
2025
        } else {
2026
            throw new Exception('Cell range ' . $range . ' not known as protected.');
1✔
2027
        }
2028

2029
        return $this;
21✔
2030
    }
2031

2032
    /**
2033
     * Get protected cells.
2034
     *
2035
     * @return ProtectedRange[]
2036
     */
2037
    public function getProtectedCellRanges(): array
708✔
2038
    {
2039
        return $this->protectedCells;
708✔
2040
    }
2041

2042
    /**
2043
     * Get Autofilter.
2044
     */
2045
    public function getAutoFilter(): AutoFilter
926✔
2046
    {
2047
        return $this->autoFilter;
926✔
2048
    }
2049

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

2067
            $this->autoFilter->setRange($cellRange);
21✔
2068
        }
2069

2070
        return $this;
21✔
2071
    }
2072

2073
    /**
2074
     * Remove autofilter.
2075
     */
2076
    public function removeAutoFilter(): self
1✔
2077
    {
2078
        $this->autoFilter->setRange('');
1✔
2079

2080
        return $this;
1✔
2081
    }
2082

2083
    /**
2084
     * Get collection of Tables.
2085
     *
2086
     * @return ArrayObject<int, Table>
2087
     */
2088
    public function getTableCollection(): ArrayObject
10,945✔
2089
    {
2090
        return $this->tableCollection;
10,945✔
2091
    }
2092

2093
    /**
2094
     * Add Table.
2095
     *
2096
     * @return $this
2097
     */
2098
    public function addTable(Table $table): self
105✔
2099
    {
2100
        $table->setWorksheet($this);
105✔
2101
        $this->tableCollection[] = $table;
105✔
2102

2103
        return $this;
105✔
2104
    }
2105

2106
    /**
2107
     * @return string[] array of Table names
2108
     */
2109
    public function getTableNames(): array
1✔
2110
    {
2111
        $tableNames = [];
1✔
2112

2113
        foreach ($this->tableCollection as $table) {
1✔
2114
            /** @var Table $table */
2115
            $tableNames[] = $table->getName();
1✔
2116
        }
2117

2118
        return $tableNames;
1✔
2119
    }
2120

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

2130
        return ($tableIndex === null) ? null : $this->tableCollection[$tableIndex];
97✔
2131
    }
2132

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

2148
        return null;
41✔
2149
    }
2150

2151
    /**
2152
     * Remove Table by name.
2153
     *
2154
     * @param string $name Table name
2155
     *
2156
     * @return $this
2157
     */
2158
    public function removeTableByName(string $name): self
1✔
2159
    {
2160
        $tableIndex = $this->getTableIndexByName($name);
1✔
2161

2162
        if ($tableIndex !== null) {
1✔
2163
            unset($this->tableCollection[$tableIndex]);
1✔
2164
        }
2165

2166
        return $this;
1✔
2167
    }
2168

2169
    /**
2170
     * Remove collection of Tables.
2171
     */
2172
    public function removeTableCollection(): self
1✔
2173
    {
2174
        $this->tableCollection = new ArrayObject();
1✔
2175

2176
        return $this;
1✔
2177
    }
2178

2179
    /**
2180
     * Get Freeze Pane.
2181
     */
2182
    public function getFreezePane(): ?string
317✔
2183
    {
2184
        return $this->freezePane;
317✔
2185
    }
2186

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

2223
        if ($cellAddress !== null && $topLeftCell === null) {
49✔
2224
            $coordinate = Coordinate::coordinateFromString($cellAddress);
37✔
2225
            $topLeftCell = $coordinate[0] . $coordinate[1];
37✔
2226
        }
2227

2228
        $topLeftCell = "$topLeftCell";
49✔
2229
        $this->paneTopLeftCell = $topLeftCell;
49✔
2230

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

2251
        return $this;
49✔
2252
    }
2253

2254
    public function setTopLeftCell(string $topLeftCell): self
61✔
2255
    {
2256
        $this->topLeftCell = $topLeftCell;
61✔
2257

2258
        return $this;
61✔
2259
    }
2260

2261
    /**
2262
     * Unfreeze Pane.
2263
     *
2264
     * @return $this
2265
     */
2266
    public function unfreezePane(): static
1✔
2267
    {
2268
        return $this->freezePane(null);
1✔
2269
    }
2270

2271
    /**
2272
     * Get the default position of the right bottom pane.
2273
     */
2274
    public function getTopLeftCell(): ?string
530✔
2275
    {
2276
        return $this->topLeftCell;
530✔
2277
    }
2278

2279
    public function getPaneTopLeftCell(): string
11✔
2280
    {
2281
        return $this->paneTopLeftCell;
11✔
2282
    }
2283

2284
    public function setPaneTopLeftCell(string $paneTopLeftCell): self
26✔
2285
    {
2286
        $this->paneTopLeftCell = $paneTopLeftCell;
26✔
2287

2288
        return $this;
26✔
2289
    }
2290

2291
    public function usesPanes(): bool
518✔
2292
    {
2293
        return $this->xSplit > 0 || $this->ySplit > 0;
518✔
2294
    }
2295

2296
    public function getPane(string $position): ?Pane
2✔
2297
    {
2298
        return $this->panes[$position] ?? null;
2✔
2299
    }
2300

2301
    public function setPane(string $position, ?Pane $pane): self
47✔
2302
    {
2303
        if (array_key_exists($position, $this->panes)) {
47✔
2304
            $this->panes[$position] = $pane;
47✔
2305
        }
2306

2307
        return $this;
47✔
2308
    }
2309

2310
    /** @return (null|Pane)[] */
2311
    public function getPanes(): array
3✔
2312
    {
2313
        return $this->panes;
3✔
2314
    }
2315

2316
    public function getActivePane(): string
14✔
2317
    {
2318
        return $this->activePane;
14✔
2319
    }
2320

2321
    public function setActivePane(string $activePane): self
49✔
2322
    {
2323
        $this->activePane = array_key_exists($activePane, $this->panes) ? $activePane : '';
49✔
2324

2325
        return $this;
49✔
2326
    }
2327

2328
    public function getXSplit(): int
11✔
2329
    {
2330
        return $this->xSplit;
11✔
2331
    }
2332

2333
    public function setXSplit(int $xSplit): self
11✔
2334
    {
2335
        $this->xSplit = $xSplit;
11✔
2336
        if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
11✔
2337
            $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
1✔
2338
        }
2339

2340
        return $this;
11✔
2341
    }
2342

2343
    public function getYSplit(): int
11✔
2344
    {
2345
        return $this->ySplit;
11✔
2346
    }
2347

2348
    public function setYSplit(int $ySplit): self
26✔
2349
    {
2350
        $this->ySplit = $ySplit;
26✔
2351
        if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
26✔
2352
            $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
1✔
2353
        }
2354

2355
        return $this;
26✔
2356
    }
2357

2358
    public function getPaneState(): string
30✔
2359
    {
2360
        return $this->paneState;
30✔
2361
    }
2362

2363
    public const PANE_FROZEN = 'frozen';
2364
    public const PANE_FROZENSPLIT = 'frozenSplit';
2365
    public const PANE_SPLIT = 'split';
2366
    private const VALIDPANESTATE = [self::PANE_FROZEN, self::PANE_SPLIT, self::PANE_FROZENSPLIT];
2367
    private const VALIDFROZENSTATE = [self::PANE_FROZEN, self::PANE_FROZENSPLIT];
2368

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

2378
        return $this;
26✔
2379
    }
2380

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

2398
        return $this;
42✔
2399
    }
2400

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

2418
        return $this;
50✔
2419
    }
2420

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

2435
        throw new Exception('Columns can only be inserted before at least column A (1).');
1✔
2436
    }
2437

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

2488
        $holdRowDimensions = $this->removeRowDimensions($row, $numberOfRows);
52✔
2489
        $highestRow = $this->getHighestDataRow();
52✔
2490
        $removedRowsCounter = 0;
52✔
2491

2492
        for ($r = 0; $r < $numberOfRows; ++$r) {
52✔
2493
            if ($row + $r <= $highestRow) {
52✔
2494
                $this->cellCollection->removeRow($row + $r);
40✔
2495
                ++$removedRowsCounter;
40✔
2496
            }
2497
        }
2498

2499
        $objReferenceHelper = ReferenceHelper::getInstance();
52✔
2500
        $objReferenceHelper->insertNewBefore('A' . ($row + $numberOfRows), 0, -$numberOfRows, $this);
52✔
2501
        for ($r = 0; $r < $removedRowsCounter; ++$r) {
52✔
2502
            $this->cellCollection->removeRow($highestRow);
40✔
2503
            --$highestRow;
40✔
2504
        }
2505

2506
        $this->rowDimensions = $holdRowDimensions;
52✔
2507

2508
        return $this;
52✔
2509
    }
2510

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

2528
        return $holdRowDimensions;
52✔
2529
    }
2530

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

2582
        $highestColumn = $this->getHighestDataColumn();
42✔
2583
        $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
42✔
2584
        $pColumnIndex = Coordinate::columnIndexFromString($column);
42✔
2585

2586
        $holdColumnDimensions = $this->removeColumnDimensions($pColumnIndex, $numberOfColumns);
42✔
2587

2588
        $column = Coordinate::stringFromColumnIndex($pColumnIndex + $numberOfColumns);
42✔
2589
        $objReferenceHelper = ReferenceHelper::getInstance();
42✔
2590
        $objReferenceHelper->insertNewBefore($column . '1', -$numberOfColumns, 0, $this);
42✔
2591

2592
        $this->columnDimensions = $holdColumnDimensions;
42✔
2593

2594
        if ($pColumnIndex > $highestColumnIndex) {
42✔
2595
            return $this;
9✔
2596
        }
2597

2598
        $maxPossibleColumnsToBeRemoved = $highestColumnIndex - $pColumnIndex + 1;
33✔
2599

2600
        for ($c = 0, $n = min($maxPossibleColumnsToBeRemoved, $numberOfColumns); $c < $n; ++$c) {
33✔
2601
            $this->cellCollection->removeColumn($highestColumn);
33✔
2602
            $highestColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($highestColumn) - 1);
33✔
2603
        }
2604

2605
        $this->garbageCollect();
33✔
2606

2607
        return $this;
33✔
2608
    }
2609

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

2628
        return $holdColumnDimensions;
42✔
2629
    }
2630

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

2645
        throw new Exception('Columns to be deleted should at least start from column A (1)');
1✔
2646
    }
2647

2648
    /**
2649
     * Show gridlines?
2650
     */
2651
    public function getShowGridlines(): bool
1,162✔
2652
    {
2653
        return $this->showGridlines;
1,162✔
2654
    }
2655

2656
    /**
2657
     * Set show gridlines.
2658
     *
2659
     * @param bool $showGridLines Show gridlines (true/false)
2660
     *
2661
     * @return $this
2662
     */
2663
    public function setShowGridlines(bool $showGridLines): self
934✔
2664
    {
2665
        $this->showGridlines = $showGridLines;
934✔
2666

2667
        return $this;
934✔
2668
    }
2669

2670
    /**
2671
     * Print gridlines?
2672
     */
2673
    public function getPrintGridlines(): bool
1,185✔
2674
    {
2675
        return $this->printGridlines;
1,185✔
2676
    }
2677

2678
    /**
2679
     * Set print gridlines.
2680
     *
2681
     * @param bool $printGridLines Print gridlines (true/false)
2682
     *
2683
     * @return $this
2684
     */
2685
    public function setPrintGridlines(bool $printGridLines): self
609✔
2686
    {
2687
        $this->printGridlines = $printGridLines;
609✔
2688

2689
        return $this;
609✔
2690
    }
2691

2692
    /**
2693
     * Show row and column headers?
2694
     */
2695
    public function getShowRowColHeaders(): bool
594✔
2696
    {
2697
        return $this->showRowColHeaders;
594✔
2698
    }
2699

2700
    /**
2701
     * Set show row and column headers.
2702
     *
2703
     * @param bool $showRowColHeaders Show row and column headers (true/false)
2704
     *
2705
     * @return $this
2706
     */
2707
    public function setShowRowColHeaders(bool $showRowColHeaders): self
448✔
2708
    {
2709
        $this->showRowColHeaders = $showRowColHeaders;
448✔
2710

2711
        return $this;
448✔
2712
    }
2713

2714
    /**
2715
     * Show summary below? (Row/Column outlining).
2716
     */
2717
    public function getShowSummaryBelow(): bool
595✔
2718
    {
2719
        return $this->showSummaryBelow;
595✔
2720
    }
2721

2722
    /**
2723
     * Set show summary below.
2724
     *
2725
     * @param bool $showSummaryBelow Show summary below (true/false)
2726
     *
2727
     * @return $this
2728
     */
2729
    public function setShowSummaryBelow(bool $showSummaryBelow): self
447✔
2730
    {
2731
        $this->showSummaryBelow = $showSummaryBelow;
447✔
2732

2733
        return $this;
447✔
2734
    }
2735

2736
    /**
2737
     * Show summary right? (Row/Column outlining).
2738
     */
2739
    public function getShowSummaryRight(): bool
595✔
2740
    {
2741
        return $this->showSummaryRight;
595✔
2742
    }
2743

2744
    /**
2745
     * Set show summary right.
2746
     *
2747
     * @param bool $showSummaryRight Show summary right (true/false)
2748
     *
2749
     * @return $this
2750
     */
2751
    public function setShowSummaryRight(bool $showSummaryRight): self
447✔
2752
    {
2753
        $this->showSummaryRight = $showSummaryRight;
447✔
2754

2755
        return $this;
447✔
2756
    }
2757

2758
    /**
2759
     * Get comments.
2760
     *
2761
     * @return Comment[]
2762
     */
2763
    public function getComments(): array
1,238✔
2764
    {
2765
        return $this->comments;
1,238✔
2766
    }
2767

2768
    /**
2769
     * Set comments array for the entire sheet.
2770
     *
2771
     * @param Comment[] $comments
2772
     *
2773
     * @return $this
2774
     */
2775
    public function setComments(array $comments): self
127✔
2776
    {
2777
        $this->comments = $comments;
127✔
2778

2779
        return $this;
127✔
2780
    }
2781

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

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

2806
        return $this;
55✔
2807
    }
2808

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

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

2827
        // Check if we already have a comment for this cell.
2828
        if (isset($this->comments[$cellAddress])) {
117✔
2829
            return $this->comments[$cellAddress];
83✔
2830
        }
2831

2832
        // If not, create a new comment.
2833
        $newComment = new Comment();
117✔
2834
        if ($attachNew) {
117✔
2835
            $this->comments[$cellAddress] = $newComment;
117✔
2836
        }
2837

2838
        return $newComment;
117✔
2839
    }
2840

2841
    /**
2842
     * Get active cell.
2843
     *
2844
     * @return string Example: 'A1'
2845
     */
2846
    public function getActiveCell(): string
10,981✔
2847
    {
2848
        return $this->activeCell;
10,981✔
2849
    }
2850

2851
    /**
2852
     * Get selected cells.
2853
     */
2854
    public function getSelectedCells(): string
11,035✔
2855
    {
2856
        return $this->selectedCells;
11,035✔
2857
    }
2858

2859
    /**
2860
     * Selected cell.
2861
     *
2862
     * @param string $coordinate Cell (i.e. A1)
2863
     *
2864
     * @return $this
2865
     */
2866
    public function setSelectedCell(string $coordinate): static
38✔
2867
    {
2868
        return $this->setSelectedCells($coordinate);
38✔
2869
    }
2870

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

2887
        if (Coordinate::coordinateIsRange($coordinate)) {
10,984✔
2888
            [$first] = Coordinate::splitRange($coordinate);
544✔
2889
            $this->activeCell = $first[0];
544✔
2890
        } else {
2891
            $this->activeCell = $coordinate;
10,953✔
2892
        }
2893
        $this->selectedCells = $coordinate;
10,984✔
2894
        $this->setSelectedCellsActivePane();
10,984✔
2895

2896
        return $this;
10,984✔
2897
    }
2898

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

2918
    /**
2919
     * Get right-to-left.
2920
     */
2921
    public function getRightToLeft(): bool
1,188✔
2922
    {
2923
        return $this->rightToLeft;
1,188✔
2924
    }
2925

2926
    /**
2927
     * Set right-to-left.
2928
     *
2929
     * @param bool $value Right-to-left true/false
2930
     *
2931
     * @return $this
2932
     */
2933
    public function setRightToLeft(bool $value): static
168✔
2934
    {
2935
        $this->rightToLeft = $value;
168✔
2936

2937
        return $this;
168✔
2938
    }
2939

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

2958
        // start coordinate
2959
        [$startColumn, $startRow] = Coordinate::coordinateFromString($startCell);
912✔
2960
        $startRow = (int) $startRow;
912✔
2961

2962
        // Loop through $source
2963
        if ($strictNullComparison) {
912✔
2964
            foreach ($source as $rowData) {
411✔
2965
                /** @var string */
2966
                $currentColumn = $startColumn;
411✔
2967
                foreach ($rowData as $cellValue) {
411✔
2968
                    if ($cellValue !== $nullValue) {
411✔
2969
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
411✔
2970
                    }
2971
                    StringHelper::stringIncrement($currentColumn);
411✔
2972
                }
2973
                ++$startRow;
411✔
2974
            }
2975
        } else {
2976
            foreach ($source as $rowData) {
510✔
2977
                $currentColumn = $startColumn;
510✔
2978
                foreach ($rowData as $cellValue) {
510✔
2979
                    if ($cellValue != $nullValue) {
509✔
2980
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
503✔
2981
                    }
2982
                    StringHelper::stringIncrement($currentColumn);
509✔
2983
                }
2984
                ++$startRow;
510✔
2985
            }
2986
        }
2987

2988
        return $this;
912✔
2989
    }
2990

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

3005
        if ($cell->getValue() !== null) {
202✔
3006
            if ($cell->getValue() instanceof RichText) {
202✔
3007
                $returnValue = $cell->getValue()->getPlainText();
4✔
3008
            } elseif ($calculateFormulas) {
202✔
3009
                $returnValue = $cell->getCalculatedValue();
173✔
3010
            } elseif ($oldCalculatedValue && ($cell->getDataType() === DataType::TYPE_FORMULA)) {
35✔
3011
                $returnValue = $cell->getOldCalculatedValue() ?? $cell->getValue();
2✔
3012
            } else {
3013
                $returnValue = $cell->getValue();
35✔
3014
            }
3015

3016
            if ($formatData) {
202✔
3017
                $style = $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex());
129✔
3018
                /** @var null|bool|float|int|RichText|string */
3019
                $returnValuex = $returnValue;
129✔
3020
                $returnValue = NumberFormat::toFormattedString(
129✔
3021
                    $returnValuex,
129✔
3022
                    $style->getNumberFormat()->getFormatCode() ?? NumberFormat::FORMAT_GENERAL,
129✔
3023
                    lessFloatPrecision: $lessFloatPrecision
129✔
3024
                );
129✔
3025
            }
3026
        }
3027

3028
        return $returnValue;
202✔
3029
    }
3030

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

3060
        // Loop through rows
3061
        foreach ($this->rangeToArrayYieldRows($range, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue) as $rowRef => $rowArray) {
167✔
3062
            /** @var int $rowRef */
3063
            $returnValue[$rowRef] = $rowArray;
167✔
3064
        }
3065

3066
        // Return
3067
        return $returnValue;
167✔
3068
    }
3069

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

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

3108
        // Return
3109
        return $returnValue;
6✔
3110
    }
3111

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

3141
        //    Identify the range that we need to extract from the worksheet
3142
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range);
202✔
3143
        $minCol = Coordinate::stringFromColumnIndex($rangeStart[0]);
202✔
3144
        $minRow = $rangeStart[1];
202✔
3145
        $maxCol = Coordinate::stringFromColumnIndex($rangeEnd[0]);
202✔
3146
        $maxRow = $rangeEnd[1];
202✔
3147
        $minColInt = $rangeStart[0];
202✔
3148
        $maxColInt = $rangeEnd[0];
202✔
3149

3150
        StringHelper::stringIncrement($maxCol);
202✔
3151
        /** @var array<string, bool> */
3152
        $hiddenColumns = [];
202✔
3153
        $nullRow = $this->buildNullRow($nullValue, $minCol, $maxCol, $returnCellRef, $ignoreHidden, $hiddenColumns);
202✔
3154
        $hideColumns = !empty($hiddenColumns);
202✔
3155

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

3167
            $index = ($row - 1) * AddressRange::MAX_COLUMN_INT + 1;
202✔
3168
            $indexPlus = $index + AddressRange::MAX_COLUMN_INT - 1;
202✔
3169

3170
            // Binary search to quickly approach the correct index
3171
            $keyIndex = intdiv($keysCount, 2);
202✔
3172
            $boundLow = 0;
202✔
3173
            $boundHigh = $keysCount - 1;
202✔
3174
            while ($boundLow <= $boundHigh) {
202✔
3175
                $keyIndex = intdiv($boundLow + $boundHigh, 2);
202✔
3176
                if ($keys[$keyIndex] < $index) {
202✔
3177
                    $boundLow = $keyIndex + 1;
168✔
3178
                } elseif ($keys[$keyIndex] > $index) {
202✔
3179
                    $boundHigh = $keyIndex - 1;
184✔
3180
                } else {
3181
                    break;
194✔
3182
                }
3183
            }
3184

3185
            // Realign to the proper index value
3186
            while ($keyIndex > 0 && $keys[$keyIndex] > $index) {
202✔
3187
                --$keyIndex;
14✔
3188
            }
3189
            while ($keyIndex < $keysCount && $keys[$keyIndex] < $index) {
202✔
3190
                ++$keyIndex;
21✔
3191
            }
3192

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

3218
            yield $rowRef => $returnValue;
202✔
3219
        }
3220
    }
3221

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

3255
        return $nullRow;
202✔
3256
    }
3257

3258
    private function validateNamedRange(string $definedName, bool $returnNullIfInvalid = false): ?DefinedName
19✔
3259
    {
3260
        $namedRange = DefinedName::resolveName($definedName, $this);
19✔
3261
        if ($namedRange === null) {
19✔
3262
            if ($returnNullIfInvalid) {
6✔
3263
                return null;
5✔
3264
            }
3265

3266
            throw new Exception('Named Range ' . $definedName . ' does not exist.');
1✔
3267
        }
3268

3269
        if ($namedRange->isFormula()) {
13✔
3270
            if ($returnNullIfInvalid) {
×
UNCOV
3271
                return null;
×
3272
            }
3273

UNCOV
3274
            throw new Exception('Defined Named ' . $definedName . ' is a formula, not a range or cell.');
×
3275
        }
3276

3277
        if ($namedRange->getLocalOnly()) {
13✔
3278
            $worksheet = $namedRange->getWorksheet();
2✔
3279
            if ($worksheet === null || $this !== $worksheet) {
2✔
3280
                if ($returnNullIfInvalid) {
×
UNCOV
3281
                    return null;
×
3282
                }
3283

3284
                throw new Exception(
×
3285
                    'Named range ' . $definedName . ' is not accessible from within sheet ' . $this->getTitle()
×
UNCOV
3286
                );
×
3287
            }
3288
        }
3289

3290
        return $namedRange;
13✔
3291
    }
3292

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

3332
        return $retVal;
1✔
3333
    }
3334

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

3365
        //    Identify the range that we need to extract from the worksheet
3366
        $maxCol = $this->getHighestColumn();
98✔
3367
        $maxRow = $this->getHighestRow();
98✔
3368

3369
        // Return
3370
        return $this->rangeToArray("A1:{$maxCol}{$maxRow}", $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue);
98✔
3371
    }
3372

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

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

3395
    /**
3396
     * Run PhpSpreadsheet garbage collector.
3397
     *
3398
     * @return $this
3399
     */
3400
    public function garbageCollect(): static
1,335✔
3401
    {
3402
        // Flush cache
3403
        $this->cellCollection->get('A1');
1,335✔
3404

3405
        // Lookup highest column and highest row if cells are cleaned
3406
        $colRow = $this->cellCollection->getHighestRowAndColumn();
1,335✔
3407
        $highestRow = $colRow['row'];
1,335✔
3408
        $highestColumn = Coordinate::columnIndexFromString($colRow['column']);
1,335✔
3409

3410
        // Loop through column dimensions
3411
        foreach ($this->columnDimensions as $dimension) {
1,335✔
3412
            $highestColumn = max($highestColumn, Coordinate::columnIndexFromString($dimension->getColumnIndex()));
197✔
3413
        }
3414

3415
        // Loop through row dimensions
3416
        foreach ($this->rowDimensions as $dimension) {
1,335✔
3417
            $highestRow = max($highestRow, $dimension->getRowIndex());
125✔
3418
        }
3419

3420
        // Cache values
3421
        $this->cachedHighestColumn = max(1, $highestColumn);
1,335✔
3422
        /** @var int $highestRow */
3423
        $this->cachedHighestRow = $highestRow;
1,335✔
3424

3425
        // Return
3426
        return $this;
1,335✔
3427
    }
3428

3429
    /**
3430
     * @deprecated 5.2.0 Serves no useful purpose. No replacement.
3431
     *
3432
     * @codeCoverageIgnore
3433
     */
3434
    public function getHashInt(): int
3435
    {
3436
        return spl_object_id($this);
3437
    }
3438

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

3460
        // Sheet title included?
3461
        if (($sep = strrpos($range, '!')) === false) {
11,213✔
3462
            return $returnRange ? ['', $range] : '';
11,184✔
3463
        }
3464

3465
        if ($returnRange) {
1,499✔
3466
            $title = substr($range, 0, $sep);
1,499✔
3467
            if ($unapostrophize) {
1,499✔
3468
                $title = self::unApostrophizeTitle($title);
1,439✔
3469
            }
3470

3471
            return [$title, substr($range, $sep + 1)];
1,499✔
3472
        }
3473

3474
        return substr($range, $sep + 1);
7✔
3475
    }
3476

3477
    public static function unApostrophizeTitle(?string $title): string
1,453✔
3478
    {
3479
        $title ??= '';
1,453✔
3480
        if (str_starts_with($title, "'") && str_ends_with($title, "'")) {
1,453✔
3481
            $title = str_replace("''", "'", substr($title, 1, -1));
1,377✔
3482
        }
3483

3484
        return $title;
1,453✔
3485
    }
3486

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

3500
        // else create hyperlink
3501
        $this->hyperlinkCollection[$cellCoordinate] = new Hyperlink();
113✔
3502

3503
        return $this->hyperlinkCollection[$cellCoordinate];
113✔
3504
    }
3505

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

3526
        return $this;
592✔
3527
    }
3528

3529
    /**
3530
     * Hyperlink at a specific coordinate exists?
3531
     *
3532
     * @param string $coordinate eg: 'A1'
3533
     */
3534
    public function hyperlinkExists(string $coordinate): bool
689✔
3535
    {
3536
        return isset($this->hyperlinkCollection[$coordinate]);
689✔
3537
    }
3538

3539
    /**
3540
     * Get collection of hyperlinks.
3541
     *
3542
     * @return Hyperlink[]
3543
     */
3544
    public function getHyperlinkCollection(): array
703✔
3545
    {
3546
        return $this->hyperlinkCollection;
703✔
3547
    }
3548

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

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

3576
        // else create data validation
3577
        $dataValidation = new DataValidation();
20✔
3578
        $dataValidation->setSqref($cellCoordinate);
20✔
3579
        $this->dataValidationCollection[$cellCoordinate] = $dataValidation;
20✔
3580

3581
        return $dataValidation;
20✔
3582
    }
3583

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

3600
        return $this;
92✔
3601
    }
3602

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

3627
        return false;
6✔
3628
    }
3629

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

3647
        return array_merge($collectionCells, $collectionRanges);
704✔
3648
    }
3649

3650
    /**
3651
     * Accepts a range, returning it as a range that falls within the current highest row and column of the worksheet.
3652
     *
3653
     * @return string Adjusted range value
3654
     */
3655
    public function shrinkRangeToFit(string $range): string
1✔
3656
    {
3657
        $maxCol = $this->getHighestColumn();
1✔
3658
        $maxRow = $this->getHighestRow();
1✔
3659
        $maxCol = Coordinate::columnIndexFromString($maxCol);
1✔
3660

3661
        $rangeBlocks = explode(' ', $range);
1✔
3662
        foreach ($rangeBlocks as &$rangeSet) {
1✔
3663
            $rangeBoundaries = Coordinate::getRangeBoundaries($rangeSet);
1✔
3664

3665
            if (Coordinate::columnIndexFromString($rangeBoundaries[0][0]) > $maxCol) {
1✔
3666
                $rangeBoundaries[0][0] = Coordinate::stringFromColumnIndex($maxCol);
1✔
3667
            }
3668
            if ($rangeBoundaries[0][1] > $maxRow) {
1✔
3669
                $rangeBoundaries[0][1] = $maxRow;
1✔
3670
            }
3671
            if (Coordinate::columnIndexFromString($rangeBoundaries[1][0]) > $maxCol) {
1✔
3672
                $rangeBoundaries[1][0] = Coordinate::stringFromColumnIndex($maxCol);
1✔
3673
            }
3674
            if ($rangeBoundaries[1][1] > $maxRow) {
1✔
3675
                $rangeBoundaries[1][1] = $maxRow;
1✔
3676
            }
3677
            $rangeSet = $rangeBoundaries[0][0] . $rangeBoundaries[0][1] . ':' . $rangeBoundaries[1][0] . $rangeBoundaries[1][1];
1✔
3678
        }
3679
        unset($rangeSet);
1✔
3680

3681
        return implode(' ', $rangeBlocks);
1✔
3682
    }
3683

3684
    /**
3685
     * Get tab color.
3686
     */
3687
    public function getTabColor(): Color
23✔
3688
    {
3689
        if ($this->tabColor === null) {
23✔
3690
            $this->tabColor = new Color();
23✔
3691
        }
3692

3693
        return $this->tabColor;
23✔
3694
    }
3695

3696
    /**
3697
     * Reset tab color.
3698
     *
3699
     * @return $this
3700
     */
3701
    public function resetTabColor(): static
1✔
3702
    {
3703
        $this->tabColor = null;
1✔
3704

3705
        return $this;
1✔
3706
    }
3707

3708
    /**
3709
     * Tab color set?
3710
     */
3711
    public function isTabColorSet(): bool
596✔
3712
    {
3713
        return $this->tabColor !== null;
596✔
3714
    }
3715

3716
    /**
3717
     * Copy worksheet (!= clone!).
3718
     */
3719
    public function copy(): static
1✔
3720
    {
3721
        return clone $this;
1✔
3722
    }
3723

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

3751
        return $row->isEmpty($definitionOfEmptyFlags);
8✔
3752
    }
3753

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

3781
        return $column->isEmpty($definitionOfEmptyFlags);
8✔
3782
    }
3783

3784
    /**
3785
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
3786
     */
3787
    public function __clone()
28✔
3788
    {
3789
        foreach (get_object_vars($this) as $key => $val) {
28✔
3790
            if ($key == 'parent') {
28✔
3791
                continue;
28✔
3792
            }
3793

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

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

3855
        if ($validate) {
11,465✔
3856
            $codeName = str_replace(' ', '_', $codeName); //Excel does this automatically without flinching, we are doing the same
11,465✔
3857

3858
            // Syntax check
3859
            // throw an exception if not valid
3860
            self::checkSheetCodeName($codeName);
11,465✔
3861

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

3864
            if ($this->parent !== null) {
11,465✔
3865
                // Is there already such sheet name?
3866
                if ($this->parent->sheetCodeNameExists($codeName)) {
11,424✔
3867
                    // Use name, but append with lowest possible integer
3868

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

3886
                    $codeName .= '_' . $i; // ok, we have a valid name
741✔
3887
                }
3888
            }
3889
        }
3890

3891
        $this->codeName = $codeName;
11,465✔
3892

3893
        return $this;
11,465✔
3894
    }
3895

3896
    /**
3897
     * Return the code name of the sheet.
3898
     */
3899
    public function getCodeName(): ?string
11,465✔
3900
    {
3901
        return $this->codeName;
11,465✔
3902
    }
3903

3904
    /**
3905
     * Sheet has a code name ?
3906
     */
3907
    public function hasCodeName(): bool
2✔
3908
    {
3909
        return $this->codeName !== null;
2✔
3910
    }
3911

3912
    public static function nameRequiresQuotes(string $sheetName): bool
4✔
3913
    {
3914
        return !Preg::isMatch(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName);
4✔
3915
    }
3916

3917
    public function isRowVisible(int $row): bool
130✔
3918
    {
3919
        return !$this->rowDimensionExists($row) || $this->getRowDimension($row)->getVisible();
130✔
3920
    }
3921

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

UNCOV
3939
        return $spreadsheet->getCellXfByIndex($xfIndex)->getProtection()->getLocked() !== StyleProtection::PROTECTION_UNPROTECTED;
×
3940
    }
3941

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

3951
        // cell doesn't exist, therefore isn't a formula,
3952
        // therefore isn't hidden on formula bar.
3953
        return false;
1✔
3954
    }
3955

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

3968
        return $xfIndex;
1✔
3969
    }
3970

3971
    private string $backgroundImage = '';
3972

3973
    private string $backgroundMime = '';
3974

3975
    private string $backgroundExtension = '';
3976

3977
    public function getBackgroundImage(): string
1,112✔
3978
    {
3979
        return $this->backgroundImage;
1,112✔
3980
    }
3981

3982
    public function getBackgroundMime(): string
449✔
3983
    {
3984
        return $this->backgroundMime;
449✔
3985
    }
3986

3987
    public function getBackgroundExtension(): string
449✔
3988
    {
3989
        return $this->backgroundExtension;
449✔
3990
    }
3991

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

4011
        return $this;
4✔
4012
    }
4013

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

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

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

4072
        return false;
1✔
4073
    }
4074

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

4090
        return true;
1✔
4091
    }
4092

4093
    public function copyFormula(string $fromCell, string $toCell): void
1✔
4094
    {
4095
        $formula = $this->getCell($fromCell)->getValue();
1✔
4096
        $newFormula = $formula;
1✔
4097
        if (is_string($formula) && $this->getCell($fromCell)->getDataType() === DataType::TYPE_FORMULA) {
1✔
4098
            [$fromColInt, $fromRow] = Coordinate::indexesFromString($fromCell);
1✔
4099
            [$toColInt, $toRow] = Coordinate::indexesFromString($toCell);
1✔
4100
            $helper = ReferenceHelper::getInstance();
1✔
4101
            $newFormula = $helper->updateFormulaReferences(
1✔
4102
                $formula,
1✔
4103
                'A1',
1✔
4104
                $toColInt - $fromColInt,
1✔
4105
                $toRow - $fromRow
1✔
4106
            );
1✔
4107
        }
4108
        $this->setCellValue($toCell, $newFormula);
1✔
4109
    }
4110
}
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