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

PHPOffice / PhpSpreadsheet / 17606633869

10 Sep 2025 07:33AM UTC coverage: 95.298%. Remained the same
17606633869

Pull #4640

github

web-flow
Merge ebdbdd41b into 109b2b282
Pull Request #4640: Option To Display Numbers With Less Precision

14 of 15 new or added lines in 4 files covered. (93.33%)

40311 of 42300 relevant lines covered (95.3%)

348.4 hits per line

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

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

3
namespace PhpOffice\PhpSpreadsheet\Worksheet;
4

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

111
    /**
112
     * Collection of Chart objects.
113
     *
114
     * @var ArrayObject<int, Chart>
115
     */
116
    private ArrayObject $chartCollection;
117

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

125
    /**
126
     * Worksheet title.
127
     */
128
    private string $title = '';
129

130
    /**
131
     * Sheet state.
132
     */
133
    private string $sheetState;
134

135
    /**
136
     * Page setup.
137
     */
138
    private PageSetup $pageSetup;
139

140
    /**
141
     * Page margins.
142
     */
143
    private PageMargins $pageMargins;
144

145
    /**
146
     * Page header/footer.
147
     */
148
    private HeaderFooter $headerFooter;
149

150
    /**
151
     * Sheet view.
152
     */
153
    private SheetView $sheetView;
154

155
    /**
156
     * Protection.
157
     */
158
    private Protection $protection;
159

160
    /**
161
     * Conditional styles. Indexed by cell coordinate, e.g. 'A1'.
162
     *
163
     * @var Conditional[][]
164
     */
165
    private array $conditionalStylesCollection = [];
166

167
    /**
168
     * Collection of row breaks.
169
     *
170
     * @var PageBreak[]
171
     */
172
    private array $rowBreaks = [];
173

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

181
    /**
182
     * Collection of merged cell ranges.
183
     *
184
     * @var string[]
185
     */
186
    private array $mergeCells = [];
187

188
    /**
189
     * Collection of protected cell ranges.
190
     *
191
     * @var ProtectedRange[]
192
     */
193
    private array $protectedCells = [];
194

195
    /**
196
     * Autofilter Range and selection.
197
     */
198
    private AutoFilter $autoFilter;
199

200
    /**
201
     * Freeze pane.
202
     */
203
    private ?string $freezePane = null;
204

205
    /**
206
     * Default position of the right bottom pane.
207
     */
208
    private ?string $topLeftCell = null;
209

210
    private string $paneTopLeftCell = '';
211

212
    private string $activePane = '';
213

214
    private int $xSplit = 0;
215

216
    private int $ySplit = 0;
217

218
    private string $paneState = '';
219

220
    /**
221
     * Properties of the 4 panes.
222
     *
223
     * @var (null|Pane)[]
224
     */
225
    private array $panes = [
226
        'bottomRight' => null,
227
        'bottomLeft' => null,
228
        'topRight' => null,
229
        'topLeft' => null,
230
    ];
231

232
    /**
233
     * Show gridlines?
234
     */
235
    private bool $showGridlines = true;
236

237
    /**
238
     * Print gridlines?
239
     */
240
    private bool $printGridlines = false;
241

242
    /**
243
     * Show row and column headers?
244
     */
245
    private bool $showRowColHeaders = true;
246

247
    /**
248
     * Show summary below? (Row/Column outline).
249
     */
250
    private bool $showSummaryBelow = true;
251

252
    /**
253
     * Show summary right? (Row/Column outline).
254
     */
255
    private bool $showSummaryRight = true;
256

257
    /**
258
     * Collection of comments.
259
     *
260
     * @var Comment[]
261
     */
262
    private array $comments = [];
263

264
    /**
265
     * Active cell. (Only one!).
266
     */
267
    private string $activeCell = 'A1';
268

269
    /**
270
     * Selected cells.
271
     */
272
    private string $selectedCells = 'A1';
273

274
    /**
275
     * Cached highest column.
276
     */
277
    private int $cachedHighestColumn = 1;
278

279
    /**
280
     * Cached highest row.
281
     */
282
    private int $cachedHighestRow = 1;
283

284
    /**
285
     * Right-to-left?
286
     */
287
    private bool $rightToLeft = false;
288

289
    /**
290
     * Hyperlinks. Indexed by cell coordinate, e.g. 'A1'.
291
     *
292
     * @var Hyperlink[]
293
     */
294
    private array $hyperlinkCollection = [];
295

296
    /**
297
     * Data validation objects. Indexed by cell coordinate, e.g. 'A1'.
298
     * Index can include ranges, and multiple cells/ranges.
299
     *
300
     * @var DataValidation[]
301
     */
302
    private array $dataValidationCollection = [];
303

304
    /**
305
     * Tab color.
306
     */
307
    private ?Color $tabColor = null;
308

309
    /**
310
     * Hash.
311
     */
312
    private int $hash;
313

314
    /**
315
     * CodeName.
316
     */
317
    private ?string $codeName = null;
318

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

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

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

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

379
        $this->disconnectCells();
130✔
380
        unset($this->rowDimensions, $this->columnDimensions, $this->tableCollection, $this->drawingCollection, $this->chartCollection, $this->autoFilter);
130✔
381
    }
382

383
    public function __wakeup(): void
384
    {
385
        $this->hash = spl_object_id($this);
9✔
386
    }
387

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

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

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

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

433
        return $sheetCodeName;
10,738✔
434
    }
435

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

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

455
        return $sheetTitle;
10,738✔
456
    }
457

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

471
        if ($sorted) {
1,511✔
472
            return $this->cellCollection->getSortedCoordinates();
576✔
473
        }
474

475
        return $this->cellCollection->getCoordinates();
1,370✔
476
    }
477

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

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

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

507
        return $this->columnDimensions;
1,231✔
508
    }
509

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

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

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

533
    /**
534
     * Get collection of charts.
535
     *
536
     * @return ArrayObject<int, Chart>
537
     */
538
    public function getChartCollection(): ArrayObject
539
    {
540
        return $this->chartCollection;
100✔
541
    }
542

543
    public function addChart(Chart $chart): Chart
544
    {
545
        $chart->setWorksheet($this);
106✔
546
        $this->chartCollection[] = $chart;
106✔
547

548
        return $chart;
106✔
549
    }
550

551
    /**
552
     * Return the count of charts on this worksheet.
553
     *
554
     * @return int The number of charts
555
     */
556
    public function getChartCount(): int
557
    {
558
        return count($this->chartCollection);
83✔
559
    }
560

561
    /**
562
     * Get a chart by its index position.
563
     *
564
     * @param ?string $index Chart index position
565
     *
566
     * @return Chart|false
567
     */
568
    public function getChartByIndex(?string $index)
569
    {
570
        $chartCount = count($this->chartCollection);
78✔
571
        if ($chartCount == 0) {
78✔
572
            return false;
×
573
        }
574
        if ($index === null) {
78✔
575
            $index = --$chartCount;
×
576
        }
577
        if (!isset($this->chartCollection[$index])) {
78✔
578
            return false;
×
579
        }
580

581
        return $this->chartCollection[$index];
78✔
582
    }
583

584
    /**
585
     * Return an array of the names of charts on this worksheet.
586
     *
587
     * @return string[] The names of charts
588
     */
589
    public function getChartNames(): array
590
    {
591
        $chartNames = [];
5✔
592
        foreach ($this->chartCollection as $chart) {
5✔
593
            $chartNames[] = $chart->getName();
5✔
594
        }
595

596
        return $chartNames;
5✔
597
    }
598

599
    /**
600
     * Get a chart by name.
601
     *
602
     * @param string $chartName Chart name
603
     *
604
     * @return Chart|false
605
     */
606
    public function getChartByName(string $chartName)
607
    {
608
        foreach ($this->chartCollection as $index => $chart) {
6✔
609
            if ($chart->getName() == $chartName) {
6✔
610
                return $chart;
6✔
611
            }
612
        }
613

614
        return false;
1✔
615
    }
616

617
    public function getChartByNameOrThrow(string $chartName): Chart
618
    {
619
        $chart = $this->getChartByName($chartName);
6✔
620
        if ($chart !== false) {
6✔
621
            return $chart;
6✔
622
        }
623

624
        throw new Exception("Sheet does not have a chart named $chartName.");
1✔
625
    }
626

627
    /**
628
     * Refresh column dimensions.
629
     *
630
     * @return $this
631
     */
632
    public function refreshColumnDimensions(): static
633
    {
634
        $newColumnDimensions = [];
25✔
635
        foreach ($this->getColumnDimensions() as $objColumnDimension) {
25✔
636
            $newColumnDimensions[$objColumnDimension->getColumnIndex()] = $objColumnDimension;
25✔
637
        }
638

639
        $this->columnDimensions = $newColumnDimensions;
25✔
640

641
        return $this;
25✔
642
    }
643

644
    /**
645
     * Refresh row dimensions.
646
     *
647
     * @return $this
648
     */
649
    public function refreshRowDimensions(): static
650
    {
651
        $newRowDimensions = [];
7✔
652
        foreach ($this->getRowDimensions() as $objRowDimension) {
7✔
653
            $newRowDimensions[$objRowDimension->getRowIndex()] = $objRowDimension;
7✔
654
        }
655

656
        $this->rowDimensions = $newRowDimensions;
7✔
657

658
        return $this;
7✔
659
    }
660

661
    /**
662
     * Calculate worksheet dimension.
663
     *
664
     * @return string String containing the dimension of this worksheet
665
     */
666
    public function calculateWorksheetDimension(): string
667
    {
668
        // Return
669
        return 'A1:' . $this->getHighestColumn() . $this->getHighestRow();
484✔
670
    }
671

672
    /**
673
     * Calculate worksheet data dimension.
674
     *
675
     * @return string String containing the dimension of this worksheet that actually contain data
676
     */
677
    public function calculateWorksheetDataDimension(): string
678
    {
679
        // Return
680
        return 'A1:' . $this->getHighestDataColumn() . $this->getHighestDataRow();
559✔
681
    }
682

683
    /**
684
     * Calculate widths for auto-size columns.
685
     *
686
     * @return $this
687
     */
688
    public function calculateColumnWidths(): static
689
    {
690
        $activeSheet = $this->getParent()?->getActiveSheetIndex();
790✔
691
        $selectedCells = $this->selectedCells;
790✔
692
        // initialize $autoSizes array
693
        $autoSizes = [];
790✔
694
        foreach ($this->getColumnDimensions() as $colDimension) {
790✔
695
            if ($colDimension->getAutoSize()) {
160✔
696
                $autoSizes[$colDimension->getColumnIndex()] = -1;
65✔
697
            }
698
        }
699

700
        // There is only something to do if there are some auto-size columns
701
        if (!empty($autoSizes)) {
790✔
702
            $holdActivePane = $this->activePane;
65✔
703
            // build list of cells references that participate in a merge
704
            $isMergeCell = [];
65✔
705
            foreach ($this->getMergeCells() as $cells) {
65✔
706
                foreach (Coordinate::extractAllCellReferencesInRange($cells) as $cellReference) {
16✔
707
                    $isMergeCell[$cellReference] = true;
16✔
708
                }
709
            }
710

711
            $autoFilterIndentRanges = (new AutoFit($this))->getAutoFilterIndentRanges();
65✔
712

713
            // loop through all cells in the worksheet
714
            foreach ($this->getCoordinates(false) as $coordinate) {
65✔
715
                $cell = $this->getCellOrNull($coordinate);
65✔
716

717
                if ($cell !== null && isset($autoSizes[$this->cellCollection->getCurrentColumn()])) {
65✔
718
                    //Determine if cell is in merge range
719
                    $isMerged = isset($isMergeCell[$this->cellCollection->getCurrentCoordinate()]);
65✔
720

721
                    //By default merged cells should be ignored
722
                    $isMergedButProceed = false;
65✔
723

724
                    //The only exception is if it's a merge range value cell of a 'vertical' range (1 column wide)
725
                    if ($isMerged && $cell->isMergeRangeValueCell()) {
65✔
726
                        $range = (string) $cell->getMergeRange();
×
727
                        $rangeBoundaries = Coordinate::rangeDimension($range);
×
728
                        if ($rangeBoundaries[0] === 1) {
×
729
                            $isMergedButProceed = true;
×
730
                        }
731
                    }
732

733
                    // Determine width if cell is not part of a merge or does and is a value cell of 1-column wide range
734
                    if (!$isMerged || $isMergedButProceed) {
65✔
735
                        // Determine if we need to make an adjustment for the first row in an AutoFilter range that
736
                        //    has a column filter dropdown
737
                        $filterAdjustment = false;
65✔
738
                        if (!empty($autoFilterIndentRanges)) {
65✔
739
                            foreach ($autoFilterIndentRanges as $autoFilterFirstRowRange) {
4✔
740
                                /** @var string $autoFilterFirstRowRange */
741
                                if ($cell->isInRange($autoFilterFirstRowRange)) {
4✔
742
                                    $filterAdjustment = true;
4✔
743

744
                                    break;
4✔
745
                                }
746
                            }
747
                        }
748

749
                        $indentAdjustment = $cell->getStyle()->getAlignment()->getIndent();
65✔
750
                        $indentAdjustment += (int) ($cell->getStyle()->getAlignment()->getHorizontal() === Alignment::HORIZONTAL_CENTER);
65✔
751

752
                        // Calculated value
753
                        // To formatted string
754
                        $cellValue = NumberFormat::toFormattedString(
65✔
755
                            $cell->getCalculatedValueString(),
65✔
756
                            (string) $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())
65✔
757
                                ->getNumberFormat()->getFormatCode(true)
65✔
758
                        );
65✔
759

760
                        if ($cellValue !== '') {
65✔
761
                            $autoSizes[$this->cellCollection->getCurrentColumn()] = max(
65✔
762
                                $autoSizes[$this->cellCollection->getCurrentColumn()],
65✔
763
                                round(
65✔
764
                                    Shared\Font::calculateColumnWidth(
65✔
765
                                        $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont(),
65✔
766
                                        $cellValue,
65✔
767
                                        (int) $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())
65✔
768
                                            ->getAlignment()->getTextRotation(),
65✔
769
                                        $this->getParentOrThrow()->getDefaultStyle()->getFont(),
65✔
770
                                        $filterAdjustment,
65✔
771
                                        $indentAdjustment
65✔
772
                                    ),
65✔
773
                                    3
65✔
774
                                )
65✔
775
                            );
65✔
776
                        }
777
                    }
778
                }
779
            }
780

781
            // adjust column widths
782
            foreach ($autoSizes as $columnIndex => $width) {
65✔
783
                if ($width == -1) {
65✔
784
                    $width = $this->getDefaultColumnDimension()->getWidth();
×
785
                }
786
                $this->getColumnDimension($columnIndex)->setWidth($width);
65✔
787
            }
788
            $this->activePane = $holdActivePane;
65✔
789
        }
790
        if ($activeSheet !== null && $activeSheet >= 0) {
790✔
791
            $this->getParent()?->setActiveSheetIndex($activeSheet);
790✔
792
        }
793
        $this->setSelectedCells($selectedCells);
790✔
794

795
        return $this;
790✔
796
    }
797

798
    /**
799
     * Get parent or null.
800
     */
801
    public function getParent(): ?Spreadsheet
802
    {
803
        return $this->parent;
10,347✔
804
    }
805

806
    /**
807
     * Get parent, throw exception if null.
808
     */
809
    public function getParentOrThrow(): Spreadsheet
810
    {
811
        if ($this->parent !== null) {
10,418✔
812
            return $this->parent;
10,417✔
813
        }
814

815
        throw new Exception('Sheet does not have a parent.');
1✔
816
    }
817

818
    /**
819
     * Re-bind parent.
820
     *
821
     * @return $this
822
     */
823
    public function rebindParent(Spreadsheet $parent): static
824
    {
825
        if ($this->parent !== null) {
54✔
826
            $definedNames = $this->parent->getDefinedNames();
4✔
827
            foreach ($definedNames as $definedName) {
4✔
828
                $parent->addDefinedName($definedName);
×
829
            }
830

831
            $this->parent->removeSheetByIndex(
4✔
832
                $this->parent->getIndex($this)
4✔
833
            );
4✔
834
        }
835
        $this->parent = $parent;
54✔
836

837
        return $this;
54✔
838
    }
839

840
    public function setParent(Spreadsheet $parent): self
841
    {
842
        $this->parent = $parent;
5✔
843

844
        return $this;
5✔
845
    }
846

847
    /**
848
     * Get title.
849
     */
850
    public function getTitle(): string
851
    {
852
        return $this->title;
10,739✔
853
    }
854

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

876
        // Old title
877
        $oldTitle = $this->getTitle();
10,738✔
878

879
        if ($validate) {
10,738✔
880
            // Syntax check
881
            self::checkSheetTitle($title);
10,738✔
882

883
            if ($this->parent && $this->parent->getIndex($this, true) >= 0) {
10,738✔
884
                // Is there already such sheet name?
885
                if ($this->parent->sheetNameExists($title)) {
814✔
886
                    // Use name, but append with lowest possible integer
887

888
                    if (StringHelper::countCharacters($title) > 29) {
2✔
889
                        $title = StringHelper::substring($title, 0, 29);
×
890
                    }
891
                    $i = 1;
2✔
892
                    while ($this->parent->sheetNameExists($title . ' ' . $i)) {
2✔
893
                        ++$i;
1✔
894
                        if ($i == 10) {
1✔
895
                            if (StringHelper::countCharacters($title) > 28) {
×
896
                                $title = StringHelper::substring($title, 0, 28);
×
897
                            }
898
                        } elseif ($i == 100) {
1✔
899
                            if (StringHelper::countCharacters($title) > 27) {
×
900
                                $title = StringHelper::substring($title, 0, 27);
×
901
                            }
902
                        }
903
                    }
904

905
                    $title .= " $i";
2✔
906
                }
907
            }
908
        }
909

910
        // Set title
911
        $this->title = $title;
10,738✔
912

913
        if ($this->parent && $this->parent->getIndex($this, true) >= 0) {
10,738✔
914
            // New title
915
            $newTitle = $this->getTitle();
1,435✔
916
            $this->parent->getCalculationEngine()
1,435✔
917
                ->renameCalculationCacheForWorksheet($oldTitle, $newTitle);
1,435✔
918
            if ($updateFormulaCellReferences) {
1,435✔
919
                ReferenceHelper::getInstance()->updateNamedFormulae($this->parent, $oldTitle, $newTitle);
814✔
920
            }
921
        }
922

923
        return $this;
10,738✔
924
    }
925

926
    /**
927
     * Get sheet state.
928
     *
929
     * @return string Sheet state (visible, hidden, veryHidden)
930
     */
931
    public function getSheetState(): string
932
    {
933
        return $this->sheetState;
515✔
934
    }
935

936
    /**
937
     * Set sheet state.
938
     *
939
     * @param string $value Sheet state (visible, hidden, veryHidden)
940
     *
941
     * @return $this
942
     */
943
    public function setSheetState(string $value): static
944
    {
945
        $this->sheetState = $value;
10,738✔
946

947
        return $this;
10,738✔
948
    }
949

950
    /**
951
     * Get page setup.
952
     */
953
    public function getPageSetup(): PageSetup
954
    {
955
        return $this->pageSetup;
1,624✔
956
    }
957

958
    /**
959
     * Set page setup.
960
     *
961
     * @return $this
962
     */
963
    public function setPageSetup(PageSetup $pageSetup): static
964
    {
965
        $this->pageSetup = $pageSetup;
1✔
966

967
        return $this;
1✔
968
    }
969

970
    /**
971
     * Get page margins.
972
     */
973
    public function getPageMargins(): PageMargins
974
    {
975
        return $this->pageMargins;
1,620✔
976
    }
977

978
    /**
979
     * Set page margins.
980
     *
981
     * @return $this
982
     */
983
    public function setPageMargins(PageMargins $pageMargins): static
984
    {
985
        $this->pageMargins = $pageMargins;
1✔
986

987
        return $this;
1✔
988
    }
989

990
    /**
991
     * Get page header/footer.
992
     */
993
    public function getHeaderFooter(): HeaderFooter
994
    {
995
        return $this->headerFooter;
589✔
996
    }
997

998
    /**
999
     * Set page header/footer.
1000
     *
1001
     * @return $this
1002
     */
1003
    public function setHeaderFooter(HeaderFooter $headerFooter): static
1004
    {
1005
        $this->headerFooter = $headerFooter;
1✔
1006

1007
        return $this;
1✔
1008
    }
1009

1010
    /**
1011
     * Get sheet view.
1012
     */
1013
    public function getSheetView(): SheetView
1014
    {
1015
        return $this->sheetView;
619✔
1016
    }
1017

1018
    /**
1019
     * Set sheet view.
1020
     *
1021
     * @return $this
1022
     */
1023
    public function setSheetView(SheetView $sheetView): static
1024
    {
1025
        $this->sheetView = $sheetView;
1✔
1026

1027
        return $this;
1✔
1028
    }
1029

1030
    /**
1031
     * Get Protection.
1032
     */
1033
    public function getProtection(): Protection
1034
    {
1035
        return $this->protection;
642✔
1036
    }
1037

1038
    /**
1039
     * Set Protection.
1040
     *
1041
     * @return $this
1042
     */
1043
    public function setProtection(Protection $protection): static
1044
    {
1045
        $this->protection = $protection;
1✔
1046

1047
        return $this;
1✔
1048
    }
1049

1050
    /**
1051
     * Get highest worksheet column.
1052
     *
1053
     * @param null|int|string $row Return the data highest column for the specified row,
1054
     *                                     or the highest column of any row if no row number is passed
1055
     *
1056
     * @return string Highest column name
1057
     */
1058
    public function getHighestColumn($row = null): string
1059
    {
1060
        if ($row === null) {
1,526✔
1061
            return Coordinate::stringFromColumnIndex($this->cachedHighestColumn);
1,525✔
1062
        }
1063

1064
        return $this->getHighestDataColumn($row);
1✔
1065
    }
1066

1067
    /**
1068
     * Get highest worksheet column that contains data.
1069
     *
1070
     * @param null|int|string $row Return the highest data column for the specified row,
1071
     *                                     or the highest data column of any row if no row number is passed
1072
     *
1073
     * @return string Highest column name that contains data
1074
     */
1075
    public function getHighestDataColumn($row = null): string
1076
    {
1077
        return $this->cellCollection->getHighestColumn($row);
724✔
1078
    }
1079

1080
    /**
1081
     * Get highest worksheet row.
1082
     *
1083
     * @param null|string $column Return the highest data row for the specified column,
1084
     *                                     or the highest row of any column if no column letter is passed
1085
     *
1086
     * @return int Highest row number
1087
     */
1088
    public function getHighestRow(?string $column = null): int
1089
    {
1090
        if ($column === null) {
1,006✔
1091
            return $this->cachedHighestRow;
1,005✔
1092
        }
1093

1094
        return $this->getHighestDataRow($column);
1✔
1095
    }
1096

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

1110
    /**
1111
     * Get highest worksheet column and highest row that have cell records.
1112
     *
1113
     * @return array{row: int, column: string} Highest column name and highest row number
1114
     */
1115
    public function getHighestRowAndColumn(): array
1116
    {
1117
        return $this->cellCollection->getHighestRowAndColumn();
1✔
1118
    }
1119

1120
    /**
1121
     * Set a cell value.
1122
     *
1123
     * @param array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1124
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1125
     * @param mixed $value Value for the cell
1126
     * @param null|IValueBinder $binder Value Binder to override the currently set Value Binder
1127
     *
1128
     * @return $this
1129
     */
1130
    public function setCellValue(CellAddress|string|array $coordinate, mixed $value, ?IValueBinder $binder = null): static
1131
    {
1132
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
5,011✔
1133
        $this->getCell($cellAddress)->setValue($value, $binder);
5,011✔
1134

1135
        return $this;
5,011✔
1136
    }
1137

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

1160
        return $this;
104✔
1161
    }
1162

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

1180
        // Shortcut for increased performance for the vast majority of simple cases
1181
        if ($this->cellCollection->has($cellAddress)) {
10,279✔
1182
            /** @var Cell $cell */
1183
            $cell = $this->cellCollection->get($cellAddress);
10,258✔
1184

1185
            return $cell;
10,258✔
1186
        }
1187

1188
        /** @var Worksheet $sheet */
1189
        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
10,278✔
1190
        $cell = $sheet->getCellCollection()->get($finalCoordinate);
10,278✔
1191

1192
        return $cell ?? $sheet->createNewCell($finalCoordinate);
10,278✔
1193
    }
1194

1195
    /**
1196
     * Get the correct Worksheet and coordinate from a coordinate that may
1197
     * contains reference to another sheet or a named range.
1198
     *
1199
     * @return array{0: Worksheet, 1: string}
1200
     */
1201
    private function getWorksheetAndCoordinate(string $coordinate): array
1202
    {
1203
        $sheet = null;
10,301✔
1204
        $finalCoordinate = null;
10,301✔
1205

1206
        // Worksheet reference?
1207
        if (str_contains($coordinate, '!')) {
10,301✔
1208
            $worksheetReference = self::extractSheetTitle($coordinate, true, true);
×
1209

1210
            $sheet = $this->getParentOrThrow()->getSheetByName($worksheetReference[0]);
×
1211
            $finalCoordinate = strtoupper($worksheetReference[1]);
×
1212

1213
            if ($sheet === null) {
×
1214
                throw new Exception('Sheet not found for name: ' . $worksheetReference[0]);
×
1215
            }
1216
        } elseif (
1217
            !Preg::isMatch('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $coordinate)
10,301✔
1218
            && Preg::isMatch('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/iu', $coordinate)
10,301✔
1219
        ) {
1220
            // Named range?
1221
            $namedRange = $this->validateNamedRange($coordinate, true);
17✔
1222
            if ($namedRange !== null) {
17✔
1223
                $sheet = $namedRange->getWorksheet();
12✔
1224
                if ($sheet === null) {
12✔
1225
                    throw new Exception('Sheet not found for named range: ' . $namedRange->getName());
×
1226
                }
1227

1228
                /** @phpstan-ignore-next-line */
1229
                $cellCoordinate = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!');
12✔
1230
                $finalCoordinate = str_replace('$', '', $cellCoordinate);
12✔
1231
            }
1232
        }
1233

1234
        if ($sheet === null || $finalCoordinate === null) {
10,301✔
1235
            $sheet = $this;
10,301✔
1236
            $finalCoordinate = strtoupper($coordinate);
10,301✔
1237
        }
1238

1239
        if (Coordinate::coordinateIsRange($finalCoordinate)) {
10,301✔
1240
            throw new Exception('Cell coordinate string can not be a range of cells.');
2✔
1241
        }
1242
        $finalCoordinate = str_replace('$', '', $finalCoordinate);
10,301✔
1243

1244
        return [$sheet, $finalCoordinate];
10,301✔
1245
    }
1246

1247
    /**
1248
     * Get an existing cell at a specific coordinate, or null.
1249
     *
1250
     * @param string $coordinate Coordinate of the cell, eg: 'A1'
1251
     *
1252
     * @return null|Cell Cell that was found or null
1253
     */
1254
    private function getCellOrNull(string $coordinate): ?Cell
1255
    {
1256
        // Check cell collection
1257
        if ($this->cellCollection->has($coordinate)) {
65✔
1258
            return $this->cellCollection->get($coordinate);
65✔
1259
        }
1260

1261
        return null;
×
1262
    }
1263

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

1282
        // Coordinates
1283
        if ($column > $this->cachedHighestColumn) {
10,278✔
1284
            $this->cachedHighestColumn = $column;
7,082✔
1285
        }
1286
        if ($row > $this->cachedHighestRow) {
10,278✔
1287
            $this->cachedHighestRow = $row;
8,633✔
1288
        }
1289

1290
        // Cell needs appropriate xfIndex from dimensions records
1291
        //    but don't create dimension records if they don't already exist
1292
        $rowDimension = $this->rowDimensions[$row] ?? null;
10,278✔
1293
        $columnDimension = $this->columnDimensions[$columnString] ?? null;
10,278✔
1294

1295
        $xfSet = false;
10,278✔
1296
        if ($rowDimension !== null) {
10,278✔
1297
            $rowXf = (int) $rowDimension->getXfIndex();
394✔
1298
            if ($rowXf > 0) {
394✔
1299
                // then there is a row dimension with explicit style, assign it to the cell
1300
                $cell->setXfIndex($rowXf);
203✔
1301
                $xfSet = true;
203✔
1302
            }
1303
        }
1304
        if (!$xfSet && $columnDimension !== null) {
10,278✔
1305
            $colXf = (int) $columnDimension->getXfIndex();
570✔
1306
            if ($colXf > 0) {
570✔
1307
                // then there is a column dimension, assign it to the cell
1308
                $cell->setXfIndex($colXf);
215✔
1309
            }
1310
        }
1311

1312
        return $cell;
10,278✔
1313
    }
1314

1315
    /**
1316
     * Does the cell at a specific coordinate exist?
1317
     *
1318
     * @param array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1319
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1320
     */
1321
    public function cellExists(CellAddress|string|array $coordinate): bool
1322
    {
1323
        $cellAddress = Validations::validateCellAddress($coordinate);
10,217✔
1324
        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
10,217✔
1325

1326
        return $sheet->getCellCollection()->has($finalCoordinate);
10,217✔
1327
    }
1328

1329
    /**
1330
     * Get row dimension at a specific row.
1331
     *
1332
     * @param int $row Numeric index of the row
1333
     */
1334
    public function getRowDimension(int $row): RowDimension
1335
    {
1336
        // Get row dimension
1337
        if (!isset($this->rowDimensions[$row])) {
562✔
1338
            $this->rowDimensions[$row] = new RowDimension($row);
562✔
1339

1340
            $this->cachedHighestRow = max($this->cachedHighestRow, $row);
562✔
1341
        }
1342

1343
        return $this->rowDimensions[$row];
562✔
1344
    }
1345

1346
    public function getRowStyle(int $row): ?Style
1347
    {
1348
        return $this->parent?->getCellXfByIndexOrNull(
1✔
1349
            ($this->rowDimensions[$row] ?? null)?->getXfIndex()
1✔
1350
        );
1✔
1351
    }
1352

1353
    public function rowDimensionExists(int $row): bool
1354
    {
1355
        return isset($this->rowDimensions[$row]);
630✔
1356
    }
1357

1358
    public function columnDimensionExists(string $column): bool
1359
    {
1360
        return isset($this->columnDimensions[$column]);
39✔
1361
    }
1362

1363
    /**
1364
     * Get column dimension at a specific column.
1365
     *
1366
     * @param string $column String index of the column eg: 'A'
1367
     */
1368
    public function getColumnDimension(string $column): ColumnDimension
1369
    {
1370
        // Uppercase coordinate
1371
        $column = strtoupper($column);
665✔
1372

1373
        // Fetch dimensions
1374
        if (!isset($this->columnDimensions[$column])) {
665✔
1375
            $this->columnDimensions[$column] = new ColumnDimension($column);
665✔
1376

1377
            $columnIndex = Coordinate::columnIndexFromString($column);
665✔
1378
            if ($this->cachedHighestColumn < $columnIndex) {
665✔
1379
                $this->cachedHighestColumn = $columnIndex;
463✔
1380
            }
1381
        }
1382

1383
        return $this->columnDimensions[$column];
665✔
1384
    }
1385

1386
    /**
1387
     * Get column dimension at a specific column by using numeric cell coordinates.
1388
     *
1389
     * @param int $columnIndex Numeric column coordinate of the cell
1390
     */
1391
    public function getColumnDimensionByColumn(int $columnIndex): ColumnDimension
1392
    {
1393
        return $this->getColumnDimension(Coordinate::stringFromColumnIndex($columnIndex));
110✔
1394
    }
1395

1396
    public function getColumnStyle(string $column): ?Style
1397
    {
1398
        return $this->parent?->getCellXfByIndexOrNull(
1✔
1399
            ($this->columnDimensions[$column] ?? null)?->getXfIndex()
1✔
1400
        );
1✔
1401
    }
1402

1403
    /**
1404
     * Get style for cell.
1405
     *
1406
     * @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
1407
     *              A simple string containing a cell address like 'A1' or a cell range like 'A1:E10'
1408
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1409
     *              or a CellAddress or AddressRange object.
1410
     */
1411
    public function getStyle(AddressRange|CellAddress|int|string|array $cellCoordinate): Style
1412
    {
1413
        if (is_string($cellCoordinate)) {
10,252✔
1414
            $cellCoordinate = Validations::definedNameToCoordinate($cellCoordinate, $this);
10,250✔
1415
        }
1416
        $cellCoordinate = Validations::validateCellOrCellRange($cellCoordinate);
10,252✔
1417
        $cellCoordinate = str_replace('$', '', $cellCoordinate);
10,252✔
1418

1419
        // set this sheet as active
1420
        $this->getParentOrThrow()->setActiveSheetIndex($this->getParentOrThrow()->getIndex($this));
10,252✔
1421

1422
        // set cell coordinate as active
1423
        $this->setSelectedCells($cellCoordinate);
10,252✔
1424

1425
        return $this->getParentOrThrow()->getCellXfSupervisor();
10,252✔
1426
    }
1427

1428
    /**
1429
     * Get table styles set for the for given cell.
1430
     *
1431
     * @param Cell $cell
1432
     *              The Cell for which the tables are retrieved
1433
     *
1434
     * @return mixed[]
1435
     */
1436
    public function getTablesWithStylesForCell(Cell $cell): array
1437
    {
1438
        $retVal = [];
550✔
1439

1440
        foreach ($this->tableCollection as $table) {
550✔
1441
            /** @var Table $table */
1442
            $dxfsTableStyle = $table->getStyle()->getTableDxfsStyle();
4✔
1443
            if ($dxfsTableStyle !== null) {
4✔
1444
                if ($dxfsTableStyle->getHeaderRowStyle() !== null || $dxfsTableStyle->getFirstRowStripeStyle() !== null || $dxfsTableStyle->getSecondRowStripeStyle() !== null) {
4✔
1445
                    $range = $table->getRange();
4✔
1446
                    if ($cell->isInRange($range)) {
4✔
1447
                        $retVal[] = $table;
4✔
1448
                    }
1449
                }
1450
            }
1451
        }
1452

1453
        return $retVal;
550✔
1454
    }
1455

1456
    /**
1457
     * Get conditional styles for a cell.
1458
     *
1459
     * @param string $coordinate eg: 'A1' or 'A1:A3'.
1460
     *          If a single cell is referenced, then the array of conditional styles will be returned if the cell is
1461
     *               included in a conditional style range.
1462
     *          If a range of cells is specified, then the styles will only be returned if the range matches the entire
1463
     *               range of the conditional.
1464
     * @param bool $firstOnly default true, return all matching
1465
     *          conditionals ordered by priority if false, first only if true
1466
     *
1467
     * @return Conditional[]
1468
     */
1469
    public function getConditionalStyles(string $coordinate, bool $firstOnly = true): array
1470
    {
1471
        $coordinate = strtoupper($coordinate);
792✔
1472
        if (Preg::isMatch('/[: ,]/', $coordinate)) {
792✔
1473
            return $this->conditionalStylesCollection[$coordinate] ?? [];
49✔
1474
        }
1475

1476
        $conditionalStyles = [];
768✔
1477
        foreach ($this->conditionalStylesCollection as $keyStylesOrig => $conditionalRange) {
768✔
1478
            $keyStyles = Coordinate::resolveUnionAndIntersection($keyStylesOrig);
222✔
1479
            $keyParts = explode(',', $keyStyles);
222✔
1480
            foreach ($keyParts as $keyPart) {
222✔
1481
                if ($keyPart === $coordinate) {
222✔
1482
                    if ($firstOnly) {
14✔
1483
                        return $conditionalRange;
14✔
1484
                    }
1485
                    $conditionalStyles[$keyStylesOrig] = $conditionalRange;
×
1486

1487
                    break;
×
1488
                } elseif (str_contains($keyPart, ':')) {
217✔
1489
                    if (Coordinate::coordinateIsInsideRange($keyPart, $coordinate)) {
212✔
1490
                        if ($firstOnly) {
198✔
1491
                            return $conditionalRange;
197✔
1492
                        }
1493
                        $conditionalStyles[$keyStylesOrig] = $conditionalRange;
1✔
1494

1495
                        break;
1✔
1496
                    }
1497
                }
1498
            }
1499
        }
1500
        $outArray = [];
594✔
1501
        foreach ($conditionalStyles as $conditionalArray) {
594✔
1502
            foreach ($conditionalArray as $conditional) {
1✔
1503
                $outArray[] = $conditional;
1✔
1504
            }
1505
        }
1506
        usort($outArray, [self::class, 'comparePriority']);
594✔
1507

1508
        return $outArray;
594✔
1509
    }
1510

1511
    private static function comparePriority(Conditional $condA, Conditional $condB): int
1512
    {
1513
        $a = $condA->getPriority();
1✔
1514
        $b = $condB->getPriority();
1✔
1515
        if ($a === $b) {
1✔
1516
            return 0;
×
1517
        }
1518
        if ($a === 0) {
1✔
1519
            return 1;
×
1520
        }
1521
        if ($b === 0) {
1✔
1522
            return -1;
×
1523
        }
1524

1525
        return ($a < $b) ? -1 : 1;
1✔
1526
    }
1527

1528
    public function getConditionalRange(string $coordinate): ?string
1529
    {
1530
        $coordinate = strtoupper($coordinate);
189✔
1531
        $cell = $this->getCell($coordinate);
189✔
1532
        foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
189✔
1533
            $cellBlocks = explode(',', Coordinate::resolveUnionAndIntersection($conditionalRange));
189✔
1534
            foreach ($cellBlocks as $cellBlock) {
189✔
1535
                if ($cell->isInRange($cellBlock)) {
189✔
1536
                    return $conditionalRange;
188✔
1537
                }
1538
            }
1539
        }
1540

1541
        return null;
3✔
1542
    }
1543

1544
    /**
1545
     * Do conditional styles exist for this cell?
1546
     *
1547
     * @param string $coordinate eg: 'A1' or 'A1:A3'.
1548
     *          If a single cell is specified, then this method will return true if that cell is included in a
1549
     *               conditional style range.
1550
     *          If a range of cells is specified, then true will only be returned if the range matches the entire
1551
     *               range of the conditional.
1552
     */
1553
    public function conditionalStylesExists(string $coordinate): bool
1554
    {
1555
        return !empty($this->getConditionalStyles($coordinate));
22✔
1556
    }
1557

1558
    /**
1559
     * Removes conditional styles for a cell.
1560
     *
1561
     * @param string $coordinate eg: 'A1'
1562
     *
1563
     * @return $this
1564
     */
1565
    public function removeConditionalStyles(string $coordinate): static
1566
    {
1567
        unset($this->conditionalStylesCollection[strtoupper($coordinate)]);
54✔
1568

1569
        return $this;
54✔
1570
    }
1571

1572
    /**
1573
     * Get collection of conditional styles.
1574
     *
1575
     * @return Conditional[][]
1576
     */
1577
    public function getConditionalStylesCollection(): array
1578
    {
1579
        return $this->conditionalStylesCollection;
1,352✔
1580
    }
1581

1582
    /**
1583
     * Set conditional styles.
1584
     *
1585
     * @param string $coordinate eg: 'A1'
1586
     * @param Conditional[] $styles
1587
     *
1588
     * @return $this
1589
     */
1590
    public function setConditionalStyles(string $coordinate, array $styles): static
1591
    {
1592
        $this->conditionalStylesCollection[strtoupper($coordinate)] = $styles;
345✔
1593

1594
        return $this;
345✔
1595
    }
1596

1597
    /**
1598
     * Duplicate cell style to a range of cells.
1599
     *
1600
     * Please note that this will overwrite existing cell styles for cells in range!
1601
     *
1602
     * @param Style $style Cell style to duplicate
1603
     * @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
1604
     *
1605
     * @return $this
1606
     */
1607
    public function duplicateStyle(Style $style, string $range): static
1608
    {
1609
        // Add the style to the workbook if necessary
1610
        $workbook = $this->getParentOrThrow();
2✔
1611
        if ($existingStyle = $workbook->getCellXfByHashCode($style->getHashCode())) {
2✔
1612
            // there is already such cell Xf in our collection
1613
            $xfIndex = $existingStyle->getIndex();
1✔
1614
        } else {
1615
            // we don't have such a cell Xf, need to add
1616
            $workbook->addCellXf($style);
2✔
1617
            $xfIndex = $style->getIndex();
2✔
1618
        }
1619

1620
        // Calculate range outer borders
1621
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
2✔
1622

1623
        // Make sure we can loop upwards on rows and columns
1624
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
2✔
1625
            $tmp = $rangeStart;
×
1626
            $rangeStart = $rangeEnd;
×
1627
            $rangeEnd = $tmp;
×
1628
        }
1629

1630
        // Loop through cells and apply styles
1631
        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
2✔
1632
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
2✔
1633
                $this->getCell(Coordinate::stringFromColumnIndex($col) . $row)->setXfIndex($xfIndex);
2✔
1634
            }
1635
        }
1636

1637
        return $this;
2✔
1638
    }
1639

1640
    /**
1641
     * Duplicate conditional style to a range of cells.
1642
     *
1643
     * Please note that this will overwrite existing cell styles for cells in range!
1644
     *
1645
     * @param Conditional[] $styles Cell style to duplicate
1646
     * @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
1647
     *
1648
     * @return $this
1649
     */
1650
    public function duplicateConditionalStyle(array $styles, string $range = ''): static
1651
    {
1652
        foreach ($styles as $cellStyle) {
18✔
1653
            if (!($cellStyle instanceof Conditional)) { // @phpstan-ignore-line
18✔
1654
                throw new Exception('Style is not a conditional style');
×
1655
            }
1656
        }
1657

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

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

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

1675
        return $this;
18✔
1676
    }
1677

1678
    /**
1679
     * Set break on a cell.
1680
     *
1681
     * @param array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1682
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1683
     * @param int $break Break type (type of Worksheet::BREAK_*)
1684
     *
1685
     * @return $this
1686
     */
1687
    public function setBreak(CellAddress|string|array $coordinate, int $break, int $max = -1): static
1688
    {
1689
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
33✔
1690

1691
        if ($break === self::BREAK_NONE) {
33✔
1692
            unset($this->rowBreaks[$cellAddress], $this->columnBreaks[$cellAddress]);
7✔
1693
        } elseif ($break === self::BREAK_ROW) {
33✔
1694
            $this->rowBreaks[$cellAddress] = new PageBreak($break, $cellAddress, $max);
23✔
1695
        } elseif ($break === self::BREAK_COLUMN) {
19✔
1696
            $this->columnBreaks[$cellAddress] = new PageBreak($break, $cellAddress, $max);
19✔
1697
        }
1698

1699
        return $this;
33✔
1700
    }
1701

1702
    /**
1703
     * Get breaks.
1704
     *
1705
     * @return int[]
1706
     */
1707
    public function getBreaks(): array
1708
    {
1709
        $breaks = [];
671✔
1710
        /** @var callable $compareFunction */
1711
        $compareFunction = [self::class, 'compareRowBreaks'];
671✔
1712
        uksort($this->rowBreaks, $compareFunction);
671✔
1713
        foreach ($this->rowBreaks as $break) {
671✔
1714
            $breaks[$break->getCoordinate()] = self::BREAK_ROW;
10✔
1715
        }
1716
        /** @var callable $compareFunction */
1717
        $compareFunction = [self::class, 'compareColumnBreaks'];
671✔
1718
        uksort($this->columnBreaks, $compareFunction);
671✔
1719
        foreach ($this->columnBreaks as $break) {
671✔
1720
            $breaks[$break->getCoordinate()] = self::BREAK_COLUMN;
8✔
1721
        }
1722

1723
        return $breaks;
671✔
1724
    }
1725

1726
    /**
1727
     * Get row breaks.
1728
     *
1729
     * @return PageBreak[]
1730
     */
1731
    public function getRowBreaks(): array
1732
    {
1733
        /** @var callable $compareFunction */
1734
        $compareFunction = [self::class, 'compareRowBreaks'];
538✔
1735
        uksort($this->rowBreaks, $compareFunction);
538✔
1736

1737
        return $this->rowBreaks;
538✔
1738
    }
1739

1740
    protected static function compareRowBreaks(string $coordinate1, string $coordinate2): int
1741
    {
1742
        $row1 = Coordinate::indexesFromString($coordinate1)[1];
9✔
1743
        $row2 = Coordinate::indexesFromString($coordinate2)[1];
9✔
1744

1745
        return $row1 - $row2;
9✔
1746
    }
1747

1748
    protected static function compareColumnBreaks(string $coordinate1, string $coordinate2): int
1749
    {
1750
        $column1 = Coordinate::indexesFromString($coordinate1)[0];
5✔
1751
        $column2 = Coordinate::indexesFromString($coordinate2)[0];
5✔
1752

1753
        return $column1 - $column2;
5✔
1754
    }
1755

1756
    /**
1757
     * Get column breaks.
1758
     *
1759
     * @return PageBreak[]
1760
     */
1761
    public function getColumnBreaks(): array
1762
    {
1763
        /** @var callable $compareFunction */
1764
        $compareFunction = [self::class, 'compareColumnBreaks'];
537✔
1765
        uksort($this->columnBreaks, $compareFunction);
537✔
1766

1767
        return $this->columnBreaks;
537✔
1768
    }
1769

1770
    /**
1771
     * Set merge on a cell range.
1772
     *
1773
     * @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'
1774
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1775
     *              or an AddressRange.
1776
     * @param string $behaviour How the merged cells should behave.
1777
     *               Possible values are:
1778
     *                   MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells
1779
     *                   MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells
1780
     *                   MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell
1781
     *
1782
     * @return $this
1783
     */
1784
    public function mergeCells(AddressRange|string|array $range, string $behaviour = self::MERGE_CELL_CONTENT_EMPTY): static
1785
    {
1786
        $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range));
174✔
1787

1788
        if (!str_contains($range, ':')) {
173✔
1789
            $range .= ":{$range}";
1✔
1790
        }
1791

1792
        if (!Preg::isMatch('/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/', $range, $matches)) {
173✔
1793
            throw new Exception('Merge must be on a valid range of cells.');
1✔
1794
        }
1795

1796
        $this->mergeCells[$range] = $range;
172✔
1797
        $firstRow = (int) $matches[2];
172✔
1798
        $lastRow = (int) $matches[4];
172✔
1799
        $firstColumn = $matches[1];
172✔
1800
        $lastColumn = $matches[3];
172✔
1801
        $firstColumnIndex = Coordinate::columnIndexFromString($firstColumn);
172✔
1802
        $lastColumnIndex = Coordinate::columnIndexFromString($lastColumn);
172✔
1803
        $numberRows = $lastRow - $firstRow;
172✔
1804
        $numberColumns = $lastColumnIndex - $firstColumnIndex;
172✔
1805

1806
        if ($numberRows === 1 && $numberColumns === 1) {
172✔
1807
            return $this;
35✔
1808
        }
1809

1810
        // create upper left cell if it does not already exist
1811
        $upperLeft = "{$firstColumn}{$firstRow}";
165✔
1812
        if (!$this->cellExists($upperLeft)) {
165✔
1813
            $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL);
36✔
1814
        }
1815

1816
        if ($behaviour !== self::MERGE_CELL_CONTENT_HIDE) {
165✔
1817
            // Blank out the rest of the cells in the range (if they exist)
1818
            if ($numberRows > $numberColumns) {
58✔
1819
                $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft, $behaviour);
18✔
1820
            } else {
1821
                $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft, $behaviour);
40✔
1822
            }
1823
        }
1824

1825
        return $this;
165✔
1826
    }
1827

1828
    private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
1829
    {
1830
        $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
18✔
1831
            ? [$this->getCell($upperLeft)->getFormattedValue()]
×
1832
            : [];
18✔
1833

1834
        foreach ($this->getColumnIterator($firstColumn, $lastColumn) as $column) {
18✔
1835
            $iterator = $column->getCellIterator($firstRow);
18✔
1836
            $iterator->setIterateOnlyExistingCells(true);
18✔
1837
            foreach ($iterator as $cell) {
18✔
1838
                $row = $cell->getRow();
18✔
1839
                if ($row > $lastRow) {
18✔
1840
                    break;
8✔
1841
                }
1842
                $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue);
18✔
1843
            }
1844
        }
1845

1846
        if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
18✔
1847
            $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
×
1848
        }
1849
    }
1850

1851
    private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
1852
    {
1853
        $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
40✔
1854
            ? [$this->getCell($upperLeft)->getFormattedValue()]
4✔
1855
            : [];
36✔
1856

1857
        foreach ($this->getRowIterator($firstRow, $lastRow) as $row) {
40✔
1858
            $iterator = $row->getCellIterator($firstColumn);
40✔
1859
            $iterator->setIterateOnlyExistingCells(true);
40✔
1860
            foreach ($iterator as $cell) {
40✔
1861
                $column = $cell->getColumn();
40✔
1862
                $columnIndex = Coordinate::columnIndexFromString($column);
40✔
1863
                if ($columnIndex > $lastColumnIndex) {
40✔
1864
                    break;
9✔
1865
                }
1866
                $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue);
40✔
1867
            }
1868
        }
1869

1870
        if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
40✔
1871
            $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
4✔
1872
        }
1873
    }
1874

1875
    /**
1876
     * @param mixed[] $leftCellValue
1877
     *
1878
     * @return mixed[]
1879
     */
1880
    public function mergeCellBehaviour(Cell $cell, string $upperLeft, string $behaviour, array $leftCellValue): array
1881
    {
1882
        if ($cell->getCoordinate() !== $upperLeft) {
58✔
1883
            Calculation::getInstance($cell->getWorksheet()->getParentOrThrow())->flushInstance();
24✔
1884
            if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
24✔
1885
                $cellValue = $cell->getFormattedValue();
4✔
1886
                if ($cellValue !== '') {
4✔
1887
                    $leftCellValue[] = $cellValue;
4✔
1888
                }
1889
            }
1890
            $cell->setValueExplicit(null, DataType::TYPE_NULL);
24✔
1891
        }
1892

1893
        return $leftCellValue;
58✔
1894
    }
1895

1896
    /**
1897
     * Remove merge on a cell range.
1898
     *
1899
     * @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'
1900
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1901
     *              or an AddressRange.
1902
     *
1903
     * @return $this
1904
     */
1905
    public function unmergeCells(AddressRange|string|array $range): static
1906
    {
1907
        $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range));
23✔
1908

1909
        if (str_contains($range, ':')) {
23✔
1910
            if (isset($this->mergeCells[$range])) {
22✔
1911
                unset($this->mergeCells[$range]);
22✔
1912
            } else {
1913
                throw new Exception('Cell range ' . $range . ' not known as merged.');
×
1914
            }
1915
        } else {
1916
            throw new Exception('Merge can only be removed from a range of cells.');
1✔
1917
        }
1918

1919
        return $this;
22✔
1920
    }
1921

1922
    /**
1923
     * Get merge cells array.
1924
     *
1925
     * @return string[]
1926
     */
1927
    public function getMergeCells(): array
1928
    {
1929
        return $this->mergeCells;
1,236✔
1930
    }
1931

1932
    /**
1933
     * Set merge cells array for the entire sheet. Use instead mergeCells() to merge
1934
     * a single cell range.
1935
     *
1936
     * @param string[] $mergeCells
1937
     *
1938
     * @return $this
1939
     */
1940
    public function setMergeCells(array $mergeCells): static
1941
    {
1942
        $this->mergeCells = $mergeCells;
122✔
1943

1944
        return $this;
122✔
1945
    }
1946

1947
    /**
1948
     * Set protection on a cell or cell range.
1949
     *
1950
     * @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'
1951
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1952
     *              or a CellAddress or AddressRange object.
1953
     * @param string $password Password to unlock the protection
1954
     * @param bool $alreadyHashed If the password has already been hashed, set this to true
1955
     *
1956
     * @return $this
1957
     */
1958
    public function protectCells(AddressRange|CellAddress|int|string|array $range, string $password = '', bool $alreadyHashed = false, string $name = '', string $securityDescriptor = ''): static
1959
    {
1960
        $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
26✔
1961

1962
        if (!$alreadyHashed && $password !== '') {
26✔
1963
            $password = Shared\PasswordHasher::hashPassword($password);
24✔
1964
        }
1965
        $this->protectedCells[$range] = new ProtectedRange($range, $password, $name, $securityDescriptor);
26✔
1966

1967
        return $this;
26✔
1968
    }
1969

1970
    /**
1971
     * Remove protection on a cell or cell range.
1972
     *
1973
     * @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'
1974
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1975
     *              or a CellAddress or AddressRange object.
1976
     *
1977
     * @return $this
1978
     */
1979
    public function unprotectCells(AddressRange|CellAddress|int|string|array $range): static
1980
    {
1981
        $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
20✔
1982

1983
        if (isset($this->protectedCells[$range])) {
20✔
1984
            unset($this->protectedCells[$range]);
19✔
1985
        } else {
1986
            throw new Exception('Cell range ' . $range . ' not known as protected.');
1✔
1987
        }
1988

1989
        return $this;
19✔
1990
    }
1991

1992
    /**
1993
     * Get protected cells.
1994
     *
1995
     * @return ProtectedRange[]
1996
     */
1997
    public function getProtectedCellRanges(): array
1998
    {
1999
        return $this->protectedCells;
645✔
2000
    }
2001

2002
    /**
2003
     * Get Autofilter.
2004
     */
2005
    public function getAutoFilter(): AutoFilter
2006
    {
2007
        return $this->autoFilter;
842✔
2008
    }
2009

2010
    /**
2011
     * Set AutoFilter.
2012
     *
2013
     * @param AddressRange<CellAddress>|AddressRange<int>|AddressRange<string>|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|AutoFilter|string $autoFilterOrRange
2014
     *            A simple string containing a Cell range like 'A1:E10' is permitted for backward compatibility
2015
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2016
     *              or an AddressRange.
2017
     *
2018
     * @return $this
2019
     */
2020
    public function setAutoFilter(AddressRange|string|array|AutoFilter $autoFilterOrRange): static
2021
    {
2022
        if (is_object($autoFilterOrRange) && ($autoFilterOrRange instanceof AutoFilter)) {
20✔
2023
            $this->autoFilter = $autoFilterOrRange;
×
2024
        } else {
2025
            $cellRange = Functions::trimSheetFromCellReference(Validations::validateCellRange($autoFilterOrRange));
20✔
2026

2027
            $this->autoFilter->setRange($cellRange);
20✔
2028
        }
2029

2030
        return $this;
20✔
2031
    }
2032

2033
    /**
2034
     * Remove autofilter.
2035
     */
2036
    public function removeAutoFilter(): self
2037
    {
2038
        $this->autoFilter->setRange('');
1✔
2039

2040
        return $this;
1✔
2041
    }
2042

2043
    /**
2044
     * Get collection of Tables.
2045
     *
2046
     * @return ArrayObject<int, Table>
2047
     */
2048
    public function getTableCollection(): ArrayObject
2049
    {
2050
        return $this->tableCollection;
10,290✔
2051
    }
2052

2053
    /**
2054
     * Add Table.
2055
     *
2056
     * @return $this
2057
     */
2058
    public function addTable(Table $table): self
2059
    {
2060
        $table->setWorksheet($this);
103✔
2061
        $this->tableCollection[] = $table;
103✔
2062

2063
        return $this;
103✔
2064
    }
2065

2066
    /**
2067
     * @return string[] array of Table names
2068
     */
2069
    public function getTableNames(): array
2070
    {
2071
        $tableNames = [];
1✔
2072

2073
        foreach ($this->tableCollection as $table) {
1✔
2074
            /** @var Table $table */
2075
            $tableNames[] = $table->getName();
1✔
2076
        }
2077

2078
        return $tableNames;
1✔
2079
    }
2080

2081
    /**
2082
     * @param string $name the table name to search
2083
     *
2084
     * @return null|Table The table from the tables collection, or null if not found
2085
     */
2086
    public function getTableByName(string $name): ?Table
2087
    {
2088
        $tableIndex = $this->getTableIndexByName($name);
96✔
2089

2090
        return ($tableIndex === null) ? null : $this->tableCollection[$tableIndex];
96✔
2091
    }
2092

2093
    /**
2094
     * @param string $name the table name to search
2095
     *
2096
     * @return null|int The index of the located table in the tables collection, or null if not found
2097
     */
2098
    protected function getTableIndexByName(string $name): ?int
2099
    {
2100
        $name = StringHelper::strToUpper($name);
97✔
2101
        foreach ($this->tableCollection as $index => $table) {
97✔
2102
            /** @var Table $table */
2103
            if (StringHelper::strToUpper($table->getName()) === $name) {
63✔
2104
                return $index;
62✔
2105
            }
2106
        }
2107

2108
        return null;
40✔
2109
    }
2110

2111
    /**
2112
     * Remove Table by name.
2113
     *
2114
     * @param string $name Table name
2115
     *
2116
     * @return $this
2117
     */
2118
    public function removeTableByName(string $name): self
2119
    {
2120
        $tableIndex = $this->getTableIndexByName($name);
1✔
2121

2122
        if ($tableIndex !== null) {
1✔
2123
            unset($this->tableCollection[$tableIndex]);
1✔
2124
        }
2125

2126
        return $this;
1✔
2127
    }
2128

2129
    /**
2130
     * Remove collection of Tables.
2131
     */
2132
    public function removeTableCollection(): self
2133
    {
2134
        $this->tableCollection = new ArrayObject();
1✔
2135

2136
        return $this;
1✔
2137
    }
2138

2139
    /**
2140
     * Get Freeze Pane.
2141
     */
2142
    public function getFreezePane(): ?string
2143
    {
2144
        return $this->freezePane;
287✔
2145
    }
2146

2147
    /**
2148
     * Freeze Pane.
2149
     *
2150
     * Examples:
2151
     *
2152
     *     - A2 will freeze the rows above cell A2 (i.e row 1)
2153
     *     - B1 will freeze the columns to the left of cell B1 (i.e column A)
2154
     *     - B2 will freeze the rows above and to the left of cell B2 (i.e row 1 and column A)
2155
     *
2156
     * @param null|array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
2157
     *            or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2158
     *        Passing a null value for this argument will clear any existing freeze pane for this worksheet.
2159
     * @param null|array{0: int, 1: int}|CellAddress|string $topLeftCell default position of the right bottom pane
2160
     *            Coordinate of the cell as a string, eg: 'C5'; or as an array of [$columnIndex, $row] (e.g. [3, 5]),
2161
     *            or a CellAddress object.
2162
     *
2163
     * @return $this
2164
     */
2165
    public function freezePane(null|CellAddress|string|array $coordinate, null|CellAddress|string|array $topLeftCell = null, bool $frozenSplit = false): static
2166
    {
2167
        $this->panes = [
49✔
2168
            'bottomRight' => null,
49✔
2169
            'bottomLeft' => null,
49✔
2170
            'topRight' => null,
49✔
2171
            'topLeft' => null,
49✔
2172
        ];
49✔
2173
        $cellAddress = ($coordinate !== null)
49✔
2174
            ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate))
49✔
2175
            : null;
1✔
2176
        if ($cellAddress !== null && Coordinate::coordinateIsRange($cellAddress)) {
49✔
2177
            throw new Exception('Freeze pane can not be set on a range of cells.');
1✔
2178
        }
2179
        $topLeftCell = ($topLeftCell !== null)
48✔
2180
            ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($topLeftCell))
37✔
2181
            : null;
36✔
2182

2183
        if ($cellAddress !== null && $topLeftCell === null) {
48✔
2184
            $coordinate = Coordinate::coordinateFromString($cellAddress);
36✔
2185
            $topLeftCell = $coordinate[0] . $coordinate[1];
36✔
2186
        }
2187

2188
        $topLeftCell = "$topLeftCell";
48✔
2189
        $this->paneTopLeftCell = $topLeftCell;
48✔
2190

2191
        $this->freezePane = $cellAddress;
48✔
2192
        $this->topLeftCell = $topLeftCell;
48✔
2193
        if ($cellAddress === null) {
48✔
2194
            $this->paneState = '';
1✔
2195
            $this->xSplit = $this->ySplit = 0;
1✔
2196
            $this->activePane = '';
1✔
2197
        } else {
2198
            $coordinates = Coordinate::indexesFromString($cellAddress);
48✔
2199
            $this->xSplit = $coordinates[0] - 1;
48✔
2200
            $this->ySplit = $coordinates[1] - 1;
48✔
2201
            if ($this->xSplit > 0 || $this->ySplit > 0) {
48✔
2202
                $this->paneState = $frozenSplit ? self::PANE_FROZENSPLIT : self::PANE_FROZEN;
47✔
2203
                $this->setSelectedCellsActivePane();
47✔
2204
            } else {
2205
                $this->paneState = '';
1✔
2206
                $this->freezePane = null;
1✔
2207
                $this->activePane = '';
1✔
2208
            }
2209
        }
2210

2211
        return $this;
48✔
2212
    }
2213

2214
    public function setTopLeftCell(string $topLeftCell): self
2215
    {
2216
        $this->topLeftCell = $topLeftCell;
47✔
2217

2218
        return $this;
47✔
2219
    }
2220

2221
    /**
2222
     * Unfreeze Pane.
2223
     *
2224
     * @return $this
2225
     */
2226
    public function unfreezePane(): static
2227
    {
2228
        return $this->freezePane(null);
1✔
2229
    }
2230

2231
    /**
2232
     * Get the default position of the right bottom pane.
2233
     */
2234
    public function getTopLeftCell(): ?string
2235
    {
2236
        return $this->topLeftCell;
478✔
2237
    }
2238

2239
    public function getPaneTopLeftCell(): string
2240
    {
2241
        return $this->paneTopLeftCell;
11✔
2242
    }
2243

2244
    public function setPaneTopLeftCell(string $paneTopLeftCell): self
2245
    {
2246
        $this->paneTopLeftCell = $paneTopLeftCell;
26✔
2247

2248
        return $this;
26✔
2249
    }
2250

2251
    public function usesPanes(): bool
2252
    {
2253
        return $this->xSplit > 0 || $this->ySplit > 0;
466✔
2254
    }
2255

2256
    public function getPane(string $position): ?Pane
2257
    {
2258
        return $this->panes[$position] ?? null;
2✔
2259
    }
2260

2261
    public function setPane(string $position, ?Pane $pane): self
2262
    {
2263
        if (array_key_exists($position, $this->panes)) {
35✔
2264
            $this->panes[$position] = $pane;
35✔
2265
        }
2266

2267
        return $this;
35✔
2268
    }
2269

2270
    /** @return (null|Pane)[] */
2271
    public function getPanes(): array
2272
    {
2273
        return $this->panes;
3✔
2274
    }
2275

2276
    public function getActivePane(): string
2277
    {
2278
        return $this->activePane;
14✔
2279
    }
2280

2281
    public function setActivePane(string $activePane): self
2282
    {
2283
        $this->activePane = array_key_exists($activePane, $this->panes) ? $activePane : '';
48✔
2284

2285
        return $this;
48✔
2286
    }
2287

2288
    public function getXSplit(): int
2289
    {
2290
        return $this->xSplit;
11✔
2291
    }
2292

2293
    public function setXSplit(int $xSplit): self
2294
    {
2295
        $this->xSplit = $xSplit;
11✔
2296
        if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
11✔
2297
            $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
1✔
2298
        }
2299

2300
        return $this;
11✔
2301
    }
2302

2303
    public function getYSplit(): int
2304
    {
2305
        return $this->ySplit;
11✔
2306
    }
2307

2308
    public function setYSplit(int $ySplit): self
2309
    {
2310
        $this->ySplit = $ySplit;
26✔
2311
        if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
26✔
2312
            $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
1✔
2313
        }
2314

2315
        return $this;
26✔
2316
    }
2317

2318
    public function getPaneState(): string
2319
    {
2320
        return $this->paneState;
21✔
2321
    }
2322

2323
    public const PANE_FROZEN = 'frozen';
2324
    public const PANE_FROZENSPLIT = 'frozenSplit';
2325
    public const PANE_SPLIT = 'split';
2326
    private const VALIDPANESTATE = [self::PANE_FROZEN, self::PANE_SPLIT, self::PANE_FROZENSPLIT];
2327
    private const VALIDFROZENSTATE = [self::PANE_FROZEN, self::PANE_FROZENSPLIT];
2328

2329
    public function setPaneState(string $paneState): self
2330
    {
2331
        $this->paneState = in_array($paneState, self::VALIDPANESTATE, true) ? $paneState : '';
26✔
2332
        if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
26✔
2333
            $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
25✔
2334
        } else {
2335
            $this->freezePane = null;
3✔
2336
        }
2337

2338
        return $this;
26✔
2339
    }
2340

2341
    /**
2342
     * Insert a new row, updating all possible related data.
2343
     *
2344
     * @param int $before Insert before this row number
2345
     * @param int $numberOfRows Number of new rows to insert
2346
     *
2347
     * @return $this
2348
     */
2349
    public function insertNewRowBefore(int $before, int $numberOfRows = 1): static
2350
    {
2351
        if ($before >= 1) {
42✔
2352
            $objReferenceHelper = ReferenceHelper::getInstance();
41✔
2353
            $objReferenceHelper->insertNewBefore('A' . $before, 0, $numberOfRows, $this);
41✔
2354
        } else {
2355
            throw new Exception('Rows can only be inserted before at least row 1.');
1✔
2356
        }
2357

2358
        return $this;
41✔
2359
    }
2360

2361
    /**
2362
     * Insert a new column, updating all possible related data.
2363
     *
2364
     * @param string $before Insert before this column Name, eg: 'A'
2365
     * @param int $numberOfColumns Number of new columns to insert
2366
     *
2367
     * @return $this
2368
     */
2369
    public function insertNewColumnBefore(string $before, int $numberOfColumns = 1): static
2370
    {
2371
        if (!is_numeric($before)) {
49✔
2372
            $objReferenceHelper = ReferenceHelper::getInstance();
48✔
2373
            $objReferenceHelper->insertNewBefore($before . '1', $numberOfColumns, 0, $this);
48✔
2374
        } else {
2375
            throw new Exception('Column references should not be numeric.');
1✔
2376
        }
2377

2378
        return $this;
48✔
2379
    }
2380

2381
    /**
2382
     * Insert a new column, updating all possible related data.
2383
     *
2384
     * @param int $beforeColumnIndex Insert before this column ID (numeric column coordinate of the cell)
2385
     * @param int $numberOfColumns Number of new columns to insert
2386
     *
2387
     * @return $this
2388
     */
2389
    public function insertNewColumnBeforeByIndex(int $beforeColumnIndex, int $numberOfColumns = 1): static
2390
    {
2391
        if ($beforeColumnIndex >= 1) {
2✔
2392
            return $this->insertNewColumnBefore(Coordinate::stringFromColumnIndex($beforeColumnIndex), $numberOfColumns);
1✔
2393
        }
2394

2395
        throw new Exception('Columns can only be inserted before at least column A (1).');
1✔
2396
    }
2397

2398
    /**
2399
     * Delete a row, updating all possible related data.
2400
     *
2401
     * @param int $row Remove rows, starting with this row number
2402
     * @param int $numberOfRows Number of rows to remove
2403
     *
2404
     * @return $this
2405
     */
2406
    public function removeRow(int $row, int $numberOfRows = 1): static
2407
    {
2408
        if ($row < 1) {
51✔
2409
            throw new Exception('Rows to be deleted should at least start from row 1.');
1✔
2410
        }
2411
        $startRow = $row;
50✔
2412
        $endRow = $startRow + $numberOfRows - 1;
50✔
2413
        $removeKeys = [];
50✔
2414
        $addKeys = [];
50✔
2415
        foreach ($this->mergeCells as $key => $value) {
50✔
2416
            if (
2417
                Preg::isMatch(
21✔
2418
                    '/^([a-z]{1,3})(\d+):([a-z]{1,3})(\d+)/i',
21✔
2419
                    $key,
21✔
2420
                    $matches
21✔
2421
                )
21✔
2422
            ) {
2423
                $startMergeInt = (int) $matches[2];
21✔
2424
                $endMergeInt = (int) $matches[4];
21✔
2425
                if ($startMergeInt >= $startRow) {
21✔
2426
                    if ($startMergeInt <= $endRow) {
21✔
2427
                        $removeKeys[] = $key;
3✔
2428
                    }
2429
                } elseif ($endMergeInt >= $startRow) {
1✔
2430
                    if ($endMergeInt <= $endRow) {
1✔
2431
                        $temp = $endMergeInt - 1;
1✔
2432
                        $removeKeys[] = $key;
1✔
2433
                        if ($temp !== $startMergeInt) {
1✔
2434
                            $temp3 = $matches[1] . $matches[2] . ':' . $matches[3] . $temp;
1✔
2435
                            $addKeys[] = $temp3;
1✔
2436
                        }
2437
                    }
2438
                }
2439
            }
2440
        }
2441
        foreach ($removeKeys as $key) {
50✔
2442
            unset($this->mergeCells[$key]);
3✔
2443
        }
2444
        foreach ($addKeys as $key) {
50✔
2445
            $this->mergeCells[$key] = $key;
1✔
2446
        }
2447

2448
        $holdRowDimensions = $this->removeRowDimensions($row, $numberOfRows);
50✔
2449
        $highestRow = $this->getHighestDataRow();
50✔
2450
        $removedRowsCounter = 0;
50✔
2451

2452
        for ($r = 0; $r < $numberOfRows; ++$r) {
50✔
2453
            if ($row + $r <= $highestRow) {
50✔
2454
                $this->cellCollection->removeRow($row + $r);
39✔
2455
                ++$removedRowsCounter;
39✔
2456
            }
2457
        }
2458

2459
        $objReferenceHelper = ReferenceHelper::getInstance();
50✔
2460
        $objReferenceHelper->insertNewBefore('A' . ($row + $numberOfRows), 0, -$numberOfRows, $this);
50✔
2461
        for ($r = 0; $r < $removedRowsCounter; ++$r) {
50✔
2462
            $this->cellCollection->removeRow($highestRow);
39✔
2463
            --$highestRow;
39✔
2464
        }
2465

2466
        $this->rowDimensions = $holdRowDimensions;
50✔
2467

2468
        return $this;
50✔
2469
    }
2470

2471
    /** @return RowDimension[] */
2472
    private function removeRowDimensions(int $row, int $numberOfRows): array
2473
    {
2474
        $highRow = $row + $numberOfRows - 1;
50✔
2475
        $holdRowDimensions = [];
50✔
2476
        foreach ($this->rowDimensions as $rowDimension) {
50✔
2477
            $num = $rowDimension->getRowIndex();
4✔
2478
            if ($num < $row) {
4✔
2479
                $holdRowDimensions[$num] = $rowDimension;
3✔
2480
            } elseif ($num > $highRow) {
4✔
2481
                $num -= $numberOfRows;
4✔
2482
                $cloneDimension = clone $rowDimension;
4✔
2483
                $cloneDimension->setRowIndex($num);
4✔
2484
                $holdRowDimensions[$num] = $cloneDimension;
4✔
2485
            }
2486
        }
2487

2488
        return $holdRowDimensions;
50✔
2489
    }
2490

2491
    /**
2492
     * Remove a column, updating all possible related data.
2493
     *
2494
     * @param string $column Remove columns starting with this column name, eg: 'A'
2495
     * @param int $numberOfColumns Number of columns to remove
2496
     *
2497
     * @return $this
2498
     */
2499
    public function removeColumn(string $column, int $numberOfColumns = 1): static
2500
    {
2501
        if (is_numeric($column)) {
43✔
2502
            throw new Exception('Column references should not be numeric.');
1✔
2503
        }
2504
        $startColumnInt = Coordinate::columnIndexFromString($column);
42✔
2505
        $endColumnInt = $startColumnInt + $numberOfColumns - 1;
42✔
2506
        $removeKeys = [];
42✔
2507
        $addKeys = [];
42✔
2508
        foreach ($this->mergeCells as $key => $value) {
42✔
2509
            if (
2510
                Preg::isMatch(
19✔
2511
                    '/^([a-z]{1,3})(\d+):([a-z]{1,3})(\d+)/i',
19✔
2512
                    $key,
19✔
2513
                    $matches
19✔
2514
                )
19✔
2515
            ) {
2516
                $startMergeInt = Coordinate::columnIndexFromString($matches[1]);
19✔
2517
                $endMergeInt = Coordinate::columnIndexFromString($matches[3]);
19✔
2518
                if ($startMergeInt >= $startColumnInt) {
19✔
2519
                    if ($startMergeInt <= $endColumnInt) {
2✔
2520
                        $removeKeys[] = $key;
2✔
2521
                    }
2522
                } elseif ($endMergeInt >= $startColumnInt) {
18✔
2523
                    if ($endMergeInt <= $endColumnInt) {
18✔
2524
                        $temp = Coordinate::columnIndexFromString($matches[3]) - 1;
1✔
2525
                        $temp2 = Coordinate::stringFromColumnIndex($temp);
1✔
2526
                        $removeKeys[] = $key;
1✔
2527
                        if ($temp2 !== $matches[1]) {
1✔
2528
                            $temp3 = $matches[1] . $matches[2] . ':' . $temp2 . $matches[4];
1✔
2529
                            $addKeys[] = $temp3;
1✔
2530
                        }
2531
                    }
2532
                }
2533
            }
2534
        }
2535
        foreach ($removeKeys as $key) {
42✔
2536
            unset($this->mergeCells[$key]);
2✔
2537
        }
2538
        foreach ($addKeys as $key) {
42✔
2539
            $this->mergeCells[$key] = $key;
1✔
2540
        }
2541

2542
        $highestColumn = $this->getHighestDataColumn();
42✔
2543
        $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
42✔
2544
        $pColumnIndex = Coordinate::columnIndexFromString($column);
42✔
2545

2546
        $holdColumnDimensions = $this->removeColumnDimensions($pColumnIndex, $numberOfColumns);
42✔
2547

2548
        $column = Coordinate::stringFromColumnIndex($pColumnIndex + $numberOfColumns);
42✔
2549
        $objReferenceHelper = ReferenceHelper::getInstance();
42✔
2550
        $objReferenceHelper->insertNewBefore($column . '1', -$numberOfColumns, 0, $this);
42✔
2551

2552
        $this->columnDimensions = $holdColumnDimensions;
42✔
2553

2554
        if ($pColumnIndex > $highestColumnIndex) {
42✔
2555
            return $this;
9✔
2556
        }
2557

2558
        $maxPossibleColumnsToBeRemoved = $highestColumnIndex - $pColumnIndex + 1;
33✔
2559

2560
        for ($c = 0, $n = min($maxPossibleColumnsToBeRemoved, $numberOfColumns); $c < $n; ++$c) {
33✔
2561
            $this->cellCollection->removeColumn($highestColumn);
33✔
2562
            $highestColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($highestColumn) - 1);
33✔
2563
        }
2564

2565
        $this->garbageCollect();
33✔
2566

2567
        return $this;
33✔
2568
    }
2569

2570
    /** @return ColumnDimension[] */
2571
    private function removeColumnDimensions(int $pColumnIndex, int $numberOfColumns): array
2572
    {
2573
        $highCol = $pColumnIndex + $numberOfColumns - 1;
42✔
2574
        $holdColumnDimensions = [];
42✔
2575
        foreach ($this->columnDimensions as $columnDimension) {
42✔
2576
            $num = $columnDimension->getColumnNumeric();
18✔
2577
            if ($num < $pColumnIndex) {
18✔
2578
                $str = $columnDimension->getColumnIndex();
18✔
2579
                $holdColumnDimensions[$str] = $columnDimension;
18✔
2580
            } elseif ($num > $highCol) {
18✔
2581
                $cloneDimension = clone $columnDimension;
18✔
2582
                $cloneDimension->setColumnNumeric($num - $numberOfColumns);
18✔
2583
                $str = $cloneDimension->getColumnIndex();
18✔
2584
                $holdColumnDimensions[$str] = $cloneDimension;
18✔
2585
            }
2586
        }
2587

2588
        return $holdColumnDimensions;
42✔
2589
    }
2590

2591
    /**
2592
     * Remove a column, updating all possible related data.
2593
     *
2594
     * @param int $columnIndex Remove starting with this column Index (numeric column coordinate)
2595
     * @param int $numColumns Number of columns to remove
2596
     *
2597
     * @return $this
2598
     */
2599
    public function removeColumnByIndex(int $columnIndex, int $numColumns = 1): static
2600
    {
2601
        if ($columnIndex >= 1) {
3✔
2602
            return $this->removeColumn(Coordinate::stringFromColumnIndex($columnIndex), $numColumns);
2✔
2603
        }
2604

2605
        throw new Exception('Columns to be deleted should at least start from column A (1)');
1✔
2606
    }
2607

2608
    /**
2609
     * Show gridlines?
2610
     */
2611
    public function getShowGridlines(): bool
2612
    {
2613
        return $this->showGridlines;
1,067✔
2614
    }
2615

2616
    /**
2617
     * Set show gridlines.
2618
     *
2619
     * @param bool $showGridLines Show gridlines (true/false)
2620
     *
2621
     * @return $this
2622
     */
2623
    public function setShowGridlines(bool $showGridLines): self
2624
    {
2625
        $this->showGridlines = $showGridLines;
862✔
2626

2627
        return $this;
862✔
2628
    }
2629

2630
    /**
2631
     * Print gridlines?
2632
     */
2633
    public function getPrintGridlines(): bool
2634
    {
2635
        return $this->printGridlines;
1,073✔
2636
    }
2637

2638
    /**
2639
     * Set print gridlines.
2640
     *
2641
     * @param bool $printGridLines Print gridlines (true/false)
2642
     *
2643
     * @return $this
2644
     */
2645
    public function setPrintGridlines(bool $printGridLines): self
2646
    {
2647
        $this->printGridlines = $printGridLines;
581✔
2648

2649
        return $this;
581✔
2650
    }
2651

2652
    /**
2653
     * Show row and column headers?
2654
     */
2655
    public function getShowRowColHeaders(): bool
2656
    {
2657
        return $this->showRowColHeaders;
534✔
2658
    }
2659

2660
    /**
2661
     * Set show row and column headers.
2662
     *
2663
     * @param bool $showRowColHeaders Show row and column headers (true/false)
2664
     *
2665
     * @return $this
2666
     */
2667
    public function setShowRowColHeaders(bool $showRowColHeaders): self
2668
    {
2669
        $this->showRowColHeaders = $showRowColHeaders;
397✔
2670

2671
        return $this;
397✔
2672
    }
2673

2674
    /**
2675
     * Show summary below? (Row/Column outlining).
2676
     */
2677
    public function getShowSummaryBelow(): bool
2678
    {
2679
        return $this->showSummaryBelow;
535✔
2680
    }
2681

2682
    /**
2683
     * Set show summary below.
2684
     *
2685
     * @param bool $showSummaryBelow Show summary below (true/false)
2686
     *
2687
     * @return $this
2688
     */
2689
    public function setShowSummaryBelow(bool $showSummaryBelow): self
2690
    {
2691
        $this->showSummaryBelow = $showSummaryBelow;
406✔
2692

2693
        return $this;
406✔
2694
    }
2695

2696
    /**
2697
     * Show summary right? (Row/Column outlining).
2698
     */
2699
    public function getShowSummaryRight(): bool
2700
    {
2701
        return $this->showSummaryRight;
535✔
2702
    }
2703

2704
    /**
2705
     * Set show summary right.
2706
     *
2707
     * @param bool $showSummaryRight Show summary right (true/false)
2708
     *
2709
     * @return $this
2710
     */
2711
    public function setShowSummaryRight(bool $showSummaryRight): self
2712
    {
2713
        $this->showSummaryRight = $showSummaryRight;
406✔
2714

2715
        return $this;
406✔
2716
    }
2717

2718
    /**
2719
     * Get comments.
2720
     *
2721
     * @return Comment[]
2722
     */
2723
    public function getComments(): array
2724
    {
2725
        return $this->comments;
1,125✔
2726
    }
2727

2728
    /**
2729
     * Set comments array for the entire sheet.
2730
     *
2731
     * @param Comment[] $comments
2732
     *
2733
     * @return $this
2734
     */
2735
    public function setComments(array $comments): self
2736
    {
2737
        $this->comments = $comments;
122✔
2738

2739
        return $this;
122✔
2740
    }
2741

2742
    /**
2743
     * Remove comment from cell.
2744
     *
2745
     * @param array{0: int, 1: int}|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
2746
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2747
     *
2748
     * @return $this
2749
     */
2750
    public function removeComment(CellAddress|string|array $cellCoordinate): self
2751
    {
2752
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
56✔
2753

2754
        if (Coordinate::coordinateIsRange($cellAddress)) {
56✔
2755
            throw new Exception('Cell coordinate string can not be a range of cells.');
1✔
2756
        } elseif (str_contains($cellAddress, '$')) {
55✔
2757
            throw new Exception('Cell coordinate string must not be absolute.');
1✔
2758
        } elseif ($cellAddress == '') {
54✔
2759
            throw new Exception('Cell coordinate can not be zero-length string.');
1✔
2760
        }
2761
        // Check if we have a comment for this cell and delete it
2762
        if (isset($this->comments[$cellAddress])) {
53✔
2763
            unset($this->comments[$cellAddress]);
2✔
2764
        }
2765

2766
        return $this;
53✔
2767
    }
2768

2769
    /**
2770
     * Get comment for cell.
2771
     *
2772
     * @param array{0: int, 1: int}|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
2773
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2774
     */
2775
    public function getComment(CellAddress|string|array $cellCoordinate, bool $attachNew = true): Comment
2776
    {
2777
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
110✔
2778

2779
        if (Coordinate::coordinateIsRange($cellAddress)) {
110✔
2780
            throw new Exception('Cell coordinate string can not be a range of cells.');
1✔
2781
        } elseif (str_contains($cellAddress, '$')) {
109✔
2782
            throw new Exception('Cell coordinate string must not be absolute.');
1✔
2783
        } elseif ($cellAddress == '') {
108✔
2784
            throw new Exception('Cell coordinate can not be zero-length string.');
1✔
2785
        }
2786

2787
        // Check if we already have a comment for this cell.
2788
        if (isset($this->comments[$cellAddress])) {
107✔
2789
            return $this->comments[$cellAddress];
74✔
2790
        }
2791

2792
        // If not, create a new comment.
2793
        $newComment = new Comment();
107✔
2794
        if ($attachNew) {
107✔
2795
            $this->comments[$cellAddress] = $newComment;
107✔
2796
        }
2797

2798
        return $newComment;
107✔
2799
    }
2800

2801
    /**
2802
     * Get active cell.
2803
     *
2804
     * @return string Example: 'A1'
2805
     */
2806
    public function getActiveCell(): string
2807
    {
2808
        return $this->activeCell;
10,318✔
2809
    }
2810

2811
    /**
2812
     * Get selected cells.
2813
     */
2814
    public function getSelectedCells(): string
2815
    {
2816
        return $this->selectedCells;
10,367✔
2817
    }
2818

2819
    /**
2820
     * Selected cell.
2821
     *
2822
     * @param string $coordinate Cell (i.e. A1)
2823
     *
2824
     * @return $this
2825
     */
2826
    public function setSelectedCell(string $coordinate): static
2827
    {
2828
        return $this->setSelectedCells($coordinate);
38✔
2829
    }
2830

2831
    /**
2832
     * Select a range of cells.
2833
     *
2834
     * @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'
2835
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2836
     *              or a CellAddress or AddressRange object.
2837
     *
2838
     * @return $this
2839
     */
2840
    public function setSelectedCells(AddressRange|CellAddress|int|string|array $coordinate): static
2841
    {
2842
        if (is_string($coordinate)) {
10,315✔
2843
            $coordinate = Validations::definedNameToCoordinate($coordinate, $this);
10,315✔
2844
        }
2845
        $coordinate = Validations::validateCellOrCellRange($coordinate);
10,315✔
2846

2847
        if (Coordinate::coordinateIsRange($coordinate)) {
10,315✔
2848
            [$first] = Coordinate::splitRange($coordinate);
503✔
2849
            $this->activeCell = $first[0];
503✔
2850
        } else {
2851
            $this->activeCell = $coordinate;
10,284✔
2852
        }
2853
        $this->selectedCells = $coordinate;
10,315✔
2854
        $this->setSelectedCellsActivePane();
10,315✔
2855

2856
        return $this;
10,315✔
2857
    }
2858

2859
    private function setSelectedCellsActivePane(): void
2860
    {
2861
        if (!empty($this->freezePane)) {
10,316✔
2862
            $coordinateC = Coordinate::indexesFromString($this->freezePane);
47✔
2863
            $coordinateT = Coordinate::indexesFromString($this->activeCell);
47✔
2864
            if ($coordinateC[0] === 1) {
47✔
2865
                $activePane = ($coordinateT[1] <= $coordinateC[1]) ? 'topLeft' : 'bottomLeft';
26✔
2866
            } elseif ($coordinateC[1] === 1) {
23✔
2867
                $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'topLeft' : 'topRight';
3✔
2868
            } elseif ($coordinateT[1] <= $coordinateC[1]) {
21✔
2869
                $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'topLeft' : 'topRight';
21✔
2870
            } else {
2871
                $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'bottomLeft' : 'bottomRight';
10✔
2872
            }
2873
            $this->setActivePane($activePane);
47✔
2874
            $this->panes[$activePane] = new Pane($activePane, $this->selectedCells, $this->activeCell);
47✔
2875
        }
2876
    }
2877

2878
    /**
2879
     * Get right-to-left.
2880
     */
2881
    public function getRightToLeft(): bool
2882
    {
2883
        return $this->rightToLeft;
1,076✔
2884
    }
2885

2886
    /**
2887
     * Set right-to-left.
2888
     *
2889
     * @param bool $value Right-to-left true/false
2890
     *
2891
     * @return $this
2892
     */
2893
    public function setRightToLeft(bool $value): static
2894
    {
2895
        $this->rightToLeft = $value;
146✔
2896

2897
        return $this;
146✔
2898
    }
2899

2900
    /**
2901
     * Fill worksheet from values in array.
2902
     *
2903
     * @param mixed[]|mixed[][] $source Source array
2904
     * @param mixed $nullValue Value in source array that stands for blank cell
2905
     * @param string $startCell Insert array starting from this cell address as the top left coordinate
2906
     * @param bool $strictNullComparison Apply strict comparison when testing for null values in the array
2907
     *
2908
     * @return $this
2909
     */
2910
    public function fromArray(array $source, mixed $nullValue = null, string $startCell = 'A1', bool $strictNullComparison = false): static
2911
    {
2912
        //    Convert a 1-D array to 2-D (for ease of looping)
2913
        if (!is_array(end($source))) {
799✔
2914
            $source = [$source];
47✔
2915
        }
2916
        /** @var mixed[][] $source */
2917

2918
        // start coordinate
2919
        [$startColumn, $startRow] = Coordinate::coordinateFromString($startCell);
799✔
2920
        $startRow = (int) $startRow;
799✔
2921

2922
        // Loop through $source
2923
        if ($strictNullComparison) {
799✔
2924
            foreach ($source as $rowData) {
394✔
2925
                /** @var string */
2926
                $currentColumn = $startColumn;
394✔
2927
                foreach ($rowData as $cellValue) {
394✔
2928
                    if ($cellValue !== $nullValue) {
394✔
2929
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
394✔
2930
                    }
2931
                    StringHelper::stringIncrement($currentColumn);
394✔
2932
                }
2933
                ++$startRow;
394✔
2934
            }
2935
        } else {
2936
            foreach ($source as $rowData) {
406✔
2937
                $currentColumn = $startColumn;
406✔
2938
                foreach ($rowData as $cellValue) {
406✔
2939
                    if ($cellValue != $nullValue) {
405✔
2940
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
399✔
2941
                    }
2942
                    StringHelper::stringIncrement($currentColumn);
405✔
2943
                }
2944
                ++$startRow;
406✔
2945
            }
2946
        }
2947

2948
        return $this;
799✔
2949
    }
2950

2951
    /**
2952
     * @param bool $calculateFormulas Whether to calculate cell's value if it is a formula.
2953
     * @param null|bool|float|int|RichText|string $nullValue value to use when null
2954
     * @param bool $formatData Whether to format data according to cell's style.
2955
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
2956
     *
2957
     * @throws Exception
2958
     * @throws \PhpOffice\PhpSpreadsheet\Calculation\Exception
2959
     */
2960
    protected function cellToArray(Cell $cell, bool $calculateFormulas, bool $formatData, mixed $nullValue, bool $lessFloatPrecision = false): mixed
2961
    {
2962
        $returnValue = $nullValue;
182✔
2963

2964
        if ($cell->getValue() !== null) {
182✔
2965
            if ($cell->getValue() instanceof RichText) {
182✔
2966
                $returnValue = $cell->getValue()->getPlainText();
4✔
2967
            } else {
2968
                $returnValue = ($calculateFormulas) ? $cell->getCalculatedValue() : $cell->getValue();
182✔
2969
            }
2970

2971
            if ($formatData) {
182✔
2972
                $style = $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex());
120✔
2973
                /** @var null|bool|float|int|RichText|string */
2974
                $returnValuex = $returnValue;
120✔
2975
                $returnValue = NumberFormat::toFormattedString(
120✔
2976
                    $returnValuex,
120✔
2977
                    $style->getNumberFormat()->getFormatCode() ?? NumberFormat::FORMAT_GENERAL,
120✔
2978
                    lessFloatPrecision: $lessFloatPrecision
120✔
2979
                );
120✔
2980
            }
2981
        }
2982

2983
        return $returnValue;
182✔
2984
    }
2985

2986
    /**
2987
     * Create array from a range of cells.
2988
     *
2989
     * @param null|bool|float|int|RichText|string $nullValue Value returned in the array entry if a cell doesn't exist
2990
     * @param bool $calculateFormulas Should formulas be calculated?
2991
     * @param bool $formatData Should formatting be applied to cell values?
2992
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
2993
     *                             True - Return rows and columns indexed by their actual row and column IDs
2994
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
2995
     *                            True - Don't return values for rows/columns that are defined as hidden.
2996
     * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
2997
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
2998
     *
2999
     * @return mixed[][]
3000
     */
3001
    public function rangeToArray(
3002
        string $range,
3003
        mixed $nullValue = null,
3004
        bool $calculateFormulas = true,
3005
        bool $formatData = true,
3006
        bool $returnCellRef = false,
3007
        bool $ignoreHidden = false,
3008
        bool $reduceArrays = false,
3009
        bool $lessFloatPrecision = false
3010
    ): array {
3011
        $returnValue = [];
150✔
3012

3013
        // Loop through rows
3014
        foreach ($this->rangeToArrayYieldRows($range, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision) as $rowRef => $rowArray) {
150✔
3015
            $returnValue[$rowRef] = $rowArray;
150✔
3016
        }
3017

3018
        // Return
3019
        return $returnValue;
150✔
3020
    }
3021

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

3049
        $parts = explode(',', $ranges);
3✔
3050
        foreach ($parts as $part) {
3✔
3051
            // Loop through rows
3052
            foreach ($this->rangeToArrayYieldRows($part, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision) as $rowRef => $rowArray) {
3✔
3053
                $returnValue[$rowRef] = $rowArray;
3✔
3054
            }
3055
        }
3056

3057
        // Return
3058
        return $returnValue;
3✔
3059
    }
3060

3061
    /**
3062
     * Create array from a range of cells, yielding each row in turn.
3063
     *
3064
     * @param null|bool|float|int|RichText|string $nullValue Value returned in the array entry if a cell doesn't exist
3065
     * @param bool $calculateFormulas Should formulas be calculated?
3066
     * @param bool $formatData Should formatting be applied to cell values?
3067
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3068
     *                             True - Return rows and columns indexed by their actual row and column IDs
3069
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3070
     *                            True - Don't return values for rows/columns that are defined as hidden.
3071
     * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3072
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3073
     *
3074
     * @return Generator<array<mixed>>
3075
     */
3076
    public function rangeToArrayYieldRows(
3077
        string $range,
3078
        mixed $nullValue = null,
3079
        bool $calculateFormulas = true,
3080
        bool $formatData = true,
3081
        bool $returnCellRef = false,
3082
        bool $ignoreHidden = false,
3083
        bool $reduceArrays = false,
3084
        bool $lessFloatPrecision = false
3085
    ) {
3086
        $range = Validations::validateCellOrCellRange($range);
182✔
3087

3088
        //    Identify the range that we need to extract from the worksheet
3089
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range);
182✔
3090
        $minCol = Coordinate::stringFromColumnIndex($rangeStart[0]);
182✔
3091
        $minRow = $rangeStart[1];
182✔
3092
        $maxCol = Coordinate::stringFromColumnIndex($rangeEnd[0]);
182✔
3093
        $maxRow = $rangeEnd[1];
182✔
3094
        $minColInt = $rangeStart[0];
182✔
3095
        $maxColInt = $rangeEnd[0];
182✔
3096

3097
        StringHelper::stringIncrement($maxCol);
182✔
3098
        /** @var array<string, bool> */
3099
        $hiddenColumns = [];
182✔
3100
        $nullRow = $this->buildNullRow($nullValue, $minCol, $maxCol, $returnCellRef, $ignoreHidden, $hiddenColumns);
182✔
3101
        $hideColumns = !empty($hiddenColumns);
182✔
3102

3103
        $keys = $this->cellCollection->getSortedCoordinatesInt();
182✔
3104
        $keyIndex = 0;
182✔
3105
        $keysCount = count($keys);
182✔
3106
        // Loop through rows
3107
        for ($row = $minRow; $row <= $maxRow; ++$row) {
182✔
3108
            if (($ignoreHidden === true) && ($this->isRowVisible($row) === false)) {
182✔
3109
                continue;
4✔
3110
            }
3111
            $rowRef = $returnCellRef ? $row : ($row - $minRow);
182✔
3112
            $returnValue = $nullRow;
182✔
3113

3114
            $index = ($row - 1) * AddressRange::MAX_COLUMN_INT + 1;
182✔
3115
            $indexPlus = $index + AddressRange::MAX_COLUMN_INT - 1;
182✔
3116

3117
            // Binary search to quickly approach the correct index
3118
            $keyIndex = intdiv($keysCount, 2);
182✔
3119
            $boundLow = 0;
182✔
3120
            $boundHigh = $keysCount - 1;
182✔
3121
            while ($boundLow <= $boundHigh) {
182✔
3122
                $keyIndex = intdiv($boundLow + $boundHigh, 2);
182✔
3123
                if ($keys[$keyIndex] < $index) {
182✔
3124
                    $boundLow = $keyIndex + 1;
150✔
3125
                } elseif ($keys[$keyIndex] > $index) {
182✔
3126
                    $boundHigh = $keyIndex - 1;
164✔
3127
                } else {
3128
                    break;
174✔
3129
                }
3130
            }
3131

3132
            // Realign to the proper index value
3133
            while ($keyIndex > 0 && $keys[$keyIndex] > $index) {
182✔
3134
                --$keyIndex;
16✔
3135
            }
3136
            while ($keyIndex < $keysCount && $keys[$keyIndex] < $index) {
182✔
3137
                ++$keyIndex;
22✔
3138
            }
3139

3140
            while ($keyIndex < $keysCount && $keys[$keyIndex] <= $indexPlus) {
182✔
3141
                $key = $keys[$keyIndex];
182✔
3142
                $thisRow = intdiv($key - 1, AddressRange::MAX_COLUMN_INT) + 1;
182✔
3143
                $thisCol = ($key % AddressRange::MAX_COLUMN_INT) ?: AddressRange::MAX_COLUMN_INT;
182✔
3144
                if ($thisCol >= $minColInt && $thisCol <= $maxColInt) {
182✔
3145
                    $col = Coordinate::stringFromColumnIndex($thisCol);
182✔
3146
                    if ($hideColumns === false || !isset($hiddenColumns[$col])) {
182✔
3147
                        $columnRef = $returnCellRef ? $col : ($thisCol - $minColInt);
182✔
3148
                        $cell = $this->cellCollection->get("{$col}{$thisRow}");
182✔
3149
                        if ($cell !== null) {
182✔
3150
                            $value = $this->cellToArray($cell, $calculateFormulas, $formatData, $nullValue, lessFloatPrecision: $lessFloatPrecision);
182✔
3151
                            if ($reduceArrays) {
182✔
3152
                                while (is_array($value)) {
21✔
3153
                                    $value = array_shift($value);
19✔
3154
                                }
3155
                            }
3156
                            if ($value !== $nullValue) {
182✔
3157
                                $returnValue[$columnRef] = $value;
182✔
3158
                            }
3159
                        }
3160
                    }
3161
                }
3162
                ++$keyIndex;
182✔
3163
            }
3164

3165
            yield $rowRef => $returnValue;
182✔
3166
        }
3167
    }
3168

3169
    /**
3170
     * Prepare a row data filled with null values to deduplicate the memory areas for empty rows.
3171
     *
3172
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
3173
     * @param string $minCol Start column of the range
3174
     * @param string $maxCol End column of the range
3175
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3176
     *                              True - Return rows and columns indexed by their actual row and column IDs
3177
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3178
     *                             True - Don't return values for rows/columns that are defined as hidden.
3179
     * @param array<string, bool> $hiddenColumns
3180
     *
3181
     * @return mixed[]
3182
     */
3183
    private function buildNullRow(
3184
        mixed $nullValue,
3185
        string $minCol,
3186
        string $maxCol,
3187
        bool $returnCellRef,
3188
        bool $ignoreHidden,
3189
        array &$hiddenColumns
3190
    ): array {
3191
        $nullRow = [];
182✔
3192
        $c = -1;
182✔
3193
        for ($col = $minCol; $col !== $maxCol; StringHelper::stringIncrement($col)) {
182✔
3194
            if ($ignoreHidden === true && $this->columnDimensionExists($col) && $this->getColumnDimension($col)->getVisible() === false) {
182✔
3195
                $hiddenColumns[$col] = true;
2✔
3196
            } else {
3197
                $columnRef = $returnCellRef ? $col : ++$c;
182✔
3198
                $nullRow[$columnRef] = $nullValue;
182✔
3199
            }
3200
        }
3201

3202
        return $nullRow;
182✔
3203
    }
3204

3205
    private function validateNamedRange(string $definedName, bool $returnNullIfInvalid = false): ?DefinedName
3206
    {
3207
        $namedRange = DefinedName::resolveName($definedName, $this);
19✔
3208
        if ($namedRange === null) {
19✔
3209
            if ($returnNullIfInvalid) {
6✔
3210
                return null;
5✔
3211
            }
3212

3213
            throw new Exception('Named Range ' . $definedName . ' does not exist.');
1✔
3214
        }
3215

3216
        if ($namedRange->isFormula()) {
13✔
3217
            if ($returnNullIfInvalid) {
×
3218
                return null;
×
3219
            }
3220

3221
            throw new Exception('Defined Named ' . $definedName . ' is a formula, not a range or cell.');
×
3222
        }
3223

3224
        if ($namedRange->getLocalOnly()) {
13✔
3225
            $worksheet = $namedRange->getWorksheet();
2✔
3226
            if ($worksheet === null || $this->hash !== $worksheet->getHashInt()) {
2✔
3227
                if ($returnNullIfInvalid) {
×
3228
                    return null;
×
3229
                }
3230

NEW
3231
                throw new Exception(
×
3232
                    'Named range ' . $definedName . ' is not accessible from within sheet ' . $this->getTitle()
×
3233
                );
×
3234
            }
3235
        }
3236

3237
        return $namedRange;
13✔
3238
    }
3239

3240
    /**
3241
     * Create array from a range of cells.
3242
     *
3243
     * @param string $definedName The Named Range that should be returned
3244
     * @param null|bool|float|int|RichText|string $nullValue Value returned in the array entry if a cell doesn't exist
3245
     * @param bool $calculateFormulas Should formulas be calculated?
3246
     * @param bool $formatData Should formatting be applied to cell values?
3247
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3248
     *                             True - Return rows and columns indexed by their actual row and column IDs
3249
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3250
     *                            True - Don't return values for rows/columns that are defined as hidden.
3251
     * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3252
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3253
     *
3254
     * @return mixed[][]
3255
     */
3256
    public function namedRangeToArray(
3257
        string $definedName,
3258
        mixed $nullValue = null,
3259
        bool $calculateFormulas = true,
3260
        bool $formatData = true,
3261
        bool $returnCellRef = false,
3262
        bool $ignoreHidden = false,
3263
        bool $reduceArrays = false,
3264
        bool $lessFloatPrecision = false
3265
    ): array {
3266
        $retVal = [];
2✔
3267
        $namedRange = $this->validateNamedRange($definedName);
2✔
3268
        if ($namedRange !== null) {
1✔
3269
            $cellRange = ltrim(substr($namedRange->getValue(), (int) strrpos($namedRange->getValue(), '!')), '!');
1✔
3270
            $cellRange = str_replace('$', '', $cellRange);
1✔
3271
            $workSheet = $namedRange->getWorksheet();
1✔
3272
            if ($workSheet !== null) {
1✔
3273
                $retVal = $workSheet->rangeToArray($cellRange, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision);
1✔
3274
            }
3275
        }
3276

3277
        return $retVal;
1✔
3278
    }
3279

3280
    /**
3281
     * Create array from worksheet.
3282
     *
3283
     * @param null|bool|float|int|RichText|string $nullValue Value returned in the array entry if a cell doesn't exist
3284
     * @param bool $calculateFormulas Should formulas be calculated?
3285
     * @param bool $formatData Should formatting be applied to cell values?
3286
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3287
     *                             True - Return rows and columns indexed by their actual row and column IDs
3288
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3289
     *                            True - Don't return values for rows/columns that are defined as hidden.
3290
     * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3291
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3292
     *
3293
     * @return mixed[][]
3294
     */
3295
    public function toArray(
3296
        mixed $nullValue = null,
3297
        bool $calculateFormulas = true,
3298
        bool $formatData = true,
3299
        bool $returnCellRef = false,
3300
        bool $ignoreHidden = false,
3301
        bool $reduceArrays = false,
3302
        bool $lessFloatPrecision = false
3303
    ): array {
3304
        // Garbage collect...
3305
        $this->garbageCollect();
81✔
3306
        $this->calculateArrays($calculateFormulas);
81✔
3307

3308
        //    Identify the range that we need to extract from the worksheet
3309
        $maxCol = $this->getHighestColumn();
81✔
3310
        $maxRow = $this->getHighestRow();
81✔
3311

3312
        // Return
3313
        return $this->rangeToArray("A1:{$maxCol}{$maxRow}", $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision);
81✔
3314
    }
3315

3316
    /**
3317
     * Get row iterator.
3318
     *
3319
     * @param int $startRow The row number at which to start iterating
3320
     * @param ?int $endRow The row number at which to stop iterating
3321
     */
3322
    public function getRowIterator(int $startRow = 1, ?int $endRow = null): RowIterator
3323
    {
3324
        return new RowIterator($this, $startRow, $endRow);
95✔
3325
    }
3326

3327
    /**
3328
     * Get column iterator.
3329
     *
3330
     * @param string $startColumn The column address at which to start iterating
3331
     * @param ?string $endColumn The column address at which to stop iterating
3332
     */
3333
    public function getColumnIterator(string $startColumn = 'A', ?string $endColumn = null): ColumnIterator
3334
    {
3335
        return new ColumnIterator($this, $startColumn, $endColumn);
26✔
3336
    }
3337

3338
    /**
3339
     * Run PhpSpreadsheet garbage collector.
3340
     *
3341
     * @return $this
3342
     */
3343
    public function garbageCollect(): static
3344
    {
3345
        // Flush cache
3346
        $this->cellCollection->get('A1');
1,195✔
3347

3348
        // Lookup highest column and highest row if cells are cleaned
3349
        $colRow = $this->cellCollection->getHighestRowAndColumn();
1,195✔
3350
        $highestRow = $colRow['row'];
1,195✔
3351
        $highestColumn = Coordinate::columnIndexFromString($colRow['column']);
1,195✔
3352

3353
        // Loop through column dimensions
3354
        foreach ($this->columnDimensions as $dimension) {
1,195✔
3355
            $highestColumn = max($highestColumn, Coordinate::columnIndexFromString($dimension->getColumnIndex()));
169✔
3356
        }
3357

3358
        // Loop through row dimensions
3359
        foreach ($this->rowDimensions as $dimension) {
1,195✔
3360
            $highestRow = max($highestRow, $dimension->getRowIndex());
104✔
3361
        }
3362

3363
        // Cache values
3364
        if ($highestColumn < 1) {
1,195✔
3365
            $this->cachedHighestColumn = 1;
×
3366
        } else {
3367
            $this->cachedHighestColumn = $highestColumn;
1,195✔
3368
        }
3369
        /** @var int $highestRow */
3370
        $this->cachedHighestRow = $highestRow;
1,195✔
3371

3372
        // Return
3373
        return $this;
1,195✔
3374
    }
3375

3376
    public function getHashInt(): int
3377
    {
3378
        return $this->hash;
10,699✔
3379
    }
3380

3381
    /**
3382
     * Extract worksheet title from range.
3383
     *
3384
     * Example: extractSheetTitle("testSheet!A1") ==> 'A1'
3385
     * Example: extractSheetTitle("testSheet!A1:C3") ==> 'A1:C3'
3386
     * Example: extractSheetTitle("'testSheet 1'!A1", true) ==> ['testSheet 1', 'A1'];
3387
     * Example: extractSheetTitle("'testSheet 1'!A1:C3", true) ==> ['testSheet 1', 'A1:C3'];
3388
     * Example: extractSheetTitle("A1", true) ==> ['', 'A1'];
3389
     * Example: extractSheetTitle("A1:C3", true) ==> ['', 'A1:C3']
3390
     *
3391
     * @param ?string $range Range to extract title from
3392
     * @param bool $returnRange Return range? (see example)
3393
     *
3394
     * @return ($range is non-empty-string ? ($returnRange is true ? array{0: string, 1: string} : string) : ($returnRange is true ? array{0: null, 1: null} : null))
3395
     */
3396
    public static function extractSheetTitle(?string $range, bool $returnRange = false, bool $unapostrophize = false): array|null|string
3397
    {
3398
        if (empty($range)) {
10,538✔
3399
            return $returnRange ? [null, null] : null;
13✔
3400
        }
3401

3402
        // Sheet title included?
3403
        if (($sep = strrpos($range, '!')) === false) {
10,536✔
3404
            return $returnRange ? ['', $range] : '';
10,507✔
3405
        }
3406

3407
        if ($returnRange) {
1,406✔
3408
            $title = substr($range, 0, $sep);
1,406✔
3409
            if ($unapostrophize) {
1,406✔
3410
                $title = self::unApostrophizeTitle($title);
1,346✔
3411
            }
3412

3413
            return [$title, substr($range, $sep + 1)];
1,406✔
3414
        }
3415

3416
        return substr($range, $sep + 1);
7✔
3417
    }
3418

3419
    public static function unApostrophizeTitle(?string $title): string
3420
    {
3421
        $title ??= '';
1,360✔
3422
        if ($title[0] === "'" && substr($title, -1) === "'") {
1,360✔
3423
            $title = str_replace("''", "'", substr($title, 1, -1));
1,285✔
3424
        }
3425

3426
        return $title;
1,360✔
3427
    }
3428

3429
    /**
3430
     * Get hyperlink.
3431
     *
3432
     * @param string $cellCoordinate Cell coordinate to get hyperlink for, eg: 'A1'
3433
     */
3434
    public function getHyperlink(string $cellCoordinate): Hyperlink
3435
    {
3436
        // return hyperlink if we already have one
3437
        if (isset($this->hyperlinkCollection[$cellCoordinate])) {
97✔
3438
            return $this->hyperlinkCollection[$cellCoordinate];
45✔
3439
        }
3440

3441
        // else create hyperlink
3442
        $this->hyperlinkCollection[$cellCoordinate] = new Hyperlink();
97✔
3443

3444
        return $this->hyperlinkCollection[$cellCoordinate];
97✔
3445
    }
3446

3447
    /**
3448
     * Set hyperlink.
3449
     *
3450
     * @param string $cellCoordinate Cell coordinate to insert hyperlink, eg: 'A1'
3451
     *
3452
     * @return $this
3453
     */
3454
    public function setHyperlink(string $cellCoordinate, ?Hyperlink $hyperlink = null): static
3455
    {
3456
        if ($hyperlink === null) {
54✔
3457
            unset($this->hyperlinkCollection[$cellCoordinate]);
53✔
3458
        } else {
3459
            $this->hyperlinkCollection[$cellCoordinate] = $hyperlink;
21✔
3460
        }
3461

3462
        return $this;
54✔
3463
    }
3464

3465
    /**
3466
     * Hyperlink at a specific coordinate exists?
3467
     *
3468
     * @param string $coordinate eg: 'A1'
3469
     */
3470
    public function hyperlinkExists(string $coordinate): bool
3471
    {
3472
        return isset($this->hyperlinkCollection[$coordinate]);
550✔
3473
    }
3474

3475
    /**
3476
     * Get collection of hyperlinks.
3477
     *
3478
     * @return Hyperlink[]
3479
     */
3480
    public function getHyperlinkCollection(): array
3481
    {
3482
        return $this->hyperlinkCollection;
640✔
3483
    }
3484

3485
    /**
3486
     * Get data validation.
3487
     *
3488
     * @param string $cellCoordinate Cell coordinate to get data validation for, eg: 'A1'
3489
     */
3490
    public function getDataValidation(string $cellCoordinate): DataValidation
3491
    {
3492
        // return data validation if we already have one
3493
        if (isset($this->dataValidationCollection[$cellCoordinate])) {
35✔
3494
            return $this->dataValidationCollection[$cellCoordinate];
26✔
3495
        }
3496

3497
        // or if cell is part of a data validation range
3498
        foreach ($this->dataValidationCollection as $key => $dataValidation) {
28✔
3499
            $keyParts = explode(' ', $key);
12✔
3500
            foreach ($keyParts as $keyPart) {
12✔
3501
                if ($keyPart === $cellCoordinate) {
12✔
3502
                    return $dataValidation;
1✔
3503
                }
3504
                if (str_contains($keyPart, ':')) {
12✔
3505
                    if (Coordinate::coordinateIsInsideRange($keyPart, $cellCoordinate)) {
9✔
3506
                        return $dataValidation;
9✔
3507
                    }
3508
                }
3509
            }
3510
        }
3511

3512
        // else create data validation
3513
        $dataValidation = new DataValidation();
20✔
3514
        $dataValidation->setSqref($cellCoordinate);
20✔
3515
        $this->dataValidationCollection[$cellCoordinate] = $dataValidation;
20✔
3516

3517
        return $dataValidation;
20✔
3518
    }
3519

3520
    /**
3521
     * Set data validation.
3522
     *
3523
     * @param string $cellCoordinate Cell coordinate to insert data validation, eg: 'A1'
3524
     *
3525
     * @return $this
3526
     */
3527
    public function setDataValidation(string $cellCoordinate, ?DataValidation $dataValidation = null): static
3528
    {
3529
        if ($dataValidation === null) {
89✔
3530
            unset($this->dataValidationCollection[$cellCoordinate]);
58✔
3531
        } else {
3532
            $dataValidation->setSqref($cellCoordinate);
38✔
3533
            $this->dataValidationCollection[$cellCoordinate] = $dataValidation;
38✔
3534
        }
3535

3536
        return $this;
89✔
3537
    }
3538

3539
    /**
3540
     * Data validation at a specific coordinate exists?
3541
     *
3542
     * @param string $coordinate eg: 'A1'
3543
     */
3544
    public function dataValidationExists(string $coordinate): bool
3545
    {
3546
        if (isset($this->dataValidationCollection[$coordinate])) {
25✔
3547
            return true;
23✔
3548
        }
3549
        foreach ($this->dataValidationCollection as $key => $dataValidation) {
8✔
3550
            $keyParts = explode(' ', $key);
7✔
3551
            foreach ($keyParts as $keyPart) {
7✔
3552
                if ($keyPart === $coordinate) {
7✔
3553
                    return true;
1✔
3554
                }
3555
                if (str_contains($keyPart, ':')) {
7✔
3556
                    if (Coordinate::coordinateIsInsideRange($keyPart, $coordinate)) {
2✔
3557
                        return true;
2✔
3558
                    }
3559
                }
3560
            }
3561
        }
3562

3563
        return false;
6✔
3564
    }
3565

3566
    /**
3567
     * Get collection of data validations.
3568
     *
3569
     * @return DataValidation[]
3570
     */
3571
    public function getDataValidationCollection(): array
3572
    {
3573
        $collectionCells = [];
641✔
3574
        $collectionRanges = [];
641✔
3575
        foreach ($this->dataValidationCollection as $key => $dataValidation) {
641✔
3576
            if (Preg::isMatch('/[: ]/', $key)) {
27✔
3577
                $collectionRanges[$key] = $dataValidation;
15✔
3578
            } else {
3579
                $collectionCells[$key] = $dataValidation;
22✔
3580
            }
3581
        }
3582

3583
        return array_merge($collectionCells, $collectionRanges);
641✔
3584
    }
3585

3586
    /**
3587
     * Accepts a range, returning it as a range that falls within the current highest row and column of the worksheet.
3588
     *
3589
     * @return string Adjusted range value
3590
     */
3591
    public function shrinkRangeToFit(string $range): string
3592
    {
3593
        $maxCol = $this->getHighestColumn();
×
3594
        $maxRow = $this->getHighestRow();
×
3595
        $maxCol = Coordinate::columnIndexFromString($maxCol);
×
3596

3597
        $rangeBlocks = explode(' ', $range);
×
3598
        foreach ($rangeBlocks as &$rangeSet) {
×
3599
            $rangeBoundaries = Coordinate::getRangeBoundaries($rangeSet);
×
3600

3601
            if (Coordinate::columnIndexFromString($rangeBoundaries[0][0]) > $maxCol) {
×
3602
                $rangeBoundaries[0][0] = Coordinate::stringFromColumnIndex($maxCol);
×
3603
            }
3604
            if ($rangeBoundaries[0][1] > $maxRow) {
×
3605
                $rangeBoundaries[0][1] = $maxRow;
×
3606
            }
3607
            if (Coordinate::columnIndexFromString($rangeBoundaries[1][0]) > $maxCol) {
×
3608
                $rangeBoundaries[1][0] = Coordinate::stringFromColumnIndex($maxCol);
×
3609
            }
3610
            if ($rangeBoundaries[1][1] > $maxRow) {
×
3611
                $rangeBoundaries[1][1] = $maxRow;
×
3612
            }
3613
            $rangeSet = $rangeBoundaries[0][0] . $rangeBoundaries[0][1] . ':' . $rangeBoundaries[1][0] . $rangeBoundaries[1][1];
×
3614
        }
3615
        unset($rangeSet);
×
3616

3617
        return implode(' ', $rangeBlocks);
×
3618
    }
3619

3620
    /**
3621
     * Get tab color.
3622
     */
3623
    public function getTabColor(): Color
3624
    {
3625
        if ($this->tabColor === null) {
23✔
3626
            $this->tabColor = new Color();
23✔
3627
        }
3628

3629
        return $this->tabColor;
23✔
3630
    }
3631

3632
    /**
3633
     * Reset tab color.
3634
     *
3635
     * @return $this
3636
     */
3637
    public function resetTabColor(): static
3638
    {
3639
        $this->tabColor = null;
1✔
3640

3641
        return $this;
1✔
3642
    }
3643

3644
    /**
3645
     * Tab color set?
3646
     */
3647
    public function isTabColorSet(): bool
3648
    {
3649
        return $this->tabColor !== null;
536✔
3650
    }
3651

3652
    /**
3653
     * Copy worksheet (!= clone!).
3654
     */
3655
    public function copy(): static
3656
    {
3657
        return clone $this;
×
3658
    }
3659

3660
    /**
3661
     * Returns a boolean true if the specified row contains no cells. By default, this means that no cell records
3662
     *          exist in the collection for this row. false will be returned otherwise.
3663
     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
3664
     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
3665
     *                  cells, then the row will be considered empty.
3666
     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
3667
     *                  string value cells, then the row will be considered empty.
3668
     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3669
     *                  If the only cells in the collection are null value or empty string value cells, then the row
3670
     *                  will be considered empty.
3671
     *
3672
     * @param int $definitionOfEmptyFlags
3673
     *              Possible Flag Values are:
3674
     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
3675
     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3676
     */
3677
    public function isEmptyRow(int $rowId, int $definitionOfEmptyFlags = 0): bool
3678
    {
3679
        try {
3680
            $iterator = new RowIterator($this, $rowId, $rowId);
9✔
3681
            $iterator->seek($rowId);
8✔
3682
            $row = $iterator->current();
8✔
3683
        } catch (Exception) {
1✔
3684
            return true;
1✔
3685
        }
3686

3687
        return $row->isEmpty($definitionOfEmptyFlags);
8✔
3688
    }
3689

3690
    /**
3691
     * Returns a boolean true if the specified column contains no cells. By default, this means that no cell records
3692
     *          exist in the collection for this column. false will be returned otherwise.
3693
     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
3694
     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
3695
     *                  cells, then the column will be considered empty.
3696
     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
3697
     *                  string value cells, then the column will be considered empty.
3698
     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3699
     *                  If the only cells in the collection are null value or empty string value cells, then the column
3700
     *                  will be considered empty.
3701
     *
3702
     * @param int $definitionOfEmptyFlags
3703
     *              Possible Flag Values are:
3704
     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
3705
     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3706
     */
3707
    public function isEmptyColumn(string $columnId, int $definitionOfEmptyFlags = 0): bool
3708
    {
3709
        try {
3710
            $iterator = new ColumnIterator($this, $columnId, $columnId);
9✔
3711
            $iterator->seek($columnId);
8✔
3712
            $column = $iterator->current();
8✔
3713
        } catch (Exception) {
1✔
3714
            return true;
1✔
3715
        }
3716

3717
        return $column->isEmpty($definitionOfEmptyFlags);
8✔
3718
    }
3719

3720
    /**
3721
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
3722
     */
3723
    public function __clone()
3724
    {
3725
        foreach (get_object_vars($this) as $key => $val) {
20✔
3726
            if ($key == 'parent') {
20✔
3727
                continue;
20✔
3728
            }
3729

3730
            if (is_object($val) || (is_array($val))) {
20✔
3731
                if ($key === 'cellCollection') {
20✔
3732
                    $newCollection = $this->cellCollection->cloneCellCollection($this);
20✔
3733
                    $this->cellCollection = $newCollection;
20✔
3734
                } elseif ($key === 'drawingCollection') {
20✔
3735
                    $currentCollection = $this->drawingCollection;
20✔
3736
                    $this->drawingCollection = new ArrayObject();
20✔
3737
                    foreach ($currentCollection as $item) {
20✔
3738
                        $newDrawing = clone $item;
4✔
3739
                        $newDrawing->setWorksheet($this);
4✔
3740
                    }
3741
                } elseif ($key === 'tableCollection') {
20✔
3742
                    $currentCollection = $this->tableCollection;
20✔
3743
                    $this->tableCollection = new ArrayObject();
20✔
3744
                    foreach ($currentCollection as $item) {
20✔
3745
                        $newTable = clone $item;
1✔
3746
                        $newTable->setName($item->getName() . 'clone');
1✔
3747
                        $this->addTable($newTable);
1✔
3748
                    }
3749
                } elseif ($key === 'chartCollection') {
20✔
3750
                    $currentCollection = $this->chartCollection;
20✔
3751
                    $this->chartCollection = new ArrayObject();
20✔
3752
                    foreach ($currentCollection as $item) {
20✔
3753
                        $newChart = clone $item;
5✔
3754
                        $this->addChart($newChart);
5✔
3755
                    }
3756
                } elseif ($key === 'autoFilter') {
20✔
3757
                    $newAutoFilter = clone $this->autoFilter;
20✔
3758
                    $this->autoFilter = $newAutoFilter;
20✔
3759
                    $this->autoFilter->setParent($this);
20✔
3760
                } else {
3761
                    $this->{$key} = unserialize(serialize($val));
20✔
3762
                }
3763
            }
3764
        }
3765
        $this->hash = spl_object_id($this);
20✔
3766
    }
3767

3768
    /**
3769
     * Define the code name of the sheet.
3770
     *
3771
     * @param string $codeName Same rule as Title minus space not allowed (but, like Excel, change
3772
     *                       silently space to underscore)
3773
     * @param bool $validate False to skip validation of new title. WARNING: This should only be set
3774
     *                       at parse time (by Readers), where titles can be assumed to be valid.
3775
     *
3776
     * @return $this
3777
     */
3778
    public function setCodeName(string $codeName, bool $validate = true): static
3779
    {
3780
        // Is this a 'rename' or not?
3781
        if ($this->getCodeName() == $codeName) {
10,738✔
3782
            return $this;
×
3783
        }
3784

3785
        if ($validate) {
10,738✔
3786
            $codeName = str_replace(' ', '_', $codeName); //Excel does this automatically without flinching, we are doing the same
10,738✔
3787

3788
            // Syntax check
3789
            // throw an exception if not valid
3790
            self::checkSheetCodeName($codeName);
10,738✔
3791

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

3794
            if ($this->parent !== null) {
10,738✔
3795
                // Is there already such sheet name?
3796
                if ($this->parent->sheetCodeNameExists($codeName)) {
10,697✔
3797
                    // Use name, but append with lowest possible integer
3798

3799
                    if (StringHelper::countCharacters($codeName) > 29) {
695✔
3800
                        $codeName = StringHelper::substring($codeName, 0, 29);
×
3801
                    }
3802
                    $i = 1;
695✔
3803
                    while ($this->getParentOrThrow()->sheetCodeNameExists($codeName . '_' . $i)) {
695✔
3804
                        ++$i;
283✔
3805
                        if ($i == 10) {
283✔
3806
                            if (StringHelper::countCharacters($codeName) > 28) {
2✔
3807
                                $codeName = StringHelper::substring($codeName, 0, 28);
×
3808
                            }
3809
                        } elseif ($i == 100) {
283✔
3810
                            if (StringHelper::countCharacters($codeName) > 27) {
×
3811
                                $codeName = StringHelper::substring($codeName, 0, 27);
×
3812
                            }
3813
                        }
3814
                    }
3815

3816
                    $codeName .= '_' . $i; // ok, we have a valid name
695✔
3817
                }
3818
            }
3819
        }
3820

3821
        $this->codeName = $codeName;
10,738✔
3822

3823
        return $this;
10,738✔
3824
    }
3825

3826
    /**
3827
     * Return the code name of the sheet.
3828
     */
3829
    public function getCodeName(): ?string
3830
    {
3831
        return $this->codeName;
10,738✔
3832
    }
3833

3834
    /**
3835
     * Sheet has a code name ?
3836
     */
3837
    public function hasCodeName(): bool
3838
    {
3839
        return $this->codeName !== null;
2✔
3840
    }
3841

3842
    public static function nameRequiresQuotes(string $sheetName): bool
3843
    {
3844
        return !Preg::isMatch(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName);
4✔
3845
    }
3846

3847
    public function isRowVisible(int $row): bool
3848
    {
3849
        return !$this->rowDimensionExists($row) || $this->getRowDimension($row)->getVisible();
121✔
3850
    }
3851

3852
    /**
3853
     * Same as Cell->isLocked, but without creating cell if it doesn't exist.
3854
     */
3855
    public function isCellLocked(string $coordinate): bool
3856
    {
3857
        if ($this->getProtection()->getsheet() !== true) {
1✔
3858
            return false;
1✔
3859
        }
3860
        if ($this->cellExists($coordinate)) {
1✔
3861
            return $this->getCell($coordinate)->isLocked();
1✔
3862
        }
3863
        $spreadsheet = $this->parent;
1✔
3864
        $xfIndex = $this->getXfIndex($coordinate);
1✔
3865
        if ($spreadsheet === null || $xfIndex === null) {
1✔
3866
            return true;
1✔
3867
        }
3868

3869
        return $spreadsheet->getCellXfByIndex($xfIndex)->getProtection()->getLocked() !== StyleProtection::PROTECTION_UNPROTECTED;
×
3870
    }
3871

3872
    /**
3873
     * Same as Cell->isHiddenOnFormulaBar, but without creating cell if it doesn't exist.
3874
     */
3875
    public function isCellHiddenOnFormulaBar(string $coordinate): bool
3876
    {
3877
        if ($this->cellExists($coordinate)) {
1✔
3878
            return $this->getCell($coordinate)->isHiddenOnFormulaBar();
1✔
3879
        }
3880

3881
        // cell doesn't exist, therefore isn't a formula,
3882
        // therefore isn't hidden on formula bar.
3883
        return false;
1✔
3884
    }
3885

3886
    private function getXfIndex(string $coordinate): ?int
3887
    {
3888
        [$column, $row] = Coordinate::coordinateFromString($coordinate);
1✔
3889
        $row = (int) $row;
1✔
3890
        $xfIndex = null;
1✔
3891
        if ($this->rowDimensionExists($row)) {
1✔
3892
            $xfIndex = $this->getRowDimension($row)->getXfIndex();
×
3893
        }
3894
        if ($xfIndex === null && $this->ColumnDimensionExists($column)) {
1✔
3895
            $xfIndex = $this->getColumnDimension($column)->getXfIndex();
×
3896
        }
3897

3898
        return $xfIndex;
1✔
3899
    }
3900

3901
    private string $backgroundImage = '';
3902

3903
    private string $backgroundMime = '';
3904

3905
    private string $backgroundExtension = '';
3906

3907
    public function getBackgroundImage(): string
3908
    {
3909
        return $this->backgroundImage;
1,008✔
3910
    }
3911

3912
    public function getBackgroundMime(): string
3913
    {
3914
        return $this->backgroundMime;
400✔
3915
    }
3916

3917
    public function getBackgroundExtension(): string
3918
    {
3919
        return $this->backgroundExtension;
400✔
3920
    }
3921

3922
    /**
3923
     * Set background image.
3924
     * Used on read/write for Xlsx.
3925
     * Used on write for Html.
3926
     *
3927
     * @param string $backgroundImage Image represented as a string, e.g. results of file_get_contents
3928
     */
3929
    public function setBackgroundImage(string $backgroundImage): self
3930
    {
3931
        $imageArray = getimagesizefromstring($backgroundImage) ?: ['mime' => ''];
4✔
3932
        $mime = $imageArray['mime'];
4✔
3933
        if ($mime !== '') {
4✔
3934
            $extension = explode('/', $mime);
3✔
3935
            $extension = $extension[1];
3✔
3936
            $this->backgroundImage = $backgroundImage;
3✔
3937
            $this->backgroundMime = $mime;
3✔
3938
            $this->backgroundExtension = $extension;
3✔
3939
        }
3940

3941
        return $this;
4✔
3942
    }
3943

3944
    /**
3945
     * Copy cells, adjusting relative cell references in formulas.
3946
     * Acts similarly to Excel "fill handle" feature.
3947
     *
3948
     * @param string $fromCell Single source cell, e.g. C3
3949
     * @param string $toCells Single cell or cell range, e.g. C4 or C4:C10
3950
     * @param bool $copyStyle Copy styles as well as values, defaults to true
3951
     */
3952
    public function copyCells(string $fromCell, string $toCells, bool $copyStyle = true): void
3953
    {
3954
        $toArray = Coordinate::extractAllCellReferencesInRange($toCells);
1✔
3955
        $valueString = $this->getCell($fromCell)->getValueString();
1✔
3956
        /** @var mixed[][] */
3957
        $style = $this->getStyle($fromCell)->exportArray();
1✔
3958
        $fromIndexes = Coordinate::indexesFromString($fromCell);
1✔
3959
        $referenceHelper = ReferenceHelper::getInstance();
1✔
3960
        foreach ($toArray as $destination) {
1✔
3961
            if ($destination !== $fromCell) {
1✔
3962
                $toIndexes = Coordinate::indexesFromString($destination);
1✔
3963
                $this->getCell($destination)->setValue($referenceHelper->updateFormulaReferences($valueString, 'A1', $toIndexes[0] - $fromIndexes[0], $toIndexes[1] - $fromIndexes[1]));
1✔
3964
                if ($copyStyle) {
1✔
3965
                    $this->getCell($destination)->getStyle()->applyFromArray($style);
1✔
3966
                }
3967
            }
3968
        }
3969
    }
3970

3971
    public function calculateArrays(bool $preCalculateFormulas = true): void
3972
    {
3973
        if ($preCalculateFormulas && Calculation::getInstance($this->parent)->getInstanceArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY) {
1,170✔
3974
            $keys = $this->cellCollection->getCoordinates();
46✔
3975
            foreach ($keys as $key) {
46✔
3976
                if ($this->getCell($key)->getDataType() === DataType::TYPE_FORMULA) {
46✔
3977
                    if (!Preg::isMatch(self::FUNCTION_LIKE_GROUPBY, $this->getCell($key)->getValueString())) {
46✔
3978
                        $this->getCell($key)->getCalculatedValue();
45✔
3979
                    }
3980
                }
3981
            }
3982
        }
3983
    }
3984

3985
    public function isCellInSpillRange(string $coordinate): bool
3986
    {
3987
        if (Calculation::getInstance($this->parent)->getInstanceArrayReturnType() !== Calculation::RETURN_ARRAY_AS_ARRAY) {
2✔
3988
            return false;
1✔
3989
        }
3990
        $this->calculateArrays();
1✔
3991
        $keys = $this->cellCollection->getCoordinates();
1✔
3992
        foreach ($keys as $key) {
1✔
3993
            $attributes = $this->getCell($key)->getFormulaAttributes();
1✔
3994
            if (isset($attributes['ref'])) {
1✔
3995
                if (Coordinate::coordinateIsInsideRange($attributes['ref'], $coordinate)) {
1✔
3996
                    // false for first cell in range, true otherwise
3997
                    return $coordinate !== $key;
1✔
3998
                }
3999
            }
4000
        }
4001

4002
        return false;
1✔
4003
    }
4004

4005
    /** @param mixed[][] $styleArray */
4006
    public function applyStylesFromArray(string $coordinate, array $styleArray): bool
4007
    {
4008
        $spreadsheet = $this->parent;
2✔
4009
        if ($spreadsheet === null) {
2✔
4010
            return false;
1✔
4011
        }
4012
        $activeSheetIndex = $spreadsheet->getActiveSheetIndex();
1✔
4013
        $originalSelected = $this->selectedCells;
1✔
4014
        $this->getStyle($coordinate)->applyFromArray($styleArray);
1✔
4015
        $this->setSelectedCells($originalSelected);
1✔
4016
        if ($activeSheetIndex >= 0) {
1✔
4017
            $spreadsheet->setActiveSheetIndex($activeSheetIndex);
1✔
4018
        }
4019

4020
        return true;
1✔
4021
    }
4022

4023
    public function copyFormula(string $fromCell, string $toCell): void
4024
    {
4025
        $formula = $this->getCell($fromCell)->getValue();
1✔
4026
        $newFormula = $formula;
1✔
4027
        if (is_string($formula) && $this->getCell($fromCell)->getDataType() === DataType::TYPE_FORMULA) {
1✔
4028
            [$fromColInt, $fromRow] = Coordinate::indexesFromString($fromCell);
1✔
4029
            [$toColInt, $toRow] = Coordinate::indexesFromString($toCell);
1✔
4030
            $helper = ReferenceHelper::getInstance();
1✔
4031
            $newFormula = $helper->updateFormulaReferences(
1✔
4032
                $formula,
1✔
4033
                'A1',
1✔
4034
                $toColInt - $fromColInt,
1✔
4035
                $toRow - $fromRow
1✔
4036
            );
1✔
4037
        }
4038
        $this->setCellValue($toCell, $newFormula);
1✔
4039
    }
4040
}
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