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

PHPOffice / PhpSpreadsheet / 24966710174

26 Apr 2026 08:48PM UTC coverage: 97.066% (+0.001%) from 97.065%
24966710174

Pull #4862

github

web-flow
Merge 95ffbf0d7 into 5483cf930
Pull Request #4862: WIP Experiment With Column Auto-fit Performance

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

42 existing lines in 1 file now uncovered.

48071 of 49524 relevant lines covered (97.07%)

386.16 hits per line

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

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

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

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

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

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

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

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

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

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

432
        return $sheetCodeName;
11,444✔
433
    }
434

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

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

454
        return $sheetTitle;
11,444✔
455
    }
456

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

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

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

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

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

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

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

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

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

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

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

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

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

557
        return $chart;
107✔
558
    }
559

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

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

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

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

605
        return $chartNames;
5✔
606
    }
607

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

623
        return false;
1✔
624
    }
625

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

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

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

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

650
        return $this;
25✔
651
    }
652

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

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

667
        return $this;
9✔
668
    }
669

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

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

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

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

725
            $autoFilterIndentRanges = (new AutoFit($this))->getAutoFilterIndentRanges();
67✔
726

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

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

737
                //By default merged cells should be ignored
738
                $isMergedButProceed = false;
67✔
739

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

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

760
                                break;
4✔
761
                            }
762
                        }
763
                    }
764

765
                    $indentAdjustment = $cell->getStyle()->getAlignment()->getIndent();
67✔
766
                    $indentAdjustment += (int) ($cell->getStyle()->getAlignment()->getHorizontal() === Alignment::HORIZONTAL_CENTER);
67✔
767

768
                    // Calculated value
769
                    // To formatted string
770
                    $cellValue = NumberFormat::toFormattedString(
67✔
771
                        $cell->getCalculatedValueString(),
67✔
772
                        (string) ($spreadsheet?->getCellXfByIndex($cell->getXfIndex())
67✔
773
                            ->getNumberFormat()
67✔
774
                            ->getFormatCode(true) ?? NumberFormat::FORMAT_GENERAL)
67✔
775
                    );
67✔
776

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

808
            // adjust column widths
809
            foreach ($autoSizes as $columnIndex => $width) {
67✔
810
                if ($width == -1) {
67✔
NEW
811
                    $width = $defaultWidth;
×
812
                }
813
                $this->getColumnDimension($columnIndex)
67✔
814
                    ->setWidth($width);
67✔
815
            }
816
            $this->activePane = $holdActivePane;
67✔
817
        }
818
        if ($activeSheet !== null && $activeSheet >= 0) {
879✔
819
            $spreadsheet?->setActiveSheetIndex($activeSheet);
879✔
820
        }
821
        $this->setSelectedCells($selectedCells);
879✔
822

823
        return $this;
879✔
824
    }
825

826
    /**
827
     * Get parent or null.
828
     */
829
    public function getParent(): ?Spreadsheet
11,005✔
830
    {
831
        return $this->parent;
11,005✔
832
    }
833

834
    /**
835
     * Get parent, throw exception if null.
836
     */
837
    public function getParentOrThrow(): Spreadsheet
11,077✔
838
    {
839
        if ($this->parent !== null) {
11,077✔
840
            return $this->parent;
11,076✔
841
        }
842

843
        throw new Exception('Sheet does not have a parent.');
1✔
844
    }
845

846
    /**
847
     * Re-bind parent.
848
     *
849
     * @return $this
850
     */
851
    public function rebindParent(Spreadsheet $parent): static
54✔
852
    {
853
        if ($this->parent !== null) {
54✔
854
            $definedNames = $this->parent->getDefinedNames();
4✔
855
            foreach ($definedNames as $definedName) {
4✔
UNCOV
856
                $parent->addDefinedName($definedName);
×
857
            }
858

859
            $this->parent->removeSheetByIndex(
4✔
860
                $this->parent->getIndex($this)
4✔
861
            );
4✔
862
        }
863
        $this->parent = $parent;
54✔
864

865
        return $this;
54✔
866
    }
867

868
    public function setParent(Spreadsheet $parent): self
12✔
869
    {
870
        $this->parent = $parent;
12✔
871

872
        return $this;
12✔
873
    }
874

875
    /**
876
     * Get title.
877
     */
878
    public function getTitle(): string
11,444✔
879
    {
880
        return $this->title;
11,444✔
881
    }
882

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

904
        // Old title
905
        $oldTitle = $this->getTitle();
11,444✔
906

907
        if ($validate) {
11,444✔
908
            // Syntax check
909
            self::checkSheetTitle($title);
11,444✔
910

911
            if ($this->parent && $this->parent->getIndex($this, true) >= 0) {
11,444✔
912
                // Is there already such sheet name?
913
                if ($this->parent->sheetNameExists($title)) {
848✔
914
                    // Use name, but append with lowest possible integer
915

916
                    if (StringHelper::countCharacters($title) > 29) {
2✔
UNCOV
917
                        $title = StringHelper::substring($title, 0, 29);
×
918
                    }
919
                    $i = 1;
2✔
920
                    while ($this->parent->sheetNameExists($title . ' ' . $i)) {
2✔
921
                        ++$i;
1✔
922
                        if ($i == 10) {
1✔
923
                            if (StringHelper::countCharacters($title) > 28) {
×
UNCOV
924
                                $title = StringHelper::substring($title, 0, 28);
×
925
                            }
926
                        } elseif ($i == 100) {
1✔
927
                            if (StringHelper::countCharacters($title) > 27) {
×
UNCOV
928
                                $title = StringHelper::substring($title, 0, 27);
×
929
                            }
930
                        }
931
                    }
932

933
                    $title .= " $i";
2✔
934
                }
935
            }
936
        }
937

938
        // Set title
939
        $this->title = $title;
11,444✔
940

941
        if ($this->parent && $this->parent->getIndex($this, true) >= 0) {
11,444✔
942
            // New title
943
            $newTitle = $this->getTitle();
1,515✔
944
            $this->parent->getCalculationEngine()
1,515✔
945
                ->renameCalculationCacheForWorksheet($oldTitle, $newTitle);
1,515✔
946
            if ($updateFormulaCellReferences) {
1,515✔
947
                ReferenceHelper::getInstance()->updateNamedFormulae($this->parent, $oldTitle, $newTitle);
848✔
948
            }
949
        }
950

951
        return $this;
11,444✔
952
    }
953

954
    /**
955
     * Get sheet state.
956
     *
957
     * @return string Sheet state (visible, hidden, veryHidden)
958
     */
959
    public function getSheetState(): string
585✔
960
    {
961
        return $this->sheetState;
585✔
962
    }
963

964
    /**
965
     * Set sheet state.
966
     *
967
     * @param string $value Sheet state (visible, hidden, veryHidden)
968
     *
969
     * @return $this
970
     */
971
    public function setSheetState(string $value): static
11,444✔
972
    {
973
        $this->sheetState = $value;
11,444✔
974

975
        return $this;
11,444✔
976
    }
977

978
    /**
979
     * Get page setup.
980
     */
981
    public function getPageSetup(): PageSetup
1,778✔
982
    {
983
        return $this->pageSetup;
1,778✔
984
    }
985

986
    /**
987
     * Set page setup.
988
     *
989
     * @return $this
990
     */
991
    public function setPageSetup(PageSetup $pageSetup): static
1✔
992
    {
993
        $this->pageSetup = $pageSetup;
1✔
994

995
        return $this;
1✔
996
    }
997

998
    /**
999
     * Get page margins.
1000
     */
1001
    public function getPageMargins(): PageMargins
1,767✔
1002
    {
1003
        return $this->pageMargins;
1,767✔
1004
    }
1005

1006
    /**
1007
     * Set page margins.
1008
     *
1009
     * @return $this
1010
     */
1011
    public function setPageMargins(PageMargins $pageMargins): static
1✔
1012
    {
1013
        $this->pageMargins = $pageMargins;
1✔
1014

1015
        return $this;
1✔
1016
    }
1017

1018
    /**
1019
     * Get page header/footer.
1020
     */
1021
    public function getHeaderFooter(): HeaderFooter
649✔
1022
    {
1023
        return $this->headerFooter;
649✔
1024
    }
1025

1026
    /**
1027
     * Set page header/footer.
1028
     *
1029
     * @return $this
1030
     */
1031
    public function setHeaderFooter(HeaderFooter $headerFooter): static
1✔
1032
    {
1033
        $this->headerFooter = $headerFooter;
1✔
1034

1035
        return $this;
1✔
1036
    }
1037

1038
    /**
1039
     * Get sheet view.
1040
     */
1041
    public function getSheetView(): SheetView
680✔
1042
    {
1043
        return $this->sheetView;
680✔
1044
    }
1045

1046
    /**
1047
     * Set sheet view.
1048
     *
1049
     * @return $this
1050
     */
1051
    public function setSheetView(SheetView $sheetView): static
1✔
1052
    {
1053
        $this->sheetView = $sheetView;
1✔
1054

1055
        return $this;
1✔
1056
    }
1057

1058
    /**
1059
     * Get Protection.
1060
     */
1061
    public function getProtection(): Protection
699✔
1062
    {
1063
        return $this->protection;
699✔
1064
    }
1065

1066
    /**
1067
     * Set Protection.
1068
     *
1069
     * @return $this
1070
     */
1071
    public function setProtection(Protection $protection): static
1✔
1072
    {
1073
        $this->protection = $protection;
1✔
1074

1075
        return $this;
1✔
1076
    }
1077

1078
    /**
1079
     * Get highest worksheet column.
1080
     *
1081
     * @param null|int|string $row Return the data highest column for the specified row,
1082
     *                                     or the highest column of any row if no row number is passed
1083
     *
1084
     * @return string Highest column name
1085
     */
1086
    public function getHighestColumn($row = null): string
1,674✔
1087
    {
1088
        if ($row === null) {
1,674✔
1089
            return Coordinate::stringFromColumnIndex($this->cachedHighestColumn);
1,673✔
1090
        }
1091

1092
        return $this->getHighestDataColumn($row);
1✔
1093
    }
1094

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

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

1122
        return $this->getHighestDataRow($column);
1✔
1123
    }
1124

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

1138
    /**
1139
     * Get highest worksheet column and highest row that have cell records.
1140
     *
1141
     * @return array{row: int, column: string} Highest column name and highest row number
1142
     */
1143
    public function getHighestRowAndColumn(): array
1✔
1144
    {
1145
        return $this->cellCollection->getHighestRowAndColumn();
1✔
1146
    }
1147

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

1163
        return $this;
5,112✔
1164
    }
1165

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

1188
        return $this;
124✔
1189
    }
1190

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

1208
        // Shortcut for increased performance for the vast majority of simple cases
1209
        if ($this->cellCollection->has($cellAddress)) {
10,936✔
1210
            /** @var Cell $cell */
1211
            $cell = $this->cellCollection->get($cellAddress);
10,907✔
1212

1213
            return $cell;
10,907✔
1214
        }
1215

1216
        /** @var Worksheet $sheet */
1217
        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
10,936✔
1218
        $cell = $sheet->getCellCollection()->get($finalCoordinate);
10,936✔
1219

1220
        return $cell ?? $sheet->createNewCell($finalCoordinate);
10,936✔
1221
    }
1222

1223
    /**
1224
     * Get the correct Worksheet and coordinate from a coordinate that may
1225
     * contains reference to another sheet or a named range.
1226
     *
1227
     * @return array{0: Worksheet, 1: string}
1228
     */
1229
    private function getWorksheetAndCoordinate(string $coordinate): array
10,960✔
1230
    {
1231
        $sheet = null;
10,960✔
1232
        $finalCoordinate = null;
10,960✔
1233

1234
        // Worksheet reference?
1235
        if (str_contains($coordinate, '!')) {
10,960✔
UNCOV
1236
            $worksheetReference = self::extractSheetTitle($coordinate, true, true);
×
1237

1238
            $sheet = $this->getParentOrThrow()->getSheetByName($worksheetReference[0]);
×
UNCOV
1239
            $finalCoordinate = strtoupper($worksheetReference[1]);
×
1240

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

1256
                /** @phpstan-ignore-next-line */
1257
                $cellCoordinate = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!');
12✔
1258
                $finalCoordinate = str_replace('$', '', $cellCoordinate);
12✔
1259
            }
1260
        }
1261

1262
        if ($sheet === null || $finalCoordinate === null) {
10,960✔
1263
            $sheet = $this;
10,960✔
1264
            $finalCoordinate = strtoupper($coordinate);
10,960✔
1265
        }
1266

1267
        if (Coordinate::coordinateIsRange($finalCoordinate)) {
10,960✔
1268
            throw new Exception('Cell coordinate string can not be a range of cells.');
2✔
1269
        }
1270
        $finalCoordinate = str_replace('$', '', $finalCoordinate);
10,960✔
1271

1272
        return [$sheet, $finalCoordinate];
10,960✔
1273
    }
1274

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

UNCOV
1289
        return null;
×
1290
    }
1291

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

1310
        // Coordinates
1311
        if ($column > $this->cachedHighestColumn) {
10,936✔
1312
            $this->cachedHighestColumn = $column;
7,514✔
1313
        }
1314
        if ($row > $this->cachedHighestRow) {
10,936✔
1315
            $this->cachedHighestRow = $row;
8,957✔
1316
        }
1317

1318
        // Cell needs appropriate xfIndex from dimensions records
1319
        //    but don't create dimension records if they don't already exist
1320
        $rowDimension = $this->rowDimensions[$row] ?? null;
10,936✔
1321
        $columnDimension = $this->columnDimensions[$columnString] ?? null;
10,936✔
1322

1323
        $xfSet = false;
10,936✔
1324
        if ($rowDimension !== null) {
10,936✔
1325
            $rowXf = (int) $rowDimension->getXfIndex();
420✔
1326
            if ($rowXf > 0) {
420✔
1327
                // then there is a row dimension with explicit style, assign it to the cell
1328
                $cell->setXfIndex($rowXf);
203✔
1329
                $xfSet = true;
203✔
1330
            }
1331
        }
1332
        if (!$xfSet && $columnDimension !== null) {
10,936✔
1333
            $colXf = (int) $columnDimension->getXfIndex();
602✔
1334
            if ($colXf > 0) {
602✔
1335
                // then there is a column dimension, assign it to the cell
1336
                $cell->setXfIndex($colXf);
229✔
1337
            }
1338
        }
1339

1340
        return $cell;
10,936✔
1341
    }
1342

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

1354
        return $sheet->getCellCollection()->has($finalCoordinate);
10,872✔
1355
    }
1356

1357
    /**
1358
     * Get row dimension at a specific row.
1359
     *
1360
     * @param int $row Numeric index of the row
1361
     */
1362
    public function getRowDimension(int $row): RowDimension
627✔
1363
    {
1364
        // Get row dimension
1365
        if (!isset($this->rowDimensions[$row])) {
627✔
1366
            $this->rowDimensions[$row] = new RowDimension($row);
627✔
1367

1368
            $this->cachedHighestRow = max($this->cachedHighestRow, $row);
627✔
1369
        }
1370

1371
        return $this->rowDimensions[$row];
627✔
1372
    }
1373

1374
    public function getRowStyle(int $row): ?Style
1✔
1375
    {
1376
        return $this->parent?->getCellXfByIndexOrNull(
1✔
1377
            ($this->rowDimensions[$row] ?? null)?->getXfIndex()
1✔
1378
        );
1✔
1379
    }
1380

1381
    public function rowDimensionExists(int $row): bool
705✔
1382
    {
1383
        return isset($this->rowDimensions[$row]);
705✔
1384
    }
1385

1386
    public function columnDimensionExists(string $column): bool
107✔
1387
    {
1388
        return isset($this->columnDimensions[$column]);
107✔
1389
    }
1390

1391
    /**
1392
     * Get column dimension at a specific column.
1393
     *
1394
     * @param string $column String index of the column eg: 'A'
1395
     */
1396
    public function getColumnDimension(string $column): ColumnDimension
706✔
1397
    {
1398
        // Uppercase coordinate
1399
        $column = strtoupper($column);
706✔
1400

1401
        // Fetch dimensions
1402
        if (!isset($this->columnDimensions[$column])) {
706✔
1403
            $this->columnDimensions[$column] = new ColumnDimension($column);
706✔
1404

1405
            $columnIndex = Coordinate::columnIndexFromString($column);
706✔
1406
            if ($this->cachedHighestColumn < $columnIndex) {
706✔
1407
                $this->cachedHighestColumn = $columnIndex;
488✔
1408
            }
1409
        }
1410

1411
        return $this->columnDimensions[$column];
706✔
1412
    }
1413

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

1424
    public function getColumnStyle(string $column): ?Style
1✔
1425
    {
1426
        return $this->parent?->getCellXfByIndexOrNull(
1✔
1427
            ($this->columnDimensions[$column] ?? null)?->getXfIndex()
1✔
1428
        );
1✔
1429
    }
1430

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

1447
        // set this sheet as active
1448
        $this->getParentOrThrow()->setActiveSheetIndex($this->getParentOrThrow()->getIndex($this));
10,899✔
1449

1450
        // set cell coordinate as active
1451
        $this->setSelectedCells($cellCoordinate);
10,899✔
1452

1453
        return $this->getParentOrThrow()->getCellXfSupervisor();
10,899✔
1454
    }
1455

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

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

1480
        return $retVal;
7✔
1481
    }
1482

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

1495
        foreach ($this->tableCollection as $table) {
6✔
1496
            $range = $table->getRange();
6✔
1497
            if ($cell->isInRange($range)) {
6✔
1498
                $dxfsTableStyle = $table->getStyle()->getTableDxfsStyle();
6✔
1499
                if ($dxfsTableStyle === null || ($dxfsTableStyle->getHeaderRowStyle() === null && $dxfsTableStyle->getFirstRowStripeStyle() === null && $dxfsTableStyle->getSecondRowStripeStyle() === null)) {
6✔
1500
                    $retVal[] = $table;
2✔
1501
                }
1502
            }
1503
        }
1504

1505
        return $retVal;
6✔
1506
    }
1507

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

1528
        $conditionalStyles = [];
820✔
1529
        foreach ($this->conditionalStylesCollection as $keyStylesOrig => $conditionalRange) {
820✔
1530
            $keyStyles = Coordinate::resolveUnionAndIntersection($keyStylesOrig);
225✔
1531
            $keyParts = explode(',', $keyStyles);
225✔
1532
            foreach ($keyParts as $keyPart) {
225✔
1533
                if ($keyPart === $coordinate) {
225✔
1534
                    if ($firstOnly) {
14✔
1535
                        return $conditionalRange;
14✔
1536
                    }
UNCOV
1537
                    $conditionalStyles[$keyStylesOrig] = $conditionalRange;
×
1538

UNCOV
1539
                    break;
×
1540
                } elseif (str_contains($keyPart, ':')) {
220✔
1541
                    if (Coordinate::coordinateIsInsideRange($keyPart, $coordinate)) {
215✔
1542
                        if ($firstOnly) {
201✔
1543
                            return $conditionalRange;
200✔
1544
                        }
1545
                        $conditionalStyles[$keyStylesOrig] = $conditionalRange;
1✔
1546

1547
                        break;
1✔
1548
                    }
1549
                }
1550
            }
1551
        }
1552
        $outArray = [];
643✔
1553
        foreach ($conditionalStyles as $conditionalArray) {
643✔
1554
            foreach ($conditionalArray as $conditional) {
1✔
1555
                $outArray[] = $conditional;
1✔
1556
            }
1557
        }
1558
        usort($outArray, [self::class, 'comparePriority']);
643✔
1559

1560
        return $outArray;
643✔
1561
    }
1562

1563
    private static function comparePriority(Conditional $condA, Conditional $condB): int
1✔
1564
    {
1565
        $a = $condA->getPriority();
1✔
1566
        $b = $condB->getPriority();
1✔
1567
        if ($a === $b) {
1✔
UNCOV
1568
            return 0;
×
1569
        }
1570
        if ($a === 0) {
1✔
UNCOV
1571
            return 1;
×
1572
        }
1573
        if ($b === 0) {
1✔
UNCOV
1574
            return -1;
×
1575
        }
1576

1577
        return ($a < $b) ? -1 : 1;
1✔
1578
    }
1579

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

1593
        return null;
10✔
1594
    }
1595

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

1610
    /**
1611
     * Removes conditional styles for a cell.
1612
     *
1613
     * @param string $coordinate eg: 'A1'
1614
     *
1615
     * @return $this
1616
     */
1617
    public function removeConditionalStyles(string $coordinate): static
55✔
1618
    {
1619
        unset($this->conditionalStylesCollection[strtoupper($coordinate)]);
55✔
1620

1621
        return $this;
55✔
1622
    }
1623

1624
    /**
1625
     * Get collection of conditional styles.
1626
     *
1627
     * @return Conditional[][]
1628
     */
1629
    public function getConditionalStylesCollection(): array
1,461✔
1630
    {
1631
        return $this->conditionalStylesCollection;
1,461✔
1632
    }
1633

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

1646
        return $this;
348✔
1647
    }
1648

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

1672
        // Calculate range outer borders
1673
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
2✔
1674

1675
        // Make sure we can loop upwards on rows and columns
1676
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
2✔
1677
            $tmp = $rangeStart;
×
1678
            $rangeStart = $rangeEnd;
×
UNCOV
1679
            $rangeEnd = $tmp;
×
1680
        }
1681

1682
        // Loop through cells and apply styles
1683
        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
2✔
1684
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
2✔
1685
                $this->getCell(Coordinate::stringFromColumnIndex($col) . $row)->setXfIndex($xfIndex);
2✔
1686
            }
1687
        }
1688

1689
        return $this;
2✔
1690
    }
1691

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

1710
        // Calculate range outer borders
1711
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
18✔
1712

1713
        // Make sure we can loop upwards on rows and columns
1714
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
18✔
1715
            $tmp = $rangeStart;
×
1716
            $rangeStart = $rangeEnd;
×
UNCOV
1717
            $rangeEnd = $tmp;
×
1718
        }
1719

1720
        // Loop through cells and apply styles
1721
        for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
18✔
1722
            for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
18✔
1723
                $this->setConditionalStyles(Coordinate::stringFromColumnIndex($col) . $row, $styles);
18✔
1724
            }
1725
        }
1726

1727
        return $this;
18✔
1728
    }
1729

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

1743
        if ($break === self::BREAK_NONE) {
33✔
1744
            unset($this->rowBreaks[$cellAddress], $this->columnBreaks[$cellAddress]);
7✔
1745
        } elseif ($break === self::BREAK_ROW) {
33✔
1746
            $this->rowBreaks[$cellAddress] = new PageBreak($break, $cellAddress, $max);
23✔
1747
        } elseif ($break === self::BREAK_COLUMN) {
19✔
1748
            $this->columnBreaks[$cellAddress] = new PageBreak($break, $cellAddress, $max);
19✔
1749
        }
1750

1751
        return $this;
33✔
1752
    }
1753

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

1775
        return $breaks;
728✔
1776
    }
1777

1778
    /**
1779
     * Get row breaks.
1780
     *
1781
     * @return PageBreak[]
1782
     */
1783
    public function getRowBreaks(): array
593✔
1784
    {
1785
        /** @var callable $compareFunction */
1786
        $compareFunction = [self::class, 'compareRowBreaks'];
593✔
1787
        uksort($this->rowBreaks, $compareFunction);
593✔
1788

1789
        return $this->rowBreaks;
593✔
1790
    }
1791

1792
    protected static function compareRowBreaks(string $coordinate1, string $coordinate2): int
9✔
1793
    {
1794
        $row1 = Coordinate::indexesFromString($coordinate1)[1];
9✔
1795
        $row2 = Coordinate::indexesFromString($coordinate2)[1];
9✔
1796

1797
        return $row1 - $row2;
9✔
1798
    }
1799

1800
    protected static function compareColumnBreaks(string $coordinate1, string $coordinate2): int
5✔
1801
    {
1802
        $column1 = Coordinate::indexesFromString($coordinate1)[0];
5✔
1803
        $column2 = Coordinate::indexesFromString($coordinate2)[0];
5✔
1804

1805
        return $column1 - $column2;
5✔
1806
    }
1807

1808
    /**
1809
     * Get column breaks.
1810
     *
1811
     * @return PageBreak[]
1812
     */
1813
    public function getColumnBreaks(): array
592✔
1814
    {
1815
        /** @var callable $compareFunction */
1816
        $compareFunction = [self::class, 'compareColumnBreaks'];
592✔
1817
        uksort($this->columnBreaks, $compareFunction);
592✔
1818

1819
        return $this->columnBreaks;
592✔
1820
    }
1821

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

1840
        if (!str_contains($range, ':')) {
180✔
1841
            $range .= ":{$range}";
1✔
1842
        }
1843

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

1848
        $this->mergeCells[$range] = $range;
179✔
1849
        $firstRow = (int) $matches[2];
179✔
1850
        $lastRow = (int) $matches[4];
179✔
1851
        $firstColumn = $matches[1];
179✔
1852
        $lastColumn = $matches[3];
179✔
1853
        $firstColumnIndex = Coordinate::columnIndexFromString($firstColumn);
179✔
1854
        $lastColumnIndex = Coordinate::columnIndexFromString($lastColumn);
179✔
1855
        $numberRows = $lastRow - $firstRow;
179✔
1856
        $numberColumns = $lastColumnIndex - $firstColumnIndex;
179✔
1857

1858
        if ($numberRows === 1 && $numberColumns === 1) {
179✔
1859
            return $this;
36✔
1860
        }
1861

1862
        // create upper left cell if it does not already exist
1863
        $upperLeft = "{$firstColumn}{$firstRow}";
172✔
1864
        if (!$this->cellExists($upperLeft)) {
172✔
1865
            $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL);
37✔
1866
        }
1867

1868
        if ($behaviour !== self::MERGE_CELL_CONTENT_HIDE) {
172✔
1869
            // Blank out the rest of the cells in the range (if they exist)
1870
            if ($numberRows > $numberColumns) {
61✔
1871
                $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft, $behaviour);
20✔
1872
            } else {
1873
                $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft, $behaviour);
41✔
1874
            }
1875
        }
1876

1877
        return $this;
172✔
1878
    }
1879

1880
    private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
20✔
1881
    {
1882
        $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
20✔
1883
            ? [$this->getCell($upperLeft)->getFormattedValue()]
1✔
1884
            : [];
19✔
1885

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

1898
        if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
20✔
1899
            /** @var string[] $leftCellValue */
1900
            $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
1✔
1901
        }
1902
    }
1903

1904
    private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void
41✔
1905
    {
1906
        $leftCellValue = ($behaviour === self::MERGE_CELL_CONTENT_MERGE)
41✔
1907
            ? [$this->getCell($upperLeft)->getFormattedValue()]
4✔
1908
            : [];
37✔
1909

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

1923
        if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) {
41✔
1924
            /** @var string[] $leftCellValue */
1925
            $this->getCell($upperLeft)->setValueExplicit(implode(' ', $leftCellValue), DataType::TYPE_STRING);
4✔
1926
        }
1927
    }
1928

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

1947
        return $leftCellValue;
61✔
1948
    }
1949

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

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

1973
        return $this;
22✔
1974
    }
1975

1976
    /**
1977
     * Get merge cells array.
1978
     *
1979
     * @return string[]
1980
     */
1981
    public function getMergeCells(): array
1,363✔
1982
    {
1983
        return $this->mergeCells;
1,363✔
1984
    }
1985

1986
    /**
1987
     * Set merge cells array for the entire sheet. Use instead mergeCells() to merge
1988
     * a single cell range.
1989
     *
1990
     * @param string[] $mergeCells
1991
     *
1992
     * @return $this
1993
     */
1994
    public function setMergeCells(array $mergeCells): static
127✔
1995
    {
1996
        $this->mergeCells = $mergeCells;
127✔
1997

1998
        return $this;
127✔
1999
    }
2000

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

2016
        if (!$alreadyHashed && $password !== '') {
28✔
2017
            $password = Shared\PasswordHasher::hashPassword($password);
24✔
2018
        }
2019
        $this->protectedCells[$range] = new ProtectedRange($range, $password, $name, $securityDescriptor);
28✔
2020

2021
        return $this;
28✔
2022
    }
2023

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

2037
        if (isset($this->protectedCells[$range])) {
22✔
2038
            unset($this->protectedCells[$range]);
21✔
2039
        } else {
2040
            throw new Exception('Cell range ' . $range . ' not known as protected.');
1✔
2041
        }
2042

2043
        return $this;
21✔
2044
    }
2045

2046
    /**
2047
     * Get protected cells.
2048
     *
2049
     * @return ProtectedRange[]
2050
     */
2051
    public function getProtectedCellRanges(): array
703✔
2052
    {
2053
        return $this->protectedCells;
703✔
2054
    }
2055

2056
    /**
2057
     * Get Autofilter.
2058
     */
2059
    public function getAutoFilter(): AutoFilter
921✔
2060
    {
2061
        return $this->autoFilter;
921✔
2062
    }
2063

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

2081
            $this->autoFilter->setRange($cellRange);
21✔
2082
        }
2083

2084
        return $this;
21✔
2085
    }
2086

2087
    /**
2088
     * Remove autofilter.
2089
     */
2090
    public function removeAutoFilter(): self
1✔
2091
    {
2092
        $this->autoFilter->setRange('');
1✔
2093

2094
        return $this;
1✔
2095
    }
2096

2097
    /**
2098
     * Get collection of Tables.
2099
     *
2100
     * @return ArrayObject<int, Table>
2101
     */
2102
    public function getTableCollection(): ArrayObject
10,930✔
2103
    {
2104
        return $this->tableCollection;
10,930✔
2105
    }
2106

2107
    /**
2108
     * Add Table.
2109
     *
2110
     * @return $this
2111
     */
2112
    public function addTable(Table $table): self
105✔
2113
    {
2114
        $table->setWorksheet($this);
105✔
2115
        $this->tableCollection[] = $table;
105✔
2116

2117
        return $this;
105✔
2118
    }
2119

2120
    /**
2121
     * @return string[] array of Table names
2122
     */
2123
    public function getTableNames(): array
1✔
2124
    {
2125
        $tableNames = [];
1✔
2126

2127
        foreach ($this->tableCollection as $table) {
1✔
2128
            /** @var Table $table */
2129
            $tableNames[] = $table->getName();
1✔
2130
        }
2131

2132
        return $tableNames;
1✔
2133
    }
2134

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

2144
        return ($tableIndex === null) ? null : $this->tableCollection[$tableIndex];
97✔
2145
    }
2146

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

2162
        return null;
41✔
2163
    }
2164

2165
    /**
2166
     * Remove Table by name.
2167
     *
2168
     * @param string $name Table name
2169
     *
2170
     * @return $this
2171
     */
2172
    public function removeTableByName(string $name): self
1✔
2173
    {
2174
        $tableIndex = $this->getTableIndexByName($name);
1✔
2175

2176
        if ($tableIndex !== null) {
1✔
2177
            unset($this->tableCollection[$tableIndex]);
1✔
2178
        }
2179

2180
        return $this;
1✔
2181
    }
2182

2183
    /**
2184
     * Remove collection of Tables.
2185
     */
2186
    public function removeTableCollection(): self
1✔
2187
    {
2188
        $this->tableCollection = new ArrayObject();
1✔
2189

2190
        return $this;
1✔
2191
    }
2192

2193
    /**
2194
     * Get Freeze Pane.
2195
     */
2196
    public function getFreezePane(): ?string
317✔
2197
    {
2198
        return $this->freezePane;
317✔
2199
    }
2200

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

2237
        if ($cellAddress !== null && $topLeftCell === null) {
49✔
2238
            $coordinate = Coordinate::coordinateFromString($cellAddress);
37✔
2239
            $topLeftCell = $coordinate[0] . $coordinate[1];
37✔
2240
        }
2241

2242
        $topLeftCell = "$topLeftCell";
49✔
2243
        $this->paneTopLeftCell = $topLeftCell;
49✔
2244

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

2265
        return $this;
49✔
2266
    }
2267

2268
    public function setTopLeftCell(string $topLeftCell): self
61✔
2269
    {
2270
        $this->topLeftCell = $topLeftCell;
61✔
2271

2272
        return $this;
61✔
2273
    }
2274

2275
    /**
2276
     * Unfreeze Pane.
2277
     *
2278
     * @return $this
2279
     */
2280
    public function unfreezePane(): static
1✔
2281
    {
2282
        return $this->freezePane(null);
1✔
2283
    }
2284

2285
    /**
2286
     * Get the default position of the right bottom pane.
2287
     */
2288
    public function getTopLeftCell(): ?string
525✔
2289
    {
2290
        return $this->topLeftCell;
525✔
2291
    }
2292

2293
    public function getPaneTopLeftCell(): string
11✔
2294
    {
2295
        return $this->paneTopLeftCell;
11✔
2296
    }
2297

2298
    public function setPaneTopLeftCell(string $paneTopLeftCell): self
26✔
2299
    {
2300
        $this->paneTopLeftCell = $paneTopLeftCell;
26✔
2301

2302
        return $this;
26✔
2303
    }
2304

2305
    public function usesPanes(): bool
513✔
2306
    {
2307
        return $this->xSplit > 0 || $this->ySplit > 0;
513✔
2308
    }
2309

2310
    public function getPane(string $position): ?Pane
2✔
2311
    {
2312
        return $this->panes[$position] ?? null;
2✔
2313
    }
2314

2315
    public function setPane(string $position, ?Pane $pane): self
47✔
2316
    {
2317
        if (array_key_exists($position, $this->panes)) {
47✔
2318
            $this->panes[$position] = $pane;
47✔
2319
        }
2320

2321
        return $this;
47✔
2322
    }
2323

2324
    /** @return (null|Pane)[] */
2325
    public function getPanes(): array
3✔
2326
    {
2327
        return $this->panes;
3✔
2328
    }
2329

2330
    public function getActivePane(): string
14✔
2331
    {
2332
        return $this->activePane;
14✔
2333
    }
2334

2335
    public function setActivePane(string $activePane): self
49✔
2336
    {
2337
        $this->activePane = array_key_exists($activePane, $this->panes) ? $activePane : '';
49✔
2338

2339
        return $this;
49✔
2340
    }
2341

2342
    public function getXSplit(): int
11✔
2343
    {
2344
        return $this->xSplit;
11✔
2345
    }
2346

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

2354
        return $this;
11✔
2355
    }
2356

2357
    public function getYSplit(): int
11✔
2358
    {
2359
        return $this->ySplit;
11✔
2360
    }
2361

2362
    public function setYSplit(int $ySplit): self
26✔
2363
    {
2364
        $this->ySplit = $ySplit;
26✔
2365
        if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
26✔
2366
            $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
1✔
2367
        }
2368

2369
        return $this;
26✔
2370
    }
2371

2372
    public function getPaneState(): string
30✔
2373
    {
2374
        return $this->paneState;
30✔
2375
    }
2376

2377
    public const PANE_FROZEN = 'frozen';
2378
    public const PANE_FROZENSPLIT = 'frozenSplit';
2379
    public const PANE_SPLIT = 'split';
2380
    private const VALIDPANESTATE = [self::PANE_FROZEN, self::PANE_SPLIT, self::PANE_FROZENSPLIT];
2381
    private const VALIDFROZENSTATE = [self::PANE_FROZEN, self::PANE_FROZENSPLIT];
2382

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

2392
        return $this;
26✔
2393
    }
2394

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

2412
        return $this;
42✔
2413
    }
2414

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

2432
        return $this;
50✔
2433
    }
2434

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

2449
        throw new Exception('Columns can only be inserted before at least column A (1).');
1✔
2450
    }
2451

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

2502
        $holdRowDimensions = $this->removeRowDimensions($row, $numberOfRows);
52✔
2503
        $highestRow = $this->getHighestDataRow();
52✔
2504
        $removedRowsCounter = 0;
52✔
2505

2506
        for ($r = 0; $r < $numberOfRows; ++$r) {
52✔
2507
            if ($row + $r <= $highestRow) {
52✔
2508
                $this->cellCollection->removeRow($row + $r);
40✔
2509
                ++$removedRowsCounter;
40✔
2510
            }
2511
        }
2512

2513
        $objReferenceHelper = ReferenceHelper::getInstance();
52✔
2514
        $objReferenceHelper->insertNewBefore('A' . ($row + $numberOfRows), 0, -$numberOfRows, $this);
52✔
2515
        for ($r = 0; $r < $removedRowsCounter; ++$r) {
52✔
2516
            $this->cellCollection->removeRow($highestRow);
40✔
2517
            --$highestRow;
40✔
2518
        }
2519

2520
        $this->rowDimensions = $holdRowDimensions;
52✔
2521

2522
        return $this;
52✔
2523
    }
2524

2525
    /** @return RowDimension[] */
2526
    private function removeRowDimensions(int $row, int $numberOfRows): array
52✔
2527
    {
2528
        $highRow = $row + $numberOfRows - 1;
52✔
2529
        $holdRowDimensions = [];
52✔
2530
        foreach ($this->rowDimensions as $rowDimension) {
52✔
2531
            $num = $rowDimension->getRowIndex();
5✔
2532
            if ($num < $row) {
5✔
2533
                $holdRowDimensions[$num] = $rowDimension;
3✔
2534
            } elseif ($num > $highRow) {
5✔
2535
                $num -= $numberOfRows;
4✔
2536
                $cloneDimension = clone $rowDimension;
4✔
2537
                $cloneDimension->setRowIndex($num);
4✔
2538
                $holdRowDimensions[$num] = $cloneDimension;
4✔
2539
            }
2540
        }
2541

2542
        return $holdRowDimensions;
52✔
2543
    }
2544

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

2596
        $highestColumn = $this->getHighestDataColumn();
42✔
2597
        $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
42✔
2598
        $pColumnIndex = Coordinate::columnIndexFromString($column);
42✔
2599

2600
        $holdColumnDimensions = $this->removeColumnDimensions($pColumnIndex, $numberOfColumns);
42✔
2601

2602
        $column = Coordinate::stringFromColumnIndex($pColumnIndex + $numberOfColumns);
42✔
2603
        $objReferenceHelper = ReferenceHelper::getInstance();
42✔
2604
        $objReferenceHelper->insertNewBefore($column . '1', -$numberOfColumns, 0, $this);
42✔
2605

2606
        $this->columnDimensions = $holdColumnDimensions;
42✔
2607

2608
        if ($pColumnIndex > $highestColumnIndex) {
42✔
2609
            return $this;
9✔
2610
        }
2611

2612
        $maxPossibleColumnsToBeRemoved = $highestColumnIndex - $pColumnIndex + 1;
33✔
2613

2614
        for ($c = 0, $n = min($maxPossibleColumnsToBeRemoved, $numberOfColumns); $c < $n; ++$c) {
33✔
2615
            $this->cellCollection->removeColumn($highestColumn);
33✔
2616
            $highestColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($highestColumn) - 1);
33✔
2617
        }
2618

2619
        $this->garbageCollect();
33✔
2620

2621
        return $this;
33✔
2622
    }
2623

2624
    /** @return ColumnDimension[] */
2625
    private function removeColumnDimensions(int $pColumnIndex, int $numberOfColumns): array
42✔
2626
    {
2627
        $highCol = $pColumnIndex + $numberOfColumns - 1;
42✔
2628
        $holdColumnDimensions = [];
42✔
2629
        foreach ($this->columnDimensions as $columnDimension) {
42✔
2630
            $num = $columnDimension->getColumnNumeric();
18✔
2631
            if ($num < $pColumnIndex) {
18✔
2632
                $str = $columnDimension->getColumnIndex();
18✔
2633
                $holdColumnDimensions[$str] = $columnDimension;
18✔
2634
            } elseif ($num > $highCol) {
18✔
2635
                $cloneDimension = clone $columnDimension;
18✔
2636
                $cloneDimension->setColumnNumeric($num - $numberOfColumns);
18✔
2637
                $str = $cloneDimension->getColumnIndex();
18✔
2638
                $holdColumnDimensions[$str] = $cloneDimension;
18✔
2639
            }
2640
        }
2641

2642
        return $holdColumnDimensions;
42✔
2643
    }
2644

2645
    /**
2646
     * Remove a column, updating all possible related data.
2647
     *
2648
     * @param int $columnIndex Remove starting with this column Index (numeric column coordinate)
2649
     * @param int $numColumns Number of columns to remove
2650
     *
2651
     * @return $this
2652
     */
2653
    public function removeColumnByIndex(int $columnIndex, int $numColumns = 1): static
3✔
2654
    {
2655
        if ($columnIndex >= 1) {
3✔
2656
            return $this->removeColumn(Coordinate::stringFromColumnIndex($columnIndex), $numColumns);
2✔
2657
        }
2658

2659
        throw new Exception('Columns to be deleted should at least start from column A (1)');
1✔
2660
    }
2661

2662
    /**
2663
     * Show gridlines?
2664
     */
2665
    public function getShowGridlines(): bool
1,156✔
2666
    {
2667
        return $this->showGridlines;
1,156✔
2668
    }
2669

2670
    /**
2671
     * Set show gridlines.
2672
     *
2673
     * @param bool $showGridLines Show gridlines (true/false)
2674
     *
2675
     * @return $this
2676
     */
2677
    public function setShowGridlines(bool $showGridLines): self
930✔
2678
    {
2679
        $this->showGridlines = $showGridLines;
930✔
2680

2681
        return $this;
930✔
2682
    }
2683

2684
    /**
2685
     * Print gridlines?
2686
     */
2687
    public function getPrintGridlines(): bool
1,179✔
2688
    {
2689
        return $this->printGridlines;
1,179✔
2690
    }
2691

2692
    /**
2693
     * Set print gridlines.
2694
     *
2695
     * @param bool $printGridLines Print gridlines (true/false)
2696
     *
2697
     * @return $this
2698
     */
2699
    public function setPrintGridlines(bool $printGridLines): self
608✔
2700
    {
2701
        $this->printGridlines = $printGridLines;
608✔
2702

2703
        return $this;
608✔
2704
    }
2705

2706
    /**
2707
     * Show row and column headers?
2708
     */
2709
    public function getShowRowColHeaders(): bool
589✔
2710
    {
2711
        return $this->showRowColHeaders;
589✔
2712
    }
2713

2714
    /**
2715
     * Set show row and column headers.
2716
     *
2717
     * @param bool $showRowColHeaders Show row and column headers (true/false)
2718
     *
2719
     * @return $this
2720
     */
2721
    public function setShowRowColHeaders(bool $showRowColHeaders): self
445✔
2722
    {
2723
        $this->showRowColHeaders = $showRowColHeaders;
445✔
2724

2725
        return $this;
445✔
2726
    }
2727

2728
    /**
2729
     * Show summary below? (Row/Column outlining).
2730
     */
2731
    public function getShowSummaryBelow(): bool
590✔
2732
    {
2733
        return $this->showSummaryBelow;
590✔
2734
    }
2735

2736
    /**
2737
     * Set show summary below.
2738
     *
2739
     * @param bool $showSummaryBelow Show summary below (true/false)
2740
     *
2741
     * @return $this
2742
     */
2743
    public function setShowSummaryBelow(bool $showSummaryBelow): self
444✔
2744
    {
2745
        $this->showSummaryBelow = $showSummaryBelow;
444✔
2746

2747
        return $this;
444✔
2748
    }
2749

2750
    /**
2751
     * Show summary right? (Row/Column outlining).
2752
     */
2753
    public function getShowSummaryRight(): bool
590✔
2754
    {
2755
        return $this->showSummaryRight;
590✔
2756
    }
2757

2758
    /**
2759
     * Set show summary right.
2760
     *
2761
     * @param bool $showSummaryRight Show summary right (true/false)
2762
     *
2763
     * @return $this
2764
     */
2765
    public function setShowSummaryRight(bool $showSummaryRight): self
444✔
2766
    {
2767
        $this->showSummaryRight = $showSummaryRight;
444✔
2768

2769
        return $this;
444✔
2770
    }
2771

2772
    /**
2773
     * Get comments.
2774
     *
2775
     * @return Comment[]
2776
     */
2777
    public function getComments(): array
1,232✔
2778
    {
2779
        return $this->comments;
1,232✔
2780
    }
2781

2782
    /**
2783
     * Set comments array for the entire sheet.
2784
     *
2785
     * @param Comment[] $comments
2786
     *
2787
     * @return $this
2788
     */
2789
    public function setComments(array $comments): self
127✔
2790
    {
2791
        $this->comments = $comments;
127✔
2792

2793
        return $this;
127✔
2794
    }
2795

2796
    /**
2797
     * Remove comment from cell.
2798
     *
2799
     * @param array{0: int, 1: int}|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
2800
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2801
     *
2802
     * @return $this
2803
     */
2804
    public function removeComment(CellAddress|string|array $cellCoordinate): self
58✔
2805
    {
2806
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
58✔
2807

2808
        if (Coordinate::coordinateIsRange($cellAddress)) {
58✔
2809
            throw new Exception('Cell coordinate string can not be a range of cells.');
1✔
2810
        } elseif (str_contains($cellAddress, '$')) {
57✔
2811
            throw new Exception('Cell coordinate string must not be absolute.');
1✔
2812
        } elseif ($cellAddress == '') {
56✔
2813
            throw new Exception('Cell coordinate can not be zero-length string.');
1✔
2814
        }
2815
        // Check if we have a comment for this cell and delete it
2816
        if (isset($this->comments[$cellAddress])) {
55✔
2817
            unset($this->comments[$cellAddress]);
3✔
2818
        }
2819

2820
        return $this;
55✔
2821
    }
2822

2823
    /**
2824
     * Get comment for cell.
2825
     *
2826
     * @param array{0: int, 1: int}|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
2827
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2828
     */
2829
    public function getComment(CellAddress|string|array $cellCoordinate, bool $attachNew = true): Comment
120✔
2830
    {
2831
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
120✔
2832

2833
        if (Coordinate::coordinateIsRange($cellAddress)) {
120✔
2834
            throw new Exception('Cell coordinate string can not be a range of cells.');
1✔
2835
        } elseif (str_contains($cellAddress, '$')) {
119✔
2836
            throw new Exception('Cell coordinate string must not be absolute.');
1✔
2837
        } elseif ($cellAddress == '') {
118✔
2838
            throw new Exception('Cell coordinate can not be zero-length string.');
1✔
2839
        }
2840

2841
        // Check if we already have a comment for this cell.
2842
        if (isset($this->comments[$cellAddress])) {
117✔
2843
            return $this->comments[$cellAddress];
83✔
2844
        }
2845

2846
        // If not, create a new comment.
2847
        $newComment = new Comment();
117✔
2848
        if ($attachNew) {
117✔
2849
            $this->comments[$cellAddress] = $newComment;
117✔
2850
        }
2851

2852
        return $newComment;
117✔
2853
    }
2854

2855
    /**
2856
     * Get active cell.
2857
     *
2858
     * @return string Example: 'A1'
2859
     */
2860
    public function getActiveCell(): string
10,965✔
2861
    {
2862
        return $this->activeCell;
10,965✔
2863
    }
2864

2865
    /**
2866
     * Get selected cells.
2867
     */
2868
    public function getSelectedCells(): string
11,019✔
2869
    {
2870
        return $this->selectedCells;
11,019✔
2871
    }
2872

2873
    /**
2874
     * Selected cell.
2875
     *
2876
     * @param string $coordinate Cell (i.e. A1)
2877
     *
2878
     * @return $this
2879
     */
2880
    public function setSelectedCell(string $coordinate): static
38✔
2881
    {
2882
        return $this->setSelectedCells($coordinate);
38✔
2883
    }
2884

2885
    /**
2886
     * Select a range of cells.
2887
     *
2888
     * @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'
2889
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2890
     *              or a CellAddress or AddressRange object.
2891
     *
2892
     * @return $this
2893
     */
2894
    public function setSelectedCells(AddressRange|CellAddress|int|string|array $coordinate): static
10,968✔
2895
    {
2896
        if (is_string($coordinate)) {
10,968✔
2897
            $coordinate = Validations::definedNameToCoordinate($coordinate, $this);
10,968✔
2898
        }
2899
        $coordinate = Validations::validateCellOrCellRange($coordinate);
10,968✔
2900

2901
        if (Coordinate::coordinateIsRange($coordinate)) {
10,968✔
2902
            [$first] = Coordinate::splitRange($coordinate);
541✔
2903
            $this->activeCell = $first[0];
541✔
2904
        } else {
2905
            $this->activeCell = $coordinate;
10,937✔
2906
        }
2907
        $this->selectedCells = $coordinate;
10,968✔
2908
        $this->setSelectedCellsActivePane();
10,968✔
2909

2910
        return $this;
10,968✔
2911
    }
2912

2913
    private function setSelectedCellsActivePane(): void
10,969✔
2914
    {
2915
        if (!empty($this->freezePane)) {
10,969✔
2916
            $coordinateC = Coordinate::indexesFromString($this->freezePane);
48✔
2917
            $coordinateT = Coordinate::indexesFromString($this->activeCell);
48✔
2918
            if ($coordinateC[0] === 1) {
48✔
2919
                $activePane = ($coordinateT[1] <= $coordinateC[1]) ? 'topLeft' : 'bottomLeft';
26✔
2920
            } elseif ($coordinateC[1] === 1) {
24✔
2921
                $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'topLeft' : 'topRight';
3✔
2922
            } elseif ($coordinateT[1] <= $coordinateC[1]) {
22✔
2923
                $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'topLeft' : 'topRight';
22✔
2924
            } else {
2925
                $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'bottomLeft' : 'bottomRight';
10✔
2926
            }
2927
            $this->setActivePane($activePane);
48✔
2928
            $this->panes[$activePane] = new Pane($activePane, $this->selectedCells, $this->activeCell);
48✔
2929
        }
2930
    }
2931

2932
    /**
2933
     * Get right-to-left.
2934
     */
2935
    public function getRightToLeft(): bool
1,182✔
2936
    {
2937
        return $this->rightToLeft;
1,182✔
2938
    }
2939

2940
    /**
2941
     * Set right-to-left.
2942
     *
2943
     * @param bool $value Right-to-left true/false
2944
     *
2945
     * @return $this
2946
     */
2947
    public function setRightToLeft(bool $value): static
168✔
2948
    {
2949
        $this->rightToLeft = $value;
168✔
2950

2951
        return $this;
168✔
2952
    }
2953

2954
    /**
2955
     * Fill worksheet from values in array.
2956
     *
2957
     * @param mixed[]|mixed[][] $source Source array
2958
     * @param mixed $nullValue Value in source array that stands for blank cell
2959
     * @param string $startCell Insert array starting from this cell address as the top left coordinate
2960
     * @param bool $strictNullComparison Apply strict comparison when testing for null values in the array
2961
     *
2962
     * @return $this
2963
     */
2964
    public function fromArray(array $source, mixed $nullValue = null, string $startCell = 'A1', bool $strictNullComparison = false): static
907✔
2965
    {
2966
        //    Convert a 1-D array to 2-D (for ease of looping)
2967
        if (!is_array(end($source))) {
907✔
2968
            $source = [$source];
49✔
2969
        }
2970
        /** @var mixed[][] $source */
2971

2972
        // start coordinate
2973
        [$startColumn, $startRow] = Coordinate::coordinateFromString($startCell);
907✔
2974
        $startRow = (int) $startRow;
907✔
2975

2976
        // Loop through $source
2977
        if ($strictNullComparison) {
907✔
2978
            foreach ($source as $rowData) {
410✔
2979
                /** @var string */
2980
                $currentColumn = $startColumn;
410✔
2981
                foreach ($rowData as $cellValue) {
410✔
2982
                    if ($cellValue !== $nullValue) {
410✔
2983
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
410✔
2984
                    }
2985
                    StringHelper::stringIncrement($currentColumn);
410✔
2986
                }
2987
                ++$startRow;
410✔
2988
            }
2989
        } else {
2990
            foreach ($source as $rowData) {
506✔
2991
                $currentColumn = $startColumn;
506✔
2992
                foreach ($rowData as $cellValue) {
506✔
2993
                    if ($cellValue != $nullValue) {
505✔
2994
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
499✔
2995
                    }
2996
                    StringHelper::stringIncrement($currentColumn);
505✔
2997
                }
2998
                ++$startRow;
506✔
2999
            }
3000
        }
3001

3002
        return $this;
907✔
3003
    }
3004

3005
    /**
3006
     * @param bool $calculateFormulas Whether to calculate cell's value if it is a formula.
3007
     * @param null|bool|float|int|RichText|string $nullValue value to use when null
3008
     * @param bool $formatData Whether to format data according to cell's style.
3009
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3010
     * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3011
     *
3012
     * @throws Exception
3013
     * @throws \PhpOffice\PhpSpreadsheet\Calculation\Exception
3014
     */
3015
    protected function cellToArray(Cell $cell, bool $calculateFormulas, bool $formatData, mixed $nullValue, bool $lessFloatPrecision = false, $oldCalculatedValue = false): mixed
201✔
3016
    {
3017
        $returnValue = $nullValue;
201✔
3018

3019
        if ($cell->getValue() !== null) {
201✔
3020
            if ($cell->getValue() instanceof RichText) {
201✔
3021
                $returnValue = $cell->getValue()->getPlainText();
4✔
3022
            } elseif ($calculateFormulas) {
201✔
3023
                $returnValue = $cell->getCalculatedValue();
172✔
3024
            } elseif ($oldCalculatedValue && ($cell->getDataType() === DataType::TYPE_FORMULA)) {
35✔
3025
                $returnValue = $cell->getOldCalculatedValue() ?? $cell->getValue();
2✔
3026
            } else {
3027
                $returnValue = $cell->getValue();
35✔
3028
            }
3029

3030
            if ($formatData) {
201✔
3031
                $style = $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex());
128✔
3032
                /** @var null|bool|float|int|RichText|string */
3033
                $returnValuex = $returnValue;
128✔
3034
                $returnValue = NumberFormat::toFormattedString(
128✔
3035
                    $returnValuex,
128✔
3036
                    $style->getNumberFormat()->getFormatCode() ?? NumberFormat::FORMAT_GENERAL,
128✔
3037
                    lessFloatPrecision: $lessFloatPrecision
128✔
3038
                );
128✔
3039
            }
3040
        }
3041

3042
        return $returnValue;
201✔
3043
    }
3044

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

3074
        // Loop through rows
3075
        foreach ($this->rangeToArrayYieldRows($range, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue) as $rowRef => $rowArray) {
166✔
3076
            /** @var int $rowRef */
3077
            $returnValue[$rowRef] = $rowArray;
166✔
3078
        }
3079

3080
        // Return
3081
        return $returnValue;
166✔
3082
    }
3083

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

3113
        $parts = explode(',', $ranges);
6✔
3114
        foreach ($parts as $part) {
6✔
3115
            // Loop through rows
3116
            foreach ($this->rangeToArrayYieldRows($part, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue) as $rowRef => $rowArray) {
6✔
3117
                /** @var int $rowRef */
3118
                $returnValue[$rowRef] = $rowArray;
6✔
3119
            }
3120
        }
3121

3122
        // Return
3123
        return $returnValue;
6✔
3124
    }
3125

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

3155
        //    Identify the range that we need to extract from the worksheet
3156
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range);
201✔
3157
        $minCol = Coordinate::stringFromColumnIndex($rangeStart[0]);
201✔
3158
        $minRow = $rangeStart[1];
201✔
3159
        $maxCol = Coordinate::stringFromColumnIndex($rangeEnd[0]);
201✔
3160
        $maxRow = $rangeEnd[1];
201✔
3161
        $minColInt = $rangeStart[0];
201✔
3162
        $maxColInt = $rangeEnd[0];
201✔
3163

3164
        StringHelper::stringIncrement($maxCol);
201✔
3165
        /** @var array<string, bool> */
3166
        $hiddenColumns = [];
201✔
3167
        $nullRow = $this->buildNullRow($nullValue, $minCol, $maxCol, $returnCellRef, $ignoreHidden, $hiddenColumns);
201✔
3168
        $hideColumns = !empty($hiddenColumns);
201✔
3169

3170
        $keys = $this->cellCollection->getSortedCoordinatesInt();
201✔
3171
        $keyIndex = 0;
201✔
3172
        $keysCount = count($keys);
201✔
3173
        // Loop through rows
3174
        for ($row = $minRow; $row <= $maxRow; ++$row) {
201✔
3175
            if (($ignoreHidden === true) && ($this->isRowVisible($row) === false)) {
201✔
3176
                continue;
4✔
3177
            }
3178
            $rowRef = $returnCellRef ? $row : ($row - $minRow);
201✔
3179
            $returnValue = $nullRow;
201✔
3180

3181
            $index = ($row - 1) * AddressRange::MAX_COLUMN_INT + 1;
201✔
3182
            $indexPlus = $index + AddressRange::MAX_COLUMN_INT - 1;
201✔
3183

3184
            // Binary search to quickly approach the correct index
3185
            $keyIndex = intdiv($keysCount, 2);
201✔
3186
            $boundLow = 0;
201✔
3187
            $boundHigh = $keysCount - 1;
201✔
3188
            while ($boundLow <= $boundHigh) {
201✔
3189
                $keyIndex = intdiv($boundLow + $boundHigh, 2);
201✔
3190
                if ($keys[$keyIndex] < $index) {
201✔
3191
                    $boundLow = $keyIndex + 1;
167✔
3192
                } elseif ($keys[$keyIndex] > $index) {
201✔
3193
                    $boundHigh = $keyIndex - 1;
183✔
3194
                } else {
3195
                    break;
193✔
3196
                }
3197
            }
3198

3199
            // Realign to the proper index value
3200
            while ($keyIndex > 0 && $keys[$keyIndex] > $index) {
201✔
3201
                --$keyIndex;
14✔
3202
            }
3203
            while ($keyIndex < $keysCount && $keys[$keyIndex] < $index) {
201✔
3204
                ++$keyIndex;
21✔
3205
            }
3206

3207
            while ($keyIndex < $keysCount && $keys[$keyIndex] <= $indexPlus) {
201✔
3208
                $key = $keys[$keyIndex];
201✔
3209
                $thisRow = intdiv($key - 1, AddressRange::MAX_COLUMN_INT) + 1;
201✔
3210
                $thisCol = ($key % AddressRange::MAX_COLUMN_INT) ?: AddressRange::MAX_COLUMN_INT;
201✔
3211
                if ($thisCol >= $minColInt && $thisCol <= $maxColInt) {
201✔
3212
                    $col = Coordinate::stringFromColumnIndex($thisCol);
201✔
3213
                    if ($hideColumns === false || !isset($hiddenColumns[$col])) {
201✔
3214
                        $columnRef = $returnCellRef ? $col : ($thisCol - $minColInt);
201✔
3215
                        $cell = $this->cellCollection->get("{$col}{$thisRow}");
201✔
3216
                        if ($cell !== null) {
201✔
3217
                            $value = $this->cellToArray($cell, $calculateFormulas, $formatData, $nullValue, lessFloatPrecision: $lessFloatPrecision, oldCalculatedValue: $oldCalculatedValue);
201✔
3218
                            if ($reduceArrays) {
201✔
3219
                                while (is_array($value)) {
21✔
3220
                                    $value = array_shift($value);
19✔
3221
                                }
3222
                            }
3223
                            if ($value !== $nullValue) {
201✔
3224
                                $returnValue[$columnRef] = $value;
201✔
3225
                            }
3226
                        }
3227
                    }
3228
                }
3229
                ++$keyIndex;
201✔
3230
            }
3231

3232
            yield $rowRef => $returnValue;
201✔
3233
        }
3234
    }
3235

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

3269
        return $nullRow;
201✔
3270
    }
3271

3272
    private function validateNamedRange(string $definedName, bool $returnNullIfInvalid = false): ?DefinedName
19✔
3273
    {
3274
        $namedRange = DefinedName::resolveName($definedName, $this);
19✔
3275
        if ($namedRange === null) {
19✔
3276
            if ($returnNullIfInvalid) {
6✔
3277
                return null;
5✔
3278
            }
3279

3280
            throw new Exception('Named Range ' . $definedName . ' does not exist.');
1✔
3281
        }
3282

3283
        if ($namedRange->isFormula()) {
13✔
3284
            if ($returnNullIfInvalid) {
×
UNCOV
3285
                return null;
×
3286
            }
3287

UNCOV
3288
            throw new Exception('Defined Named ' . $definedName . ' is a formula, not a range or cell.');
×
3289
        }
3290

3291
        if ($namedRange->getLocalOnly()) {
13✔
3292
            $worksheet = $namedRange->getWorksheet();
2✔
3293
            if ($worksheet === null || $this !== $worksheet) {
2✔
3294
                if ($returnNullIfInvalid) {
×
UNCOV
3295
                    return null;
×
3296
                }
3297

3298
                throw new Exception(
×
3299
                    'Named range ' . $definedName . ' is not accessible from within sheet ' . $this->getTitle()
×
UNCOV
3300
                );
×
3301
            }
3302
        }
3303

3304
        return $namedRange;
13✔
3305
    }
3306

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

3346
        return $retVal;
1✔
3347
    }
3348

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

3379
        //    Identify the range that we need to extract from the worksheet
3380
        $maxCol = $this->getHighestColumn();
97✔
3381
        $maxRow = $this->getHighestRow();
97✔
3382

3383
        // Return
3384
        return $this->rangeToArray("A1:{$maxCol}{$maxRow}", $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue);
97✔
3385
    }
3386

3387
    /**
3388
     * Get row iterator.
3389
     *
3390
     * @param int $startRow The row number at which to start iterating
3391
     * @param ?int $endRow The row number at which to stop iterating
3392
     */
3393
    public function getRowIterator(int $startRow = 1, ?int $endRow = null): RowIterator
117✔
3394
    {
3395
        return new RowIterator($this, $startRow, $endRow);
117✔
3396
    }
3397

3398
    /**
3399
     * Get column iterator.
3400
     *
3401
     * @param string $startColumn The column address at which to start iterating
3402
     * @param ?string $endColumn The column address at which to stop iterating
3403
     */
3404
    public function getColumnIterator(string $startColumn = 'A', ?string $endColumn = null): ColumnIterator
39✔
3405
    {
3406
        return new ColumnIterator($this, $startColumn, $endColumn);
39✔
3407
    }
3408

3409
    /**
3410
     * Run PhpSpreadsheet garbage collector.
3411
     *
3412
     * @return $this
3413
     */
3414
    public function garbageCollect(): static
1,328✔
3415
    {
3416
        // Flush cache
3417
        $this->cellCollection->get('A1');
1,328✔
3418

3419
        // Lookup highest column and highest row if cells are cleaned
3420
        $colRow = $this->cellCollection->getHighestRowAndColumn();
1,328✔
3421
        $highestRow = $colRow['row'];
1,328✔
3422
        $highestColumn = Coordinate::columnIndexFromString($colRow['column']);
1,328✔
3423

3424
        // Loop through column dimensions
3425
        foreach ($this->columnDimensions as $dimension) {
1,328✔
3426
            $highestColumn = max($highestColumn, Coordinate::columnIndexFromString($dimension->getColumnIndex()));
195✔
3427
        }
3428

3429
        // Loop through row dimensions
3430
        foreach ($this->rowDimensions as $dimension) {
1,328✔
3431
            $highestRow = max($highestRow, $dimension->getRowIndex());
124✔
3432
        }
3433

3434
        // Cache values
3435
        $this->cachedHighestColumn = max(1, $highestColumn);
1,328✔
3436
        /** @var int $highestRow */
3437
        $this->cachedHighestRow = $highestRow;
1,328✔
3438

3439
        // Return
3440
        return $this;
1,328✔
3441
    }
3442

3443
    /**
3444
     * @deprecated 5.2.0 Serves no useful purpose. No replacement.
3445
     *
3446
     * @codeCoverageIgnore
3447
     */
3448
    public function getHashInt(): int
3449
    {
3450
        return spl_object_id($this);
3451
    }
3452

3453
    /**
3454
     * Extract worksheet title from range.
3455
     *
3456
     * Example: extractSheetTitle("testSheet!A1") ==> 'A1'
3457
     * Example: extractSheetTitle("testSheet!A1:C3") ==> 'A1:C3'
3458
     * Example: extractSheetTitle("'testSheet 1'!A1", true) ==> ['testSheet 1', 'A1'];
3459
     * Example: extractSheetTitle("'testSheet 1'!A1:C3", true) ==> ['testSheet 1', 'A1:C3'];
3460
     * Example: extractSheetTitle("A1", true) ==> ['', 'A1'];
3461
     * Example: extractSheetTitle("A1:C3", true) ==> ['', 'A1:C3']
3462
     *
3463
     * @param ?string $range Range to extract title from
3464
     * @param bool $returnRange Return range? (see example)
3465
     *
3466
     * @return ($range is non-empty-string ? ($returnRange is true ? array{0: string, 1: string} : string) : ($returnRange is true ? array{0: null, 1: null} : null))
3467
     */
3468
    public static function extractSheetTitle(?string $range, bool $returnRange = false, bool $unapostrophize = false): array|null|string
11,199✔
3469
    {
3470
        if (empty($range)) {
11,199✔
3471
            return $returnRange ? [null, null] : null;
13✔
3472
        }
3473

3474
        // Sheet title included?
3475
        if (($sep = strrpos($range, '!')) === false) {
11,197✔
3476
            return $returnRange ? ['', $range] : '';
11,168✔
3477
        }
3478

3479
        if ($returnRange) {
1,494✔
3480
            $title = substr($range, 0, $sep);
1,494✔
3481
            if ($unapostrophize) {
1,494✔
3482
                $title = self::unApostrophizeTitle($title);
1,434✔
3483
            }
3484

3485
            return [$title, substr($range, $sep + 1)];
1,494✔
3486
        }
3487

3488
        return substr($range, $sep + 1);
7✔
3489
    }
3490

3491
    public static function unApostrophizeTitle(?string $title): string
1,448✔
3492
    {
3493
        $title ??= '';
1,448✔
3494
        if (str_starts_with($title, "'") && str_ends_with($title, "'")) {
1,448✔
3495
            $title = str_replace("''", "'", substr($title, 1, -1));
1,372✔
3496
        }
3497

3498
        return $title;
1,448✔
3499
    }
3500

3501
    /**
3502
     * Get hyperlink.
3503
     *
3504
     * @param string $cellCoordinate Cell coordinate to get hyperlink for, eg: 'A1'
3505
     */
3506
    public function getHyperlink(string $cellCoordinate): Hyperlink
113✔
3507
    {
3508
        $this->getCell($cellCoordinate)->setHadHyperlink(true);
113✔
3509
        // return hyperlink if we already have one
3510
        if (isset($this->hyperlinkCollection[$cellCoordinate])) {
113✔
3511
            return $this->hyperlinkCollection[$cellCoordinate];
61✔
3512
        }
3513

3514
        // else create hyperlink
3515
        $this->hyperlinkCollection[$cellCoordinate] = new Hyperlink();
113✔
3516

3517
        return $this->hyperlinkCollection[$cellCoordinate];
113✔
3518
    }
3519

3520
    /**
3521
     * Set hyperlink.
3522
     *
3523
     * @param string $cellCoordinate Cell coordinate to insert hyperlink, eg: 'A1'
3524
     *
3525
     * @return $this
3526
     */
3527
    public function setHyperlink(string $cellCoordinate, ?Hyperlink $hyperlink = null, bool $reset = true): static
592✔
3528
    {
3529
        if ($hyperlink === null) {
592✔
3530
            unset($this->hyperlinkCollection[$cellCoordinate]);
591✔
3531
            if ($reset) {
591✔
3532
                $this->getCell($cellCoordinate)
557✔
3533
                    ->setHadHyperlink(false);
557✔
3534
            }
3535
        } else {
3536
            $this->hyperlinkCollection[$cellCoordinate] = $hyperlink;
36✔
3537
            $this->getCell($cellCoordinate)->setHadHyperlink(true);
36✔
3538
        }
3539

3540
        return $this;
592✔
3541
    }
3542

3543
    /**
3544
     * Hyperlink at a specific coordinate exists?
3545
     *
3546
     * @param string $coordinate eg: 'A1'
3547
     */
3548
    public function hyperlinkExists(string $coordinate): bool
688✔
3549
    {
3550
        return isset($this->hyperlinkCollection[$coordinate]);
688✔
3551
    }
3552

3553
    /**
3554
     * Get collection of hyperlinks.
3555
     *
3556
     * @return Hyperlink[]
3557
     */
3558
    public function getHyperlinkCollection(): array
698✔
3559
    {
3560
        return $this->hyperlinkCollection;
698✔
3561
    }
3562

3563
    /**
3564
     * Get data validation.
3565
     *
3566
     * @param string $cellCoordinate Cell coordinate to get data validation for, eg: 'A1'
3567
     */
3568
    public function getDataValidation(string $cellCoordinate): DataValidation
37✔
3569
    {
3570
        // return data validation if we already have one
3571
        if (isset($this->dataValidationCollection[$cellCoordinate])) {
37✔
3572
            return $this->dataValidationCollection[$cellCoordinate];
28✔
3573
        }
3574

3575
        // or if cell is part of a data validation range
3576
        foreach ($this->dataValidationCollection as $key => $dataValidation) {
28✔
3577
            $keyParts = explode(' ', $key);
12✔
3578
            foreach ($keyParts as $keyPart) {
12✔
3579
                if ($keyPart === $cellCoordinate) {
12✔
3580
                    return $dataValidation;
1✔
3581
                }
3582
                if (str_contains($keyPart, ':')) {
12✔
3583
                    if (Coordinate::coordinateIsInsideRange($keyPart, $cellCoordinate)) {
9✔
3584
                        return $dataValidation;
9✔
3585
                    }
3586
                }
3587
            }
3588
        }
3589

3590
        // else create data validation
3591
        $dataValidation = new DataValidation();
20✔
3592
        $dataValidation->setSqref($cellCoordinate);
20✔
3593
        $this->dataValidationCollection[$cellCoordinate] = $dataValidation;
20✔
3594

3595
        return $dataValidation;
20✔
3596
    }
3597

3598
    /**
3599
     * Set data validation.
3600
     *
3601
     * @param string $cellCoordinate Cell coordinate to insert data validation, eg: 'A1'
3602
     *
3603
     * @return $this
3604
     */
3605
    public function setDataValidation(string $cellCoordinate, ?DataValidation $dataValidation = null): static
92✔
3606
    {
3607
        if ($dataValidation === null) {
92✔
3608
            unset($this->dataValidationCollection[$cellCoordinate]);
59✔
3609
        } else {
3610
            $dataValidation->setSqref($cellCoordinate);
40✔
3611
            $this->dataValidationCollection[$cellCoordinate] = $dataValidation;
40✔
3612
        }
3613

3614
        return $this;
92✔
3615
    }
3616

3617
    /**
3618
     * Data validation at a specific coordinate exists?
3619
     *
3620
     * @param string $coordinate eg: 'A1'
3621
     */
3622
    public function dataValidationExists(string $coordinate): bool
25✔
3623
    {
3624
        if (isset($this->dataValidationCollection[$coordinate])) {
25✔
3625
            return true;
23✔
3626
        }
3627
        foreach ($this->dataValidationCollection as $key => $dataValidation) {
8✔
3628
            $keyParts = explode(' ', $key);
7✔
3629
            foreach ($keyParts as $keyPart) {
7✔
3630
                if ($keyPart === $coordinate) {
7✔
3631
                    return true;
1✔
3632
                }
3633
                if (str_contains($keyPart, ':')) {
7✔
3634
                    if (Coordinate::coordinateIsInsideRange($keyPart, $coordinate)) {
2✔
3635
                        return true;
2✔
3636
                    }
3637
                }
3638
            }
3639
        }
3640

3641
        return false;
6✔
3642
    }
3643

3644
    /**
3645
     * Get collection of data validations.
3646
     *
3647
     * @return DataValidation[]
3648
     */
3649
    public function getDataValidationCollection(): array
699✔
3650
    {
3651
        $collectionCells = [];
699✔
3652
        $collectionRanges = [];
699✔
3653
        foreach ($this->dataValidationCollection as $key => $dataValidation) {
699✔
3654
            if (Preg::isMatch('/[: ]/', $key)) {
27✔
3655
                $collectionRanges[$key] = $dataValidation;
15✔
3656
            } else {
3657
                $collectionCells[$key] = $dataValidation;
22✔
3658
            }
3659
        }
3660

3661
        return array_merge($collectionCells, $collectionRanges);
699✔
3662
    }
3663

3664
    /**
3665
     * Accepts a range, returning it as a range that falls within the current highest row and column of the worksheet.
3666
     *
3667
     * @return string Adjusted range value
3668
     */
UNCOV
3669
    public function shrinkRangeToFit(string $range): string
×
3670
    {
3671
        $maxCol = $this->getHighestColumn();
×
3672
        $maxRow = $this->getHighestRow();
×
UNCOV
3673
        $maxCol = Coordinate::columnIndexFromString($maxCol);
×
3674

3675
        $rangeBlocks = explode(' ', $range);
×
3676
        foreach ($rangeBlocks as &$rangeSet) {
×
UNCOV
3677
            $rangeBoundaries = Coordinate::getRangeBoundaries($rangeSet);
×
3678

3679
            if (Coordinate::columnIndexFromString($rangeBoundaries[0][0]) > $maxCol) {
×
UNCOV
3680
                $rangeBoundaries[0][0] = Coordinate::stringFromColumnIndex($maxCol);
×
3681
            }
3682
            if ($rangeBoundaries[0][1] > $maxRow) {
×
UNCOV
3683
                $rangeBoundaries[0][1] = $maxRow;
×
3684
            }
3685
            if (Coordinate::columnIndexFromString($rangeBoundaries[1][0]) > $maxCol) {
×
UNCOV
3686
                $rangeBoundaries[1][0] = Coordinate::stringFromColumnIndex($maxCol);
×
3687
            }
3688
            if ($rangeBoundaries[1][1] > $maxRow) {
×
UNCOV
3689
                $rangeBoundaries[1][1] = $maxRow;
×
3690
            }
UNCOV
3691
            $rangeSet = $rangeBoundaries[0][0] . $rangeBoundaries[0][1] . ':' . $rangeBoundaries[1][0] . $rangeBoundaries[1][1];
×
3692
        }
UNCOV
3693
        unset($rangeSet);
×
3694

UNCOV
3695
        return implode(' ', $rangeBlocks);
×
3696
    }
3697

3698
    /**
3699
     * Get tab color.
3700
     */
3701
    public function getTabColor(): Color
23✔
3702
    {
3703
        if ($this->tabColor === null) {
23✔
3704
            $this->tabColor = new Color();
23✔
3705
        }
3706

3707
        return $this->tabColor;
23✔
3708
    }
3709

3710
    /**
3711
     * Reset tab color.
3712
     *
3713
     * @return $this
3714
     */
3715
    public function resetTabColor(): static
1✔
3716
    {
3717
        $this->tabColor = null;
1✔
3718

3719
        return $this;
1✔
3720
    }
3721

3722
    /**
3723
     * Tab color set?
3724
     */
3725
    public function isTabColorSet(): bool
591✔
3726
    {
3727
        return $this->tabColor !== null;
591✔
3728
    }
3729

3730
    /**
3731
     * Copy worksheet (!= clone!).
3732
     */
UNCOV
3733
    public function copy(): static
×
3734
    {
UNCOV
3735
        return clone $this;
×
3736
    }
3737

3738
    /**
3739
     * Returns a boolean true if the specified row contains no cells. By default, this means that no cell records
3740
     *          exist in the collection for this row. false will be returned otherwise.
3741
     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
3742
     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
3743
     *                  cells, then the row will be considered empty.
3744
     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
3745
     *                  string value cells, then the row will be considered empty.
3746
     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3747
     *                  If the only cells in the collection are null value or empty string value cells, then the row
3748
     *                  will be considered empty.
3749
     *
3750
     * @param int $definitionOfEmptyFlags
3751
     *              Possible Flag Values are:
3752
     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
3753
     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3754
     */
3755
    public function isEmptyRow(int $rowId, int $definitionOfEmptyFlags = 0): bool
9✔
3756
    {
3757
        try {
3758
            $iterator = new RowIterator($this, $rowId, $rowId);
9✔
3759
            $iterator->seek($rowId);
8✔
3760
            $row = $iterator->current();
8✔
3761
        } catch (Exception) {
1✔
3762
            return true;
1✔
3763
        }
3764

3765
        return $row->isEmpty($definitionOfEmptyFlags);
8✔
3766
    }
3767

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

3795
        return $column->isEmpty($definitionOfEmptyFlags);
8✔
3796
    }
3797

3798
    /**
3799
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
3800
     */
3801
    public function __clone()
27✔
3802
    {
3803
        foreach (get_object_vars($this) as $key => $val) {
27✔
3804
            if ($key == 'parent') {
27✔
3805
                continue;
27✔
3806
            }
3807

3808
            if (is_object($val) || (is_array($val))) {
27✔
3809
                if ($key === 'cellCollection') {
27✔
3810
                    $newCollection = $this->cellCollection->cloneCellCollection($this);
27✔
3811
                    $this->cellCollection = $newCollection;
27✔
3812
                } elseif ($key === 'drawingCollection') {
27✔
3813
                    $currentCollection = $this->drawingCollection;
27✔
3814
                    $this->drawingCollection = new ArrayObject();
27✔
3815
                    foreach ($currentCollection as $item) {
27✔
3816
                        $newDrawing = clone $item;
4✔
3817
                        $newDrawing->setWorksheet($this);
4✔
3818
                    }
3819
                } elseif ($key === 'inCellDrawingCollection') {
27✔
3820
                    $currentCollection = $this->inCellDrawingCollection;
27✔
3821
                    $this->inCellDrawingCollection = new ArrayObject();
27✔
3822
                    foreach ($currentCollection as $item) {
27✔
3823
                        $newDrawing = clone $item;
1✔
3824
                        $newDrawing->setWorksheet($this);
1✔
3825
                    }
3826
                } elseif ($key === 'tableCollection') {
27✔
3827
                    $currentCollection = $this->tableCollection;
27✔
3828
                    $this->tableCollection = new ArrayObject();
27✔
3829
                    foreach ($currentCollection as $item) {
27✔
3830
                        $newTable = clone $item;
1✔
3831
                        $newTable->setName($item->getName() . 'clone');
1✔
3832
                        $this->addTable($newTable);
1✔
3833
                    }
3834
                } elseif ($key === 'chartCollection') {
27✔
3835
                    $currentCollection = $this->chartCollection;
27✔
3836
                    $this->chartCollection = new ArrayObject();
27✔
3837
                    foreach ($currentCollection as $item) {
27✔
3838
                        $newChart = clone $item;
5✔
3839
                        $this->addChart($newChart);
5✔
3840
                    }
3841
                } elseif ($key === 'autoFilter') {
27✔
3842
                    $newAutoFilter = clone $this->autoFilter;
27✔
3843
                    $this->autoFilter = $newAutoFilter;
27✔
3844
                    $this->autoFilter->setParent($this);
27✔
3845
                } else {
3846
                    $this->{$key} = unserialize(serialize($val));
27✔
3847
                }
3848
            }
3849
        }
3850
    }
3851

3852
    /**
3853
     * Define the code name of the sheet.
3854
     *
3855
     * @param string $codeName Same rule as Title minus space not allowed (but, like Excel, change
3856
     *                       silently space to underscore)
3857
     * @param bool $validate False to skip validation of new title. WARNING: This should only be set
3858
     *                       at parse time (by Readers), where titles can be assumed to be valid.
3859
     *
3860
     * @return $this
3861
     */
3862
    public function setCodeName(string $codeName, bool $validate = true): static
11,444✔
3863
    {
3864
        // Is this a 'rename' or not?
3865
        if ($this->getCodeName() == $codeName) {
11,444✔
UNCOV
3866
            return $this;
×
3867
        }
3868

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

3872
            // Syntax check
3873
            // throw an exception if not valid
3874
            self::checkSheetCodeName($codeName);
11,444✔
3875

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

3878
            if ($this->parent !== null) {
11,444✔
3879
                // Is there already such sheet name?
3880
                if ($this->parent->sheetCodeNameExists($codeName)) {
11,403✔
3881
                    // Use name, but append with lowest possible integer
3882

3883
                    if (StringHelper::countCharacters($codeName) > 29) {
737✔
UNCOV
3884
                        $codeName = StringHelper::substring($codeName, 0, 29);
×
3885
                    }
3886
                    $i = 1;
737✔
3887
                    while ($this->getParentOrThrow()->sheetCodeNameExists($codeName . '_' . $i)) {
737✔
3888
                        ++$i;
300✔
3889
                        if ($i == 10) {
300✔
3890
                            if (StringHelper::countCharacters($codeName) > 28) {
2✔
UNCOV
3891
                                $codeName = StringHelper::substring($codeName, 0, 28);
×
3892
                            }
3893
                        } elseif ($i == 100) {
300✔
3894
                            if (StringHelper::countCharacters($codeName) > 27) {
×
UNCOV
3895
                                $codeName = StringHelper::substring($codeName, 0, 27);
×
3896
                            }
3897
                        }
3898
                    }
3899

3900
                    $codeName .= '_' . $i; // ok, we have a valid name
737✔
3901
                }
3902
            }
3903
        }
3904

3905
        $this->codeName = $codeName;
11,444✔
3906

3907
        return $this;
11,444✔
3908
    }
3909

3910
    /**
3911
     * Return the code name of the sheet.
3912
     */
3913
    public function getCodeName(): ?string
11,444✔
3914
    {
3915
        return $this->codeName;
11,444✔
3916
    }
3917

3918
    /**
3919
     * Sheet has a code name ?
3920
     */
3921
    public function hasCodeName(): bool
2✔
3922
    {
3923
        return $this->codeName !== null;
2✔
3924
    }
3925

3926
    public static function nameRequiresQuotes(string $sheetName): bool
4✔
3927
    {
3928
        return !Preg::isMatch(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName);
4✔
3929
    }
3930

3931
    public function isRowVisible(int $row): bool
130✔
3932
    {
3933
        return !$this->rowDimensionExists($row) || $this->getRowDimension($row)->getVisible();
130✔
3934
    }
3935

3936
    /**
3937
     * Same as Cell->isLocked, but without creating cell if it doesn't exist.
3938
     */
3939
    public function isCellLocked(string $coordinate): bool
1✔
3940
    {
3941
        if ($this->getProtection()->getsheet() !== true) {
1✔
3942
            return false;
1✔
3943
        }
3944
        if ($this->cellExists($coordinate)) {
1✔
3945
            return $this->getCell($coordinate)->isLocked();
1✔
3946
        }
3947
        $spreadsheet = $this->parent;
1✔
3948
        $xfIndex = $this->getXfIndex($coordinate);
1✔
3949
        if ($spreadsheet === null || $xfIndex === null) {
1✔
3950
            return true;
1✔
3951
        }
3952

UNCOV
3953
        return $spreadsheet->getCellXfByIndex($xfIndex)->getProtection()->getLocked() !== StyleProtection::PROTECTION_UNPROTECTED;
×
3954
    }
3955

3956
    /**
3957
     * Same as Cell->isHiddenOnFormulaBar, but without creating cell if it doesn't exist.
3958
     */
3959
    public function isCellHiddenOnFormulaBar(string $coordinate): bool
1✔
3960
    {
3961
        if ($this->cellExists($coordinate)) {
1✔
3962
            return $this->getCell($coordinate)->isHiddenOnFormulaBar();
1✔
3963
        }
3964

3965
        // cell doesn't exist, therefore isn't a formula,
3966
        // therefore isn't hidden on formula bar.
3967
        return false;
1✔
3968
    }
3969

3970
    private function getXfIndex(string $coordinate): ?int
1✔
3971
    {
3972
        [$column, $row] = Coordinate::coordinateFromString($coordinate);
1✔
3973
        $row = (int) $row;
1✔
3974
        $xfIndex = null;
1✔
3975
        if ($this->rowDimensionExists($row)) {
1✔
UNCOV
3976
            $xfIndex = $this->getRowDimension($row)->getXfIndex();
×
3977
        }
3978
        if ($xfIndex === null && $this->ColumnDimensionExists($column)) {
1✔
UNCOV
3979
            $xfIndex = $this->getColumnDimension($column)->getXfIndex();
×
3980
        }
3981

3982
        return $xfIndex;
1✔
3983
    }
3984

3985
    private string $backgroundImage = '';
3986

3987
    private string $backgroundMime = '';
3988

3989
    private string $backgroundExtension = '';
3990

3991
    public function getBackgroundImage(): string
1,106✔
3992
    {
3993
        return $this->backgroundImage;
1,106✔
3994
    }
3995

3996
    public function getBackgroundMime(): string
444✔
3997
    {
3998
        return $this->backgroundMime;
444✔
3999
    }
4000

4001
    public function getBackgroundExtension(): string
444✔
4002
    {
4003
        return $this->backgroundExtension;
444✔
4004
    }
4005

4006
    /**
4007
     * Set background image.
4008
     * Used on read/write for Xlsx.
4009
     * Used on write for Html.
4010
     *
4011
     * @param string $backgroundImage Image represented as a string, e.g. results of file_get_contents
4012
     */
4013
    public function setBackgroundImage(string $backgroundImage): self
4✔
4014
    {
4015
        $imageArray = getimagesizefromstring($backgroundImage) ?: ['mime' => ''];
4✔
4016
        $mime = $imageArray['mime'];
4✔
4017
        if ($mime !== '') {
4✔
4018
            $extension = explode('/', $mime);
3✔
4019
            $extension = $extension[1];
3✔
4020
            $this->backgroundImage = $backgroundImage;
3✔
4021
            $this->backgroundMime = $mime;
3✔
4022
            $this->backgroundExtension = $extension;
3✔
4023
        }
4024

4025
        return $this;
4✔
4026
    }
4027

4028
    /**
4029
     * Copy cells, adjusting relative cell references in formulas.
4030
     * Acts similarly to Excel "fill handle" feature.
4031
     *
4032
     * @param string $fromCell Single source cell, e.g. C3
4033
     * @param string $toCells Single cell or cell range, e.g. C4 or C4:C10
4034
     * @param bool $copyStyle Copy styles as well as values, defaults to true
4035
     */
4036
    public function copyCells(string $fromCell, string $toCells, bool $copyStyle = true): void
1✔
4037
    {
4038
        $toArray = Coordinate::extractAllCellReferencesInRange($toCells);
1✔
4039
        $valueString = $this->getCell($fromCell)->getValueString();
1✔
4040
        /** @var mixed[][] */
4041
        $style = $this->getStyle($fromCell)->exportArray();
1✔
4042
        $fromIndexes = Coordinate::indexesFromString($fromCell);
1✔
4043
        $referenceHelper = ReferenceHelper::getInstance();
1✔
4044
        foreach ($toArray as $destination) {
1✔
4045
            if ($destination !== $fromCell) {
1✔
4046
                $toIndexes = Coordinate::indexesFromString($destination);
1✔
4047
                $this->getCell($destination)->setValue($referenceHelper->updateFormulaReferences($valueString, 'A1', $toIndexes[0] - $fromIndexes[0], $toIndexes[1] - $fromIndexes[1]));
1✔
4048
                if ($copyStyle) {
1✔
4049
                    $this->getCell($destination)->getStyle()->applyFromArray($style);
1✔
4050
                }
4051
            }
4052
        }
4053
    }
4054

4055
    public function calculateArrays(bool $preCalculateFormulas = true): void
1,299✔
4056
    {
4057
        if ($preCalculateFormulas && Calculation::getInstance($this->parent)->getInstanceArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY) {
1,299✔
4058
            $keys = $this->cellCollection->getCoordinates();
52✔
4059
            foreach ($keys as $key) {
52✔
4060
                if ($this->getCell($key)->getDataType() === DataType::TYPE_FORMULA) {
52✔
4061
                    if (!Preg::isMatch(self::FUNCTION_LIKE_GROUPBY, $this->getCell($key)->getValueString())) {
48✔
4062
                        $this->getCell($key)->getCalculatedValue();
47✔
4063
                    }
4064
                }
4065
            }
4066
        }
4067
    }
4068

4069
    public function isCellInSpillRange(string $coordinate): bool
2✔
4070
    {
4071
        if (Calculation::getInstance($this->parent)->getInstanceArrayReturnType() !== Calculation::RETURN_ARRAY_AS_ARRAY) {
2✔
4072
            return false;
1✔
4073
        }
4074
        $this->calculateArrays();
1✔
4075
        $keys = $this->cellCollection->getCoordinates();
1✔
4076
        foreach ($keys as $key) {
1✔
4077
            $attributes = $this->getCell($key)->getFormulaAttributes();
1✔
4078
            if (isset($attributes['ref'])) {
1✔
4079
                if (Coordinate::coordinateIsInsideRange($attributes['ref'], $coordinate)) {
1✔
4080
                    // false for first cell in range, true otherwise
4081
                    return $coordinate !== $key;
1✔
4082
                }
4083
            }
4084
        }
4085

4086
        return false;
1✔
4087
    }
4088

4089
    /** @param mixed[][] $styleArray */
4090
    public function applyStylesFromArray(string $coordinate, array $styleArray): bool
2✔
4091
    {
4092
        $spreadsheet = $this->parent;
2✔
4093
        if ($spreadsheet === null) {
2✔
4094
            return false;
1✔
4095
        }
4096
        $activeSheetIndex = $spreadsheet->getActiveSheetIndex();
1✔
4097
        $originalSelected = $this->selectedCells;
1✔
4098
        $this->getStyle($coordinate)->applyFromArray($styleArray);
1✔
4099
        $this->setSelectedCells($originalSelected);
1✔
4100
        if ($activeSheetIndex >= 0) {
1✔
4101
            $spreadsheet->setActiveSheetIndex($activeSheetIndex);
1✔
4102
        }
4103

4104
        return true;
1✔
4105
    }
4106

4107
    public function copyFormula(string $fromCell, string $toCell): void
1✔
4108
    {
4109
        $formula = $this->getCell($fromCell)->getValue();
1✔
4110
        $newFormula = $formula;
1✔
4111
        if (is_string($formula) && $this->getCell($fromCell)->getDataType() === DataType::TYPE_FORMULA) {
1✔
4112
            [$fromColInt, $fromRow] = Coordinate::indexesFromString($fromCell);
1✔
4113
            [$toColInt, $toRow] = Coordinate::indexesFromString($toCell);
1✔
4114
            $helper = ReferenceHelper::getInstance();
1✔
4115
            $newFormula = $helper->updateFormulaReferences(
1✔
4116
                $formula,
1✔
4117
                'A1',
1✔
4118
                $toColInt - $fromColInt,
1✔
4119
                $toRow - $fromRow
1✔
4120
            );
1✔
4121
        }
4122
        $this->setCellValue($toCell, $newFormula);
1✔
4123
    }
4124
}
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