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

PHPOffice / PhpSpreadsheet / 28718216921

04 Jul 2026 08:09PM UTC coverage: 97.168% (+0.001%) from 97.167%
28718216921

Pull #4862

github

web-flow
Merge 5bef2c238 into 0577c7488
Pull Request #4862: WIP Experiment With Column Auto-fit Performance

52 of 57 new or added lines in 1 file covered. (91.23%)

48382 of 49792 relevant lines covered (97.17%)

386.4 hits per line

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

96.68
/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\Font;
33
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
34
use PhpOffice\PhpSpreadsheet\Style\Protection as StyleProtection;
35
use PhpOffice\PhpSpreadsheet\Style\Style;
36

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

218
    private string $paneTopLeftCell = '';
219

220
    private string $activePane = '';
221

222
    private int $xSplit = 0;
223

224
    private int $ySplit = 0;
225

226
    private string $paneState = '';
227

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

435
        return $sheetCodeName;
11,480✔
436
    }
437

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

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

457
        return $sheetTitle;
11,480✔
458
    }
459

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

474
        if ($sorted) {
1,664✔
475
            return $this->cellCollection->getSortedCoordinates();
637✔
476
        }
477

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

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

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

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

510
        return $this->columnDimensions;
1,371✔
511
    }
512

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

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

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

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

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

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

561
        return $chart;
112✔
562
    }
563

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

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

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

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

609
        return $chartNames;
5✔
610
    }
611

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

627
        return false;
1✔
628
    }
629

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

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

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

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

654
        return $this;
25✔
655
    }
656

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

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

671
        return $this;
9✔
672
    }
673

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

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

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

715
        // There is only something to do if there are some auto-size columns
716
        if (!empty($autoSizes)) {
882✔
717
            /** @var float[][][][][][] */
718
            $knownWidths = [];
67✔
719
            $defaultWidth = $this->getDefaultColumnDimension()->getWidth();
67✔
720
            $holdActivePane = $this->activePane;
67✔
721
            // build list of cells references that participate in a merge
722
            $isMergeCell = [];
67✔
723
            foreach ($this->getMergeCells() as $cells) {
67✔
724
                foreach (Coordinate::extractAllCellReferencesInRange($cells) as $cellReference) {
16✔
725
                    $isMergeCell[$cellReference] = true;
16✔
726
                }
727
            }
728

729
            $autoFilterIndentRanges = (new AutoFit($this))->getAutoFilterIndentRanges();
67✔
730

731
            // loop through all cells in the worksheet
732
            foreach ($this->getCoordinates(false) as $coordinate) {
67✔
733
                $cell = $this->getCellOrNull($coordinate);
67✔
734

735
                if ($cell === null || !isset($autoSizes[$this->cellCollection->getCurrentColumn()])) {
67✔
736
                    continue;
51✔
737
                }
738
                //Determine if cell is in merge range
739
                $isMerged = isset($isMergeCell[$this->cellCollection->getCurrentCoordinate()]);
67✔
740

741
                //By default merged cells should be ignored
742
                $isMergedButProceed = false;
67✔
743

744
                //The only exception is if it's a merge range value cell of a 'vertical' range (1 column wide)
745
                if ($isMerged && $cell->isMergeRangeValueCell()) {
67✔
NEW
746
                    $range = (string) $cell->getMergeRange();
×
NEW
747
                    $rangeBoundaries = Coordinate::rangeDimension($range);
×
NEW
748
                    if ($rangeBoundaries[0] === 1) {
×
NEW
749
                        $isMergedButProceed = true;
×
750
                    }
751
                }
752

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

764
                                break;
4✔
765
                            }
766
                        }
767
                    }
768

769
                    $indentAdjustment = $cell->getStyle()->getAlignment()->getIndent();
67✔
770
                    $indentAdjustment += (int) ($cell->getStyle()->getAlignment()->getHorizontal() === Alignment::HORIZONTAL_CENTER);
67✔
771

772
                    // Calculated value
773
                    // To formatted string
774
                    $xfIndex = ($spreadsheet?->getCellXfByIndex($cell->getXfIndex())) ?? new Style();
67✔
775
                    $cellValue = NumberFormat::toFormattedString(
67✔
776
                        $cell->getCalculatedValueString(),
67✔
777
                        ($xfIndex
67✔
778
                            ->getNumberFormat()
67✔
779
                            ->getFormatCode(true) ?? NumberFormat::FORMAT_GENERAL)
67✔
780
                    );
67✔
781

782
                    if ($cellValue !== '') {
67✔
783
                        $curr = $this->cellCollection->getCurrentColumn();
67✔
784
                        $font = $xfIndex->getFont();
67✔
785
                        $fontName = $font->getName();
67✔
786
                        $fontSize = (string) ($font->getSize());
67✔
787
                        $intFilterAdjustment = (int) $filterAdjustment;
67✔
788
                        $rotation = (int) $xfIndex
67✔
789
                            ->getAlignment()->getTextRotation();
67✔
790
                        $width = $knownWidths[$cellValue][$fontName][$fontSize][$indentAdjustment][$intFilterAdjustment][$rotation] ?? null;
67✔
791
                        if ($width === null) {
67✔
792
                            $width = round(
67✔
793
                                Shared\Font::calculateColumnWidth(
67✔
794
                                    $font,
67✔
795
                                    $cellValue,
67✔
796
                                    $rotation,
67✔
797
                                    $defaultFont,
67✔
798
                                    $filterAdjustment,
67✔
799
                                    $indentAdjustment
67✔
800
                                ),
67✔
801
                                3
67✔
802
                            );
67✔
803
                             $knownWidths[$cellValue][$fontName][$fontSize][$indentAdjustment][$intFilterAdjustment][$rotation] = $width;
67✔
804
                        }
805
                        if ($autoSizes[$curr] < $width) {
67✔
806
                            $autoSizes[$curr] = $width;
67✔
807
                        }
808
                    }
809
                }
810
            }
811

812
            // adjust column widths
813
            foreach ($autoSizes as $columnIndex => $width) {
67✔
814
                if ($width == -1) {
67✔
NEW
815
                    $width = $defaultWidth;
×
816
                }
817
                $this->getColumnDimension($columnIndex)
67✔
818
                    ->setWidth($width);
67✔
819
            }
820
            $this->activePane = $holdActivePane;
67✔
821
        }
822
        if ($activeSheet !== null && $activeSheet >= 0) {
882✔
823
            // Okay, I get it now - if $activeSheet is not null,
824
            // then $spreadsheet must also be non-null.
825
            $spreadsheet->setActiveSheetIndex($activeSheet);
882✔
826
        }
827
        $this->setSelectedCells($selectedCells);
882✔
828

829
        return $this;
882✔
830
    }
831

832
    /**
833
     * Get parent or null.
834
     */
835
    public function getParent(): ?Spreadsheet
11,036✔
836
    {
837
        return $this->parent;
11,036✔
838
    }
839

840
    /**
841
     * Get parent, throw exception if null.
842
     */
843
    public function getParentOrThrow(): Spreadsheet
11,108✔
844
    {
845
        if ($this->parent !== null) {
11,108✔
846
            return $this->parent;
11,107✔
847
        }
848

849
        throw new Exception('Sheet does not have a parent.');
1✔
850
    }
851

852
    /**
853
     * Re-bind parent.
854
     *
855
     * @return $this
856
     */
857
    public function rebindParent(Spreadsheet $parent): static
54✔
858
    {
859
        if ($this->parent !== null) {
54✔
860
            $definedNames = $this->parent->getDefinedNames();
4✔
861
            foreach ($definedNames as $definedName) {
4✔
862
                $parent->addDefinedName($definedName);
×
863
            }
864

865
            $this->parent->removeSheetByIndex(
4✔
866
                $this->parent->getIndex($this)
4✔
867
            );
4✔
868
        }
869
        $this->parent = $parent;
54✔
870

871
        return $this;
54✔
872
    }
873

874
    public function setParent(Spreadsheet $parent): self
12✔
875
    {
876
        $this->parent = $parent;
12✔
877

878
        return $this;
12✔
879
    }
880

881
    /**
882
     * Get title.
883
     */
884
    public function getTitle(): string
11,480✔
885
    {
886
        return $this->title;
11,480✔
887
    }
888

889
    /**
890
     * Set title.
891
     *
892
     * @param string $title String containing the dimension of this worksheet
893
     * @param bool $updateFormulaCellReferences Flag indicating whether cell references in formulae should
894
     *            be updated to reflect the new sheet name.
895
     *          This should be left as the default true, unless you are
896
     *          certain that no formula cells on any worksheet contain
897
     *          references to this worksheet
898
     * @param bool $validate False to skip validation of new title. WARNING: This should only be set
899
     *                       at parse time (by Readers), where titles can be assumed to be valid.
900
     *
901
     * @return $this
902
     */
903
    public function setTitle(string $title, bool $updateFormulaCellReferences = true, bool $validate = true): static
11,480✔
904
    {
905
        // Is this a 'rename' or not?
906
        if ($this->getTitle() == $title) {
11,480✔
907
            return $this;
340✔
908
        }
909

910
        // Old title
911
        $oldTitle = $this->getTitle();
11,480✔
912

913
        if ($validate) {
11,480✔
914
            // Syntax check
915
            self::checkSheetTitle($title);
11,480✔
916

917
            if ($this->parent && $this->parent->getIndex($this, true) >= 0) {
11,480✔
918
                // Is there already such sheet name?
919
                if ($this->parent->sheetNameExists($title)) {
851✔
920
                    // Use name, but append with lowest possible integer
921

922
                    if (StringHelper::countCharacters($title) > 29) {
2✔
923
                        $title = StringHelper::substring($title, 0, 29);
×
924
                    }
925
                    $i = 1;
2✔
926
                    while ($this->parent->sheetNameExists($title . ' ' . $i)) {
2✔
927
                        ++$i;
1✔
928
                        if ($i == 10) {
1✔
929
                            if (StringHelper::countCharacters($title) > 28) {
×
930
                                $title = StringHelper::substring($title, 0, 28);
×
931
                            }
932
                        } elseif ($i == 100) {
1✔
933
                            if (StringHelper::countCharacters($title) > 27) {
×
934
                                $title = StringHelper::substring($title, 0, 27);
×
935
                            }
936
                        }
937
                    }
938

939
                    $title .= " $i";
2✔
940
                }
941
            }
942
        }
943

944
        // Set title
945
        $this->title = $title;
11,480✔
946

947
        if ($this->parent && $this->parent->getIndex($this, true) >= 0) {
11,480✔
948
            // New title
949
            $newTitle = $this->getTitle();
1,522✔
950
            $this->parent->getCalculationEngine()
1,522✔
951
                ->renameCalculationCacheForWorksheet($oldTitle, $newTitle);
1,522✔
952
            if ($updateFormulaCellReferences) {
1,522✔
953
                ReferenceHelper::getInstance()->updateNamedFormulae($this->parent, $oldTitle, $newTitle);
851✔
954
            }
955
        }
956

957
        return $this;
11,480✔
958
    }
959

960
    /**
961
     * Get sheet state.
962
     *
963
     * @return string Sheet state (visible, hidden, veryHidden)
964
     */
965
    public function getSheetState(): string
590✔
966
    {
967
        return $this->sheetState;
590✔
968
    }
969

970
    /**
971
     * Set sheet state.
972
     *
973
     * @param string $value Sheet state (visible, hidden, veryHidden)
974
     *
975
     * @return $this
976
     */
977
    public function setSheetState(string $value): static
11,480✔
978
    {
979
        $this->sheetState = $value;
11,480✔
980

981
        return $this;
11,480✔
982
    }
983

984
    /**
985
     * Get page setup.
986
     */
987
    public function getPageSetup(): PageSetup
1,792✔
988
    {
989
        return $this->pageSetup;
1,792✔
990
    }
991

992
    /**
993
     * Set page setup.
994
     *
995
     * @return $this
996
     */
997
    public function setPageSetup(PageSetup $pageSetup): static
1✔
998
    {
999
        $this->pageSetup = $pageSetup;
1✔
1000

1001
        return $this;
1✔
1002
    }
1003

1004
    /**
1005
     * Get page margins.
1006
     */
1007
    public function getPageMargins(): PageMargins
1,775✔
1008
    {
1009
        return $this->pageMargins;
1,775✔
1010
    }
1011

1012
    /**
1013
     * Set page margins.
1014
     *
1015
     * @return $this
1016
     */
1017
    public function setPageMargins(PageMargins $pageMargins): static
1✔
1018
    {
1019
        $this->pageMargins = $pageMargins;
1✔
1020

1021
        return $this;
1✔
1022
    }
1023

1024
    /**
1025
     * Get page header/footer.
1026
     */
1027
    public function getHeaderFooter(): HeaderFooter
654✔
1028
    {
1029
        return $this->headerFooter;
654✔
1030
    }
1031

1032
    /**
1033
     * Set page header/footer.
1034
     *
1035
     * @return $this
1036
     */
1037
    public function setHeaderFooter(HeaderFooter $headerFooter): static
1✔
1038
    {
1039
        $this->headerFooter = $headerFooter;
1✔
1040

1041
        return $this;
1✔
1042
    }
1043

1044
    /**
1045
     * Get sheet view.
1046
     */
1047
    public function getSheetView(): SheetView
686✔
1048
    {
1049
        return $this->sheetView;
686✔
1050
    }
1051

1052
    /**
1053
     * Set sheet view.
1054
     *
1055
     * @return $this
1056
     */
1057
    public function setSheetView(SheetView $sheetView): static
1✔
1058
    {
1059
        $this->sheetView = $sheetView;
1✔
1060

1061
        return $this;
1✔
1062
    }
1063

1064
    /**
1065
     * Get Protection.
1066
     */
1067
    public function getProtection(): Protection
704✔
1068
    {
1069
        return $this->protection;
704✔
1070
    }
1071

1072
    /**
1073
     * Set Protection.
1074
     *
1075
     * @return $this
1076
     */
1077
    public function setProtection(Protection $protection): static
1✔
1078
    {
1079
        $this->protection = $protection;
1✔
1080

1081
        return $this;
1✔
1082
    }
1083

1084
    /**
1085
     * Get highest worksheet column.
1086
     *
1087
     * @param null|int|string $row Return the data highest column for the specified row,
1088
     *                                     or the highest column of any row if no row number is passed
1089
     *
1090
     * @return string Highest column name
1091
     */
1092
    public function getHighestColumn($row = null): string
1,690✔
1093
    {
1094
        if ($row === null) {
1,690✔
1095
            return Coordinate::stringFromColumnIndex($this->cachedHighestColumn);
1,689✔
1096
        }
1097

1098
        return $this->getHighestDataColumn($row);
1✔
1099
    }
1100

1101
    /**
1102
     * Get highest worksheet column that contains data.
1103
     *
1104
     * @param null|int|string $row Return the highest data column for the specified row,
1105
     *                                     or the highest data column of any row if no row number is passed
1106
     *
1107
     * @return string Highest column name that contains data
1108
     */
1109
    public function getHighestDataColumn($row = null): string
924✔
1110
    {
1111
        return $this->cellCollection->getHighestColumn($row);
924✔
1112
    }
1113

1114
    /**
1115
     * Get highest worksheet row.
1116
     *
1117
     * @param null|string $column Return the highest data row for the specified column,
1118
     *                                     or the highest row of any column if no column letter is passed
1119
     *
1120
     * @return int Highest row number
1121
     */
1122
    public function getHighestRow(?string $column = null): int
1,123✔
1123
    {
1124
        if ($column === null) {
1,123✔
1125
            return $this->cachedHighestRow;
1,122✔
1126
        }
1127

1128
        return $this->getHighestDataRow($column);
1✔
1129
    }
1130

1131
    /**
1132
     * Get highest worksheet row that contains data.
1133
     *
1134
     * @param null|string $column Return the highest data row for the specified column,
1135
     *                                     or the highest data row of any column if no column letter is passed
1136
     *
1137
     * @return int Highest row number that contains data
1138
     */
1139
    public function getHighestDataRow(?string $column = null): int
828✔
1140
    {
1141
        return $this->cellCollection->getHighestRow($column);
828✔
1142
    }
1143

1144
    /**
1145
     * Get highest worksheet column and highest row that have cell records.
1146
     *
1147
     * @return array{row: int, column: string} Highest column name and highest row number
1148
     */
1149
    public function getHighestRowAndColumn(): array
1✔
1150
    {
1151
        return $this->cellCollection->getHighestRowAndColumn();
1✔
1152
    }
1153

1154
    /**
1155
     * Set a cell value.
1156
     *
1157
     * @param array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1158
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1159
     * @param mixed $value Value for the cell
1160
     * @param null|IValueBinder $binder Value Binder to override the currently set Value Binder
1161
     *
1162
     * @return $this
1163
     */
1164
    public function setCellValue(CellAddress|string|array $coordinate, mixed $value, ?IValueBinder $binder = null): static
5,120✔
1165
    {
1166
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
5,120✔
1167
        $this->getCell($cellAddress)->setValue($value, $binder);
5,120✔
1168

1169
        return $this;
5,114✔
1170
    }
1171

1172
    /**
1173
     * Set a cell value.
1174
     *
1175
     * @param array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1176
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1177
     * @param mixed $value Value of the cell
1178
     * @param string $dataType Explicit data type, see DataType::TYPE_*
1179
     *        Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this
1180
     *             method, then it is your responsibility as an end-user developer to validate that the value and
1181
     *             the datatype match.
1182
     *       If you do mismatch value and datatpe, then the value you enter may be changed to match the datatype
1183
     *          that you specify.
1184
     *
1185
     * @see DataType
1186
     *
1187
     * @return $this
1188
     */
1189
    public function setCellValueExplicit(CellAddress|string|array $coordinate, mixed $value, string $dataType): static
124✔
1190
    {
1191
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
124✔
1192
        $this->getCell($cellAddress)->setValueExplicit($value, $dataType);
124✔
1193

1194
        return $this;
124✔
1195
    }
1196

1197
    /**
1198
     * Get cell at a specific coordinate.
1199
     *
1200
     * @param array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1201
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1202
     *
1203
     * @return Cell Cell that was found or created
1204
     *              WARNING: Because the cell collection can be cached to reduce memory, it only allows one
1205
     *              "active" cell at a time in memory. If you assign that cell to a variable, then select
1206
     *              another cell using getCell() or any of its variants, the newly selected cell becomes
1207
     *              the "active" cell, and any previous assignment becomes a disconnected reference because
1208
     *              the active cell has changed.
1209
     */
1210
    public function getCell(CellAddress|string|array $coordinate): Cell
10,967✔
1211
    {
1212
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
10,967✔
1213

1214
        // Shortcut for increased performance for the vast majority of simple cases
1215
        if ($this->cellCollection->has($cellAddress)) {
10,967✔
1216
            /** @var Cell $cell */
1217
            $cell = $this->cellCollection->get($cellAddress);
10,938✔
1218

1219
            return $cell;
10,938✔
1220
        }
1221

1222
        /** @var Worksheet $sheet */
1223
        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
10,967✔
1224
        $cell = $sheet->getCellCollection()->get($finalCoordinate);
10,967✔
1225

1226
        return $cell ?? $sheet->createNewCell($finalCoordinate);
10,967✔
1227
    }
1228

1229
    /**
1230
     * Get the correct Worksheet and coordinate from a coordinate that may
1231
     * contains reference to another sheet or a named range.
1232
     *
1233
     * @return array{0: Worksheet, 1: string}
1234
     */
1235
    private function getWorksheetAndCoordinate(string $coordinate): array
10,991✔
1236
    {
1237
        $sheet = null;
10,991✔
1238
        $finalCoordinate = null;
10,991✔
1239

1240
        // Worksheet reference?
1241
        if (str_contains($coordinate, '!')) {
10,991✔
1242
            $worksheetReference = self::extractSheetTitle($coordinate, true, true);
×
1243

1244
            $sheet = $this->getParentOrThrow()->getSheetByName($worksheetReference[0]);
×
1245
            $finalCoordinate = strtoupper($worksheetReference[1]);
×
1246

1247
            if ($sheet === null) {
×
1248
                throw new Exception('Sheet not found for name: ' . $worksheetReference[0]);
×
1249
            }
1250
        } elseif (
1251
            !Preg::isMatch('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $coordinate)
10,991✔
1252
            && Preg::isMatch('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/iu', $coordinate)
10,991✔
1253
        ) {
1254
            // Named range?
1255
            $namedRange = $this->validateNamedRange($coordinate, true);
17✔
1256
            if ($namedRange !== null) {
17✔
1257
                $sheet = $namedRange->getWorksheet();
12✔
1258
                if ($sheet === null) {
12✔
1259
                    throw new Exception('Sheet not found for named range: ' . $namedRange->getName());
×
1260
                }
1261

1262
                $cellCoordinate = ltrim(substr($namedRange->getValue(), (int) strrpos($namedRange->getValue(), '!')), '!');
12✔
1263
                $finalCoordinate = str_replace('$', '', $cellCoordinate);
12✔
1264
            }
1265
        }
1266

1267
        if ($sheet === null || $finalCoordinate === null) {
10,991✔
1268
            $sheet = $this;
10,991✔
1269
            $finalCoordinate = strtoupper($coordinate);
10,991✔
1270
        }
1271

1272
        if (Coordinate::coordinateIsRange($finalCoordinate)) {
10,991✔
1273
            throw new Exception('Cell coordinate string can not be a range of cells.');
2✔
1274
        }
1275
        $finalCoordinate = str_replace('$', '', $finalCoordinate);
10,991✔
1276

1277
        return [$sheet, $finalCoordinate];
10,991✔
1278
    }
1279

1280
    /**
1281
     * Get an existing cell at a specific coordinate, or null.
1282
     *
1283
     * @param string $coordinate Coordinate of the cell, eg: 'A1'
1284
     *
1285
     * @return null|Cell Cell that was found or null
1286
     */
1287
    private function getCellOrNull(string $coordinate): ?Cell
67✔
1288
    {
1289
        // Check cell collection
1290
        if ($this->cellCollection->has($coordinate)) {
67✔
1291
            return $this->cellCollection->get($coordinate);
67✔
1292
        }
1293

1294
        return null;
×
1295
    }
1296

1297
    /**
1298
     * Create a new cell at the specified coordinate.
1299
     *
1300
     * @param string $coordinate Coordinate of the cell
1301
     *
1302
     * @return Cell Cell that was created
1303
     *              WARNING: Because the cell collection can be cached to reduce memory, it only allows one
1304
     *              "active" cell at a time in memory. If you assign that cell to a variable, then select
1305
     *              another cell using getCell() or any of its variants, the newly selected cell becomes
1306
     *              the "active" cell, and any previous assignment becomes a disconnected reference because
1307
     *              the active cell has changed.
1308
     */
1309
    public function createNewCell(string $coordinate): Cell
10,967✔
1310
    {
1311
        [$column, $row, $columnString] = Coordinate::indexesFromString($coordinate);
10,967✔
1312
        $cell = new Cell(null, DataType::TYPE_NULL, $this);
10,967✔
1313
        $this->cellCollection->add($coordinate, $cell);
10,967✔
1314

1315
        // Coordinates
1316
        if ($column > $this->cachedHighestColumn) {
10,967✔
1317
            $this->cachedHighestColumn = $column;
7,538✔
1318
        }
1319
        if ($row > $this->cachedHighestRow) {
10,967✔
1320
            $this->cachedHighestRow = $row;
8,984✔
1321
        }
1322

1323
        // Cell needs appropriate xfIndex from dimensions records
1324
        //    but don't create dimension records if they don't already exist
1325
        $rowDimension = $this->rowDimensions[$row] ?? null;
10,967✔
1326
        $columnDimension = $this->columnDimensions[$columnString] ?? null;
10,967✔
1327

1328
        $xfSet = false;
10,967✔
1329
        if ($rowDimension !== null) {
10,967✔
1330
            $rowXf = (int) $rowDimension->getXfIndex();
422✔
1331
            if ($rowXf > 0) {
422✔
1332
                // then there is a row dimension with explicit style, assign it to the cell
1333
                $cell->setXfIndex($rowXf);
203✔
1334
                $xfSet = true;
203✔
1335
            }
1336
        }
1337
        if (!$xfSet && $columnDimension !== null) {
10,967✔
1338
            $colXf = (int) $columnDimension->getXfIndex();
606✔
1339
            if ($colXf > 0) {
606✔
1340
                // then there is a column dimension, assign it to the cell
1341
                $cell->setXfIndex($colXf);
229✔
1342
            }
1343
        }
1344

1345
        return $cell;
10,967✔
1346
    }
1347

1348
    /**
1349
     * Does the cell at a specific coordinate exist?
1350
     *
1351
     * @param array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1352
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1353
     */
1354
    public function cellExists(CellAddress|string|array $coordinate): bool
10,903✔
1355
    {
1356
        $cellAddress = Validations::validateCellAddress($coordinate);
10,903✔
1357
        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
10,903✔
1358

1359
        return $sheet->getCellCollection()->has($finalCoordinate);
10,903✔
1360
    }
1361

1362
    /**
1363
     * Get row dimension at a specific row.
1364
     *
1365
     * @param int $row Numeric index of the row
1366
     */
1367
    public function getRowDimension(int $row): RowDimension
636✔
1368
    {
1369
        // Get row dimension
1370
        if (!isset($this->rowDimensions[$row])) {
636✔
1371
            $this->rowDimensions[$row] = new RowDimension($row);
636✔
1372

1373
            $this->cachedHighestRow = max($this->cachedHighestRow, $row);
636✔
1374
        }
1375

1376
        return $this->rowDimensions[$row];
636✔
1377
    }
1378

1379
    public function getRowStyle(int $row): ?Style
1✔
1380
    {
1381
        return $this->parent?->getCellXfByIndexOrNull(
1✔
1382
            ($this->rowDimensions[$row] ?? null)?->getXfIndex()
1✔
1383
        );
1✔
1384
    }
1385

1386
    public function rowDimensionExists(int $row): bool
716✔
1387
    {
1388
        return isset($this->rowDimensions[$row]);
716✔
1389
    }
1390

1391
    public function columnDimensionExists(string $column): bool
107✔
1392
    {
1393
        return isset($this->columnDimensions[$column]);
107✔
1394
    }
1395

1396
    /**
1397
     * Get column dimension at a specific column.
1398
     *
1399
     * @param string $column String index of the column eg: 'A'
1400
     */
1401
    public function getColumnDimension(string $column): ColumnDimension
711✔
1402
    {
1403
        // Uppercase coordinate
1404
        $column = strtoupper($column);
711✔
1405

1406
        // Fetch dimensions
1407
        if (!isset($this->columnDimensions[$column])) {
711✔
1408
            $this->columnDimensions[$column] = new ColumnDimension($column);
711✔
1409

1410
            $columnIndex = Coordinate::columnIndexFromString($column);
711✔
1411
            if ($this->cachedHighestColumn < $columnIndex) {
711✔
1412
                $this->cachedHighestColumn = $columnIndex;
491✔
1413
            }
1414
        }
1415

1416
        return $this->columnDimensions[$column];
711✔
1417
    }
1418

1419
    /**
1420
     * Get column dimension at a specific column by using numeric cell coordinates.
1421
     *
1422
     * @param int $columnIndex Numeric column coordinate of the cell
1423
     */
1424
    public function getColumnDimensionByColumn(int $columnIndex): ColumnDimension
136✔
1425
    {
1426
        return $this->getColumnDimension(Coordinate::stringFromColumnIndex($columnIndex));
136✔
1427
    }
1428

1429
    public function getColumnStyle(string $column): ?Style
1✔
1430
    {
1431
        return $this->parent?->getCellXfByIndexOrNull(
1✔
1432
            ($this->columnDimensions[$column] ?? null)?->getXfIndex()
1✔
1433
        );
1✔
1434
    }
1435

1436
    /**
1437
     * Get style for cell.
1438
     *
1439
     * @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
1440
     *              A simple string containing a cell address like 'A1' or a cell range like 'A1:E10'
1441
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1442
     *              or a CellAddress or AddressRange object.
1443
     */
1444
    public function getStyle(AddressRange|CellAddress|int|string|array $cellCoordinate): Style
10,930✔
1445
    {
1446
        if (is_string($cellCoordinate)) {
10,930✔
1447
            $cellCoordinate = Validations::definedNameToCoordinate($cellCoordinate, $this);
10,928✔
1448
        }
1449
        $cellCoordinate = Validations::validateCellOrCellRange($cellCoordinate);
10,930✔
1450
        $cellCoordinate = str_replace('$', '', $cellCoordinate);
10,930✔
1451

1452
        // set this sheet as active
1453
        $this->getParentOrThrow()->setActiveSheetIndex($this->getParentOrThrow()->getIndex($this));
10,930✔
1454

1455
        // set cell coordinate as active
1456
        $this->setSelectedCells($cellCoordinate);
10,930✔
1457

1458
        return $this->getParentOrThrow()->getCellXfSupervisor();
10,930✔
1459
    }
1460

1461
    /**
1462
     * Get table styles set for the for given cell.
1463
     *
1464
     * @param Cell $cell
1465
     *              The Cell for which the tables are retrieved
1466
     *
1467
     * @return Table[]
1468
     */
1469
    public function getTablesWithStylesForCell(Cell $cell): array
7✔
1470
    {
1471
        $retVal = [];
7✔
1472

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

1485
        return $retVal;
7✔
1486
    }
1487

1488
    /**
1489
     * Get tables without styles set for the for given cell.
1490
     *
1491
     * @param Cell $cell
1492
     *              The Cell for which the tables are retrieved
1493
     *
1494
     * @return Table[]
1495
     */
1496
    public function getTablesWithoutStylesForCell(Cell $cell): array
6✔
1497
    {
1498
        $retVal = [];
6✔
1499

1500
        foreach ($this->tableCollection as $table) {
6✔
1501
            $range = $table->getRange();
6✔
1502
            if ($cell->isInRange($range)) {
6✔
1503
                $dxfsTableStyle = $table->getStyle()->getTableDxfsStyle();
6✔
1504
                if ($dxfsTableStyle === null || ($dxfsTableStyle->getHeaderRowStyle() === null && $dxfsTableStyle->getFirstRowStripeStyle() === null && $dxfsTableStyle->getSecondRowStripeStyle() === null)) {
6✔
1505
                    $retVal[] = $table;
2✔
1506
                }
1507
            }
1508
        }
1509

1510
        return $retVal;
6✔
1511
    }
1512

1513
    /**
1514
     * Get conditional styles for a cell.
1515
     *
1516
     * @param string $coordinate eg: 'A1' or 'A1:A3'.
1517
     *          If a single cell is referenced, then the array of conditional styles will be returned if the cell is
1518
     *               included in a conditional style range.
1519
     *          If a range of cells is specified, then the styles will only be returned if the range matches the entire
1520
     *               range of the conditional.
1521
     * @param bool $firstOnly default true, return all matching
1522
     *          conditionals ordered by priority if false, first only if true
1523
     *
1524
     * @return Conditional[]
1525
     */
1526
    public function getConditionalStyles(string $coordinate, bool $firstOnly = true): array
845✔
1527
    {
1528
        $coordinate = strtoupper($coordinate);
845✔
1529
        if (Preg::isMatch('/[: ,]/', $coordinate)) {
845✔
1530
            return $this->conditionalStylesCollection[$coordinate] ?? [];
49✔
1531
        }
1532

1533
        $conditionalStyles = [];
821✔
1534
        foreach ($this->conditionalStylesCollection as $keyStylesOrig => $conditionalRange) {
821✔
1535
            $keyStyles = Coordinate::resolveUnionAndIntersection($keyStylesOrig);
225✔
1536
            $keyParts = explode(',', $keyStyles);
225✔
1537
            foreach ($keyParts as $keyPart) {
225✔
1538
                if ($keyPart === $coordinate) {
225✔
1539
                    if ($firstOnly) {
14✔
1540
                        return $conditionalRange;
14✔
1541
                    }
1542
                    $conditionalStyles[$keyStylesOrig] = $conditionalRange;
×
1543

1544
                    break;
×
1545
                } elseif (str_contains($keyPart, ':')) {
220✔
1546
                    if (Coordinate::coordinateIsInsideRange($keyPart, $coordinate)) {
215✔
1547
                        if ($firstOnly) {
201✔
1548
                            return $conditionalRange;
200✔
1549
                        }
1550
                        $conditionalStyles[$keyStylesOrig] = $conditionalRange;
1✔
1551

1552
                        break;
1✔
1553
                    }
1554
                }
1555
            }
1556
        }
1557
        $outArray = [];
644✔
1558
        foreach ($conditionalStyles as $conditionalArray) {
644✔
1559
            foreach ($conditionalArray as $conditional) {
1✔
1560
                $outArray[] = $conditional;
1✔
1561
            }
1562
        }
1563
        usort($outArray, [self::class, 'comparePriority']);
644✔
1564

1565
        return $outArray;
644✔
1566
    }
1567

1568
    private static function comparePriority(Conditional $condA, Conditional $condB): int
1✔
1569
    {
1570
        $a = $condA->getPriority();
1✔
1571
        $b = $condB->getPriority();
1✔
1572
        if ($a === $b) {
1✔
1573
            return 0;
×
1574
        }
1575
        if ($a === 0) {
1✔
1576
            return 1;
×
1577
        }
1578
        if ($b === 0) {
1✔
1579
            return -1;
×
1580
        }
1581

1582
        return ($a < $b) ? -1 : 1;
1✔
1583
    }
1584

1585
    public function getConditionalRange(string $coordinate): ?string
192✔
1586
    {
1587
        $coordinate = strtoupper($coordinate);
192✔
1588
        $cell = $this->getCell($coordinate);
192✔
1589
        foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
192✔
1590
            $cellBlocks = explode(',', Coordinate::resolveUnionAndIntersection($conditionalRange));
192✔
1591
            foreach ($cellBlocks as $cellBlock) {
192✔
1592
                if ($cell->isInRange($cellBlock)) {
192✔
1593
                    return $conditionalRange;
191✔
1594
                }
1595
            }
1596
        }
1597

1598
        return null;
10✔
1599
    }
1600

1601
    /**
1602
     * Do conditional styles exist for this cell?
1603
     *
1604
     * @param string $coordinate eg: 'A1' or 'A1:A3'.
1605
     *          If a single cell is specified, then this method will return true if that cell is included in a
1606
     *               conditional style range.
1607
     *          If a range of cells is specified, then true will only be returned if the range matches the entire
1608
     *               range of the conditional.
1609
     */
1610
    public function conditionalStylesExists(string $coordinate): bool
22✔
1611
    {
1612
        return !empty($this->getConditionalStyles($coordinate));
22✔
1613
    }
1614

1615
    /**
1616
     * Removes conditional styles for a cell.
1617
     *
1618
     * @param string $coordinate eg: 'A1'
1619
     *
1620
     * @return $this
1621
     */
1622
    public function removeConditionalStyles(string $coordinate): static
60✔
1623
    {
1624
        unset($this->conditionalStylesCollection[strtoupper($coordinate)]);
60✔
1625

1626
        return $this;
60✔
1627
    }
1628

1629
    /**
1630
     * Get collection of conditional styles.
1631
     *
1632
     * @return Conditional[][]
1633
     */
1634
    public function getConditionalStylesCollection(): array
1,473✔
1635
    {
1636
        return $this->conditionalStylesCollection;
1,473✔
1637
    }
1638

1639
    /**
1640
     * Set conditional styles.
1641
     *
1642
     * @param string $coordinate eg: 'A1'
1643
     * @param Conditional[] $styles
1644
     *
1645
     * @return $this
1646
     */
1647
    public function setConditionalStyles(string $coordinate, array $styles): static
348✔
1648
    {
1649
        $this->conditionalStylesCollection[strtoupper($coordinate)] = $styles;
348✔
1650

1651
        return $this;
348✔
1652
    }
1653

1654
    /**
1655
     * Duplicate cell style to a range of cells.
1656
     *
1657
     * Please note that this will overwrite existing cell styles for cells in range!
1658
     *
1659
     * @param Style $style Cell style to duplicate
1660
     * @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
1661
     *
1662
     * @return $this
1663
     */
1664
    public function duplicateStyle(Style $style, string $range): static
2✔
1665
    {
1666
        // Add the style to the workbook if necessary
1667
        $workbook = $this->getParentOrThrow();
2✔
1668
        if ($existingStyle = $workbook->getCellXfByHashCode($style->getHashCode())) {
2✔
1669
            // there is already such cell Xf in our collection
1670
            $xfIndex = $existingStyle->getIndex();
1✔
1671
        } else {
1672
            // we don't have such a cell Xf, need to add
1673
            $workbook->addCellXf($style);
2✔
1674
            $xfIndex = $style->getIndex();
2✔
1675
        }
1676

1677
        // Calculate range outer borders
1678
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
2✔
1679

1680
        // Make sure we can loop upwards on rows and columns
1681
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
2✔
1682
            $tmp = $rangeStart;
×
1683
            $rangeStart = $rangeEnd;
×
1684
            $rangeEnd = $tmp;
×
1685
        }
1686

1687
        // Loop through cells and apply styles
1688
        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
2✔
1689
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
2✔
1690
                $this->getCell(Coordinate::stringFromColumnIndex($col) . $row)->setXfIndex($xfIndex);
2✔
1691
            }
1692
        }
1693

1694
        return $this;
2✔
1695
    }
1696

1697
    /**
1698
     * Duplicate conditional style to a range of cells.
1699
     *
1700
     * Please note that this will overwrite existing cell styles for cells in range!
1701
     *
1702
     * @param Conditional[] $styles Cell style to duplicate
1703
     * @param string $range Range of cells (i.e. "A1:B10"), or just one cell (i.e. "A1")
1704
     *
1705
     * @return $this
1706
     */
1707
    public function duplicateConditionalStyle(array $styles, string $range = ''): static
18✔
1708
    {
1709
        foreach ($styles as $cellStyle) {
18✔
1710
            if (!($cellStyle instanceof Conditional)) {
18✔
1711
                throw new Exception('Style is not a conditional style');
×
1712
            }
1713
        }
1714

1715
        // Calculate range outer borders
1716
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
18✔
1717

1718
        // Make sure we can loop upwards on rows and columns
1719
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
18✔
1720
            $tmp = $rangeStart;
×
1721
            $rangeStart = $rangeEnd;
×
1722
            $rangeEnd = $tmp;
×
1723
        }
1724

1725
        // Loop through cells and apply styles
1726
        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
18✔
1727
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
18✔
1728
                $this->setConditionalStyles(Coordinate::stringFromColumnIndex($col) . $row, $styles);
18✔
1729
            }
1730
        }
1731

1732
        return $this;
18✔
1733
    }
1734

1735
    /**
1736
     * Set break on a cell.
1737
     *
1738
     * @param array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
1739
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
1740
     * @param int $break Break type (type of Worksheet::BREAK_*)
1741
     *
1742
     * @return $this
1743
     */
1744
    public function setBreak(CellAddress|string|array $coordinate, int $break, int $max = -1): static
33✔
1745
    {
1746
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate));
33✔
1747

1748
        if ($break === self::BREAK_NONE) {
33✔
1749
            unset($this->rowBreaks[$cellAddress], $this->columnBreaks[$cellAddress]);
7✔
1750
        } elseif ($break === self::BREAK_ROW) {
33✔
1751
            $this->rowBreaks[$cellAddress] = new PageBreak($break, $cellAddress, $max);
23✔
1752
        } elseif ($break === self::BREAK_COLUMN) {
19✔
1753
            $this->columnBreaks[$cellAddress] = new PageBreak($break, $cellAddress, $max);
19✔
1754
        }
1755

1756
        return $this;
33✔
1757
    }
1758

1759
    /**
1760
     * Get breaks.
1761
     *
1762
     * @return int[]
1763
     */
1764
    public function getBreaks(): array
735✔
1765
    {
1766
        $breaks = [];
735✔
1767
        /** @var callable $compareFunction */
1768
        $compareFunction = [self::class, 'compareRowBreaks'];
735✔
1769
        uksort($this->rowBreaks, $compareFunction);
735✔
1770
        foreach ($this->rowBreaks as $break) {
735✔
1771
            $breaks[$break->getCoordinate()] = self::BREAK_ROW;
10✔
1772
        }
1773
        /** @var callable $compareFunction */
1774
        $compareFunction = [self::class, 'compareColumnBreaks'];
735✔
1775
        uksort($this->columnBreaks, $compareFunction);
735✔
1776
        foreach ($this->columnBreaks as $break) {
735✔
1777
            $breaks[$break->getCoordinate()] = self::BREAK_COLUMN;
8✔
1778
        }
1779

1780
        return $breaks;
735✔
1781
    }
1782

1783
    /**
1784
     * Get row breaks.
1785
     *
1786
     * @return PageBreak[]
1787
     */
1788
    public function getRowBreaks(): array
598✔
1789
    {
1790
        /** @var callable $compareFunction */
1791
        $compareFunction = [self::class, 'compareRowBreaks'];
598✔
1792
        uksort($this->rowBreaks, $compareFunction);
598✔
1793

1794
        return $this->rowBreaks;
598✔
1795
    }
1796

1797
    protected static function compareRowBreaks(string $coordinate1, string $coordinate2): int
9✔
1798
    {
1799
        $row1 = Coordinate::indexesFromString($coordinate1)[1];
9✔
1800
        $row2 = Coordinate::indexesFromString($coordinate2)[1];
9✔
1801

1802
        return $row1 - $row2;
9✔
1803
    }
1804

1805
    protected static function compareColumnBreaks(string $coordinate1, string $coordinate2): int
5✔
1806
    {
1807
        $column1 = Coordinate::indexesFromString($coordinate1)[0];
5✔
1808
        $column2 = Coordinate::indexesFromString($coordinate2)[0];
5✔
1809

1810
        return $column1 - $column2;
5✔
1811
    }
1812

1813
    /**
1814
     * Get column breaks.
1815
     *
1816
     * @return PageBreak[]
1817
     */
1818
    public function getColumnBreaks(): array
597✔
1819
    {
1820
        /** @var callable $compareFunction */
1821
        $compareFunction = [self::class, 'compareColumnBreaks'];
597✔
1822
        uksort($this->columnBreaks, $compareFunction);
597✔
1823

1824
        return $this->columnBreaks;
597✔
1825
    }
1826

1827
    /**
1828
     * Set merge on a cell range.
1829
     *
1830
     * @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'
1831
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1832
     *              or an AddressRange.
1833
     * @param string $behaviour How the merged cells should behave.
1834
     *               Possible values are:
1835
     *                   MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells
1836
     *                   MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells
1837
     *                   MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell
1838
     *
1839
     * @return $this
1840
     */
1841
    public function mergeCells(AddressRange|string|array $range, string $behaviour = self::MERGE_CELL_CONTENT_EMPTY): static
184✔
1842
    {
1843
        $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range));
184✔
1844

1845
        if (!str_contains($range, ':')) {
183✔
1846
            $range .= ":{$range}";
1✔
1847
        }
1848

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

1853
        $this->mergeCells[$range] = $range;
182✔
1854
        $firstRow = (int) $matches[2];
182✔
1855
        $lastRow = (int) $matches[4];
182✔
1856
        $firstColumn = $matches[1];
182✔
1857
        $lastColumn = $matches[3];
182✔
1858
        $firstColumnIndex = Coordinate::columnIndexFromString($firstColumn);
182✔
1859
        $lastColumnIndex = Coordinate::columnIndexFromString($lastColumn);
182✔
1860
        $numberRows = $lastRow - $firstRow;
182✔
1861
        $numberColumns = $lastColumnIndex - $firstColumnIndex;
182✔
1862

1863
        if ($numberRows === 1 && $numberColumns === 1) {
182✔
1864
            return $this;
36✔
1865
        }
1866

1867
        // create upper left cell if it does not already exist
1868
        $upperLeft = "{$firstColumn}{$firstRow}";
175✔
1869
        if (!$this->cellExists($upperLeft)) {
175✔
1870
            $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL);
37✔
1871
        }
1872

1873
        if ($behaviour !== self::MERGE_CELL_CONTENT_HIDE) {
175✔
1874
            // Blank out the rest of the cells in the range (if they exist)
1875
            if ($numberRows > $numberColumns) {
61✔
1876
                $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft, $behaviour);
20✔
1877
            } else {
1878
                $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft, $behaviour);
41✔
1879
            }
1880
        }
1881

1882
        return $this;
175✔
1883
    }
1884

1885
    private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
20✔
1886
    {
1887
        $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
20✔
1888
            ? [$this->getCell($upperLeft)->getFormattedValue()]
1✔
1889
            : [];
19✔
1890

1891
        foreach ($this->getColumnIterator($firstColumn, $lastColumn) as $column) {
20✔
1892
            $iterator = $column->getCellIterator($firstRow);
20✔
1893
            $iterator->setIterateOnlyExistingCells(true);
20✔
1894
            foreach ($iterator as $cell) {
20✔
1895
                $row = $cell->getRow();
20✔
1896
                if ($row > $lastRow) {
20✔
1897
                    break;
8✔
1898
                }
1899
                $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue);
20✔
1900
            }
1901
        }
1902

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

1909
    private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
41✔
1910
    {
1911
        $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
41✔
1912
            ? [$this->getCell($upperLeft)->getFormattedValue()]
4✔
1913
            : [];
37✔
1914

1915
        foreach ($this->getRowIterator($firstRow, $lastRow) as $row) {
41✔
1916
            $iterator = $row->getCellIterator($firstColumn);
41✔
1917
            $iterator->setIterateOnlyExistingCells(true);
41✔
1918
            foreach ($iterator as $cell) {
41✔
1919
                $column = $cell->getColumn();
41✔
1920
                $columnIndex = Coordinate::columnIndexFromString($column);
41✔
1921
                if ($columnIndex > $lastColumnIndex) {
41✔
1922
                    break;
9✔
1923
                }
1924
                $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue);
41✔
1925
            }
1926
        }
1927

1928
        if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
41✔
1929
            /** @var string[] $leftCellValue */
1930
            $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
4✔
1931
        }
1932
    }
1933

1934
    /**
1935
     * @param mixed[] $leftCellValue
1936
     *
1937
     * @return mixed[]
1938
     */
1939
    public function mergeCellBehaviour(Cell $cell, string $upperLeft, string $behaviour, array $leftCellValue): array
61✔
1940
    {
1941
        if ($cell->getCoordinate() !== $upperLeft) {
61✔
1942
            Calculation::getInstance($cell->getWorksheet()->getParentOrThrow())->flushInstance();
25✔
1943
            if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
25✔
1944
                $cellValue = $cell->getFormattedValue();
5✔
1945
                if ($cellValue !== '') {
5✔
1946
                    $leftCellValue[] = $cellValue;
5✔
1947
                }
1948
            }
1949
            $cell->setValueExplicit(null, DataType::TYPE_NULL);
25✔
1950
        }
1951

1952
        return $leftCellValue;
61✔
1953
    }
1954

1955
    /**
1956
     * Remove merge on a cell range.
1957
     *
1958
     * @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'
1959
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
1960
     *              or an AddressRange.
1961
     *
1962
     * @return $this
1963
     */
1964
    public function unmergeCells(AddressRange|string|array $range): static
23✔
1965
    {
1966
        $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range));
23✔
1967

1968
        if (str_contains($range, ':')) {
23✔
1969
            if (isset($this->mergeCells[$range])) {
22✔
1970
                unset($this->mergeCells[$range]);
22✔
1971
            } else {
1972
                throw new Exception('Cell range ' . $range . ' not known as merged.');
×
1973
            }
1974
        } else {
1975
            throw new Exception('Merge can only be removed from a range of cells.');
1✔
1976
        }
1977

1978
        return $this;
22✔
1979
    }
1980

1981
    /**
1982
     * Get merge cells array.
1983
     *
1984
     * @return string[]
1985
     */
1986
    public function getMergeCells(): array
1,375✔
1987
    {
1988
        return $this->mergeCells;
1,375✔
1989
    }
1990

1991
    /**
1992
     * Set merge cells array for the entire sheet. Use instead mergeCells() to merge
1993
     * a single cell range.
1994
     *
1995
     * @param string[] $mergeCells
1996
     *
1997
     * @return $this
1998
     */
1999
    public function setMergeCells(array $mergeCells): static
133✔
2000
    {
2001
        $this->mergeCells = $mergeCells;
133✔
2002

2003
        return $this;
133✔
2004
    }
2005

2006
    /**
2007
     * Set protection on a cell or cell range.
2008
     *
2009
     * @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'
2010
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2011
     *              or a CellAddress or AddressRange object.
2012
     * @param string $password Password to unlock the protection
2013
     * @param bool $alreadyHashed If the password has already been hashed, set this to true
2014
     *
2015
     * @return $this
2016
     */
2017
    public function protectCells(AddressRange|CellAddress|int|string|array $range, string $password = '', bool $alreadyHashed = false, string $name = '', string $securityDescriptor = ''): static
28✔
2018
    {
2019
        $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
28✔
2020

2021
        if (!$alreadyHashed && $password !== '') {
28✔
2022
            $password = Shared\PasswordHasher::hashPassword($password);
24✔
2023
        }
2024
        $this->protectedCells[$range] = new ProtectedRange($range, $password, $name, $securityDescriptor);
28✔
2025

2026
        return $this;
28✔
2027
    }
2028

2029
    /**
2030
     * Remove protection on a cell or cell range.
2031
     *
2032
     * @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'
2033
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2034
     *              or a CellAddress or AddressRange object.
2035
     *
2036
     * @return $this
2037
     */
2038
    public function unprotectCells(AddressRange|CellAddress|int|string|array $range): static
22✔
2039
    {
2040
        $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range));
22✔
2041

2042
        if (isset($this->protectedCells[$range])) {
22✔
2043
            unset($this->protectedCells[$range]);
21✔
2044
        } else {
2045
            throw new Exception('Cell range ' . $range . ' not known as protected.');
1✔
2046
        }
2047

2048
        return $this;
21✔
2049
    }
2050

2051
    /**
2052
     * Get protected cells.
2053
     *
2054
     * @return ProtectedRange[]
2055
     */
2056
    public function getProtectedCellRanges(): array
714✔
2057
    {
2058
        return $this->protectedCells;
714✔
2059
    }
2060

2061
    /**
2062
     * Get Autofilter.
2063
     */
2064
    public function getAutoFilter(): AutoFilter
939✔
2065
    {
2066
        return $this->autoFilter;
939✔
2067
    }
2068

2069
    /**
2070
     * Set AutoFilter.
2071
     *
2072
     * @param AddressRange<CellAddress>|AddressRange<int>|AddressRange<string>|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|AutoFilter|string $autoFilterOrRange
2073
     *            A simple string containing a Cell range like 'A1:E10' is permitted for backward compatibility
2074
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2075
     *              or an AddressRange.
2076
     *
2077
     * @return $this
2078
     */
2079
    public function setAutoFilter(AddressRange|string|array|AutoFilter $autoFilterOrRange): static
21✔
2080
    {
2081
        if (is_object($autoFilterOrRange) && ($autoFilterOrRange instanceof AutoFilter)) {
21✔
2082
            $this->autoFilter = $autoFilterOrRange;
×
2083
        } else {
2084
            $cellRange = Functions::trimSheetFromCellReference(Validations::validateCellRange($autoFilterOrRange));
21✔
2085

2086
            $this->autoFilter->setRange($cellRange);
21✔
2087
        }
2088

2089
        return $this;
21✔
2090
    }
2091

2092
    /**
2093
     * Remove autofilter.
2094
     */
2095
    public function removeAutoFilter(): self
1✔
2096
    {
2097
        $this->autoFilter->setRange('');
1✔
2098

2099
        return $this;
1✔
2100
    }
2101

2102
    /**
2103
     * Get collection of Tables.
2104
     *
2105
     * @return ArrayObject<int, Table>
2106
     */
2107
    public function getTableCollection(): ArrayObject
10,960✔
2108
    {
2109
        return $this->tableCollection;
10,960✔
2110
    }
2111

2112
    /**
2113
     * Add Table.
2114
     *
2115
     * @return $this
2116
     */
2117
    public function addTable(Table $table): self
105✔
2118
    {
2119
        $table->setWorksheet($this);
105✔
2120
        $this->tableCollection[] = $table;
105✔
2121

2122
        return $this;
105✔
2123
    }
2124

2125
    /**
2126
     * @return string[] array of Table names
2127
     */
2128
    public function getTableNames(): array
1✔
2129
    {
2130
        $tableNames = [];
1✔
2131

2132
        foreach ($this->tableCollection as $table) {
1✔
2133
            /** @var Table $table */
2134
            $tableNames[] = $table->getName();
1✔
2135
        }
2136

2137
        return $tableNames;
1✔
2138
    }
2139

2140
    /**
2141
     * @param string $name the table name to search
2142
     *
2143
     * @return null|Table The table from the tables collection, or null if not found
2144
     */
2145
    public function getTableByName(string $name): ?Table
97✔
2146
    {
2147
        $tableIndex = $this->getTableIndexByName($name);
97✔
2148

2149
        return ($tableIndex === null) ? null : $this->tableCollection[$tableIndex];
97✔
2150
    }
2151

2152
    /**
2153
     * @param string $name the table name to search
2154
     *
2155
     * @return null|int The index of the located table in the tables collection, or null if not found
2156
     */
2157
    protected function getTableIndexByName(string $name): ?int
98✔
2158
    {
2159
        $name = StringHelper::strToUpper($name);
98✔
2160
        foreach ($this->tableCollection as $index => $table) {
98✔
2161
            /** @var Table $table */
2162
            if (StringHelper::strToUpper($table->getName()) === $name) {
63✔
2163
                return $index;
62✔
2164
            }
2165
        }
2166

2167
        return null;
41✔
2168
    }
2169

2170
    /**
2171
     * Remove Table by name.
2172
     *
2173
     * @param string $name Table name
2174
     *
2175
     * @return $this
2176
     */
2177
    public function removeTableByName(string $name): self
1✔
2178
    {
2179
        $tableIndex = $this->getTableIndexByName($name);
1✔
2180

2181
        if ($tableIndex !== null) {
1✔
2182
            unset($this->tableCollection[$tableIndex]);
1✔
2183
        }
2184

2185
        return $this;
1✔
2186
    }
2187

2188
    /**
2189
     * Remove collection of Tables.
2190
     */
2191
    public function removeTableCollection(): self
1✔
2192
    {
2193
        $this->tableCollection = new ArrayObject();
1✔
2194

2195
        return $this;
1✔
2196
    }
2197

2198
    /**
2199
     * Get Freeze Pane.
2200
     */
2201
    public function getFreezePane(): ?string
323✔
2202
    {
2203
        return $this->freezePane;
323✔
2204
    }
2205

2206
    /**
2207
     * Freeze Pane.
2208
     *
2209
     * Examples:
2210
     *
2211
     *     - A2 will freeze the rows above cell A2 (i.e row 1)
2212
     *     - B1 will freeze the columns to the left of cell B1 (i.e column A)
2213
     *     - B2 will freeze the rows above and to the left of cell B2 (i.e row 1 and column A)
2214
     *
2215
     * @param null|array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
2216
     *            or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2217
     *        Passing a null value for this argument will clear any existing freeze pane for this worksheet.
2218
     * @param null|array{0: int, 1: int}|CellAddress|string $topLeftCell default position of the right bottom pane
2219
     *            Coordinate of the cell as a string, eg: 'C5'; or as an array of [$columnIndex, $row] (e.g. [3, 5]),
2220
     *            or a CellAddress object.
2221
     *
2222
     * @return $this
2223
     */
2224
    public function freezePane(null|CellAddress|string|array $coordinate, null|CellAddress|string|array $topLeftCell = null, bool $frozenSplit = false): static
50✔
2225
    {
2226
        $this->panes = [
50✔
2227
            'bottomRight' => null,
50✔
2228
            'bottomLeft' => null,
50✔
2229
            'topRight' => null,
50✔
2230
            'topLeft' => null,
50✔
2231
        ];
50✔
2232
        $cellAddress = ($coordinate !== null)
50✔
2233
            ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate))
50✔
2234
            : null;
1✔
2235
        if ($cellAddress !== null && Coordinate::coordinateIsRange($cellAddress)) {
50✔
2236
            throw new Exception('Freeze pane can not be set on a range of cells.');
1✔
2237
        }
2238
        $topLeftCell = ($topLeftCell !== null)
49✔
2239
            ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($topLeftCell))
37✔
2240
            : null;
37✔
2241

2242
        if ($cellAddress !== null && $topLeftCell === null) {
49✔
2243
            $coordinate = Coordinate::coordinateFromString($cellAddress);
37✔
2244
            $topLeftCell = $coordinate[0] . $coordinate[1];
37✔
2245
        }
2246

2247
        $topLeftCell = "$topLeftCell";
49✔
2248
        $this->paneTopLeftCell = $topLeftCell;
49✔
2249

2250
        $this->freezePane = $cellAddress;
49✔
2251
        $this->topLeftCell = $topLeftCell;
49✔
2252
        if ($cellAddress === null) {
49✔
2253
            $this->paneState = '';
1✔
2254
            $this->xSplit = $this->ySplit = 0;
1✔
2255
            $this->activePane = '';
1✔
2256
        } else {
2257
            $coordinates = Coordinate::indexesFromString($cellAddress);
49✔
2258
            $this->xSplit = $coordinates[0] - 1;
49✔
2259
            $this->ySplit = $coordinates[1] - 1;
49✔
2260
            if ($this->xSplit > 0 || $this->ySplit > 0) {
49✔
2261
                $this->paneState = $frozenSplit ? self::PANE_FROZENSPLIT : self::PANE_FROZEN;
48✔
2262
                $this->setSelectedCellsActivePane();
48✔
2263
            } else {
2264
                $this->paneState = '';
1✔
2265
                $this->freezePane = null;
1✔
2266
                $this->activePane = '';
1✔
2267
            }
2268
        }
2269

2270
        return $this;
49✔
2271
    }
2272

2273
    public function setTopLeftCell(string $topLeftCell): self
61✔
2274
    {
2275
        $this->topLeftCell = $topLeftCell;
61✔
2276

2277
        return $this;
61✔
2278
    }
2279

2280
    /**
2281
     * Unfreeze Pane.
2282
     *
2283
     * @return $this
2284
     */
2285
    public function unfreezePane(): static
1✔
2286
    {
2287
        return $this->freezePane(null);
1✔
2288
    }
2289

2290
    /**
2291
     * Get the default position of the right bottom pane.
2292
     */
2293
    public function getTopLeftCell(): ?string
530✔
2294
    {
2295
        return $this->topLeftCell;
530✔
2296
    }
2297

2298
    public function getPaneTopLeftCell(): string
11✔
2299
    {
2300
        return $this->paneTopLeftCell;
11✔
2301
    }
2302

2303
    public function setPaneTopLeftCell(string $paneTopLeftCell): self
26✔
2304
    {
2305
        $this->paneTopLeftCell = $paneTopLeftCell;
26✔
2306

2307
        return $this;
26✔
2308
    }
2309

2310
    public function usesPanes(): bool
518✔
2311
    {
2312
        return $this->xSplit > 0 || $this->ySplit > 0;
518✔
2313
    }
2314

2315
    public function getPane(string $position): ?Pane
2✔
2316
    {
2317
        return $this->panes[$position] ?? null;
2✔
2318
    }
2319

2320
    public function setPane(string $position, ?Pane $pane): self
47✔
2321
    {
2322
        if (array_key_exists($position, $this->panes)) {
47✔
2323
            $this->panes[$position] = $pane;
47✔
2324
        }
2325

2326
        return $this;
47✔
2327
    }
2328

2329
    /** @return (null|Pane)[] */
2330
    public function getPanes(): array
3✔
2331
    {
2332
        return $this->panes;
3✔
2333
    }
2334

2335
    public function getActivePane(): string
14✔
2336
    {
2337
        return $this->activePane;
14✔
2338
    }
2339

2340
    public function setActivePane(string $activePane): self
49✔
2341
    {
2342
        $this->activePane = array_key_exists($activePane, $this->panes) ? $activePane : '';
49✔
2343

2344
        return $this;
49✔
2345
    }
2346

2347
    public function getXSplit(): int
11✔
2348
    {
2349
        return $this->xSplit;
11✔
2350
    }
2351

2352
    public function setXSplit(int $xSplit): self
11✔
2353
    {
2354
        $this->xSplit = $xSplit;
11✔
2355
        if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
11✔
2356
            $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
1✔
2357
        }
2358

2359
        return $this;
11✔
2360
    }
2361

2362
    public function getYSplit(): int
11✔
2363
    {
2364
        return $this->ySplit;
11✔
2365
    }
2366

2367
    public function setYSplit(int $ySplit): self
26✔
2368
    {
2369
        $this->ySplit = $ySplit;
26✔
2370
        if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
26✔
2371
            $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
1✔
2372
        }
2373

2374
        return $this;
26✔
2375
    }
2376

2377
    public function getPaneState(): string
30✔
2378
    {
2379
        return $this->paneState;
30✔
2380
    }
2381

2382
    public const PANE_FROZEN = 'frozen';
2383
    public const PANE_FROZENSPLIT = 'frozenSplit';
2384
    public const PANE_SPLIT = 'split';
2385
    private const VALIDPANESTATE = [self::PANE_FROZEN, self::PANE_SPLIT, self::PANE_FROZENSPLIT];
2386
    private const VALIDFROZENSTATE = [self::PANE_FROZEN, self::PANE_FROZENSPLIT];
2387

2388
    public function setPaneState(string $paneState): self
26✔
2389
    {
2390
        $this->paneState = in_array($paneState, self::VALIDPANESTATE, true) ? $paneState : '';
26✔
2391
        if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
26✔
2392
            $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
25✔
2393
        } else {
2394
            $this->freezePane = null;
3✔
2395
        }
2396

2397
        return $this;
26✔
2398
    }
2399

2400
    /**
2401
     * Insert a new row, updating all possible related data.
2402
     *
2403
     * @param int $before Insert before this row number
2404
     * @param int $numberOfRows Number of new rows to insert
2405
     *
2406
     * @return $this
2407
     */
2408
    public function insertNewRowBefore(int $before, int $numberOfRows = 1): static
43✔
2409
    {
2410
        if ($before >= 1) {
43✔
2411
            $objReferenceHelper = ReferenceHelper::getInstance();
42✔
2412
            $objReferenceHelper->insertNewBefore('A' . $before, 0, $numberOfRows, $this);
42✔
2413
        } else {
2414
            throw new Exception('Rows can only be inserted before at least row 1.');
1✔
2415
        }
2416

2417
        return $this;
42✔
2418
    }
2419

2420
    /**
2421
     * Insert a new column, updating all possible related data.
2422
     *
2423
     * @param string $before Insert before this column Name, eg: 'A'
2424
     * @param int $numberOfColumns Number of new columns to insert
2425
     *
2426
     * @return $this
2427
     */
2428
    public function insertNewColumnBefore(string $before, int $numberOfColumns = 1): static
51✔
2429
    {
2430
        if (!is_numeric($before)) {
51✔
2431
            $objReferenceHelper = ReferenceHelper::getInstance();
50✔
2432
            $objReferenceHelper->insertNewBefore($before . '1', $numberOfColumns, 0, $this);
50✔
2433
        } else {
2434
            throw new Exception('Column references should not be numeric.');
1✔
2435
        }
2436

2437
        return $this;
50✔
2438
    }
2439

2440
    /**
2441
     * Insert a new column, updating all possible related data.
2442
     *
2443
     * @param int $beforeColumnIndex Insert before this column ID (numeric column coordinate of the cell)
2444
     * @param int $numberOfColumns Number of new columns to insert
2445
     *
2446
     * @return $this
2447
     */
2448
    public function insertNewColumnBeforeByIndex(int $beforeColumnIndex, int $numberOfColumns = 1): static
2✔
2449
    {
2450
        if ($beforeColumnIndex >= 1) {
2✔
2451
            return $this->insertNewColumnBefore(Coordinate::stringFromColumnIndex($beforeColumnIndex), $numberOfColumns);
1✔
2452
        }
2453

2454
        throw new Exception('Columns can only be inserted before at least column A (1).');
1✔
2455
    }
2456

2457
    /**
2458
     * Delete a row, updating all possible related data.
2459
     *
2460
     * @param int $row Remove rows, starting with this row number
2461
     * @param int $numberOfRows Number of rows to remove
2462
     *
2463
     * @return $this
2464
     */
2465
    public function removeRow(int $row, int $numberOfRows = 1): static
58✔
2466
    {
2467
        if ($row < 1) {
58✔
2468
            throw new Exception('Rows to be deleted should at least start from row 1.');
1✔
2469
        }
2470
        if ($numberOfRows === 0) {
57✔
2471
            return $this;
1✔
2472
        }
2473
        if ($numberOfRows < 0) {
56✔
2474
            $newRow = max(1, $row + $numberOfRows + 1);
1✔
2475
            $numberOfRows = $row - $newRow + 1;
1✔
2476
            $row = $newRow;
1✔
2477
        }
2478
        $newHighestRow = $this->cachedHighestRow;
56✔
2479
        if ($newHighestRow >= $row) {
56✔
2480
            $newHighestRow = max($row - 1, $this->cachedHighestRow - $numberOfRows);
43✔
2481
        }
2482
        $startRow = $row;
56✔
2483
        $endRow = $startRow + $numberOfRows - 1;
56✔
2484
        $removeKeys = [];
56✔
2485
        $addKeys = [];
56✔
2486
        foreach ($this->mergeCells as $key => $value) {
56✔
2487
            if (
2488
                Preg::isMatch(
21✔
2489
                    '/^([a-z]{1,3})(\d+):([a-z]{1,3})(\d+)/i',
21✔
2490
                    $key,
21✔
2491
                    $matches
21✔
2492
                )
21✔
2493
            ) {
2494
                $startMergeInt = (int) $matches[2];
21✔
2495
                $endMergeInt = (int) $matches[4];
21✔
2496
                if ($startMergeInt >= $startRow) {
21✔
2497
                    if ($startMergeInt <= $endRow) {
21✔
2498
                        $removeKeys[] = $key;
3✔
2499
                    }
2500
                } elseif ($endMergeInt >= $startRow) {
1✔
2501
                    if ($endMergeInt <= $endRow) {
1✔
2502
                        $temp = $endMergeInt - 1;
1✔
2503
                        $removeKeys[] = $key;
1✔
2504
                        if ($temp !== $startMergeInt) {
1✔
2505
                            $temp3 = $matches[1] . $matches[2] . ':' . $matches[3] . $temp;
1✔
2506
                            $addKeys[] = $temp3;
1✔
2507
                        }
2508
                    }
2509
                }
2510
            }
2511
        }
2512
        foreach ($removeKeys as $key) {
56✔
2513
            unset($this->mergeCells[$key]);
3✔
2514
        }
2515
        foreach ($addKeys as $key) {
56✔
2516
            $this->mergeCells[$key] = $key;
1✔
2517
        }
2518

2519
        $holdRowDimensions = $this->removeRowDimensions($row, $numberOfRows);
56✔
2520
        $highestRow = $this->getHighestDataRow();
56✔
2521
        $removedRowsCounter = 0;
56✔
2522

2523
        for ($r = 0; $r < $numberOfRows; ++$r) {
56✔
2524
            if ($row + $r <= $highestRow) {
56✔
2525
                $this->cellCollection->removeRow($row + $r);
42✔
2526
                ++$removedRowsCounter;
42✔
2527
            }
2528
        }
2529

2530
        $objReferenceHelper = ReferenceHelper::getInstance();
56✔
2531
        $objReferenceHelper->insertNewBefore('A' . ($row + $numberOfRows), 0, -$numberOfRows, $this);
56✔
2532
        for ($r = 0; $r < $removedRowsCounter; ++$r) {
56✔
2533
            $this->cellCollection->removeRow($highestRow);
42✔
2534
            --$highestRow;
42✔
2535
        }
2536

2537
        $this->rowDimensions = $holdRowDimensions;
56✔
2538
        $this->cachedHighestRow = $newHighestRow;
56✔
2539

2540
        return $this;
56✔
2541
    }
2542

2543
    /** @return RowDimension[] */
2544
    private function removeRowDimensions(int $row, int $numberOfRows): array
56✔
2545
    {
2546
        $highRow = $row + $numberOfRows - 1;
56✔
2547
        $holdRowDimensions = [];
56✔
2548
        foreach ($this->rowDimensions as $rowDimension) {
56✔
2549
            $num = $rowDimension->getRowIndex();
6✔
2550
            if ($num < $row) {
6✔
2551
                $holdRowDimensions[$num] = $rowDimension;
4✔
2552
            } elseif ($num > $highRow) {
6✔
2553
                $num -= $numberOfRows;
5✔
2554
                $cloneDimension = clone $rowDimension;
5✔
2555
                $cloneDimension->setRowIndex($num);
5✔
2556
                $holdRowDimensions[$num] = $cloneDimension;
5✔
2557
            }
2558
        }
2559

2560
        return $holdRowDimensions;
56✔
2561
    }
2562

2563
    /**
2564
     * Remove a column, updating all possible related data.
2565
     *
2566
     * @param string $column Remove columns starting with this column name, eg: 'A'
2567
     * @param int $numberOfColumns Number of columns to remove
2568
     *
2569
     * @return $this
2570
     */
2571
    public function removeColumn(string $column, int $numberOfColumns = 1): static
48✔
2572
    {
2573
        if (is_numeric($column)) {
48✔
2574
            throw new Exception('Column references should not be numeric.');
1✔
2575
        }
2576
        $startColumnInt = Coordinate::columnIndexFromString($column);
47✔
2577
        if ($numberOfColumns === 0) {
47✔
2578
            return $this;
1✔
2579
        }
2580
        if ($numberOfColumns < 0) {
46✔
2581
            $newStartColumnInt = max(1, $startColumnInt + $numberOfColumns + 1);
1✔
2582
            $numberOfColumns = $startColumnInt - $newStartColumnInt + 1;
1✔
2583
            $startColumnInt = $newStartColumnInt;
1✔
2584
            $column = Coordinate::stringFromColumnIndex($startColumnInt);
1✔
2585
        }
2586
        $newHighestColumn = $this->cachedHighestColumn;
46✔
2587
        if ($newHighestColumn >= $startColumnInt) {
46✔
2588
            $newHighestColumn = max($startColumnInt - 1, $this->cachedHighestColumn - $numberOfColumns);
36✔
2589
        }
2590
        $endColumnInt = $startColumnInt + $numberOfColumns - 1;
46✔
2591
        $removeKeys = [];
46✔
2592
        $addKeys = [];
46✔
2593
        foreach ($this->mergeCells as $key => $value) {
46✔
2594
            if (
2595
                Preg::isMatch(
19✔
2596
                    '/^([a-z]{1,3})(\d+):([a-z]{1,3})(\d+)/i',
19✔
2597
                    $key,
19✔
2598
                    $matches
19✔
2599
                )
19✔
2600
            ) {
2601
                $startMergeInt = Coordinate::columnIndexFromString($matches[1]);
19✔
2602
                $endMergeInt = Coordinate::columnIndexFromString($matches[3]);
19✔
2603
                if ($startMergeInt >= $startColumnInt) {
19✔
2604
                    if ($startMergeInt <= $endColumnInt) {
2✔
2605
                        $removeKeys[] = $key;
2✔
2606
                    }
2607
                } elseif ($endMergeInt >= $startColumnInt) {
18✔
2608
                    if ($endMergeInt <= $endColumnInt) {
18✔
2609
                        $temp = Coordinate::columnIndexFromString($matches[3]) - 1;
1✔
2610
                        $temp2 = Coordinate::stringFromColumnIndex($temp);
1✔
2611
                        $removeKeys[] = $key;
1✔
2612
                        if ($temp2 !== $matches[1]) {
1✔
2613
                            $temp3 = $matches[1] . $matches[2] . ':' . $temp2 . $matches[4];
1✔
2614
                            $addKeys[] = $temp3;
1✔
2615
                        }
2616
                    }
2617
                }
2618
            }
2619
        }
2620
        foreach ($removeKeys as $key) {
46✔
2621
            unset($this->mergeCells[$key]);
2✔
2622
        }
2623
        foreach ($addKeys as $key) {
46✔
2624
            $this->mergeCells[$key] = $key;
1✔
2625
        }
2626

2627
        $highestColumn = $this->getHighestDataColumn();
46✔
2628
        $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
46✔
2629
        $pColumnIndex = Coordinate::columnIndexFromString($column);
46✔
2630

2631
        $holdColumnDimensions = $this->removeColumnDimensions($pColumnIndex, $numberOfColumns);
46✔
2632

2633
        $column = Coordinate::stringFromColumnIndex($pColumnIndex + $numberOfColumns);
46✔
2634
        $objReferenceHelper = ReferenceHelper::getInstance();
46✔
2635
        $objReferenceHelper->insertNewBefore($column . '1', -$numberOfColumns, 0, $this);
46✔
2636

2637
        $this->columnDimensions = $holdColumnDimensions;
46✔
2638

2639
        if ($pColumnIndex > $highestColumnIndex) {
46✔
2640
            $this->cachedHighestColumn = $newHighestColumn;
11✔
2641

2642
            return $this;
11✔
2643
        }
2644

2645
        $maxPossibleColumnsToBeRemoved = $highestColumnIndex - $pColumnIndex + 1;
35✔
2646

2647
        for ($c = 0, $n = min($maxPossibleColumnsToBeRemoved, $numberOfColumns); $c < $n; ++$c) {
35✔
2648
            $this->cellCollection->removeColumn($highestColumn);
35✔
2649
            $highestColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($highestColumn) - 1);
35✔
2650
        }
2651
        $this->cachedHighestColumn = $newHighestColumn;
35✔
2652

2653
        $this->garbageCollect();
35✔
2654

2655
        return $this;
35✔
2656
    }
2657

2658
    /** @return ColumnDimension[] */
2659
    private function removeColumnDimensions(int $pColumnIndex, int $numberOfColumns): array
46✔
2660
    {
2661
        $highCol = $pColumnIndex + $numberOfColumns - 1;
46✔
2662
        $holdColumnDimensions = [];
46✔
2663
        foreach ($this->columnDimensions as $columnDimension) {
46✔
2664
            $num = $columnDimension->getColumnNumeric();
19✔
2665
            if ($num < $pColumnIndex) {
19✔
2666
                $str = $columnDimension->getColumnIndex();
18✔
2667
                $holdColumnDimensions[$str] = $columnDimension;
18✔
2668
            } elseif ($num > $highCol) {
19✔
2669
                $cloneDimension = clone $columnDimension;
19✔
2670
                $cloneDimension->setColumnNumeric($num - $numberOfColumns);
19✔
2671
                $str = $cloneDimension->getColumnIndex();
19✔
2672
                $holdColumnDimensions[$str] = $cloneDimension;
19✔
2673
            }
2674
        }
2675

2676
        return $holdColumnDimensions;
46✔
2677
    }
2678

2679
    /**
2680
     * Remove a column, updating all possible related data.
2681
     *
2682
     * @param int $columnIndex Remove starting with this column Index (numeric column coordinate)
2683
     * @param int $numColumns Number of columns to remove
2684
     *
2685
     * @return $this
2686
     */
2687
    public function removeColumnByIndex(int $columnIndex, int $numColumns = 1): static
3✔
2688
    {
2689
        if ($columnIndex >= 1) {
3✔
2690
            return $this->removeColumn(Coordinate::stringFromColumnIndex($columnIndex), $numColumns);
2✔
2691
        }
2692

2693
        throw new Exception('Columns to be deleted should at least start from column A (1)');
1✔
2694
    }
2695

2696
    /**
2697
     * Show gridlines?
2698
     */
2699
    public function getShowGridlines(): bool
1,162✔
2700
    {
2701
        return $this->showGridlines;
1,162✔
2702
    }
2703

2704
    /**
2705
     * Set show gridlines.
2706
     *
2707
     * @param bool $showGridLines Show gridlines (true/false)
2708
     *
2709
     * @return $this
2710
     */
2711
    public function setShowGridlines(bool $showGridLines): self
934✔
2712
    {
2713
        $this->showGridlines = $showGridLines;
934✔
2714

2715
        return $this;
934✔
2716
    }
2717

2718
    /**
2719
     * Print gridlines?
2720
     */
2721
    public function getPrintGridlines(): bool
1,185✔
2722
    {
2723
        return $this->printGridlines;
1,185✔
2724
    }
2725

2726
    /**
2727
     * Set print gridlines.
2728
     *
2729
     * @param bool $printGridLines Print gridlines (true/false)
2730
     *
2731
     * @return $this
2732
     */
2733
    public function setPrintGridlines(bool $printGridLines): self
609✔
2734
    {
2735
        $this->printGridlines = $printGridLines;
609✔
2736

2737
        return $this;
609✔
2738
    }
2739

2740
    /**
2741
     * Show row and column headers?
2742
     */
2743
    public function getShowRowColHeaders(): bool
594✔
2744
    {
2745
        return $this->showRowColHeaders;
594✔
2746
    }
2747

2748
    /**
2749
     * Set show row and column headers.
2750
     *
2751
     * @param bool $showRowColHeaders Show row and column headers (true/false)
2752
     *
2753
     * @return $this
2754
     */
2755
    public function setShowRowColHeaders(bool $showRowColHeaders): self
448✔
2756
    {
2757
        $this->showRowColHeaders = $showRowColHeaders;
448✔
2758

2759
        return $this;
448✔
2760
    }
2761

2762
    /**
2763
     * Show summary below? (Row/Column outlining).
2764
     */
2765
    public function getShowSummaryBelow(): bool
595✔
2766
    {
2767
        return $this->showSummaryBelow;
595✔
2768
    }
2769

2770
    /**
2771
     * Set show summary below.
2772
     *
2773
     * @param bool $showSummaryBelow Show summary below (true/false)
2774
     *
2775
     * @return $this
2776
     */
2777
    public function setShowSummaryBelow(bool $showSummaryBelow): self
447✔
2778
    {
2779
        $this->showSummaryBelow = $showSummaryBelow;
447✔
2780

2781
        return $this;
447✔
2782
    }
2783

2784
    /**
2785
     * Show summary right? (Row/Column outlining).
2786
     */
2787
    public function getShowSummaryRight(): bool
595✔
2788
    {
2789
        return $this->showSummaryRight;
595✔
2790
    }
2791

2792
    /**
2793
     * Set show summary right.
2794
     *
2795
     * @param bool $showSummaryRight Show summary right (true/false)
2796
     *
2797
     * @return $this
2798
     */
2799
    public function setShowSummaryRight(bool $showSummaryRight): self
447✔
2800
    {
2801
        $this->showSummaryRight = $showSummaryRight;
447✔
2802

2803
        return $this;
447✔
2804
    }
2805

2806
    /**
2807
     * Get comments.
2808
     *
2809
     * @return Comment[]
2810
     */
2811
    public function getComments(): array
1,244✔
2812
    {
2813
        return $this->comments;
1,244✔
2814
    }
2815

2816
    /**
2817
     * Set comments array for the entire sheet.
2818
     *
2819
     * @param Comment[] $comments
2820
     *
2821
     * @return $this
2822
     */
2823
    public function setComments(array $comments): self
133✔
2824
    {
2825
        $this->comments = $comments;
133✔
2826

2827
        return $this;
133✔
2828
    }
2829

2830
    /**
2831
     * Remove comment from cell.
2832
     *
2833
     * @param array{0: int, 1: int}|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
2834
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2835
     *
2836
     * @return $this
2837
     */
2838
    public function removeComment(CellAddress|string|array $cellCoordinate): self
63✔
2839
    {
2840
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
63✔
2841

2842
        if (Coordinate::coordinateIsRange($cellAddress)) {
63✔
2843
            throw new Exception('Cell coordinate string can not be a range of cells.');
1✔
2844
        } elseif (str_contains($cellAddress, '$')) {
62✔
2845
            throw new Exception('Cell coordinate string must not be absolute.');
1✔
2846
        } elseif ($cellAddress == '') {
61✔
2847
            throw new Exception('Cell coordinate can not be zero-length string.');
1✔
2848
        }
2849
        // Check if we have a comment for this cell and delete it
2850
        if (isset($this->comments[$cellAddress])) {
60✔
2851
            unset($this->comments[$cellAddress]);
3✔
2852
        }
2853

2854
        return $this;
60✔
2855
    }
2856

2857
    /**
2858
     * Get comment for cell.
2859
     *
2860
     * @param array{0: int, 1: int}|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
2861
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2862
     */
2863
    public function getComment(CellAddress|string|array $cellCoordinate, bool $attachNew = true): Comment
120✔
2864
    {
2865
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
120✔
2866

2867
        if (Coordinate::coordinateIsRange($cellAddress)) {
120✔
2868
            throw new Exception('Cell coordinate string can not be a range of cells.');
1✔
2869
        } elseif (str_contains($cellAddress, '$')) {
119✔
2870
            throw new Exception('Cell coordinate string must not be absolute.');
1✔
2871
        } elseif ($cellAddress == '') {
118✔
2872
            throw new Exception('Cell coordinate can not be zero-length string.');
1✔
2873
        }
2874

2875
        // Check if we already have a comment for this cell.
2876
        if (isset($this->comments[$cellAddress])) {
117✔
2877
            return $this->comments[$cellAddress];
83✔
2878
        }
2879

2880
        // If not, create a new comment.
2881
        $newComment = new Comment();
117✔
2882
        if ($attachNew) {
117✔
2883
            $this->comments[$cellAddress] = $newComment;
117✔
2884
        }
2885

2886
        return $newComment;
117✔
2887
    }
2888

2889
    /**
2890
     * Get active cell.
2891
     *
2892
     * @return string Example: 'A1'
2893
     */
2894
    public function getActiveCell(): string
10,996✔
2895
    {
2896
        return $this->activeCell;
10,996✔
2897
    }
2898

2899
    /**
2900
     * Get selected cells.
2901
     */
2902
    public function getSelectedCells(): string
11,050✔
2903
    {
2904
        return $this->selectedCells;
11,050✔
2905
    }
2906

2907
    /**
2908
     * Selected cell.
2909
     *
2910
     * @param string $coordinate Cell (i.e. A1)
2911
     *
2912
     * @return $this
2913
     */
2914
    public function setSelectedCell(string $coordinate): static
38✔
2915
    {
2916
        return $this->setSelectedCells($coordinate);
38✔
2917
    }
2918

2919
    /**
2920
     * Select a range of cells.
2921
     *
2922
     * @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'
2923
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2924
     *              or a CellAddress or AddressRange object.
2925
     *
2926
     * @return $this
2927
     */
2928
    public function setSelectedCells(AddressRange|CellAddress|int|string|array $coordinate): static
10,999✔
2929
    {
2930
        if (is_string($coordinate)) {
10,999✔
2931
            $coordinate = Validations::definedNameToCoordinate($coordinate, $this);
10,999✔
2932
        }
2933
        $coordinate = Validations::validateCellOrCellRange($coordinate);
10,999✔
2934

2935
        if (Coordinate::coordinateIsRange($coordinate)) {
10,999✔
2936
            [$first] = Coordinate::splitRange($coordinate);
544✔
2937
            $this->activeCell = $first[0];
544✔
2938
        } else {
2939
            $this->activeCell = $coordinate;
10,968✔
2940
        }
2941
        $this->selectedCells = $coordinate;
10,999✔
2942
        $this->setSelectedCellsActivePane();
10,999✔
2943

2944
        return $this;
10,999✔
2945
    }
2946

2947
    private function setSelectedCellsActivePane(): void
11,000✔
2948
    {
2949
        if (!empty($this->freezePane)) {
11,000✔
2950
            $coordinateC = Coordinate::indexesFromString($this->freezePane);
48✔
2951
            $coordinateT = Coordinate::indexesFromString($this->activeCell);
48✔
2952
            if ($coordinateC[0] === 1) {
48✔
2953
                $activePane = ($coordinateT[1] <= $coordinateC[1]) ? 'topLeft' : 'bottomLeft';
26✔
2954
            } elseif ($coordinateC[1] === 1) {
24✔
2955
                $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'topLeft' : 'topRight';
3✔
2956
            } elseif ($coordinateT[1] <= $coordinateC[1]) {
22✔
2957
                $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'topLeft' : 'topRight';
22✔
2958
            } else {
2959
                $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'bottomLeft' : 'bottomRight';
10✔
2960
            }
2961
            $this->setActivePane($activePane);
48✔
2962
            $this->panes[$activePane] = new Pane($activePane, $this->selectedCells, $this->activeCell);
48✔
2963
        }
2964
    }
2965

2966
    /**
2967
     * Get right-to-left.
2968
     */
2969
    public function getRightToLeft(): bool
1,188✔
2970
    {
2971
        return $this->rightToLeft;
1,188✔
2972
    }
2973

2974
    /**
2975
     * Set right-to-left.
2976
     *
2977
     * @param bool $value Right-to-left true/false
2978
     *
2979
     * @return $this
2980
     */
2981
    public function setRightToLeft(bool $value): static
168✔
2982
    {
2983
        $this->rightToLeft = $value;
168✔
2984

2985
        return $this;
168✔
2986
    }
2987

2988
    /**
2989
     * Fill worksheet from values in array.
2990
     *
2991
     * @param mixed[]|mixed[][] $source Source array
2992
     * @param mixed $nullValue Value in source array that stands for blank cell
2993
     * @param string $startCell Insert array starting from this cell address as the top left coordinate
2994
     * @param bool $strictNullComparison Apply strict comparison when testing for null values in the array
2995
     *
2996
     * @return $this
2997
     */
2998
    public function fromArray(array $source, mixed $nullValue = null, string $startCell = 'A1', bool $strictNullComparison = false): static
920✔
2999
    {
3000
        //    Convert a 1-D array to 2-D (for ease of looping)
3001
        if (!is_array(end($source))) {
920✔
3002
            $source = [$source];
54✔
3003
        }
3004
        /** @var mixed[][] $source */
3005

3006
        // start coordinate
3007
        [$startColumn, $startRow] = Coordinate::coordinateFromString($startCell);
920✔
3008
        $startRow = (int) $startRow;
920✔
3009

3010
        // Loop through $source
3011
        if ($strictNullComparison) {
920✔
3012
            foreach ($source as $rowData) {
411✔
3013
                /** @var string */
3014
                $currentColumn = $startColumn;
411✔
3015
                foreach ($rowData as $cellValue) {
411✔
3016
                    if ($cellValue !== $nullValue) {
411✔
3017
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
411✔
3018
                    }
3019
                    StringHelper::stringIncrement($currentColumn);
411✔
3020
                }
3021
                ++$startRow;
411✔
3022
            }
3023
        } else {
3024
            foreach ($source as $rowData) {
518✔
3025
                $currentColumn = $startColumn;
518✔
3026
                foreach ($rowData as $cellValue) {
518✔
3027
                    if ($cellValue != $nullValue) {
517✔
3028
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
511✔
3029
                    }
3030
                    StringHelper::stringIncrement($currentColumn);
517✔
3031
                }
3032
                ++$startRow;
518✔
3033
            }
3034
        }
3035

3036
        return $this;
920✔
3037
    }
3038

3039
    /**
3040
     * @param bool $calculateFormulas Whether to calculate cell's value if it is a formula.
3041
     * @param null|bool|float|int|RichText|string $nullValue value to use when null
3042
     * @param bool $formatData Whether to format data according to cell's style.
3043
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3044
     * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3045
     *
3046
     * @throws Exception
3047
     * @throws \PhpOffice\PhpSpreadsheet\Calculation\Exception
3048
     */
3049
    protected function cellToArray(Cell $cell, bool $calculateFormulas, bool $formatData, mixed $nullValue, bool $lessFloatPrecision = false, $oldCalculatedValue = false): mixed
210✔
3050
    {
3051
        $returnValue = $nullValue;
210✔
3052

3053
        if ($cell->getValue() !== null) {
210✔
3054
            if ($cell->getValue() instanceof RichText) {
210✔
3055
                $returnValue = $cell->getValue()->getPlainText();
4✔
3056
            } elseif ($calculateFormulas) {
210✔
3057
                $returnValue = $cell->getCalculatedValue();
181✔
3058
            } elseif ($oldCalculatedValue && ($cell->getDataType() === DataType::TYPE_FORMULA)) {
35✔
3059
                $returnValue = $cell->getOldCalculatedValue() ?? $cell->getValue();
2✔
3060
            } else {
3061
                $returnValue = $cell->getValue();
35✔
3062
            }
3063

3064
            if ($formatData) {
210✔
3065
                $style = $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex());
129✔
3066
                /** @var null|bool|float|int|RichText|string */
3067
                $returnValuex = $returnValue;
129✔
3068
                $returnValue = NumberFormat::toFormattedString(
129✔
3069
                    $returnValuex,
129✔
3070
                    $style->getNumberFormat()->getFormatCode() ?? NumberFormat::FORMAT_GENERAL,
129✔
3071
                    lessFloatPrecision: $lessFloatPrecision
129✔
3072
                );
129✔
3073
            }
3074
        }
3075

3076
        return $returnValue;
210✔
3077
    }
3078

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

3108
        // Loop through rows
3109
        foreach ($this->rangeToArrayYieldRows($range, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue) as $rowRef => $rowArray) {
175✔
3110
            /** @var int $rowRef */
3111
            $returnValue[$rowRef] = $rowArray;
175✔
3112
        }
3113

3114
        // Return
3115
        return $returnValue;
175✔
3116
    }
3117

3118
    /**
3119
     * Create array from a multiple ranges of cells. (such as A1:A3,A15,B17:C17).
3120
     *
3121
     * @param null|bool|float|int|RichText|string $nullValue Value returned in the array entry if a cell doesn't exist
3122
     * @param bool $calculateFormulas Should formulas be calculated?
3123
     * @param bool $formatData Should formatting be applied to cell values?
3124
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3125
     *                             True - Return rows and columns indexed by their actual row and column IDs
3126
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3127
     *                            True - Don't return values for rows/columns that are defined as hidden.
3128
     * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3129
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3130
     * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3131
     *
3132
     * @return mixed[][]
3133
     */
3134
    public function rangesToArray(
6✔
3135
        string $ranges,
3136
        mixed $nullValue = null,
3137
        bool $calculateFormulas = true,
3138
        bool $formatData = true,
3139
        bool $returnCellRef = false,
3140
        bool $ignoreHidden = false,
3141
        bool $reduceArrays = false,
3142
        bool $lessFloatPrecision = false,
3143
        bool $oldCalculatedValue = false,
3144
    ): array {
3145
        $returnValue = [];
6✔
3146

3147
        $parts = explode(',', $ranges);
6✔
3148
        foreach ($parts as $part) {
6✔
3149
            // Loop through rows
3150
            foreach ($this->rangeToArrayYieldRows($part, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue) as $rowRef => $rowArray) {
6✔
3151
                /** @var int $rowRef */
3152
                $returnValue[$rowRef] = $rowArray;
6✔
3153
            }
3154
        }
3155

3156
        // Return
3157
        return $returnValue;
6✔
3158
    }
3159

3160
    /**
3161
     * Create array from a range of cells, yielding each row in turn.
3162
     *
3163
     * @param null|bool|float|int|RichText|string $nullValue Value returned in the array entry if a cell doesn't exist
3164
     * @param bool $calculateFormulas Should formulas be calculated?
3165
     * @param bool $formatData Should formatting be applied to cell values?
3166
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3167
     *                             True - Return rows and columns indexed by their actual row and column IDs
3168
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3169
     *                            True - Don't return values for rows/columns that are defined as hidden.
3170
     * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3171
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3172
     * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3173
     *
3174
     * @return Generator<array<mixed>>
3175
     */
3176
    public function rangeToArrayYieldRows(
210✔
3177
        string $range,
3178
        mixed $nullValue = null,
3179
        bool $calculateFormulas = true,
3180
        bool $formatData = true,
3181
        bool $returnCellRef = false,
3182
        bool $ignoreHidden = false,
3183
        bool $reduceArrays = false,
3184
        bool $lessFloatPrecision = false,
3185
        bool $oldCalculatedValue = false,
3186
    ) {
3187
        $range = Validations::validateCellOrCellRange($range);
210✔
3188

3189
        //    Identify the range that we need to extract from the worksheet
3190
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range);
210✔
3191
        $minCol = Coordinate::stringFromColumnIndex($rangeStart[0]);
210✔
3192
        $minRow = $rangeStart[1];
210✔
3193
        $maxCol = Coordinate::stringFromColumnIndex($rangeEnd[0]);
210✔
3194
        $maxRow = $rangeEnd[1];
210✔
3195
        $minColInt = $rangeStart[0];
210✔
3196
        $maxColInt = $rangeEnd[0];
210✔
3197

3198
        StringHelper::stringIncrement($maxCol);
210✔
3199
        /** @var array<string, bool> */
3200
        $hiddenColumns = [];
210✔
3201
        $nullRow = $this->buildNullRow($nullValue, $minCol, $maxCol, $returnCellRef, $ignoreHidden, $hiddenColumns);
210✔
3202
        $hideColumns = !empty($hiddenColumns);
210✔
3203

3204
        $keys = $this->cellCollection->getSortedCoordinatesInt();
210✔
3205
        $keyIndex = 0;
210✔
3206
        $keysCount = count($keys);
210✔
3207
        // Loop through rows
3208
        for ($row = $minRow; $row <= $maxRow; ++$row) {
210✔
3209
            if (($ignoreHidden === true) && ($this->isRowVisible($row) === false)) {
210✔
3210
                continue;
4✔
3211
            }
3212
            $rowRef = $returnCellRef ? $row : ($row - $minRow);
210✔
3213
            $returnValue = $nullRow;
210✔
3214

3215
            $index = ($row - 1) * AddressRange::MAX_COLUMN_INT + 1;
210✔
3216
            $indexPlus = $index + AddressRange::MAX_COLUMN_INT - 1;
210✔
3217

3218
            // Binary search to quickly approach the correct index
3219
            $keyIndex = intdiv($keysCount, 2);
210✔
3220
            $boundLow = 0;
210✔
3221
            $boundHigh = $keysCount - 1;
210✔
3222
            while ($boundLow <= $boundHigh) {
210✔
3223
                $keyIndex = intdiv($boundLow + $boundHigh, 2);
210✔
3224
                if ($keys[$keyIndex] < $index) {
210✔
3225
                    $boundLow = $keyIndex + 1;
172✔
3226
                } elseif ($keys[$keyIndex] > $index) {
210✔
3227
                    $boundHigh = $keyIndex - 1;
192✔
3228
                } else {
3229
                    break;
202✔
3230
                }
3231
            }
3232

3233
            // Realign to the proper index value
3234
            while ($keyIndex > 0 && $keys[$keyIndex] > $index) {
210✔
3235
                --$keyIndex;
14✔
3236
            }
3237
            while ($keyIndex < $keysCount && $keys[$keyIndex] < $index) {
210✔
3238
                ++$keyIndex;
21✔
3239
            }
3240

3241
            while ($keyIndex < $keysCount && $keys[$keyIndex] <= $indexPlus) {
210✔
3242
                $key = $keys[$keyIndex];
210✔
3243
                $thisRow = intdiv($key - 1, AddressRange::MAX_COLUMN_INT) + 1;
210✔
3244
                $thisCol = ($key % AddressRange::MAX_COLUMN_INT) ?: AddressRange::MAX_COLUMN_INT;
210✔
3245
                if ($thisCol >= $minColInt && $thisCol <= $maxColInt) {
210✔
3246
                    $col = Coordinate::stringFromColumnIndex($thisCol);
210✔
3247
                    if ($hideColumns === false || !isset($hiddenColumns[$col])) {
210✔
3248
                        $columnRef = $returnCellRef ? $col : ($thisCol - $minColInt);
210✔
3249
                        $cell = $this->cellCollection->get("{$col}{$thisRow}");
210✔
3250
                        if ($cell !== null) {
210✔
3251
                            $value = $this->cellToArray($cell, $calculateFormulas, $formatData, $nullValue, lessFloatPrecision: $lessFloatPrecision, oldCalculatedValue: $oldCalculatedValue);
210✔
3252
                            if ($reduceArrays) {
210✔
3253
                                while (is_array($value)) {
21✔
3254
                                    $value = array_shift($value);
19✔
3255
                                }
3256
                            }
3257
                            if ($value !== $nullValue) {
210✔
3258
                                $returnValue[$columnRef] = $value;
210✔
3259
                            }
3260
                        }
3261
                    }
3262
                }
3263
                ++$keyIndex;
210✔
3264
            }
3265

3266
            yield $rowRef => $returnValue;
210✔
3267
        }
3268
    }
3269

3270
    /**
3271
     * Prepare a row data filled with null values to deduplicate the memory areas for empty rows.
3272
     *
3273
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
3274
     * @param string $minCol Start column of the range
3275
     * @param string $maxCol End column of the range
3276
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3277
     *                              True - Return rows and columns indexed by their actual row and column IDs
3278
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3279
     *                             True - Don't return values for rows/columns that are defined as hidden.
3280
     * @param array<string, bool> $hiddenColumns
3281
     *
3282
     * @return mixed[]
3283
     */
3284
    private function buildNullRow(
210✔
3285
        mixed $nullValue,
3286
        string $minCol,
3287
        string $maxCol,
3288
        bool $returnCellRef,
3289
        bool $ignoreHidden,
3290
        array &$hiddenColumns
3291
    ): array {
3292
        $nullRow = [];
210✔
3293
        $c = -1;
210✔
3294
        for ($col = $minCol; $col !== $maxCol; StringHelper::stringIncrement($col)) {
210✔
3295
            if ($ignoreHidden === true && $this->columnDimensionExists($col) && $this->getColumnDimension($col)->getVisible() === false) {
210✔
3296
                $hiddenColumns[$col] = true;
2✔
3297
            } else {
3298
                $columnRef = $returnCellRef ? $col : ++$c;
210✔
3299
                $nullRow[$columnRef] = $nullValue;
210✔
3300
            }
3301
        }
3302

3303
        return $nullRow;
210✔
3304
    }
3305

3306
    private function validateNamedRange(string $definedName, bool $returnNullIfInvalid = false): ?DefinedName
19✔
3307
    {
3308
        $namedRange = DefinedName::resolveName($definedName, $this);
19✔
3309
        if ($namedRange === null) {
19✔
3310
            if ($returnNullIfInvalid) {
6✔
3311
                return null;
5✔
3312
            }
3313

3314
            throw new Exception('Named Range ' . $definedName . ' does not exist.');
1✔
3315
        }
3316

3317
        if ($namedRange->isFormula()) {
13✔
3318
            if ($returnNullIfInvalid) {
×
3319
                return null;
×
3320
            }
3321

3322
            throw new Exception('Defined Named ' . $definedName . ' is a formula, not a range or cell.');
×
3323
        }
3324

3325
        if ($namedRange->getLocalOnly()) {
13✔
3326
            $worksheet = $namedRange->getWorksheet();
2✔
3327
            if ($worksheet === null || $this !== $worksheet) {
2✔
3328
                if ($returnNullIfInvalid) {
×
3329
                    return null;
×
3330
                }
3331

3332
                throw new Exception(
×
3333
                    'Named range ' . $definedName . ' is not accessible from within sheet ' . $this->getTitle()
×
3334
                );
×
3335
            }
3336
        }
3337

3338
        return $namedRange;
13✔
3339
    }
3340

3341
    /**
3342
     * Create array from a range of cells.
3343
     *
3344
     * @param string $definedName The Named Range that should be returned
3345
     * @param null|bool|float|int|RichText|string $nullValue Value returned in the array entry if a cell doesn't exist
3346
     * @param bool $calculateFormulas Should formulas be calculated?
3347
     * @param bool $formatData Should formatting be applied to cell values?
3348
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3349
     *                             True - Return rows and columns indexed by their actual row and column IDs
3350
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3351
     *                            True - Don't return values for rows/columns that are defined as hidden.
3352
     * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3353
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3354
     * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3355
     *
3356
     * @return mixed[][]
3357
     */
3358
    public function namedRangeToArray(
2✔
3359
        string $definedName,
3360
        mixed $nullValue = null,
3361
        bool $calculateFormulas = true,
3362
        bool $formatData = true,
3363
        bool $returnCellRef = false,
3364
        bool $ignoreHidden = false,
3365
        bool $reduceArrays = false,
3366
        bool $lessFloatPrecision = false,
3367
        bool $oldCalculatedValue = false,
3368
    ): array {
3369
        $retVal = [];
2✔
3370
        $namedRange = $this->validateNamedRange($definedName);
2✔
3371
        if ($namedRange !== null) {
1✔
3372
            $cellRange = ltrim(substr($namedRange->getValue(), (int) strrpos($namedRange->getValue(), '!')), '!');
1✔
3373
            $cellRange = str_replace('$', '', $cellRange);
1✔
3374
            $workSheet = $namedRange->getWorksheet();
1✔
3375
            if ($workSheet !== null) {
1✔
3376
                $retVal = $workSheet->rangeToArray($cellRange, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue);
1✔
3377
            }
3378
        }
3379

3380
        return $retVal;
1✔
3381
    }
3382

3383
    /**
3384
     * Create array from worksheet.
3385
     *
3386
     * @param null|bool|float|int|RichText|string $nullValue Value returned in the array entry if a cell doesn't exist
3387
     * @param bool $calculateFormulas Should formulas be calculated?
3388
     * @param bool $formatData Should formatting be applied to cell values?
3389
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3390
     *                             True - Return rows and columns indexed by their actual row and column IDs
3391
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3392
     *                            True - Don't return values for rows/columns that are defined as hidden.
3393
     * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3394
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3395
     * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3396
     *
3397
     * @return mixed[][]
3398
     */
3399
    public function toArray(
106✔
3400
        mixed $nullValue = null,
3401
        bool $calculateFormulas = true,
3402
        bool $formatData = true,
3403
        bool $returnCellRef = false,
3404
        bool $ignoreHidden = false,
3405
        bool $reduceArrays = false,
3406
        bool $lessFloatPrecision = false,
3407
        bool $oldCalculatedValue = false,
3408
    ): array {
3409
        // Garbage collect...
3410
        $this->garbageCollect();
106✔
3411
        $this->calculateArrays($calculateFormulas);
106✔
3412

3413
        //    Identify the range that we need to extract from the worksheet
3414
        $maxCol = $this->getHighestColumn();
106✔
3415
        $maxRow = $this->getHighestRow();
106✔
3416

3417
        // Return
3418
        return $this->rangeToArray("A1:{$maxCol}{$maxRow}", $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue);
106✔
3419
    }
3420

3421
    /**
3422
     * Get row iterator.
3423
     *
3424
     * @param int $startRow The row number at which to start iterating
3425
     * @param ?int $endRow The row number at which to stop iterating
3426
     */
3427
    public function getRowIterator(int $startRow = 1, ?int $endRow = null): RowIterator
117✔
3428
    {
3429
        return new RowIterator($this, $startRow, $endRow);
117✔
3430
    }
3431

3432
    /**
3433
     * Get column iterator.
3434
     *
3435
     * @param string $startColumn The column address at which to start iterating
3436
     * @param ?string $endColumn The column address at which to stop iterating
3437
     */
3438
    public function getColumnIterator(string $startColumn = 'A', ?string $endColumn = null): ColumnIterator
39✔
3439
    {
3440
        return new ColumnIterator($this, $startColumn, $endColumn);
39✔
3441
    }
3442

3443
    /**
3444
     * Run PhpSpreadsheet garbage collector.
3445
     *
3446
     * @return $this
3447
     */
3448
    public function garbageCollect(): static
1,343✔
3449
    {
3450
        // Flush cache
3451
        $this->cellCollection->get('A1');
1,343✔
3452

3453
        // Lookup highest column and highest row if cells are cleaned
3454
        $colRow = $this->cellCollection->getHighestRowAndColumn();
1,343✔
3455
        $highestRow = $colRow['row'];
1,343✔
3456
        $highestColumn = Coordinate::columnIndexFromString($colRow['column']);
1,343✔
3457

3458
        // Loop through column dimensions
3459
        foreach ($this->columnDimensions as $dimension) {
1,343✔
3460
            $highestColumn = max($highestColumn, Coordinate::columnIndexFromString($dimension->getColumnIndex()));
197✔
3461
        }
3462

3463
        // Loop through row dimensions
3464
        foreach ($this->rowDimensions as $dimension) {
1,343✔
3465
            $highestRow = max($highestRow, $dimension->getRowIndex());
125✔
3466
        }
3467

3468
        // Cache values
3469
        $this->cachedHighestColumn = max(1, $highestColumn);
1,343✔
3470
        /** @var int $highestRow */
3471
        $this->cachedHighestRow = $highestRow;
1,343✔
3472

3473
        // Return
3474
        return $this;
1,343✔
3475
    }
3476

3477
    /**
3478
     * @deprecated 5.2.0 Serves no useful purpose. No replacement.
3479
     *
3480
     * @codeCoverageIgnore
3481
     */
3482
    public function getHashInt(): int
3483
    {
3484
        return spl_object_id($this);
3485
    }
3486

3487
    /**
3488
     * Extract worksheet title from range.
3489
     *
3490
     * Example: extractSheetTitle("testSheet!A1") ==> 'A1'
3491
     * Example: extractSheetTitle("testSheet!A1:C3") ==> 'A1:C3'
3492
     * Example: extractSheetTitle("'testSheet 1'!A1", true) ==> ['testSheet 1', 'A1'];
3493
     * Example: extractSheetTitle("'testSheet 1'!A1:C3", true) ==> ['testSheet 1', 'A1:C3'];
3494
     * Example: extractSheetTitle("A1", true) ==> ['', 'A1'];
3495
     * Example: extractSheetTitle("A1:C3", true) ==> ['', 'A1:C3']
3496
     *
3497
     * @param ?string $range Range to extract title from
3498
     * @param bool $returnRange Return range? (see example)
3499
     *
3500
     * @return ($range is non-empty-string ? ($returnRange is true ? array{0: string, 1: string} : string) : ($returnRange is true ? array{0: null, 1: null} : null))
3501
     */
3502
    public static function extractSheetTitle(?string $range, bool $returnRange = false, bool $unapostrophize = false): array|null|string
11,230✔
3503
    {
3504
        if (empty($range)) {
11,230✔
3505
            return $returnRange ? [null, null] : null;
13✔
3506
        }
3507

3508
        // Sheet title included?
3509
        if (($sep = strrpos($range, '!')) === false) {
11,228✔
3510
            return $returnRange ? ['', $range] : '';
11,199✔
3511
        }
3512

3513
        if ($returnRange) {
1,499✔
3514
            $title = substr($range, 0, $sep);
1,499✔
3515
            if ($unapostrophize) {
1,499✔
3516
                $title = self::unApostrophizeTitle($title);
1,439✔
3517
            }
3518

3519
            return [$title, substr($range, $sep + 1)];
1,499✔
3520
        }
3521

3522
        return substr($range, $sep + 1);
7✔
3523
    }
3524

3525
    public static function unApostrophizeTitle(?string $title): string
1,453✔
3526
    {
3527
        $title ??= '';
1,453✔
3528
        if (str_starts_with($title, "'") && str_ends_with($title, "'")) {
1,453✔
3529
            $title = str_replace("''", "'", substr($title, 1, -1));
1,377✔
3530
        }
3531

3532
        return $title;
1,453✔
3533
    }
3534

3535
    /**
3536
     * Get hyperlink.
3537
     *
3538
     * @param string $cellCoordinate Cell coordinate to get hyperlink for, eg: 'A1'
3539
     */
3540
    public function getHyperlink(string $cellCoordinate): Hyperlink
113✔
3541
    {
3542
        $this->getCell($cellCoordinate)->setHadHyperlink(true);
113✔
3543
        // return hyperlink if we already have one
3544
        if (isset($this->hyperlinkCollection[$cellCoordinate])) {
113✔
3545
            return $this->hyperlinkCollection[$cellCoordinate];
61✔
3546
        }
3547

3548
        // else create hyperlink
3549
        $this->hyperlinkCollection[$cellCoordinate] = new Hyperlink();
113✔
3550

3551
        return $this->hyperlinkCollection[$cellCoordinate];
113✔
3552
    }
3553

3554
    /**
3555
     * Set hyperlink.
3556
     *
3557
     * @param string $cellCoordinate Cell coordinate to insert hyperlink, eg: 'A1'
3558
     *
3559
     * @return $this
3560
     */
3561
    public function setHyperlink(string $cellCoordinate, ?Hyperlink $hyperlink = null, bool $reset = true): static
597✔
3562
    {
3563
        if ($hyperlink === null) {
597✔
3564
            unset($this->hyperlinkCollection[$cellCoordinate]);
596✔
3565
            if ($reset) {
596✔
3566
                $this->getCell($cellCoordinate)
557✔
3567
                    ->setHadHyperlink(false);
557✔
3568
            }
3569
        } else {
3570
            $this->hyperlinkCollection[$cellCoordinate] = $hyperlink;
36✔
3571
            $this->getCell($cellCoordinate)->setHadHyperlink(true);
36✔
3572
        }
3573

3574
        return $this;
597✔
3575
    }
3576

3577
    /**
3578
     * Hyperlink at a specific coordinate exists?
3579
     *
3580
     * @param string $coordinate eg: 'A1'
3581
     */
3582
    public function hyperlinkExists(string $coordinate): bool
689✔
3583
    {
3584
        return isset($this->hyperlinkCollection[$coordinate]);
689✔
3585
    }
3586

3587
    /**
3588
     * Get collection of hyperlinks.
3589
     *
3590
     * @return Hyperlink[]
3591
     */
3592
    public function getHyperlinkCollection(): array
709✔
3593
    {
3594
        return $this->hyperlinkCollection;
709✔
3595
    }
3596

3597
    /**
3598
     * Get data validation.
3599
     *
3600
     * @param string $cellCoordinate Cell coordinate to get data validation for, eg: 'A1'
3601
     */
3602
    public function getDataValidation(string $cellCoordinate): DataValidation
37✔
3603
    {
3604
        // return data validation if we already have one
3605
        if (isset($this->dataValidationCollection[$cellCoordinate])) {
37✔
3606
            return $this->dataValidationCollection[$cellCoordinate];
28✔
3607
        }
3608

3609
        // or if cell is part of a data validation range
3610
        foreach ($this->dataValidationCollection as $key => $dataValidation) {
28✔
3611
            $keyParts = explode(' ', $key);
12✔
3612
            foreach ($keyParts as $keyPart) {
12✔
3613
                if ($keyPart === $cellCoordinate) {
12✔
3614
                    return $dataValidation;
1✔
3615
                }
3616
                if (str_contains($keyPart, ':')) {
12✔
3617
                    if (Coordinate::coordinateIsInsideRange($keyPart, $cellCoordinate)) {
9✔
3618
                        return $dataValidation;
9✔
3619
                    }
3620
                }
3621
            }
3622
        }
3623

3624
        // else create data validation
3625
        $dataValidation = new DataValidation();
20✔
3626
        $dataValidation->setSqref($cellCoordinate);
20✔
3627
        $this->dataValidationCollection[$cellCoordinate] = $dataValidation;
20✔
3628

3629
        return $dataValidation;
20✔
3630
    }
3631

3632
    /**
3633
     * Set data validation.
3634
     *
3635
     * @param string $cellCoordinate Cell coordinate to insert data validation, eg: 'A1'
3636
     *
3637
     * @return $this
3638
     */
3639
    public function setDataValidation(string $cellCoordinate, ?DataValidation $dataValidation = null): static
97✔
3640
    {
3641
        if ($dataValidation === null) {
97✔
3642
            unset($this->dataValidationCollection[$cellCoordinate]);
64✔
3643
        } else {
3644
            $dataValidation->setSqref($cellCoordinate);
40✔
3645
            $this->dataValidationCollection[$cellCoordinate] = $dataValidation;
40✔
3646
        }
3647

3648
        return $this;
97✔
3649
    }
3650

3651
    /**
3652
     * Data validation at a specific coordinate exists?
3653
     *
3654
     * @param string $coordinate eg: 'A1'
3655
     */
3656
    public function dataValidationExists(string $coordinate): bool
25✔
3657
    {
3658
        if (isset($this->dataValidationCollection[$coordinate])) {
25✔
3659
            return true;
23✔
3660
        }
3661
        foreach ($this->dataValidationCollection as $key => $dataValidation) {
8✔
3662
            $keyParts = explode(' ', $key);
7✔
3663
            foreach ($keyParts as $keyPart) {
7✔
3664
                if ($keyPart === $coordinate) {
7✔
3665
                    return true;
1✔
3666
                }
3667
                if (str_contains($keyPart, ':')) {
7✔
3668
                    if (Coordinate::coordinateIsInsideRange($keyPart, $coordinate)) {
2✔
3669
                        return true;
2✔
3670
                    }
3671
                }
3672
            }
3673
        }
3674

3675
        return false;
6✔
3676
    }
3677

3678
    /**
3679
     * Get collection of data validations.
3680
     *
3681
     * @return DataValidation[]
3682
     */
3683
    public function getDataValidationCollection(): array
710✔
3684
    {
3685
        $collectionCells = [];
710✔
3686
        $collectionRanges = [];
710✔
3687
        foreach ($this->dataValidationCollection as $key => $dataValidation) {
710✔
3688
            if (Preg::isMatch('/[: ]/', $key)) {
27✔
3689
                $collectionRanges[$key] = $dataValidation;
15✔
3690
            } else {
3691
                $collectionCells[$key] = $dataValidation;
22✔
3692
            }
3693
        }
3694

3695
        return array_merge($collectionCells, $collectionRanges);
710✔
3696
    }
3697

3698
    /**
3699
     * Accepts a range, returning it as a range that falls within the current highest row and column of the worksheet.
3700
     *
3701
     * @return string Adjusted range value
3702
     */
3703
    public function shrinkRangeToFit(string $range): string
1✔
3704
    {
3705
        $maxCol = $this->getHighestColumn();
1✔
3706
        $maxRow = $this->getHighestRow();
1✔
3707
        $maxCol = Coordinate::columnIndexFromString($maxCol);
1✔
3708

3709
        $rangeBlocks = explode(' ', $range);
1✔
3710
        foreach ($rangeBlocks as &$rangeSet) {
1✔
3711
            $rangeBoundaries = Coordinate::getRangeBoundaries($rangeSet);
1✔
3712

3713
            if (Coordinate::columnIndexFromString($rangeBoundaries[0][0]) > $maxCol) {
1✔
3714
                $rangeBoundaries[0][0] = Coordinate::stringFromColumnIndex($maxCol);
1✔
3715
            }
3716
            if ($rangeBoundaries[0][1] > $maxRow) {
1✔
3717
                $rangeBoundaries[0][1] = $maxRow;
1✔
3718
            }
3719
            if (Coordinate::columnIndexFromString($rangeBoundaries[1][0]) > $maxCol) {
1✔
3720
                $rangeBoundaries[1][0] = Coordinate::stringFromColumnIndex($maxCol);
1✔
3721
            }
3722
            if ($rangeBoundaries[1][1] > $maxRow) {
1✔
3723
                $rangeBoundaries[1][1] = $maxRow;
1✔
3724
            }
3725
            $rangeSet = $rangeBoundaries[0][0] . $rangeBoundaries[0][1] . ':' . $rangeBoundaries[1][0] . $rangeBoundaries[1][1];
1✔
3726
        }
3727
        unset($rangeSet);
1✔
3728

3729
        return implode(' ', $rangeBlocks);
1✔
3730
    }
3731

3732
    /**
3733
     * Get tab color.
3734
     */
3735
    public function getTabColor(): Color
23✔
3736
    {
3737
        if ($this->tabColor === null) {
23✔
3738
            $this->tabColor = new Color();
23✔
3739
        }
3740

3741
        return $this->tabColor;
23✔
3742
    }
3743

3744
    /**
3745
     * Reset tab color.
3746
     *
3747
     * @return $this
3748
     */
3749
    public function resetTabColor(): static
1✔
3750
    {
3751
        $this->tabColor = null;
1✔
3752

3753
        return $this;
1✔
3754
    }
3755

3756
    /**
3757
     * Tab color set?
3758
     */
3759
    public function isTabColorSet(): bool
596✔
3760
    {
3761
        return $this->tabColor !== null;
596✔
3762
    }
3763

3764
    /**
3765
     * Copy worksheet (!= clone!).
3766
     */
3767
    public function copy(): static
1✔
3768
    {
3769
        return clone $this;
1✔
3770
    }
3771

3772
    /**
3773
     * Returns a boolean true if the specified row contains no cells. By default, this means that no cell records
3774
     *          exist in the collection for this row. false will be returned otherwise.
3775
     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
3776
     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
3777
     *                  cells, then the row will be considered empty.
3778
     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
3779
     *                  string value cells, then the row will be considered empty.
3780
     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3781
     *                  If the only cells in the collection are null value or empty string value cells, then the row
3782
     *                  will be considered empty.
3783
     *
3784
     * @param int $definitionOfEmptyFlags
3785
     *              Possible Flag Values are:
3786
     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
3787
     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3788
     */
3789
    public function isEmptyRow(int $rowId, int $definitionOfEmptyFlags = 0): bool
9✔
3790
    {
3791
        try {
3792
            $iterator = new RowIterator($this, $rowId, $rowId);
9✔
3793
            $iterator->seek($rowId);
8✔
3794
            $row = $iterator->current();
8✔
3795
        } catch (Exception) {
1✔
3796
            return true;
1✔
3797
        }
3798

3799
        return $row->isEmpty($definitionOfEmptyFlags);
8✔
3800
    }
3801

3802
    /**
3803
     * Returns a boolean true if the specified column contains no cells. By default, this means that no cell records
3804
     *          exist in the collection for this column. false will be returned otherwise.
3805
     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
3806
     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
3807
     *                  cells, then the column will be considered empty.
3808
     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
3809
     *                  string value cells, then the column will be considered empty.
3810
     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3811
     *                  If the only cells in the collection are null value or empty string value cells, then the column
3812
     *                  will be considered empty.
3813
     *
3814
     * @param int $definitionOfEmptyFlags
3815
     *              Possible Flag Values are:
3816
     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
3817
     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3818
     */
3819
    public function isEmptyColumn(string $columnId, int $definitionOfEmptyFlags = 0): bool
9✔
3820
    {
3821
        try {
3822
            $iterator = new ColumnIterator($this, $columnId, $columnId);
9✔
3823
            $iterator->seek($columnId);
8✔
3824
            $column = $iterator->current();
8✔
3825
        } catch (Exception) {
1✔
3826
            return true;
1✔
3827
        }
3828

3829
        return $column->isEmpty($definitionOfEmptyFlags);
8✔
3830
    }
3831

3832
    /**
3833
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
3834
     */
3835
    public function __clone()
28✔
3836
    {
3837
        foreach (get_object_vars($this) as $key => $val) {
28✔
3838
            if ($key == 'parent') {
28✔
3839
                continue;
28✔
3840
            }
3841

3842
            if (is_object($val) || (is_array($val))) {
28✔
3843
                if ($key === 'cellCollection') {
28✔
3844
                    $newCollection = $this->cellCollection->cloneCellCollection($this);
28✔
3845
                    $this->cellCollection = $newCollection;
28✔
3846
                } elseif ($key === 'drawingCollection') {
28✔
3847
                    $currentCollection = $this->drawingCollection;
28✔
3848
                    $this->drawingCollection = new ArrayObject();
28✔
3849
                    foreach ($currentCollection as $item) {
28✔
3850
                        $newDrawing = clone $item;
4✔
3851
                        $newDrawing->setWorksheet($this);
4✔
3852
                    }
3853
                } elseif ($key === 'inCellDrawingCollection') {
28✔
3854
                    $currentCollection = $this->inCellDrawingCollection;
28✔
3855
                    $this->inCellDrawingCollection = new ArrayObject();
28✔
3856
                    foreach ($currentCollection as $item) {
28✔
3857
                        $newDrawing = clone $item;
1✔
3858
                        $newDrawing->setWorksheet($this);
1✔
3859
                    }
3860
                } elseif ($key === 'tableCollection') {
28✔
3861
                    $currentCollection = $this->tableCollection;
28✔
3862
                    $this->tableCollection = new ArrayObject();
28✔
3863
                    foreach ($currentCollection as $item) {
28✔
3864
                        $newTable = clone $item;
1✔
3865
                        $newTable->setName($item->getName() . 'clone');
1✔
3866
                        $this->addTable($newTable);
1✔
3867
                    }
3868
                } elseif ($key === 'chartCollection') {
28✔
3869
                    $currentCollection = $this->chartCollection;
28✔
3870
                    $this->chartCollection = new ArrayObject();
28✔
3871
                    foreach ($currentCollection as $item) {
28✔
3872
                        $newChart = clone $item;
5✔
3873
                        $this->addChart($newChart);
5✔
3874
                    }
3875
                } elseif ($key === 'autoFilter') {
28✔
3876
                    $newAutoFilter = clone $this->autoFilter;
28✔
3877
                    $this->autoFilter = $newAutoFilter;
28✔
3878
                    $this->autoFilter->setParent($this);
28✔
3879
                } else {
3880
                    $this->{$key} = unserialize(serialize($val));
28✔
3881
                }
3882
            }
3883
        }
3884
    }
3885

3886
    /**
3887
     * Define the code name of the sheet.
3888
     *
3889
     * @param string $codeName Same rule as Title minus space not allowed (but, like Excel, change
3890
     *                       silently space to underscore)
3891
     * @param bool $validate False to skip validation of new title. WARNING: This should only be set
3892
     *                       at parse time (by Readers), where titles can be assumed to be valid.
3893
     *
3894
     * @return $this
3895
     */
3896
    public function setCodeName(string $codeName, bool $validate = true): static
11,480✔
3897
    {
3898
        // Is this a 'rename' or not?
3899
        if ($this->getCodeName() == $codeName) {
11,480✔
3900
            return $this;
×
3901
        }
3902

3903
        if ($validate) {
11,480✔
3904
            $codeName = str_replace(' ', '_', $codeName); //Excel does this automatically without flinching, we are doing the same
11,480✔
3905

3906
            // Syntax check
3907
            // throw an exception if not valid
3908
            self::checkSheetCodeName($codeName);
11,480✔
3909

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

3912
            if ($this->parent !== null) {
11,480✔
3913
                // Is there already such sheet name?
3914
                if ($this->parent->sheetCodeNameExists($codeName)) {
11,439✔
3915
                    // Use name, but append with lowest possible integer
3916

3917
                    if (StringHelper::countCharacters($codeName) > 29) {
741✔
3918
                        $codeName = StringHelper::substring($codeName, 0, 29);
×
3919
                    }
3920
                    $i = 1;
741✔
3921
                    while ($this->getParentOrThrow()->sheetCodeNameExists($codeName . '_' . $i)) {
741✔
3922
                        ++$i;
300✔
3923
                        if ($i == 10) {
300✔
3924
                            if (StringHelper::countCharacters($codeName) > 28) {
2✔
3925
                                $codeName = StringHelper::substring($codeName, 0, 28);
×
3926
                            }
3927
                        } elseif ($i == 100) {
300✔
3928
                            if (StringHelper::countCharacters($codeName) > 27) {
×
3929
                                $codeName = StringHelper::substring($codeName, 0, 27);
×
3930
                            }
3931
                        }
3932
                    }
3933

3934
                    $codeName .= '_' . $i; // ok, we have a valid name
741✔
3935
                }
3936
            }
3937
        }
3938

3939
        $this->codeName = $codeName;
11,480✔
3940

3941
        return $this;
11,480✔
3942
    }
3943

3944
    /**
3945
     * Return the code name of the sheet.
3946
     */
3947
    public function getCodeName(): ?string
11,480✔
3948
    {
3949
        return $this->codeName;
11,480✔
3950
    }
3951

3952
    /**
3953
     * Sheet has a code name ?
3954
     */
3955
    public function hasCodeName(): bool
2✔
3956
    {
3957
        return $this->codeName !== null;
2✔
3958
    }
3959

3960
    public static function nameRequiresQuotes(string $sheetName): bool
4✔
3961
    {
3962
        return !Preg::isMatch(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName);
4✔
3963
    }
3964

3965
    public function isRowVisible(int $row): bool
136✔
3966
    {
3967
        return !$this->rowDimensionExists($row) || $this->getRowDimension($row)->getVisible();
136✔
3968
    }
3969

3970
    /**
3971
     * Same as Cell->isLocked, but without creating cell if it doesn't exist.
3972
     */
3973
    public function isCellLocked(string $coordinate): bool
1✔
3974
    {
3975
        if ($this->getProtection()->getsheet() !== true) {
1✔
3976
            return false;
1✔
3977
        }
3978
        if ($this->cellExists($coordinate)) {
1✔
3979
            return $this->getCell($coordinate)->isLocked();
1✔
3980
        }
3981
        $spreadsheet = $this->parent;
1✔
3982
        $xfIndex = $this->getXfIndex($coordinate);
1✔
3983
        if ($spreadsheet === null || $xfIndex === null) {
1✔
3984
            return true;
1✔
3985
        }
3986

3987
        return $spreadsheet->getCellXfByIndex($xfIndex)->getProtection()->getLocked() !== StyleProtection::PROTECTION_UNPROTECTED;
×
3988
    }
3989

3990
    /**
3991
     * Same as Cell->isHiddenOnFormulaBar, but without creating cell if it doesn't exist.
3992
     */
3993
    public function isCellHiddenOnFormulaBar(string $coordinate): bool
1✔
3994
    {
3995
        if ($this->cellExists($coordinate)) {
1✔
3996
            return $this->getCell($coordinate)->isHiddenOnFormulaBar();
1✔
3997
        }
3998

3999
        // cell doesn't exist, therefore isn't a formula,
4000
        // therefore isn't hidden on formula bar.
4001
        return false;
1✔
4002
    }
4003

4004
    private function getXfIndex(string $coordinate): ?int
1✔
4005
    {
4006
        [$column, $row] = Coordinate::coordinateFromString($coordinate);
1✔
4007
        $row = (int) $row;
1✔
4008
        $xfIndex = null;
1✔
4009
        if ($this->rowDimensionExists($row)) {
1✔
4010
            $xfIndex = $this->getRowDimension($row)->getXfIndex();
×
4011
        }
4012
        if ($xfIndex === null && $this->ColumnDimensionExists($column)) {
1✔
4013
            $xfIndex = $this->getColumnDimension($column)->getXfIndex();
×
4014
        }
4015

4016
        return $xfIndex;
1✔
4017
    }
4018

4019
    private string $backgroundImage = '';
4020

4021
    private string $backgroundMime = '';
4022

4023
    private string $backgroundExtension = '';
4024

4025
    public function getBackgroundImage(): string
1,112✔
4026
    {
4027
        return $this->backgroundImage;
1,112✔
4028
    }
4029

4030
    public function getBackgroundMime(): string
449✔
4031
    {
4032
        return $this->backgroundMime;
449✔
4033
    }
4034

4035
    public function getBackgroundExtension(): string
449✔
4036
    {
4037
        return $this->backgroundExtension;
449✔
4038
    }
4039

4040
    /**
4041
     * Set background image.
4042
     * Used on read/write for Xlsx.
4043
     * Used on write for Html.
4044
     *
4045
     * @param string $backgroundImage Image represented as a string, e.g. results of file_get_contents
4046
     */
4047
    public function setBackgroundImage(string $backgroundImage): self
4✔
4048
    {
4049
        $imageArray = getimagesizefromstring($backgroundImage) ?: ['mime' => ''];
4✔
4050
        $mime = $imageArray['mime'];
4✔
4051
        if ($mime !== '') {
4✔
4052
            $extension = explode('/', $mime);
3✔
4053
            $extension = $extension[1];
3✔
4054
            $this->backgroundImage = $backgroundImage;
3✔
4055
            $this->backgroundMime = $mime;
3✔
4056
            $this->backgroundExtension = $extension;
3✔
4057
        }
4058

4059
        return $this;
4✔
4060
    }
4061

4062
    /**
4063
     * Copy cells, adjusting relative cell references in formulas.
4064
     * Acts similarly to Excel "fill handle" feature.
4065
     *
4066
     * @param string $fromCell Single source cell, e.g. C3
4067
     * @param string $toCells Single cell or cell range, e.g. C4 or C4:C10
4068
     * @param bool $copyStyle Copy styles as well as values, defaults to true
4069
     */
4070
    public function copyCells(string $fromCell, string $toCells, bool $copyStyle = true): void
1✔
4071
    {
4072
        $toArray = Coordinate::extractAllCellReferencesInRange($toCells);
1✔
4073
        $valueString = $this->getCell($fromCell)->getValueString();
1✔
4074
        /** @var mixed[][] */
4075
        $style = $this->getStyle($fromCell)->exportArray();
1✔
4076
        $fromIndexes = Coordinate::indexesFromString($fromCell);
1✔
4077
        $referenceHelper = ReferenceHelper::getInstance();
1✔
4078
        foreach ($toArray as $destination) {
1✔
4079
            if ($destination !== $fromCell) {
1✔
4080
                $toIndexes = Coordinate::indexesFromString($destination);
1✔
4081
                $this->getCell($destination)->setValue($referenceHelper->updateFormulaReferences($valueString, 'A1', $toIndexes[0] - $fromIndexes[0], $toIndexes[1] - $fromIndexes[1]));
1✔
4082
                if ($copyStyle) {
1✔
4083
                    $this->getCell($destination)->getStyle()->applyFromArray($style);
1✔
4084
                }
4085
            }
4086
        }
4087
    }
4088

4089
    public function calculateArrays(bool $preCalculateFormulas = true): void
1,314✔
4090
    {
4091
        if ($preCalculateFormulas && Calculation::getInstance($this->parent)->getInstanceArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY) {
1,314✔
4092
            $keys = $this->cellCollection->getCoordinates();
52✔
4093
            foreach ($keys as $key) {
52✔
4094
                if ($this->getCell($key)->getDataType() === DataType::TYPE_FORMULA) {
52✔
4095
                    if (!Preg::isMatch(self::FUNCTION_LIKE_GROUPBY, $this->getCell($key)->getValueString())) {
48✔
4096
                        $this->getCell($key)->getCalculatedValue();
47✔
4097
                    }
4098
                }
4099
            }
4100
        }
4101
    }
4102

4103
    public function isCellInSpillRange(string $coordinate): bool
2✔
4104
    {
4105
        if (Calculation::getInstance($this->parent)->getInstanceArrayReturnType() !== Calculation::RETURN_ARRAY_AS_ARRAY) {
2✔
4106
            return false;
1✔
4107
        }
4108
        $this->calculateArrays();
1✔
4109
        $keys = $this->cellCollection->getCoordinates();
1✔
4110
        foreach ($keys as $key) {
1✔
4111
            $attributes = $this->getCell($key)->getFormulaAttributes();
1✔
4112
            if (isset($attributes['ref'])) {
1✔
4113
                if (Coordinate::coordinateIsInsideRange($attributes['ref'], $coordinate)) {
1✔
4114
                    // false for first cell in range, true otherwise
4115
                    return $coordinate !== $key;
1✔
4116
                }
4117
            }
4118
        }
4119

4120
        return false;
1✔
4121
    }
4122

4123
    /** @param mixed[][] $styleArray */
4124
    public function applyStylesFromArray(string $coordinate, array $styleArray): bool
2✔
4125
    {
4126
        $spreadsheet = $this->parent;
2✔
4127
        if ($spreadsheet === null) {
2✔
4128
            return false;
1✔
4129
        }
4130
        $activeSheetIndex = $spreadsheet->getActiveSheetIndex();
1✔
4131
        $originalSelected = $this->selectedCells;
1✔
4132
        $this->getStyle($coordinate)->applyFromArray($styleArray);
1✔
4133
        $this->setSelectedCells($originalSelected);
1✔
4134
        if ($activeSheetIndex >= 0) {
1✔
4135
            $spreadsheet->setActiveSheetIndex($activeSheetIndex);
1✔
4136
        }
4137

4138
        return true;
1✔
4139
    }
4140

4141
    public function copyFormula(string $fromCell, string $toCell): void
1✔
4142
    {
4143
        $formula = $this->getCell($fromCell)->getValue();
1✔
4144
        $newFormula = $formula;
1✔
4145
        if (is_string($formula) && $this->getCell($fromCell)->getDataType() === DataType::TYPE_FORMULA) {
1✔
4146
            [$fromColInt, $fromRow] = Coordinate::indexesFromString($fromCell);
1✔
4147
            [$toColInt, $toRow] = Coordinate::indexesFromString($toCell);
1✔
4148
            $helper = ReferenceHelper::getInstance();
1✔
4149
            $newFormula = $helper->updateFormulaReferences(
1✔
4150
                $formula,
1✔
4151
                'A1',
1✔
4152
                $toColInt - $fromColInt,
1✔
4153
                $toRow - $fromRow
1✔
4154
            );
1✔
4155
        }
4156
        $this->setCellValue($toCell, $newFormula);
1✔
4157
    }
4158
}
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