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

PHPOffice / PhpSpreadsheet / 30319383422

28 Jul 2026 01:08AM UTC coverage: 97.136% (-0.03%) from 97.169%
30319383422

Pull #4942

github

web-flow
Merge ba358e0e3 into 540dfb463
Pull Request #4942: Feature/pivot table read

796 of 836 new or added lines in 12 files covered. (95.22%)

49185 of 50635 relevant lines covered (97.14%)

382.11 hits per line

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

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

3
namespace PhpOffice\PhpSpreadsheet\Worksheet;
4

5
use ArrayObject;
6
use Composer\Pcre\Preg;
7
use Generator;
8
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
9
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
10
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
11
use PhpOffice\PhpSpreadsheet\Cell\Cell;
12
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
13
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
14
use PhpOffice\PhpSpreadsheet\Cell\DataType;
15
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
16
use PhpOffice\PhpSpreadsheet\Cell\Hyperlink;
17
use PhpOffice\PhpSpreadsheet\Cell\IValueBinder;
18
use PhpOffice\PhpSpreadsheet\Chart\Chart;
19
use PhpOffice\PhpSpreadsheet\Collection\Cells;
20
use PhpOffice\PhpSpreadsheet\Collection\CellsFactory;
21
use PhpOffice\PhpSpreadsheet\Comment;
22
use PhpOffice\PhpSpreadsheet\DefinedName;
23
use PhpOffice\PhpSpreadsheet\Exception;
24
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
25
use PhpOffice\PhpSpreadsheet\RichText\RichText;
26
use PhpOffice\PhpSpreadsheet\Shared;
27
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
28
use PhpOffice\PhpSpreadsheet\Spreadsheet;
29
use PhpOffice\PhpSpreadsheet\Style\Alignment;
30
use PhpOffice\PhpSpreadsheet\Style\Color;
31
use PhpOffice\PhpSpreadsheet\Style\Conditional;
32
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
33
use PhpOffice\PhpSpreadsheet\Style\Protection as StyleProtection;
34
use PhpOffice\PhpSpreadsheet\Style\Style;
35
use PhpOffice\PhpSpreadsheet\Worksheet\PivotTable\PivotTable;
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
     * Collection of PivotTable objects.
135
     *
136
     * @var ArrayObject<int, PivotTable>
137
     */
138
    private ArrayObject $pivotTableCollection;
139

140
    /**
141
     * Worksheet title.
142
     */
143
    private string $title = '';
144

145
    /**
146
     * Sheet state.
147
     */
148
    private string $sheetState;
149

150
    /**
151
     * Page setup.
152
     */
153
    private PageSetup $pageSetup;
154

155
    /**
156
     * Page margins.
157
     */
158
    private PageMargins $pageMargins;
159

160
    /**
161
     * Page header/footer.
162
     */
163
    private HeaderFooter $headerFooter;
164

165
    /**
166
     * Sheet view.
167
     */
168
    private SheetView $sheetView;
169

170
    /**
171
     * Protection.
172
     */
173
    private Protection $protection;
174

175
    /**
176
     * Conditional styles. Indexed by cell coordinate, e.g. 'A1'.
177
     *
178
     * @var Conditional[][]
179
     */
180
    private array $conditionalStylesCollection = [];
181

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

189
    /**
190
     * Collection of column breaks.
191
     *
192
     * @var PageBreak[]
193
     */
194
    private array $columnBreaks = [];
195

196
    /**
197
     * Collection of merged cell ranges.
198
     *
199
     * @var string[]
200
     */
201
    private array $mergeCells = [];
202

203
    /**
204
     * Collection of protected cell ranges.
205
     *
206
     * @var ProtectedRange[]
207
     */
208
    private array $protectedCells = [];
209

210
    /**
211
     * Autofilter Range and selection.
212
     */
213
    private AutoFilter $autoFilter;
214

215
    /**
216
     * Freeze pane.
217
     */
218
    private ?string $freezePane = null;
219

220
    /**
221
     * Default position of the right bottom pane.
222
     */
223
    private ?string $topLeftCell = null;
224

225
    private string $paneTopLeftCell = '';
226

227
    private string $activePane = '';
228

229
    private int $xSplit = 0;
230

231
    private int $ySplit = 0;
232

233
    private string $paneState = '';
234

235
    /**
236
     * Properties of the 4 panes.
237
     *
238
     * @var (null|Pane)[]
239
     */
240
    private array $panes = [
241
        'bottomRight' => null,
242
        'bottomLeft' => null,
243
        'topRight' => null,
244
        'topLeft' => null,
245
    ];
246

247
    /**
248
     * Show gridlines?
249
     */
250
    private bool $showGridlines = true;
251

252
    /**
253
     * Print gridlines?
254
     */
255
    private bool $printGridlines = false;
256

257
    /**
258
     * Show row and column headers?
259
     */
260
    private bool $showRowColHeaders = true;
261

262
    /**
263
     * Show summary below? (Row/Column outline).
264
     */
265
    private bool $showSummaryBelow = true;
266

267
    /**
268
     * Show summary right? (Row/Column outline).
269
     */
270
    private bool $showSummaryRight = true;
271

272
    /**
273
     * Collection of comments.
274
     *
275
     * @var Comment[]
276
     */
277
    private array $comments = [];
278

279
    /**
280
     * Active cell. (Only one!).
281
     */
282
    private string $activeCell = 'A1';
283

284
    /**
285
     * Selected cells.
286
     */
287
    private string $selectedCells = 'A1';
288

289
    /**
290
     * Cached highest column.
291
     */
292
    private int $cachedHighestColumn = 1;
293

294
    /**
295
     * Cached highest row.
296
     */
297
    private int $cachedHighestRow = 1;
298

299
    /**
300
     * Right-to-left?
301
     */
302
    private bool $rightToLeft = false;
303

304
    /**
305
     * Hyperlinks. Indexed by cell coordinate, e.g. 'A1'.
306
     *
307
     * @var Hyperlink[]
308
     */
309
    private array $hyperlinkCollection = [];
310

311
    /**
312
     * Data validation objects. Indexed by cell coordinate, e.g. 'A1'.
313
     * Index can include ranges, and multiple cells/ranges.
314
     *
315
     * @var DataValidation[]
316
     */
317
    private array $dataValidationCollection = [];
318

319
    /**
320
     * Tab color.
321
     */
322
    private ?Color $tabColor = null;
323

324
    /**
325
     * CodeName.
326
     */
327
    private ?string $codeName = null;
328

329
    /**
330
     * Create a new worksheet.
331
     */
332
    public function __construct(?Spreadsheet $parent = null, string $title = 'Worksheet')
11,504✔
333
    {
334
        // Set parent and title
335
        $this->parent = $parent;
11,504✔
336
        $this->setTitle($title, false);
11,504✔
337
        // setTitle can change $pTitle
338
        $this->setCodeName($this->getTitle());
11,504✔
339
        $this->setSheetState(self::SHEETSTATE_VISIBLE);
11,504✔
340

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

367
        // Pivot table collection
368
        $this->pivotTableCollection = new ArrayObject();
11,504✔
369
    }
370

371
    /**
372
     * Disconnect all cells from this Worksheet object,
373
     * typically so that the worksheet object can be unset.
374
     * The worksheet will be in an unusable state after
375
     * this method has completed.
376
     */
377
    public function disconnectCells(): void
10,503✔
378
    {
379
        // isset needed to avoid problems at destruct time
380
        if (isset($this->cellCollection)) { //* @phpstan-ignore-line
10,503✔
381
            $this->cellCollection->unsetWorksheetCells();
10,503✔
382
            unset($this->cellCollection);
10,503✔
383
        }
384
        //    detach ourself from the workbook, so that it can then delete this worksheet successfully
385
        $this->parent = null;
10,503✔
386
    }
387

388
    /**
389
     * Code to execute when this worksheet is unset().
390
     */
391
    public function __destruct()
130✔
392
    {
393
        Calculation::getInstanceOrNull($this->parent)
130✔
394
            ?->clearCalculationCacheForWorksheet($this->title);
130✔
395

396
        $this->disconnectCells();
130✔
397
        unset($this->rowDimensions, $this->columnDimensions, $this->tableCollection, $this->drawingCollection, $this->inCellDrawingCollection, $this->chartCollection, $this->autoFilter);
130✔
398
    }
399

400
    /**
401
     * Return the cell collection.
402
     */
403
    public function getCellCollection(): Cells
11,050✔
404
    {
405
        return $this->cellCollection;
11,050✔
406
    }
407

408
    /**
409
     * Get array of invalid characters for sheet title.
410
     *
411
     * @return string[]
412
     */
413
    public static function getInvalidCharacters(): array
1✔
414
    {
415
        return self::INVALID_CHARACTERS;
1✔
416
    }
417

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

440
        // Enforce maximum characters allowed for sheet title
441
        if ($charCount > self::SHEET_TITLE_MAXIMUM_LENGTH) {
11,504✔
442
            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet code name.');
1✔
443
        }
444

445
        return $sheetCodeName;
11,504✔
446
    }
447

448
    /**
449
     * Check sheet title for valid Excel syntax.
450
     *
451
     * @param string $sheetTitle The string to check
452
     *
453
     * @return string The valid string
454
     */
455
    private static function checkSheetTitle(string $sheetTitle): string
11,504✔
456
    {
457
        // Some of the printable ASCII characters are invalid:  * : / \ ? [ ]
458
        if (str_replace(self::INVALID_CHARACTERS, '', $sheetTitle) !== $sheetTitle) {
11,504✔
459
            throw new Exception('Invalid character found in sheet title');
2✔
460
        }
461

462
        // Enforce maximum characters allowed for sheet title
463
        if (StringHelper::countCharacters($sheetTitle) > self::SHEET_TITLE_MAXIMUM_LENGTH) {
11,504✔
464
            throw new Exception('Maximum ' . self::SHEET_TITLE_MAXIMUM_LENGTH . ' characters allowed in sheet title.');
4✔
465
        }
466

467
        return $sheetTitle;
11,504✔
468
    }
469

470
    /**
471
     * Get a sorted list of all cell coordinates currently held in the collection by row and column.
472
     *
473
     * @param bool $sorted Also sort the cell collection?
474
     *
475
     * @return string[]
476
     */
477
    public function getCoordinates(bool $sorted = true): array
1,681✔
478
    {
479
        // isset needed to avoid problems at destruct time
480
        if (!isset($this->cellCollection)) { //* @phpstan-ignore-line
1,681✔
481
            return [];
1✔
482
        }
483

484
        if ($sorted) {
1,681✔
485
            return $this->cellCollection->getSortedCoordinates();
650✔
486
        }
487

488
        return $this->cellCollection->getCoordinates();
1,528✔
489
    }
490

491
    /**
492
     * Get collection of row dimensions.
493
     *
494
     * @return RowDimension[]
495
     */
496
    public function getRowDimensions(): array
1,378✔
497
    {
498
        return $this->rowDimensions;
1,378✔
499
    }
500

501
    /**
502
     * Get default row dimension.
503
     */
504
    public function getDefaultRowDimension(): RowDimension
1,343✔
505
    {
506
        return $this->defaultRowDimension;
1,343✔
507
    }
508

509
    /**
510
     * Get collection of column dimensions.
511
     *
512
     * @return ColumnDimension[]
513
     */
514
    public function getColumnDimensions(): array
1,384✔
515
    {
516
        /** @var callable $callable */
517
        $callable = [self::class, 'columnDimensionCompare'];
1,384✔
518
        uasort($this->columnDimensions, $callable);
1,384✔
519

520
        return $this->columnDimensions;
1,384✔
521
    }
522

523
    private static function columnDimensionCompare(ColumnDimension $a, ColumnDimension $b): int
118✔
524
    {
525
        return $a->getColumnNumeric() - $b->getColumnNumeric();
118✔
526
    }
527

528
    /**
529
     * Get default column dimension.
530
     */
531
    public function getDefaultColumnDimension(): ColumnDimension
674✔
532
    {
533
        return $this->defaultColumnDimension;
674✔
534
    }
535

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

546
    /**
547
     * Get collection of drawings.
548
     *
549
     * @return ArrayObject<int, BaseDrawing>
550
     */
551
    public function getInCellDrawingCollection(): ArrayObject
464✔
552
    {
553
        return $this->inCellDrawingCollection;
464✔
554
    }
555

556
    /**
557
     * Get collection of charts.
558
     *
559
     * @return ArrayObject<int, Chart>
560
     */
561
    public function getChartCollection(): ArrayObject
106✔
562
    {
563
        return $this->chartCollection;
106✔
564
    }
565

566
    public function addChart(Chart $chart): Chart
112✔
567
    {
568
        $chart->setWorksheet($this);
112✔
569
        $this->chartCollection[] = $chart;
112✔
570

571
        return $chart;
112✔
572
    }
573

574
    /**
575
     * Return the count of charts on this worksheet.
576
     *
577
     * @return int The number of charts
578
     */
579
    public function getChartCount(): int
87✔
580
    {
581
        return count($this->chartCollection);
87✔
582
    }
583

584
    /**
585
     * Get a chart by its index position.
586
     *
587
     * @param null|int|string $index Chart index position
588
     *
589
     * @return Chart|false
590
     */
591
    public function getChartByIndex(null|int|string $index)
1✔
592
    {
593
        $chartCount = count($this->chartCollection);
1✔
594
        if ($chartCount === 0 || (is_string($index) && $index !== (string) (int) $index)) {
1✔
595
            return false;
1✔
596
        }
597
        if ($index === null) {
1✔
598
            $index = --$chartCount;
1✔
599
        }
600
        if (!isset($this->chartCollection[$index])) {
1✔
601
            return false;
1✔
602
        }
603

604
        return $this->chartCollection[$index];
1✔
605
    }
606

607
    /**
608
     * Return an array of the names of charts on this worksheet.
609
     *
610
     * @return string[] The names of charts
611
     */
612
    public function getChartNames(): array
5✔
613
    {
614
        $chartNames = [];
5✔
615
        foreach ($this->chartCollection as $chart) {
5✔
616
            $chartNames[] = $chart->getName();
5✔
617
        }
618

619
        return $chartNames;
5✔
620
    }
621

622
    /**
623
     * Get a chart by name.
624
     *
625
     * @param string $chartName Chart name
626
     *
627
     * @return Chart|false
628
     */
629
    public function getChartByName(string $chartName)
6✔
630
    {
631
        foreach ($this->chartCollection as $index => $chart) {
6✔
632
            if ($chart->getName() == $chartName) {
6✔
633
                return $chart;
6✔
634
            }
635
        }
636

637
        return false;
1✔
638
    }
639

640
    public function getChartByNameOrThrow(string $chartName): Chart
6✔
641
    {
642
        $chart = $this->getChartByName($chartName);
6✔
643
        if ($chart !== false) {
6✔
644
            return $chart;
6✔
645
        }
646

647
        throw new Exception("Sheet does not have a chart named $chartName.");
1✔
648
    }
649

650
    /**
651
     * Refresh column dimensions.
652
     *
653
     * @return $this
654
     */
655
    public function refreshColumnDimensions(): static
25✔
656
    {
657
        $newColumnDimensions = [];
25✔
658
        foreach ($this->getColumnDimensions() as $objColumnDimension) {
25✔
659
            $newColumnDimensions[$objColumnDimension->getColumnIndex()] = $objColumnDimension;
25✔
660
        }
661

662
        $this->columnDimensions = $newColumnDimensions;
25✔
663

664
        return $this;
25✔
665
    }
666

667
    /**
668
     * Refresh row dimensions.
669
     *
670
     * @return $this
671
     */
672
    public function refreshRowDimensions(): static
9✔
673
    {
674
        $newRowDimensions = [];
9✔
675
        foreach ($this->getRowDimensions() as $objRowDimension) {
9✔
676
            $newRowDimensions[$objRowDimension->getRowIndex()] = $objRowDimension;
9✔
677
        }
678

679
        $this->rowDimensions = $newRowDimensions;
9✔
680

681
        return $this;
9✔
682
    }
683

684
    /**
685
     * Calculate worksheet dimension.
686
     *
687
     * @return string String containing the dimension of this worksheet
688
     */
689
    public function calculateWorksheetDimension(): string
549✔
690
    {
691
        // Return
692
        return 'A1:' . $this->getHighestColumn() . $this->getHighestRow();
549✔
693
    }
694

695
    /**
696
     * Calculate worksheet data dimension.
697
     *
698
     * @return string String containing the dimension of this worksheet that actually contain data
699
     */
700
    public function calculateWorksheetDataDimension(): string
612✔
701
    {
702
        // Return
703
        return 'A1:' . $this->getHighestDataColumn() . $this->getHighestDataRow();
612✔
704
    }
705

706
    /**
707
     * Calculate widths for auto-size columns.
708
     *
709
     * @return $this
710
     */
711
    public function calculateColumnWidths(): static
882✔
712
    {
713
        $activeSheet = $this->getParent()?->getActiveSheetIndex();
882✔
714
        $selectedCells = $this->selectedCells;
882✔
715
        // initialize $autoSizes array
716
        $autoSizes = [];
882✔
717
        foreach ($this->getColumnDimensions() as $colDimension) {
882✔
718
            if ($colDimension->getAutoSize()) {
185✔
719
                $autoSizes[$colDimension->getColumnIndex()] = -1;
67✔
720
            }
721
        }
722

723
        // There is only something to do if there are some auto-size columns
724
        if (!empty($autoSizes)) {
882✔
725
            $holdActivePane = $this->activePane;
67✔
726
            // build list of cells references that participate in a merge
727
            $isMergeCell = [];
67✔
728
            foreach ($this->getMergeCells() as $cells) {
67✔
729
                foreach (Coordinate::extractAllCellReferencesInRange($cells) as $cellReference) {
16✔
730
                    $isMergeCell[$cellReference] = true;
16✔
731
                }
732
            }
733

734
            $autoFilterIndentRanges = (new AutoFit($this))->getAutoFilterIndentRanges();
67✔
735

736
            // loop through all cells in the worksheet
737
            foreach ($this->getCoordinates(false) as $coordinate) {
67✔
738
                $cell = $this->getCellOrNull($coordinate);
67✔
739

740
                if ($cell !== null && isset($autoSizes[$this->cellCollection->getCurrentColumn()])) {
67✔
741
                    //Determine if cell is in merge range
742
                    $isMerged = isset($isMergeCell[$this->cellCollection->getCurrentCoordinate()]);
67✔
743

744
                    //By default merged cells should be ignored
745
                    $isMergedButProceed = false;
67✔
746

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

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

767
                                    break;
4✔
768
                                }
769
                            }
770
                        }
771

772
                        $indentAdjustment = $cell->getStyle()->getAlignment()->getIndent();
67✔
773
                        $indentAdjustment += (int) ($cell->getStyle()->getAlignment()->getHorizontal() === Alignment::HORIZONTAL_CENTER);
67✔
774

775
                        // Calculated value
776
                        // To formatted string
777
                        $cellValue = NumberFormat::toFormattedString(
67✔
778
                            $cell->getCalculatedValueString(),
67✔
779
                            (string) $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())
67✔
780
                                ->getNumberFormat()->getFormatCode(true)
67✔
781
                        );
67✔
782

783
                        if ($cellValue !== '') {
67✔
784
                            $autoSizes[$this->cellCollection->getCurrentColumn()] = max(
67✔
785
                                $autoSizes[$this->cellCollection->getCurrentColumn()],
67✔
786
                                round(
67✔
787
                                    Shared\Font::calculateColumnWidth(
67✔
788
                                        $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont(),
67✔
789
                                        $cellValue,
67✔
790
                                        (int) $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())
67✔
791
                                            ->getAlignment()->getTextRotation(),
67✔
792
                                        $this->getParentOrThrow()->getDefaultStyle()->getFont(),
67✔
793
                                        $filterAdjustment,
67✔
794
                                        $indentAdjustment
67✔
795
                                    ),
67✔
796
                                    3
67✔
797
                                )
67✔
798
                            );
67✔
799
                        }
800
                    }
801
                }
802
            }
803

804
            // adjust column widths
805
            foreach ($autoSizes as $columnIndex => $width) {
67✔
806
                if ($width == -1) {
67✔
807
                    $width = $this->getDefaultColumnDimension()->getWidth();
×
808
                }
809
                $this->getColumnDimension($columnIndex)->setWidth($width);
67✔
810
            }
811
            $this->activePane = $holdActivePane;
67✔
812
        }
813
        if ($activeSheet !== null && $activeSheet >= 0) {
882✔
814
            // Okay, I get it now - if $activeSheet is not null,
815
            // then $this->getParent() must also be non-null.
816
            $this->getParent()->setActiveSheetIndex($activeSheet);
882✔
817
        }
818
        $this->setSelectedCells($selectedCells);
882✔
819

820
        return $this;
882✔
821
    }
822

823
    /**
824
     * Get parent or null.
825
     */
826
    public function getParent(): ?Spreadsheet
11,059✔
827
    {
828
        return $this->parent;
11,059✔
829
    }
830

831
    /**
832
     * Get parent, throw exception if null.
833
     */
834
    public function getParentOrThrow(): Spreadsheet
11,131✔
835
    {
836
        if ($this->parent !== null) {
11,131✔
837
            return $this->parent;
11,130✔
838
        }
839

840
        throw new Exception('Sheet does not have a parent.');
1✔
841
    }
842

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

856
            $this->parent->removeSheetByIndex(
4✔
857
                $this->parent->getIndex($this)
4✔
858
            );
4✔
859
        }
860
        $this->parent = $parent;
54✔
861

862
        return $this;
54✔
863
    }
864

865
    public function setParent(Spreadsheet $parent): self
12✔
866
    {
867
        $this->parent = $parent;
12✔
868

869
        return $this;
12✔
870
    }
871

872
    /**
873
     * Get title.
874
     */
875
    public function getTitle(): string
11,504✔
876
    {
877
        return $this->title;
11,504✔
878
    }
879

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

901
        // Old title
902
        $oldTitle = $this->getTitle();
11,504✔
903

904
        if ($validate) {
11,504✔
905
            // Syntax check
906
            self::checkSheetTitle($title);
11,504✔
907

908
            if ($this->parent && $this->parent->getIndex($this, true) >= 0) {
11,504✔
909
                // Is there already such sheet name?
910
                if ($this->parent->sheetNameExists($title)) {
865✔
911
                    // Use name, but append with lowest possible integer
912

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

930
                    $title .= " $i";
2✔
931
                }
932
            }
933
        }
934

935
        // Set title
936
        $this->title = $title;
11,504✔
937

938
        if ($this->parent && $this->parent->getIndex($this, true) >= 0) {
11,504✔
939
            // New title
940
            $newTitle = $this->getTitle();
1,545✔
941
            $this->parent->getCalculationEngine()
1,545✔
942
                ->renameCalculationCacheForWorksheet($oldTitle, $newTitle);
1,545✔
943
            if ($updateFormulaCellReferences) {
1,545✔
944
                ReferenceHelper::getInstance()->updateNamedFormulae($this->parent, $oldTitle, $newTitle);
865✔
945
            }
946
        }
947

948
        return $this;
11,504✔
949
    }
950

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

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

972
        return $this;
11,504✔
973
    }
974

975
    /**
976
     * Get page setup.
977
     */
978
    public function getPageSetup(): PageSetup
1,805✔
979
    {
980
        return $this->pageSetup;
1,805✔
981
    }
982

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

992
        return $this;
1✔
993
    }
994

995
    /**
996
     * Get page margins.
997
     */
998
    public function getPageMargins(): PageMargins
1,788✔
999
    {
1000
        return $this->pageMargins;
1,788✔
1001
    }
1002

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

1012
        return $this;
1✔
1013
    }
1014

1015
    /**
1016
     * Get page header/footer.
1017
     */
1018
    public function getHeaderFooter(): HeaderFooter
667✔
1019
    {
1020
        return $this->headerFooter;
667✔
1021
    }
1022

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

1032
        return $this;
1✔
1033
    }
1034

1035
    /**
1036
     * Get sheet view.
1037
     */
1038
    public function getSheetView(): SheetView
699✔
1039
    {
1040
        return $this->sheetView;
699✔
1041
    }
1042

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

1052
        return $this;
1✔
1053
    }
1054

1055
    /**
1056
     * Get Protection.
1057
     */
1058
    public function getProtection(): Protection
717✔
1059
    {
1060
        return $this->protection;
717✔
1061
    }
1062

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

1072
        return $this;
1✔
1073
    }
1074

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

1089
        return $this->getHighestDataColumn($row);
1✔
1090
    }
1091

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

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

1119
        return $this->getHighestDataRow($column);
1✔
1120
    }
1121

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

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

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

1160
        return $this;
5,114✔
1161
    }
1162

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

1185
        return $this;
124✔
1186
    }
1187

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

1205
        // Shortcut for increased performance for the vast majority of simple cases
1206
        if ($this->cellCollection->has($cellAddress)) {
10,990✔
1207
            /** @var Cell $cell */
1208
            $cell = $this->cellCollection->get($cellAddress);
10,961✔
1209

1210
            return $cell;
10,961✔
1211
        }
1212

1213
        /** @var Worksheet $sheet */
1214
        [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress);
10,990✔
1215
        $cell = $sheet->getCellCollection()->get($finalCoordinate);
10,990✔
1216

1217
        return $cell ?? $sheet->createNewCell($finalCoordinate);
10,990✔
1218
    }
1219

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

1231
        // Worksheet reference?
1232
        if (str_contains($coordinate, '!')) {
11,014✔
1233
            $worksheetReference = self::extractSheetTitle($coordinate, true, true);
×
1234

1235
            $sheet = $this->getParentOrThrow()->getSheetByName($worksheetReference[0]);
×
1236
            $finalCoordinate = strtoupper($worksheetReference[1]);
×
1237

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

1253
                $cellCoordinate = ltrim(substr($namedRange->getValue(), (int) strrpos($namedRange->getValue(), '!')), '!');
12✔
1254
                $finalCoordinate = str_replace('$', '', $cellCoordinate);
12✔
1255
            }
1256
        }
1257

1258
        if ($sheet === null || $finalCoordinate === null) {
11,014✔
1259
            $sheet = $this;
11,014✔
1260
            $finalCoordinate = strtoupper($coordinate);
11,014✔
1261
        }
1262

1263
        if (Coordinate::coordinateIsRange($finalCoordinate)) {
11,014✔
1264
            throw new Exception('Cell coordinate string can not be a range of cells.');
2✔
1265
        }
1266
        $finalCoordinate = str_replace('$', '', $finalCoordinate);
11,014✔
1267

1268
        return [$sheet, $finalCoordinate];
11,014✔
1269
    }
1270

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

1285
        return null;
×
1286
    }
1287

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

1306
        // Coordinates
1307
        if ($column > $this->cachedHighestColumn) {
10,990✔
1308
            $this->cachedHighestColumn = $column;
7,561✔
1309
        }
1310
        if ($row > $this->cachedHighestRow) {
10,990✔
1311
            $this->cachedHighestRow = $row;
9,007✔
1312
        }
1313

1314
        // Cell needs appropriate xfIndex from dimensions records
1315
        //    but don't create dimension records if they don't already exist
1316
        $rowDimension = $this->rowDimensions[$row] ?? null;
10,990✔
1317
        $columnDimension = $this->columnDimensions[$columnString] ?? null;
10,990✔
1318

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

1336
        return $cell;
10,990✔
1337
    }
1338

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

1350
        return $sheet->getCellCollection()->has($finalCoordinate);
10,926✔
1351
    }
1352

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

1364
            $this->cachedHighestRow = max($this->cachedHighestRow, $row);
636✔
1365
        }
1366

1367
        return $this->rowDimensions[$row];
636✔
1368
    }
1369

1370
    public function getRowStyle(int $row): ?Style
1✔
1371
    {
1372
        return $this->parent?->getCellXfByIndexOrNull(
1✔
1373
            ($this->rowDimensions[$row] ?? null)?->getXfIndex()
1✔
1374
        );
1✔
1375
    }
1376

1377
    public function rowDimensionExists(int $row): bool
729✔
1378
    {
1379
        return isset($this->rowDimensions[$row]);
729✔
1380
    }
1381

1382
    public function columnDimensionExists(string $column): bool
107✔
1383
    {
1384
        return isset($this->columnDimensions[$column]);
107✔
1385
    }
1386

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

1397
        // Fetch dimensions
1398
        if (!isset($this->columnDimensions[$column])) {
711✔
1399
            $this->columnDimensions[$column] = new ColumnDimension($column);
711✔
1400

1401
            $columnIndex = Coordinate::columnIndexFromString($column);
711✔
1402
            if ($this->cachedHighestColumn < $columnIndex) {
711✔
1403
                $this->cachedHighestColumn = $columnIndex;
491✔
1404
            }
1405
        }
1406

1407
        return $this->columnDimensions[$column];
711✔
1408
    }
1409

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

1420
    public function getColumnStyle(string $column): ?Style
1✔
1421
    {
1422
        return $this->parent?->getCellXfByIndexOrNull(
1✔
1423
            ($this->columnDimensions[$column] ?? null)?->getXfIndex()
1✔
1424
        );
1✔
1425
    }
1426

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

1443
        // set this sheet as active
1444
        $this->getParentOrThrow()->setActiveSheetIndex($this->getParentOrThrow()->getIndex($this));
10,953✔
1445

1446
        // set cell coordinate as active
1447
        $this->setSelectedCells($cellCoordinate);
10,953✔
1448

1449
        return $this->getParentOrThrow()->getCellXfSupervisor();
10,953✔
1450
    }
1451

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

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

1476
        return $retVal;
7✔
1477
    }
1478

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

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

1501
        return $retVal;
6✔
1502
    }
1503

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

1524
        $conditionalStyles = [];
821✔
1525
        foreach ($this->conditionalStylesCollection as $keyStylesOrig => $conditionalRange) {
821✔
1526
            $keyStyles = Coordinate::resolveUnionAndIntersection($keyStylesOrig);
225✔
1527
            $keyParts = explode(',', $keyStyles);
225✔
1528
            foreach ($keyParts as $keyPart) {
225✔
1529
                if ($keyPart === $coordinate) {
225✔
1530
                    if ($firstOnly) {
14✔
1531
                        return $conditionalRange;
14✔
1532
                    }
1533
                    $conditionalStyles[$keyStylesOrig] = $conditionalRange;
×
1534

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

1543
                        break;
1✔
1544
                    }
1545
                }
1546
            }
1547
        }
1548
        $outArray = [];
644✔
1549
        foreach ($conditionalStyles as $conditionalArray) {
644✔
1550
            foreach ($conditionalArray as $conditional) {
1✔
1551
                $outArray[] = $conditional;
1✔
1552
            }
1553
        }
1554
        usort($outArray, [self::class, 'comparePriority']);
644✔
1555

1556
        return $outArray;
644✔
1557
    }
1558

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

1573
        return ($a < $b) ? -1 : 1;
1✔
1574
    }
1575

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

1589
        return null;
10✔
1590
    }
1591

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

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

1617
        return $this;
60✔
1618
    }
1619

1620
    /**
1621
     * Get collection of conditional styles.
1622
     *
1623
     * @return Conditional[][]
1624
     */
1625
    public function getConditionalStylesCollection(): array
1,486✔
1626
    {
1627
        return $this->conditionalStylesCollection;
1,486✔
1628
    }
1629

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

1642
        return $this;
348✔
1643
    }
1644

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

1668
        // Calculate range outer borders
1669
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
2✔
1670

1671
        // Make sure we can loop upwards on rows and columns
1672
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
2✔
1673
            $tmp = $rangeStart;
×
1674
            $rangeStart = $rangeEnd;
×
1675
            $rangeEnd = $tmp;
×
1676
        }
1677

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

1685
        return $this;
2✔
1686
    }
1687

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

1706
        // Calculate range outer borders
1707
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range . ':' . $range);
18✔
1708

1709
        // Make sure we can loop upwards on rows and columns
1710
        if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
18✔
1711
            $tmp = $rangeStart;
×
1712
            $rangeStart = $rangeEnd;
×
1713
            $rangeEnd = $tmp;
×
1714
        }
1715

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

1723
        return $this;
18✔
1724
    }
1725

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

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

1747
        return $this;
33✔
1748
    }
1749

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

1771
        return $breaks;
735✔
1772
    }
1773

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

1785
        return $this->rowBreaks;
611✔
1786
    }
1787

1788
    protected static function compareRowBreaks(string $coordinate1, string $coordinate2): int
9✔
1789
    {
1790
        $row1 = Coordinate::indexesFromString($coordinate1)[1];
9✔
1791
        $row2 = Coordinate::indexesFromString($coordinate2)[1];
9✔
1792

1793
        return $row1 - $row2;
9✔
1794
    }
1795

1796
    protected static function compareColumnBreaks(string $coordinate1, string $coordinate2): int
5✔
1797
    {
1798
        $column1 = Coordinate::indexesFromString($coordinate1)[0];
5✔
1799
        $column2 = Coordinate::indexesFromString($coordinate2)[0];
5✔
1800

1801
        return $column1 - $column2;
5✔
1802
    }
1803

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

1815
        return $this->columnBreaks;
610✔
1816
    }
1817

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

1836
        if (!str_contains($range, ':')) {
183✔
1837
            $range .= ":{$range}";
1✔
1838
        }
1839

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

1844
        $this->mergeCells[$range] = $range;
182✔
1845
        $firstRow = (int) $matches[2];
182✔
1846
        $lastRow = (int) $matches[4];
182✔
1847
        $firstColumn = $matches[1];
182✔
1848
        $lastColumn = $matches[3];
182✔
1849
        $firstColumnIndex = Coordinate::columnIndexFromString($firstColumn);
182✔
1850
        $lastColumnIndex = Coordinate::columnIndexFromString($lastColumn);
182✔
1851
        $numberRows = $lastRow - $firstRow;
182✔
1852
        $numberColumns = $lastColumnIndex - $firstColumnIndex;
182✔
1853

1854
        if ($numberRows === 1 && $numberColumns === 1) {
182✔
1855
            return $this;
36✔
1856
        }
1857

1858
        // create upper left cell if it does not already exist
1859
        $upperLeft = "{$firstColumn}{$firstRow}";
175✔
1860
        if (!$this->cellExists($upperLeft)) {
175✔
1861
            $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL);
37✔
1862
        }
1863

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

1873
        return $this;
175✔
1874
    }
1875

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

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

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

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

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

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

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

1943
        return $leftCellValue;
61✔
1944
    }
1945

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

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

1969
        return $this;
22✔
1970
    }
1971

1972
    /**
1973
     * Get merge cells array.
1974
     *
1975
     * @return string[]
1976
     */
1977
    public function getMergeCells(): array
1,388✔
1978
    {
1979
        return $this->mergeCells;
1,388✔
1980
    }
1981

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

1994
        return $this;
133✔
1995
    }
1996

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

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

2017
        return $this;
28✔
2018
    }
2019

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

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

2039
        return $this;
21✔
2040
    }
2041

2042
    /**
2043
     * Get protected cells.
2044
     *
2045
     * @return ProtectedRange[]
2046
     */
2047
    public function getProtectedCellRanges(): array
727✔
2048
    {
2049
        return $this->protectedCells;
727✔
2050
    }
2051

2052
    /**
2053
     * Get Autofilter.
2054
     */
2055
    public function getAutoFilter(): AutoFilter
952✔
2056
    {
2057
        return $this->autoFilter;
952✔
2058
    }
2059

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

2077
            $this->autoFilter->setRange($cellRange);
21✔
2078
        }
2079

2080
        return $this;
21✔
2081
    }
2082

2083
    /**
2084
     * Remove autofilter.
2085
     */
2086
    public function removeAutoFilter(): self
1✔
2087
    {
2088
        $this->autoFilter->setRange('');
1✔
2089

2090
        return $this;
1✔
2091
    }
2092

2093
    /**
2094
     * Get collection of Tables.
2095
     *
2096
     * @return ArrayObject<int, Table>
2097
     */
2098
    public function getTableCollection(): ArrayObject
10,983✔
2099
    {
2100
        return $this->tableCollection;
10,983✔
2101
    }
2102

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

2113
        return $this;
105✔
2114
    }
2115

2116
    /**
2117
     * @return string[] array of Table names
2118
     */
2119
    public function getTableNames(): array
1✔
2120
    {
2121
        $tableNames = [];
1✔
2122

2123
        foreach ($this->tableCollection as $table) {
1✔
2124
            /** @var Table $table */
2125
            $tableNames[] = $table->getName();
1✔
2126
        }
2127

2128
        return $tableNames;
1✔
2129
    }
2130

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

2140
        return ($tableIndex === null) ? null : $this->tableCollection[$tableIndex];
97✔
2141
    }
2142

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

2158
        return null;
41✔
2159
    }
2160

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

2172
        if ($tableIndex !== null) {
1✔
2173
            unset($this->tableCollection[$tableIndex]);
1✔
2174
        }
2175

2176
        return $this;
1✔
2177
    }
2178

2179
    /**
2180
     * Remove collection of Tables.
2181
     */
2182
    public function removeTableCollection(): self
1✔
2183
    {
2184
        $this->tableCollection = new ArrayObject();
1✔
2185

2186
        return $this;
1✔
2187
    }
2188

2189
    /**
2190
     * Get collection of PivotTables.
2191
     *
2192
     * @return ArrayObject<int, PivotTable>
2193
     */
2194
    public function getPivotTableCollection(): ArrayObject
464✔
2195
    {
2196
        return $this->pivotTableCollection;
464✔
2197
    }
2198

2199
    /**
2200
     * Get collection of PivotTables (alias of getPivotTableCollection()).
2201
     *
2202
     * @return ArrayObject<int, PivotTable>
2203
     */
NEW
2204
    public function getPivotTables(): ArrayObject
×
2205
    {
NEW
2206
        return $this->pivotTableCollection;
×
2207
    }
2208

2209
    /**
2210
     * Add a PivotTable to this worksheet.
2211
     *
2212
     * @return $this
2213
     */
2214
    public function addPivotTable(PivotTable $pivotTable): self
19✔
2215
    {
2216
        $pivotTable->setWorksheet($this);
19✔
2217
        $this->pivotTableCollection[] = $pivotTable;
19✔
2218

2219
        return $this;
19✔
2220
    }
2221

2222
    /**
2223
     * @return string[] array of PivotTable names
2224
     */
2225
    public function getPivotTableNames(): array
1✔
2226
    {
2227
        $pivotTableNames = [];
1✔
2228

2229
        foreach ($this->pivotTableCollection as $pivotTable) {
1✔
2230
            $pivotTableNames[] = $pivotTable->getName();
1✔
2231
        }
2232

2233
        return $pivotTableNames;
1✔
2234
    }
2235

2236
    /**
2237
     * @param string $name the pivot table name to search
2238
     *
2239
     * @return null|PivotTable The pivot table from the collection, or null if not found
2240
     */
2241
    public function getPivotTableByName(string $name): ?PivotTable
7✔
2242
    {
2243
        $name = StringHelper::strToUpper($name);
7✔
2244
        foreach ($this->pivotTableCollection as $pivotTable) {
7✔
2245
            if (StringHelper::strToUpper($pivotTable->getName()) === $name) {
6✔
2246
                return $pivotTable;
6✔
2247
            }
2248
        }
2249

2250
        return null;
1✔
2251
    }
2252

2253
    /**
2254
     * Remove collection of PivotTables.
2255
     */
NEW
2256
    public function removePivotTableCollection(): self
×
2257
    {
NEW
2258
        $this->pivotTableCollection = new ArrayObject();
×
2259

NEW
2260
        return $this;
×
2261
    }
2262

2263
    /**
2264
     * Get Freeze Pane.
2265
     */
2266
    public function getFreezePane(): ?string
323✔
2267
    {
2268
        return $this->freezePane;
323✔
2269
    }
2270

2271
    /**
2272
     * Freeze Pane.
2273
     *
2274
     * Examples:
2275
     *
2276
     *     - A2 will freeze the rows above cell A2 (i.e row 1)
2277
     *     - B1 will freeze the columns to the left of cell B1 (i.e column A)
2278
     *     - B2 will freeze the rows above and to the left of cell B2 (i.e row 1 and column A)
2279
     *
2280
     * @param null|array{0: int, 1: int}|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5';
2281
     *            or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2282
     *        Passing a null value for this argument will clear any existing freeze pane for this worksheet.
2283
     * @param null|array{0: int, 1: int}|CellAddress|string $topLeftCell default position of the right bottom pane
2284
     *            Coordinate of the cell as a string, eg: 'C5'; or as an array of [$columnIndex, $row] (e.g. [3, 5]),
2285
     *            or a CellAddress object.
2286
     *
2287
     * @return $this
2288
     */
2289
    public function freezePane(null|CellAddress|string|array $coordinate, null|CellAddress|string|array $topLeftCell = null, bool $frozenSplit = false): static
50✔
2290
    {
2291
        $this->panes = [
50✔
2292
            'bottomRight' => null,
50✔
2293
            'bottomLeft' => null,
50✔
2294
            'topRight' => null,
50✔
2295
            'topLeft' => null,
50✔
2296
        ];
50✔
2297
        $cellAddress = ($coordinate !== null)
50✔
2298
            ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate))
50✔
2299
            : null;
1✔
2300
        if ($cellAddress !== null && Coordinate::coordinateIsRange($cellAddress)) {
50✔
2301
            throw new Exception('Freeze pane can not be set on a range of cells.');
1✔
2302
        }
2303
        $topLeftCell = ($topLeftCell !== null)
49✔
2304
            ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($topLeftCell))
37✔
2305
            : null;
37✔
2306

2307
        if ($cellAddress !== null && $topLeftCell === null) {
49✔
2308
            $coordinate = Coordinate::coordinateFromString($cellAddress);
37✔
2309
            $topLeftCell = $coordinate[0] . $coordinate[1];
37✔
2310
        }
2311

2312
        $topLeftCell = "$topLeftCell";
49✔
2313
        $this->paneTopLeftCell = $topLeftCell;
49✔
2314

2315
        $this->freezePane = $cellAddress;
49✔
2316
        $this->topLeftCell = $topLeftCell;
49✔
2317
        if ($cellAddress === null) {
49✔
2318
            $this->paneState = '';
1✔
2319
            $this->xSplit = $this->ySplit = 0;
1✔
2320
            $this->activePane = '';
1✔
2321
        } else {
2322
            $coordinates = Coordinate::indexesFromString($cellAddress);
49✔
2323
            $this->xSplit = $coordinates[0] - 1;
49✔
2324
            $this->ySplit = $coordinates[1] - 1;
49✔
2325
            if ($this->xSplit > 0 || $this->ySplit > 0) {
49✔
2326
                $this->paneState = $frozenSplit ? self::PANE_FROZENSPLIT : self::PANE_FROZEN;
48✔
2327
                $this->setSelectedCellsActivePane();
48✔
2328
            } else {
2329
                $this->paneState = '';
1✔
2330
                $this->freezePane = null;
1✔
2331
                $this->activePane = '';
1✔
2332
            }
2333
        }
2334

2335
        return $this;
49✔
2336
    }
2337

2338
    public function setTopLeftCell(string $topLeftCell): self
61✔
2339
    {
2340
        $this->topLeftCell = $topLeftCell;
61✔
2341

2342
        return $this;
61✔
2343
    }
2344

2345
    /**
2346
     * Unfreeze Pane.
2347
     *
2348
     * @return $this
2349
     */
2350
    public function unfreezePane(): static
1✔
2351
    {
2352
        return $this->freezePane(null);
1✔
2353
    }
2354

2355
    /**
2356
     * Get the default position of the right bottom pane.
2357
     */
2358
    public function getTopLeftCell(): ?string
543✔
2359
    {
2360
        return $this->topLeftCell;
543✔
2361
    }
2362

2363
    public function getPaneTopLeftCell(): string
11✔
2364
    {
2365
        return $this->paneTopLeftCell;
11✔
2366
    }
2367

2368
    public function setPaneTopLeftCell(string $paneTopLeftCell): self
26✔
2369
    {
2370
        $this->paneTopLeftCell = $paneTopLeftCell;
26✔
2371

2372
        return $this;
26✔
2373
    }
2374

2375
    public function usesPanes(): bool
531✔
2376
    {
2377
        return $this->xSplit > 0 || $this->ySplit > 0;
531✔
2378
    }
2379

2380
    public function getPane(string $position): ?Pane
2✔
2381
    {
2382
        return $this->panes[$position] ?? null;
2✔
2383
    }
2384

2385
    public function setPane(string $position, ?Pane $pane): self
47✔
2386
    {
2387
        if (array_key_exists($position, $this->panes)) {
47✔
2388
            $this->panes[$position] = $pane;
47✔
2389
        }
2390

2391
        return $this;
47✔
2392
    }
2393

2394
    /** @return (null|Pane)[] */
2395
    public function getPanes(): array
3✔
2396
    {
2397
        return $this->panes;
3✔
2398
    }
2399

2400
    public function getActivePane(): string
14✔
2401
    {
2402
        return $this->activePane;
14✔
2403
    }
2404

2405
    public function setActivePane(string $activePane): self
49✔
2406
    {
2407
        $this->activePane = array_key_exists($activePane, $this->panes) ? $activePane : '';
49✔
2408

2409
        return $this;
49✔
2410
    }
2411

2412
    public function getXSplit(): int
11✔
2413
    {
2414
        return $this->xSplit;
11✔
2415
    }
2416

2417
    public function setXSplit(int $xSplit): self
11✔
2418
    {
2419
        $this->xSplit = $xSplit;
11✔
2420
        if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
11✔
2421
            $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
1✔
2422
        }
2423

2424
        return $this;
11✔
2425
    }
2426

2427
    public function getYSplit(): int
11✔
2428
    {
2429
        return $this->ySplit;
11✔
2430
    }
2431

2432
    public function setYSplit(int $ySplit): self
26✔
2433
    {
2434
        $this->ySplit = $ySplit;
26✔
2435
        if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
26✔
2436
            $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
1✔
2437
        }
2438

2439
        return $this;
26✔
2440
    }
2441

2442
    public function getPaneState(): string
30✔
2443
    {
2444
        return $this->paneState;
30✔
2445
    }
2446

2447
    public const PANE_FROZEN = 'frozen';
2448
    public const PANE_FROZENSPLIT = 'frozenSplit';
2449
    public const PANE_SPLIT = 'split';
2450
    private const VALIDPANESTATE = [self::PANE_FROZEN, self::PANE_SPLIT, self::PANE_FROZENSPLIT];
2451
    private const VALIDFROZENSTATE = [self::PANE_FROZEN, self::PANE_FROZENSPLIT];
2452

2453
    public function setPaneState(string $paneState): self
26✔
2454
    {
2455
        $this->paneState = in_array($paneState, self::VALIDPANESTATE, true) ? $paneState : '';
26✔
2456
        if (in_array($this->paneState, self::VALIDFROZENSTATE, true)) {
26✔
2457
            $this->freezePane([$this->xSplit + 1, $this->ySplit + 1], $this->topLeftCell, $this->paneState === self::PANE_FROZENSPLIT);
25✔
2458
        } else {
2459
            $this->freezePane = null;
3✔
2460
        }
2461

2462
        return $this;
26✔
2463
    }
2464

2465
    /**
2466
     * Insert a new row, updating all possible related data.
2467
     *
2468
     * @param int $before Insert before this row number
2469
     * @param int $numberOfRows Number of new rows to insert
2470
     *
2471
     * @return $this
2472
     */
2473
    public function insertNewRowBefore(int $before, int $numberOfRows = 1): static
43✔
2474
    {
2475
        if ($before >= 1) {
43✔
2476
            $objReferenceHelper = ReferenceHelper::getInstance();
42✔
2477
            $objReferenceHelper->insertNewBefore('A' . $before, 0, $numberOfRows, $this);
42✔
2478
        } else {
2479
            throw new Exception('Rows can only be inserted before at least row 1.');
1✔
2480
        }
2481

2482
        return $this;
42✔
2483
    }
2484

2485
    /**
2486
     * Insert a new column, updating all possible related data.
2487
     *
2488
     * @param string $before Insert before this column Name, eg: 'A'
2489
     * @param int $numberOfColumns Number of new columns to insert
2490
     *
2491
     * @return $this
2492
     */
2493
    public function insertNewColumnBefore(string $before, int $numberOfColumns = 1): static
51✔
2494
    {
2495
        if (!is_numeric($before)) {
51✔
2496
            $objReferenceHelper = ReferenceHelper::getInstance();
50✔
2497
            $objReferenceHelper->insertNewBefore($before . '1', $numberOfColumns, 0, $this);
50✔
2498
        } else {
2499
            throw new Exception('Column references should not be numeric.');
1✔
2500
        }
2501

2502
        return $this;
50✔
2503
    }
2504

2505
    /**
2506
     * Insert a new column, updating all possible related data.
2507
     *
2508
     * @param int $beforeColumnIndex Insert before this column ID (numeric column coordinate of the cell)
2509
     * @param int $numberOfColumns Number of new columns to insert
2510
     *
2511
     * @return $this
2512
     */
2513
    public function insertNewColumnBeforeByIndex(int $beforeColumnIndex, int $numberOfColumns = 1): static
2✔
2514
    {
2515
        if ($beforeColumnIndex >= 1) {
2✔
2516
            return $this->insertNewColumnBefore(Coordinate::stringFromColumnIndex($beforeColumnIndex), $numberOfColumns);
1✔
2517
        }
2518

2519
        throw new Exception('Columns can only be inserted before at least column A (1).');
1✔
2520
    }
2521

2522
    /**
2523
     * Delete a row, updating all possible related data.
2524
     *
2525
     * @param int $row Remove rows, starting with this row number
2526
     * @param int $numberOfRows Number of rows to remove
2527
     *
2528
     * @return $this
2529
     */
2530
    public function removeRow(int $row, int $numberOfRows = 1): static
58✔
2531
    {
2532
        if ($row < 1) {
58✔
2533
            throw new Exception('Rows to be deleted should at least start from row 1.');
1✔
2534
        }
2535
        if ($numberOfRows === 0) {
57✔
2536
            return $this;
1✔
2537
        }
2538
        if ($numberOfRows < 0) {
56✔
2539
            $newRow = max(1, $row + $numberOfRows + 1);
1✔
2540
            $numberOfRows = $row - $newRow + 1;
1✔
2541
            $row = $newRow;
1✔
2542
        }
2543
        $newHighestRow = $this->cachedHighestRow;
56✔
2544
        if ($newHighestRow >= $row) {
56✔
2545
            $newHighestRow = max($row - 1, $this->cachedHighestRow - $numberOfRows);
43✔
2546
        }
2547
        $startRow = $row;
56✔
2548
        $endRow = $startRow + $numberOfRows - 1;
56✔
2549
        $removeKeys = [];
56✔
2550
        $addKeys = [];
56✔
2551
        foreach ($this->mergeCells as $key => $value) {
56✔
2552
            if (
2553
                Preg::isMatch(
21✔
2554
                    '/^([a-z]{1,3})(\d+):([a-z]{1,3})(\d+)/i',
21✔
2555
                    $key,
21✔
2556
                    $matches
21✔
2557
                )
21✔
2558
            ) {
2559
                $startMergeInt = (int) $matches[2];
21✔
2560
                $endMergeInt = (int) $matches[4];
21✔
2561
                if ($startMergeInt >= $startRow) {
21✔
2562
                    if ($startMergeInt <= $endRow) {
21✔
2563
                        $removeKeys[] = $key;
3✔
2564
                    }
2565
                } elseif ($endMergeInt >= $startRow) {
1✔
2566
                    if ($endMergeInt <= $endRow) {
1✔
2567
                        $temp = $endMergeInt - 1;
1✔
2568
                        $removeKeys[] = $key;
1✔
2569
                        if ($temp !== $startMergeInt) {
1✔
2570
                            $temp3 = $matches[1] . $matches[2] . ':' . $matches[3] . $temp;
1✔
2571
                            $addKeys[] = $temp3;
1✔
2572
                        }
2573
                    }
2574
                }
2575
            }
2576
        }
2577
        foreach ($removeKeys as $key) {
56✔
2578
            unset($this->mergeCells[$key]);
3✔
2579
        }
2580
        foreach ($addKeys as $key) {
56✔
2581
            $this->mergeCells[$key] = $key;
1✔
2582
        }
2583

2584
        $holdRowDimensions = $this->removeRowDimensions($row, $numberOfRows);
56✔
2585
        $highestRow = $this->getHighestDataRow();
56✔
2586
        $removedRowsCounter = 0;
56✔
2587

2588
        for ($r = 0; $r < $numberOfRows; ++$r) {
56✔
2589
            if ($row + $r <= $highestRow) {
56✔
2590
                $this->cellCollection->removeRow($row + $r);
42✔
2591
                ++$removedRowsCounter;
42✔
2592
            }
2593
        }
2594

2595
        $objReferenceHelper = ReferenceHelper::getInstance();
56✔
2596
        $objReferenceHelper->insertNewBefore('A' . ($row + $numberOfRows), 0, -$numberOfRows, $this);
56✔
2597
        for ($r = 0; $r < $removedRowsCounter; ++$r) {
56✔
2598
            $this->cellCollection->removeRow($highestRow);
42✔
2599
            --$highestRow;
42✔
2600
        }
2601

2602
        $this->rowDimensions = $holdRowDimensions;
56✔
2603
        $this->cachedHighestRow = $newHighestRow;
56✔
2604

2605
        return $this;
56✔
2606
    }
2607

2608
    /** @return RowDimension[] */
2609
    private function removeRowDimensions(int $row, int $numberOfRows): array
56✔
2610
    {
2611
        $highRow = $row + $numberOfRows - 1;
56✔
2612
        $holdRowDimensions = [];
56✔
2613
        foreach ($this->rowDimensions as $rowDimension) {
56✔
2614
            $num = $rowDimension->getRowIndex();
6✔
2615
            if ($num < $row) {
6✔
2616
                $holdRowDimensions[$num] = $rowDimension;
4✔
2617
            } elseif ($num > $highRow) {
6✔
2618
                $num -= $numberOfRows;
5✔
2619
                $cloneDimension = clone $rowDimension;
5✔
2620
                $cloneDimension->setRowIndex($num);
5✔
2621
                $holdRowDimensions[$num] = $cloneDimension;
5✔
2622
            }
2623
        }
2624

2625
        return $holdRowDimensions;
56✔
2626
    }
2627

2628
    /**
2629
     * Remove a column, updating all possible related data.
2630
     *
2631
     * @param string $column Remove columns starting with this column name, eg: 'A'
2632
     * @param int $numberOfColumns Number of columns to remove
2633
     *
2634
     * @return $this
2635
     */
2636
    public function removeColumn(string $column, int $numberOfColumns = 1): static
48✔
2637
    {
2638
        if (is_numeric($column)) {
48✔
2639
            throw new Exception('Column references should not be numeric.');
1✔
2640
        }
2641
        $startColumnInt = Coordinate::columnIndexFromString($column);
47✔
2642
        if ($numberOfColumns === 0) {
47✔
2643
            return $this;
1✔
2644
        }
2645
        if ($numberOfColumns < 0) {
46✔
2646
            $newStartColumnInt = max(1, $startColumnInt + $numberOfColumns + 1);
1✔
2647
            $numberOfColumns = $startColumnInt - $newStartColumnInt + 1;
1✔
2648
            $startColumnInt = $newStartColumnInt;
1✔
2649
            $column = Coordinate::stringFromColumnIndex($startColumnInt);
1✔
2650
        }
2651
        $newHighestColumn = $this->cachedHighestColumn;
46✔
2652
        if ($newHighestColumn >= $startColumnInt) {
46✔
2653
            $newHighestColumn = max($startColumnInt - 1, $this->cachedHighestColumn - $numberOfColumns);
36✔
2654
        }
2655
        $endColumnInt = $startColumnInt + $numberOfColumns - 1;
46✔
2656
        $removeKeys = [];
46✔
2657
        $addKeys = [];
46✔
2658
        foreach ($this->mergeCells as $key => $value) {
46✔
2659
            if (
2660
                Preg::isMatch(
19✔
2661
                    '/^([a-z]{1,3})(\d+):([a-z]{1,3})(\d+)/i',
19✔
2662
                    $key,
19✔
2663
                    $matches
19✔
2664
                )
19✔
2665
            ) {
2666
                $startMergeInt = Coordinate::columnIndexFromString($matches[1]);
19✔
2667
                $endMergeInt = Coordinate::columnIndexFromString($matches[3]);
19✔
2668
                if ($startMergeInt >= $startColumnInt) {
19✔
2669
                    if ($startMergeInt <= $endColumnInt) {
2✔
2670
                        $removeKeys[] = $key;
2✔
2671
                    }
2672
                } elseif ($endMergeInt >= $startColumnInt) {
18✔
2673
                    if ($endMergeInt <= $endColumnInt) {
18✔
2674
                        $temp = Coordinate::columnIndexFromString($matches[3]) - 1;
1✔
2675
                        $temp2 = Coordinate::stringFromColumnIndex($temp);
1✔
2676
                        $removeKeys[] = $key;
1✔
2677
                        if ($temp2 !== $matches[1]) {
1✔
2678
                            $temp3 = $matches[1] . $matches[2] . ':' . $temp2 . $matches[4];
1✔
2679
                            $addKeys[] = $temp3;
1✔
2680
                        }
2681
                    }
2682
                }
2683
            }
2684
        }
2685
        foreach ($removeKeys as $key) {
46✔
2686
            unset($this->mergeCells[$key]);
2✔
2687
        }
2688
        foreach ($addKeys as $key) {
46✔
2689
            $this->mergeCells[$key] = $key;
1✔
2690
        }
2691

2692
        $highestColumn = $this->getHighestDataColumn();
46✔
2693
        $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
46✔
2694
        $pColumnIndex = Coordinate::columnIndexFromString($column);
46✔
2695

2696
        $holdColumnDimensions = $this->removeColumnDimensions($pColumnIndex, $numberOfColumns);
46✔
2697

2698
        $column = Coordinate::stringFromColumnIndex($pColumnIndex + $numberOfColumns);
46✔
2699
        $objReferenceHelper = ReferenceHelper::getInstance();
46✔
2700
        $objReferenceHelper->insertNewBefore($column . '1', -$numberOfColumns, 0, $this);
46✔
2701

2702
        $this->columnDimensions = $holdColumnDimensions;
46✔
2703

2704
        if ($pColumnIndex > $highestColumnIndex) {
46✔
2705
            $this->cachedHighestColumn = $newHighestColumn;
11✔
2706

2707
            return $this;
11✔
2708
        }
2709

2710
        $maxPossibleColumnsToBeRemoved = $highestColumnIndex - $pColumnIndex + 1;
35✔
2711

2712
        for ($c = 0, $n = min($maxPossibleColumnsToBeRemoved, $numberOfColumns); $c < $n; ++$c) {
35✔
2713
            $this->cellCollection->removeColumn($highestColumn);
35✔
2714
            $highestColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($highestColumn) - 1);
35✔
2715
        }
2716
        $this->cachedHighestColumn = $newHighestColumn;
35✔
2717

2718
        $this->garbageCollect();
35✔
2719

2720
        return $this;
35✔
2721
    }
2722

2723
    /** @return ColumnDimension[] */
2724
    private function removeColumnDimensions(int $pColumnIndex, int $numberOfColumns): array
46✔
2725
    {
2726
        $highCol = $pColumnIndex + $numberOfColumns - 1;
46✔
2727
        $holdColumnDimensions = [];
46✔
2728
        foreach ($this->columnDimensions as $columnDimension) {
46✔
2729
            $num = $columnDimension->getColumnNumeric();
19✔
2730
            if ($num < $pColumnIndex) {
19✔
2731
                $str = $columnDimension->getColumnIndex();
18✔
2732
                $holdColumnDimensions[$str] = $columnDimension;
18✔
2733
            } elseif ($num > $highCol) {
19✔
2734
                $cloneDimension = clone $columnDimension;
19✔
2735
                $cloneDimension->setColumnNumeric($num - $numberOfColumns);
19✔
2736
                $str = $cloneDimension->getColumnIndex();
19✔
2737
                $holdColumnDimensions[$str] = $cloneDimension;
19✔
2738
            }
2739
        }
2740

2741
        return $holdColumnDimensions;
46✔
2742
    }
2743

2744
    /**
2745
     * Remove a column, updating all possible related data.
2746
     *
2747
     * @param int $columnIndex Remove starting with this column Index (numeric column coordinate)
2748
     * @param int $numColumns Number of columns to remove
2749
     *
2750
     * @return $this
2751
     */
2752
    public function removeColumnByIndex(int $columnIndex, int $numColumns = 1): static
3✔
2753
    {
2754
        if ($columnIndex >= 1) {
3✔
2755
            return $this->removeColumn(Coordinate::stringFromColumnIndex($columnIndex), $numColumns);
2✔
2756
        }
2757

2758
        throw new Exception('Columns to be deleted should at least start from column A (1)');
1✔
2759
    }
2760

2761
    /**
2762
     * Show gridlines?
2763
     */
2764
    public function getShowGridlines(): bool
1,175✔
2765
    {
2766
        return $this->showGridlines;
1,175✔
2767
    }
2768

2769
    /**
2770
     * Set show gridlines.
2771
     *
2772
     * @param bool $showGridLines Show gridlines (true/false)
2773
     *
2774
     * @return $this
2775
     */
2776
    public function setShowGridlines(bool $showGridLines): self
938✔
2777
    {
2778
        $this->showGridlines = $showGridLines;
938✔
2779

2780
        return $this;
938✔
2781
    }
2782

2783
    /**
2784
     * Print gridlines?
2785
     */
2786
    public function getPrintGridlines(): bool
1,198✔
2787
    {
2788
        return $this->printGridlines;
1,198✔
2789
    }
2790

2791
    /**
2792
     * Set print gridlines.
2793
     *
2794
     * @param bool $printGridLines Print gridlines (true/false)
2795
     *
2796
     * @return $this
2797
     */
2798
    public function setPrintGridlines(bool $printGridLines): self
609✔
2799
    {
2800
        $this->printGridlines = $printGridLines;
609✔
2801

2802
        return $this;
609✔
2803
    }
2804

2805
    /**
2806
     * Show row and column headers?
2807
     */
2808
    public function getShowRowColHeaders(): bool
607✔
2809
    {
2810
        return $this->showRowColHeaders;
607✔
2811
    }
2812

2813
    /**
2814
     * Set show row and column headers.
2815
     *
2816
     * @param bool $showRowColHeaders Show row and column headers (true/false)
2817
     *
2818
     * @return $this
2819
     */
2820
    public function setShowRowColHeaders(bool $showRowColHeaders): self
452✔
2821
    {
2822
        $this->showRowColHeaders = $showRowColHeaders;
452✔
2823

2824
        return $this;
452✔
2825
    }
2826

2827
    /**
2828
     * Show summary below? (Row/Column outlining).
2829
     */
2830
    public function getShowSummaryBelow(): bool
608✔
2831
    {
2832
        return $this->showSummaryBelow;
608✔
2833
    }
2834

2835
    /**
2836
     * Set show summary below.
2837
     *
2838
     * @param bool $showSummaryBelow Show summary below (true/false)
2839
     *
2840
     * @return $this
2841
     */
2842
    public function setShowSummaryBelow(bool $showSummaryBelow): self
451✔
2843
    {
2844
        $this->showSummaryBelow = $showSummaryBelow;
451✔
2845

2846
        return $this;
451✔
2847
    }
2848

2849
    /**
2850
     * Show summary right? (Row/Column outlining).
2851
     */
2852
    public function getShowSummaryRight(): bool
608✔
2853
    {
2854
        return $this->showSummaryRight;
608✔
2855
    }
2856

2857
    /**
2858
     * Set show summary right.
2859
     *
2860
     * @param bool $showSummaryRight Show summary right (true/false)
2861
     *
2862
     * @return $this
2863
     */
2864
    public function setShowSummaryRight(bool $showSummaryRight): self
451✔
2865
    {
2866
        $this->showSummaryRight = $showSummaryRight;
451✔
2867

2868
        return $this;
451✔
2869
    }
2870

2871
    /**
2872
     * Get comments.
2873
     *
2874
     * @return Comment[]
2875
     */
2876
    public function getComments(): array
1,257✔
2877
    {
2878
        return $this->comments;
1,257✔
2879
    }
2880

2881
    /**
2882
     * Set comments array for the entire sheet.
2883
     *
2884
     * @param Comment[] $comments
2885
     *
2886
     * @return $this
2887
     */
2888
    public function setComments(array $comments): self
133✔
2889
    {
2890
        $this->comments = $comments;
133✔
2891

2892
        return $this;
133✔
2893
    }
2894

2895
    /**
2896
     * Remove comment from cell.
2897
     *
2898
     * @param array{0: int, 1: int}|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
2899
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2900
     *
2901
     * @return $this
2902
     */
2903
    public function removeComment(CellAddress|string|array $cellCoordinate): self
63✔
2904
    {
2905
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
63✔
2906

2907
        if (Coordinate::coordinateIsRange($cellAddress)) {
63✔
2908
            throw new Exception('Cell coordinate string can not be a range of cells.');
1✔
2909
        } elseif (str_contains($cellAddress, '$')) {
62✔
2910
            throw new Exception('Cell coordinate string must not be absolute.');
1✔
2911
        } elseif ($cellAddress == '') {
61✔
2912
            throw new Exception('Cell coordinate can not be zero-length string.');
1✔
2913
        }
2914
        // Check if we have a comment for this cell and delete it
2915
        if (isset($this->comments[$cellAddress])) {
60✔
2916
            unset($this->comments[$cellAddress]);
3✔
2917
        }
2918

2919
        return $this;
60✔
2920
    }
2921

2922
    /**
2923
     * Get comment for cell.
2924
     *
2925
     * @param array{0: int, 1: int}|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5';
2926
     *               or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object.
2927
     */
2928
    public function getComment(CellAddress|string|array $cellCoordinate, bool $attachNew = true): Comment
120✔
2929
    {
2930
        $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate));
120✔
2931

2932
        if (Coordinate::coordinateIsRange($cellAddress)) {
120✔
2933
            throw new Exception('Cell coordinate string can not be a range of cells.');
1✔
2934
        } elseif (str_contains($cellAddress, '$')) {
119✔
2935
            throw new Exception('Cell coordinate string must not be absolute.');
1✔
2936
        } elseif ($cellAddress == '') {
118✔
2937
            throw new Exception('Cell coordinate can not be zero-length string.');
1✔
2938
        }
2939

2940
        // Check if we already have a comment for this cell.
2941
        if (isset($this->comments[$cellAddress])) {
117✔
2942
            return $this->comments[$cellAddress];
83✔
2943
        }
2944

2945
        // If not, create a new comment.
2946
        $newComment = new Comment();
117✔
2947
        if ($attachNew) {
117✔
2948
            $this->comments[$cellAddress] = $newComment;
117✔
2949
        }
2950

2951
        return $newComment;
117✔
2952
    }
2953

2954
    /**
2955
     * Get active cell.
2956
     *
2957
     * @return string Example: 'A1'
2958
     */
2959
    public function getActiveCell(): string
11,019✔
2960
    {
2961
        return $this->activeCell;
11,019✔
2962
    }
2963

2964
    /**
2965
     * Get selected cells.
2966
     */
2967
    public function getSelectedCells(): string
11,073✔
2968
    {
2969
        return $this->selectedCells;
11,073✔
2970
    }
2971

2972
    /**
2973
     * Selected cell.
2974
     *
2975
     * @param string $coordinate Cell (i.e. A1)
2976
     *
2977
     * @return $this
2978
     */
2979
    public function setSelectedCell(string $coordinate): static
38✔
2980
    {
2981
        return $this->setSelectedCells($coordinate);
38✔
2982
    }
2983

2984
    /**
2985
     * Select a range of cells.
2986
     *
2987
     * @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'
2988
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
2989
     *              or a CellAddress or AddressRange object.
2990
     *
2991
     * @return $this
2992
     */
2993
    public function setSelectedCells(AddressRange|CellAddress|int|string|array $coordinate): static
11,022✔
2994
    {
2995
        if (is_string($coordinate)) {
11,022✔
2996
            $coordinate = Validations::definedNameToCoordinate($coordinate, $this);
11,022✔
2997
        }
2998
        $coordinate = Validations::validateCellOrCellRange($coordinate);
11,022✔
2999

3000
        if (Coordinate::coordinateIsRange($coordinate)) {
11,022✔
3001
            [$first] = Coordinate::splitRange($coordinate);
544✔
3002
            $this->activeCell = $first[0];
544✔
3003
        } else {
3004
            $this->activeCell = $coordinate;
10,991✔
3005
        }
3006
        $this->selectedCells = $coordinate;
11,022✔
3007
        $this->setSelectedCellsActivePane();
11,022✔
3008

3009
        return $this;
11,022✔
3010
    }
3011

3012
    private function setSelectedCellsActivePane(): void
11,023✔
3013
    {
3014
        if (!empty($this->freezePane)) {
11,023✔
3015
            $coordinateC = Coordinate::indexesFromString($this->freezePane);
48✔
3016
            $coordinateT = Coordinate::indexesFromString($this->activeCell);
48✔
3017
            if ($coordinateC[0] === 1) {
48✔
3018
                $activePane = ($coordinateT[1] <= $coordinateC[1]) ? 'topLeft' : 'bottomLeft';
26✔
3019
            } elseif ($coordinateC[1] === 1) {
24✔
3020
                $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'topLeft' : 'topRight';
3✔
3021
            } elseif ($coordinateT[1] <= $coordinateC[1]) {
22✔
3022
                $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'topLeft' : 'topRight';
22✔
3023
            } else {
3024
                $activePane = ($coordinateT[0] <= $coordinateC[0]) ? 'bottomLeft' : 'bottomRight';
10✔
3025
            }
3026
            $this->setActivePane($activePane);
48✔
3027
            $this->panes[$activePane] = new Pane($activePane, $this->selectedCells, $this->activeCell);
48✔
3028
        }
3029
    }
3030

3031
    /**
3032
     * Get right-to-left.
3033
     */
3034
    public function getRightToLeft(): bool
1,201✔
3035
    {
3036
        return $this->rightToLeft;
1,201✔
3037
    }
3038

3039
    /**
3040
     * Set right-to-left.
3041
     *
3042
     * @param bool $value Right-to-left true/false
3043
     *
3044
     * @return $this
3045
     */
3046
    public function setRightToLeft(bool $value): static
168✔
3047
    {
3048
        $this->rightToLeft = $value;
168✔
3049

3050
        return $this;
168✔
3051
    }
3052

3053
    /**
3054
     * Fill worksheet from values in array.
3055
     *
3056
     * @param mixed[]|mixed[][] $source Source array
3057
     * @param mixed $nullValue Value in source array that stands for blank cell
3058
     * @param string $startCell Insert array starting from this cell address as the top left coordinate
3059
     * @param bool $strictNullComparison Apply strict comparison when testing for null values in the array
3060
     *
3061
     * @return $this
3062
     */
3063
    public function fromArray(array $source, mixed $nullValue = null, string $startCell = 'A1', bool $strictNullComparison = false): static
934✔
3064
    {
3065
        //    Convert a 1-D array to 2-D (for ease of looping)
3066
        if (!is_array(end($source))) {
934✔
3067
            $source = [$source];
54✔
3068
        }
3069
        /** @var mixed[][] $source */
3070

3071
        // start coordinate
3072
        [$startColumn, $startRow] = Coordinate::coordinateFromString($startCell);
934✔
3073
        $startRow = (int) $startRow;
934✔
3074

3075
        // Loop through $source
3076
        if ($strictNullComparison) {
934✔
3077
            foreach ($source as $rowData) {
411✔
3078
                /** @var string */
3079
                $currentColumn = $startColumn;
411✔
3080
                foreach ($rowData as $cellValue) {
411✔
3081
                    if ($cellValue !== $nullValue) {
411✔
3082
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
411✔
3083
                    }
3084
                    StringHelper::stringIncrement($currentColumn);
411✔
3085
                }
3086
                ++$startRow;
411✔
3087
            }
3088
        } else {
3089
            foreach ($source as $rowData) {
532✔
3090
                $currentColumn = $startColumn;
532✔
3091
                foreach ($rowData as $cellValue) {
532✔
3092
                    if ($cellValue != $nullValue) {
531✔
3093
                        $this->getCell($currentColumn . $startRow)->setValue($cellValue);
525✔
3094
                    }
3095
                    StringHelper::stringIncrement($currentColumn);
531✔
3096
                }
3097
                ++$startRow;
532✔
3098
            }
3099
        }
3100

3101
        return $this;
934✔
3102
    }
3103

3104
    /**
3105
     * @param bool $calculateFormulas Whether to calculate cell's value if it is a formula.
3106
     * @param null|bool|float|int|RichText|string $nullValue value to use when null
3107
     * @param bool $formatData Whether to format data according to cell's style.
3108
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3109
     * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3110
     *
3111
     * @throws Exception
3112
     * @throws \PhpOffice\PhpSpreadsheet\Calculation\Exception
3113
     */
3114
    protected function cellToArray(Cell $cell, bool $calculateFormulas, bool $formatData, mixed $nullValue, bool $lessFloatPrecision = false, $oldCalculatedValue = false): mixed
210✔
3115
    {
3116
        $returnValue = $nullValue;
210✔
3117

3118
        if ($cell->getValue() !== null) {
210✔
3119
            if ($cell->getValue() instanceof RichText) {
210✔
3120
                $returnValue = $cell->getValue()->getPlainText();
4✔
3121
            } elseif ($calculateFormulas) {
210✔
3122
                $returnValue = $cell->getCalculatedValue();
181✔
3123
            } elseif ($oldCalculatedValue && ($cell->getDataType() === DataType::TYPE_FORMULA)) {
35✔
3124
                $returnValue = $cell->getOldCalculatedValue() ?? $cell->getValue();
2✔
3125
            } else {
3126
                $returnValue = $cell->getValue();
35✔
3127
            }
3128

3129
            if ($formatData) {
210✔
3130
                $style = $this->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex());
129✔
3131
                /** @var null|bool|float|int|RichText|string */
3132
                $returnValuex = $returnValue;
129✔
3133
                $returnValue = NumberFormat::toFormattedString(
129✔
3134
                    $returnValuex,
129✔
3135
                    $style->getNumberFormat()->getFormatCode() ?? NumberFormat::FORMAT_GENERAL,
129✔
3136
                    lessFloatPrecision: $lessFloatPrecision
129✔
3137
                );
129✔
3138
            }
3139
        }
3140

3141
        return $returnValue;
210✔
3142
    }
3143

3144
    /**
3145
     * Create array from a range of cells.
3146
     *
3147
     * @param null|bool|float|int|RichText|string $nullValue Value returned in the array entry if a cell doesn't exist
3148
     * @param bool $calculateFormulas Should formulas be calculated?
3149
     * @param bool $formatData Should formatting be applied to cell values?
3150
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3151
     *                             True - Return rows and columns indexed by their actual row and column IDs
3152
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3153
     *                            True - Don't return values for rows/columns that are defined as hidden.
3154
     * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3155
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3156
     * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3157
     *
3158
     * @return mixed[][]
3159
     */
3160
    public function rangeToArray(
175✔
3161
        string $range,
3162
        mixed $nullValue = null,
3163
        bool $calculateFormulas = true,
3164
        bool $formatData = true,
3165
        bool $returnCellRef = false,
3166
        bool $ignoreHidden = false,
3167
        bool $reduceArrays = false,
3168
        bool $lessFloatPrecision = false,
3169
        bool $oldCalculatedValue = false,
3170
    ): array {
3171
        $returnValue = [];
175✔
3172

3173
        // Loop through rows
3174
        foreach ($this->rangeToArrayYieldRows($range, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue) as $rowRef => $rowArray) {
175✔
3175
            /** @var int $rowRef */
3176
            $returnValue[$rowRef] = $rowArray;
175✔
3177
        }
3178

3179
        // Return
3180
        return $returnValue;
175✔
3181
    }
3182

3183
    /**
3184
     * Create array from a multiple ranges of cells. (such as A1:A3,A15,B17:C17).
3185
     *
3186
     * @param null|bool|float|int|RichText|string $nullValue Value returned in the array entry if a cell doesn't exist
3187
     * @param bool $calculateFormulas Should formulas be calculated?
3188
     * @param bool $formatData Should formatting be applied to cell values?
3189
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3190
     *                             True - Return rows and columns indexed by their actual row and column IDs
3191
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3192
     *                            True - Don't return values for rows/columns that are defined as hidden.
3193
     * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3194
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3195
     * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3196
     *
3197
     * @return mixed[][]
3198
     */
3199
    public function rangesToArray(
6✔
3200
        string $ranges,
3201
        mixed $nullValue = null,
3202
        bool $calculateFormulas = true,
3203
        bool $formatData = true,
3204
        bool $returnCellRef = false,
3205
        bool $ignoreHidden = false,
3206
        bool $reduceArrays = false,
3207
        bool $lessFloatPrecision = false,
3208
        bool $oldCalculatedValue = false,
3209
    ): array {
3210
        $returnValue = [];
6✔
3211

3212
        $parts = explode(',', $ranges);
6✔
3213
        foreach ($parts as $part) {
6✔
3214
            // Loop through rows
3215
            foreach ($this->rangeToArrayYieldRows($part, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue) as $rowRef => $rowArray) {
6✔
3216
                /** @var int $rowRef */
3217
                $returnValue[$rowRef] = $rowArray;
6✔
3218
            }
3219
        }
3220

3221
        // Return
3222
        return $returnValue;
6✔
3223
    }
3224

3225
    /**
3226
     * Create array from a range of cells, yielding each row in turn.
3227
     *
3228
     * @param null|bool|float|int|RichText|string $nullValue Value returned in the array entry if a cell doesn't exist
3229
     * @param bool $calculateFormulas Should formulas be calculated?
3230
     * @param bool $formatData Should formatting be applied to cell values?
3231
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3232
     *                             True - Return rows and columns indexed by their actual row and column IDs
3233
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3234
     *                            True - Don't return values for rows/columns that are defined as hidden.
3235
     * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3236
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3237
     * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3238
     *
3239
     * @return Generator<array<mixed>>
3240
     */
3241
    public function rangeToArrayYieldRows(
210✔
3242
        string $range,
3243
        mixed $nullValue = null,
3244
        bool $calculateFormulas = true,
3245
        bool $formatData = true,
3246
        bool $returnCellRef = false,
3247
        bool $ignoreHidden = false,
3248
        bool $reduceArrays = false,
3249
        bool $lessFloatPrecision = false,
3250
        bool $oldCalculatedValue = false,
3251
    ) {
3252
        $range = Validations::validateCellOrCellRange($range);
210✔
3253

3254
        //    Identify the range that we need to extract from the worksheet
3255
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range);
210✔
3256
        $minCol = Coordinate::stringFromColumnIndex($rangeStart[0]);
210✔
3257
        $minRow = $rangeStart[1];
210✔
3258
        $maxCol = Coordinate::stringFromColumnIndex($rangeEnd[0]);
210✔
3259
        $maxRow = $rangeEnd[1];
210✔
3260
        $minColInt = $rangeStart[0];
210✔
3261
        $maxColInt = $rangeEnd[0];
210✔
3262

3263
        StringHelper::stringIncrement($maxCol);
210✔
3264
        /** @var array<string, bool> */
3265
        $hiddenColumns = [];
210✔
3266
        $nullRow = $this->buildNullRow($nullValue, $minCol, $maxCol, $returnCellRef, $ignoreHidden, $hiddenColumns);
210✔
3267
        $hideColumns = !empty($hiddenColumns);
210✔
3268

3269
        $keys = $this->cellCollection->getSortedCoordinatesInt();
210✔
3270
        $keyIndex = 0;
210✔
3271
        $keysCount = count($keys);
210✔
3272
        // Loop through rows
3273
        for ($row = $minRow; $row <= $maxRow; ++$row) {
210✔
3274
            if (($ignoreHidden === true) && ($this->isRowVisible($row) === false)) {
210✔
3275
                continue;
4✔
3276
            }
3277
            $rowRef = $returnCellRef ? $row : ($row - $minRow);
210✔
3278
            $returnValue = $nullRow;
210✔
3279

3280
            $index = ($row - 1) * AddressRange::MAX_COLUMN_INT + 1;
210✔
3281
            $indexPlus = $index + AddressRange::MAX_COLUMN_INT - 1;
210✔
3282

3283
            // Binary search to quickly approach the correct index
3284
            $keyIndex = intdiv($keysCount, 2);
210✔
3285
            $boundLow = 0;
210✔
3286
            $boundHigh = $keysCount - 1;
210✔
3287
            while ($boundLow <= $boundHigh) {
210✔
3288
                $keyIndex = intdiv($boundLow + $boundHigh, 2);
210✔
3289
                if ($keys[$keyIndex] < $index) {
210✔
3290
                    $boundLow = $keyIndex + 1;
172✔
3291
                } elseif ($keys[$keyIndex] > $index) {
210✔
3292
                    $boundHigh = $keyIndex - 1;
192✔
3293
                } else {
3294
                    break;
202✔
3295
                }
3296
            }
3297

3298
            // Realign to the proper index value
3299
            while ($keyIndex > 0 && $keys[$keyIndex] > $index) {
210✔
3300
                --$keyIndex;
14✔
3301
            }
3302
            while ($keyIndex < $keysCount && $keys[$keyIndex] < $index) {
210✔
3303
                ++$keyIndex;
21✔
3304
            }
3305

3306
            while ($keyIndex < $keysCount && $keys[$keyIndex] <= $indexPlus) {
210✔
3307
                $key = $keys[$keyIndex];
210✔
3308
                $thisRow = intdiv($key - 1, AddressRange::MAX_COLUMN_INT) + 1;
210✔
3309
                $thisCol = ($key % AddressRange::MAX_COLUMN_INT) ?: AddressRange::MAX_COLUMN_INT;
210✔
3310
                if ($thisCol >= $minColInt && $thisCol <= $maxColInt) {
210✔
3311
                    $col = Coordinate::stringFromColumnIndex($thisCol);
210✔
3312
                    if ($hideColumns === false || !isset($hiddenColumns[$col])) {
210✔
3313
                        $columnRef = $returnCellRef ? $col : ($thisCol - $minColInt);
210✔
3314
                        $cell = $this->cellCollection->get("{$col}{$thisRow}");
210✔
3315
                        if ($cell !== null) {
210✔
3316
                            $value = $this->cellToArray($cell, $calculateFormulas, $formatData, $nullValue, lessFloatPrecision: $lessFloatPrecision, oldCalculatedValue: $oldCalculatedValue);
210✔
3317
                            if ($reduceArrays) {
210✔
3318
                                while (is_array($value)) {
21✔
3319
                                    $value = array_shift($value);
19✔
3320
                                }
3321
                            }
3322
                            if ($value !== $nullValue) {
210✔
3323
                                $returnValue[$columnRef] = $value;
210✔
3324
                            }
3325
                        }
3326
                    }
3327
                }
3328
                ++$keyIndex;
210✔
3329
            }
3330

3331
            yield $rowRef => $returnValue;
210✔
3332
        }
3333
    }
3334

3335
    /**
3336
     * Prepare a row data filled with null values to deduplicate the memory areas for empty rows.
3337
     *
3338
     * @param mixed $nullValue Value returned in the array entry if a cell doesn't exist
3339
     * @param string $minCol Start column of the range
3340
     * @param string $maxCol End column of the range
3341
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3342
     *                              True - Return rows and columns indexed by their actual row and column IDs
3343
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3344
     *                             True - Don't return values for rows/columns that are defined as hidden.
3345
     * @param array<string, bool> $hiddenColumns
3346
     *
3347
     * @return mixed[]
3348
     */
3349
    private function buildNullRow(
210✔
3350
        mixed $nullValue,
3351
        string $minCol,
3352
        string $maxCol,
3353
        bool $returnCellRef,
3354
        bool $ignoreHidden,
3355
        array &$hiddenColumns
3356
    ): array {
3357
        $nullRow = [];
210✔
3358
        $c = -1;
210✔
3359
        for ($col = $minCol; $col !== $maxCol; StringHelper::stringIncrement($col)) {
210✔
3360
            if ($ignoreHidden === true && $this->columnDimensionExists($col) && $this->getColumnDimension($col)->getVisible() === false) {
210✔
3361
                $hiddenColumns[$col] = true;
2✔
3362
            } else {
3363
                $columnRef = $returnCellRef ? $col : ++$c;
210✔
3364
                $nullRow[$columnRef] = $nullValue;
210✔
3365
            }
3366
        }
3367

3368
        return $nullRow;
210✔
3369
    }
3370

3371
    private function validateNamedRange(string $definedName, bool $returnNullIfInvalid = false): ?DefinedName
19✔
3372
    {
3373
        $namedRange = DefinedName::resolveName($definedName, $this);
19✔
3374
        if ($namedRange === null) {
19✔
3375
            if ($returnNullIfInvalid) {
6✔
3376
                return null;
5✔
3377
            }
3378

3379
            throw new Exception('Named Range ' . $definedName . ' does not exist.');
1✔
3380
        }
3381

3382
        if ($namedRange->isFormula()) {
13✔
3383
            if ($returnNullIfInvalid) {
×
3384
                return null;
×
3385
            }
3386

3387
            throw new Exception('Defined Named ' . $definedName . ' is a formula, not a range or cell.');
×
3388
        }
3389

3390
        if ($namedRange->getLocalOnly()) {
13✔
3391
            $worksheet = $namedRange->getWorksheet();
2✔
3392
            if ($worksheet === null || $this !== $worksheet) {
2✔
3393
                if ($returnNullIfInvalid) {
×
3394
                    return null;
×
3395
                }
3396

3397
                throw new Exception(
×
3398
                    'Named range ' . $definedName . ' is not accessible from within sheet ' . $this->getTitle()
×
3399
                );
×
3400
            }
3401
        }
3402

3403
        return $namedRange;
13✔
3404
    }
3405

3406
    /**
3407
     * Create array from a range of cells.
3408
     *
3409
     * @param string $definedName The Named Range that should be returned
3410
     * @param null|bool|float|int|RichText|string $nullValue Value returned in the array entry if a cell doesn't exist
3411
     * @param bool $calculateFormulas Should formulas be calculated?
3412
     * @param bool $formatData Should formatting be applied to cell values?
3413
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3414
     *                             True - Return rows and columns indexed by their actual row and column IDs
3415
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3416
     *                            True - Don't return values for rows/columns that are defined as hidden.
3417
     * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3418
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3419
     * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3420
     *
3421
     * @return mixed[][]
3422
     */
3423
    public function namedRangeToArray(
2✔
3424
        string $definedName,
3425
        mixed $nullValue = null,
3426
        bool $calculateFormulas = true,
3427
        bool $formatData = true,
3428
        bool $returnCellRef = false,
3429
        bool $ignoreHidden = false,
3430
        bool $reduceArrays = false,
3431
        bool $lessFloatPrecision = false,
3432
        bool $oldCalculatedValue = false,
3433
    ): array {
3434
        $retVal = [];
2✔
3435
        $namedRange = $this->validateNamedRange($definedName);
2✔
3436
        if ($namedRange !== null) {
1✔
3437
            $cellRange = ltrim(substr($namedRange->getValue(), (int) strrpos($namedRange->getValue(), '!')), '!');
1✔
3438
            $cellRange = str_replace('$', '', $cellRange);
1✔
3439
            $workSheet = $namedRange->getWorksheet();
1✔
3440
            if ($workSheet !== null) {
1✔
3441
                $retVal = $workSheet->rangeToArray($cellRange, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue);
1✔
3442
            }
3443
        }
3444

3445
        return $retVal;
1✔
3446
    }
3447

3448
    /**
3449
     * Create array from worksheet.
3450
     *
3451
     * @param null|bool|float|int|RichText|string $nullValue Value returned in the array entry if a cell doesn't exist
3452
     * @param bool $calculateFormulas Should formulas be calculated?
3453
     * @param bool $formatData Should formatting be applied to cell values?
3454
     * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero
3455
     *                             True - Return rows and columns indexed by their actual row and column IDs
3456
     * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden.
3457
     *                            True - Don't return values for rows/columns that are defined as hidden.
3458
     * @param bool $reduceArrays If true and result is a formula which evaluates to an array, reduce it to the top leftmost value.
3459
     * @param bool $lessFloatPrecision If true, formatting unstyled floats will convert them to a more human-friendly but less computationally accurate value
3460
     * @param bool $oldCalculatedValue If calculateFormulas is false and this is true, use oldCalculatedFormula instead.
3461
     *
3462
     * @return mixed[][]
3463
     */
3464
    public function toArray(
106✔
3465
        mixed $nullValue = null,
3466
        bool $calculateFormulas = true,
3467
        bool $formatData = true,
3468
        bool $returnCellRef = false,
3469
        bool $ignoreHidden = false,
3470
        bool $reduceArrays = false,
3471
        bool $lessFloatPrecision = false,
3472
        bool $oldCalculatedValue = false,
3473
    ): array {
3474
        // Garbage collect...
3475
        $this->garbageCollect();
106✔
3476
        $this->calculateArrays($calculateFormulas);
106✔
3477

3478
        //    Identify the range that we need to extract from the worksheet
3479
        $maxCol = $this->getHighestColumn();
106✔
3480
        $maxRow = $this->getHighestRow();
106✔
3481

3482
        // Return
3483
        return $this->rangeToArray("A1:{$maxCol}{$maxRow}", $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays, $lessFloatPrecision, $oldCalculatedValue);
106✔
3484
    }
3485

3486
    /**
3487
     * Get row iterator.
3488
     *
3489
     * @param int $startRow The row number at which to start iterating
3490
     * @param ?int $endRow The row number at which to stop iterating
3491
     */
3492
    public function getRowIterator(int $startRow = 1, ?int $endRow = null): RowIterator
117✔
3493
    {
3494
        return new RowIterator($this, $startRow, $endRow);
117✔
3495
    }
3496

3497
    /**
3498
     * Get column iterator.
3499
     *
3500
     * @param string $startColumn The column address at which to start iterating
3501
     * @param ?string $endColumn The column address at which to stop iterating
3502
     */
3503
    public function getColumnIterator(string $startColumn = 'A', ?string $endColumn = null): ColumnIterator
39✔
3504
    {
3505
        return new ColumnIterator($this, $startColumn, $endColumn);
39✔
3506
    }
3507

3508
    /**
3509
     * Run PhpSpreadsheet garbage collector.
3510
     *
3511
     * @return $this
3512
     */
3513
    public function garbageCollect(): static
1,356✔
3514
    {
3515
        // Flush cache
3516
        $this->cellCollection->get('A1');
1,356✔
3517

3518
        // Lookup highest column and highest row if cells are cleaned
3519
        $colRow = $this->cellCollection->getHighestRowAndColumn();
1,356✔
3520
        $highestRow = $colRow['row'];
1,356✔
3521
        $highestColumn = Coordinate::columnIndexFromString($colRow['column']);
1,356✔
3522

3523
        // Loop through column dimensions
3524
        foreach ($this->columnDimensions as $dimension) {
1,356✔
3525
            $highestColumn = max($highestColumn, Coordinate::columnIndexFromString($dimension->getColumnIndex()));
197✔
3526
        }
3527

3528
        // Loop through row dimensions
3529
        foreach ($this->rowDimensions as $dimension) {
1,356✔
3530
            $highestRow = max($highestRow, $dimension->getRowIndex());
125✔
3531
        }
3532

3533
        // Cache values
3534
        $this->cachedHighestColumn = max(1, $highestColumn);
1,356✔
3535
        /** @var int $highestRow */
3536
        $this->cachedHighestRow = $highestRow;
1,356✔
3537

3538
        // Return
3539
        return $this;
1,356✔
3540
    }
3541

3542
    /**
3543
     * @deprecated 5.2.0 Serves no useful purpose. No replacement.
3544
     *
3545
     * @codeCoverageIgnore
3546
     */
3547
    public function getHashInt(): int
3548
    {
3549
        return spl_object_id($this);
3550
    }
3551

3552
    /**
3553
     * Extract worksheet title from range.
3554
     *
3555
     * Example: extractSheetTitle("testSheet!A1") ==> 'A1'
3556
     * Example: extractSheetTitle("testSheet!A1:C3") ==> 'A1:C3'
3557
     * Example: extractSheetTitle("'testSheet 1'!A1", true) ==> ['testSheet 1', 'A1'];
3558
     * Example: extractSheetTitle("'testSheet 1'!A1:C3", true) ==> ['testSheet 1', 'A1:C3'];
3559
     * Example: extractSheetTitle("A1", true) ==> ['', 'A1'];
3560
     * Example: extractSheetTitle("A1:C3", true) ==> ['', 'A1:C3']
3561
     *
3562
     * @param ?string $range Range to extract title from
3563
     * @param bool $returnRange Return range? (see example)
3564
     *
3565
     * @return ($range is non-empty-string ? ($returnRange is true ? array{0: string, 1: string} : string) : ($returnRange is true ? array{0: null, 1: null} : null))
3566
     */
3567
    public static function extractSheetTitle(?string $range, bool $returnRange = false, bool $unapostrophize = false): array|null|string
11,253✔
3568
    {
3569
        if (empty($range)) {
11,253✔
3570
            return $returnRange ? [null, null] : null;
13✔
3571
        }
3572

3573
        // Sheet title included?
3574
        if (($sep = strrpos($range, '!')) === false) {
11,251✔
3575
            return $returnRange ? ['', $range] : '';
11,222✔
3576
        }
3577

3578
        if ($returnRange) {
1,499✔
3579
            $title = substr($range, 0, $sep);
1,499✔
3580
            if ($unapostrophize) {
1,499✔
3581
                $title = self::unApostrophizeTitle($title);
1,439✔
3582
            }
3583

3584
            return [$title, substr($range, $sep + 1)];
1,499✔
3585
        }
3586

3587
        return substr($range, $sep + 1);
7✔
3588
    }
3589

3590
    public static function unApostrophizeTitle(?string $title): string
1,453✔
3591
    {
3592
        $title ??= '';
1,453✔
3593
        if (str_starts_with($title, "'") && str_ends_with($title, "'")) {
1,453✔
3594
            $title = str_replace("''", "'", substr($title, 1, -1));
1,377✔
3595
        }
3596

3597
        return $title;
1,453✔
3598
    }
3599

3600
    /**
3601
     * Get hyperlink.
3602
     *
3603
     * @param string $cellCoordinate Cell coordinate to get hyperlink for, eg: 'A1'
3604
     */
3605
    public function getHyperlink(string $cellCoordinate): Hyperlink
113✔
3606
    {
3607
        $this->getCell($cellCoordinate)->setHadHyperlink(true);
113✔
3608
        // return hyperlink if we already have one
3609
        if (isset($this->hyperlinkCollection[$cellCoordinate])) {
113✔
3610
            return $this->hyperlinkCollection[$cellCoordinate];
61✔
3611
        }
3612

3613
        // else create hyperlink
3614
        $this->hyperlinkCollection[$cellCoordinate] = new Hyperlink();
113✔
3615

3616
        return $this->hyperlinkCollection[$cellCoordinate];
113✔
3617
    }
3618

3619
    /**
3620
     * Set hyperlink.
3621
     *
3622
     * @param string $cellCoordinate Cell coordinate to insert hyperlink, eg: 'A1'
3623
     *
3624
     * @return $this
3625
     */
3626
    public function setHyperlink(string $cellCoordinate, ?Hyperlink $hyperlink = null, bool $reset = true): static
597✔
3627
    {
3628
        if ($hyperlink === null) {
597✔
3629
            unset($this->hyperlinkCollection[$cellCoordinate]);
596✔
3630
            if ($reset) {
596✔
3631
                $this->getCell($cellCoordinate)
557✔
3632
                    ->setHadHyperlink(false);
557✔
3633
            }
3634
        } else {
3635
            $this->hyperlinkCollection[$cellCoordinate] = $hyperlink;
36✔
3636
            $this->getCell($cellCoordinate)->setHadHyperlink(true);
36✔
3637
        }
3638

3639
        return $this;
597✔
3640
    }
3641

3642
    /**
3643
     * Hyperlink at a specific coordinate exists?
3644
     *
3645
     * @param string $coordinate eg: 'A1'
3646
     */
3647
    public function hyperlinkExists(string $coordinate): bool
689✔
3648
    {
3649
        return isset($this->hyperlinkCollection[$coordinate]);
689✔
3650
    }
3651

3652
    /**
3653
     * Get collection of hyperlinks.
3654
     *
3655
     * @return Hyperlink[]
3656
     */
3657
    public function getHyperlinkCollection(): array
722✔
3658
    {
3659
        return $this->hyperlinkCollection;
722✔
3660
    }
3661

3662
    /**
3663
     * Get data validation.
3664
     *
3665
     * @param string $cellCoordinate Cell coordinate to get data validation for, eg: 'A1'
3666
     */
3667
    public function getDataValidation(string $cellCoordinate): DataValidation
37✔
3668
    {
3669
        // return data validation if we already have one
3670
        if (isset($this->dataValidationCollection[$cellCoordinate])) {
37✔
3671
            return $this->dataValidationCollection[$cellCoordinate];
28✔
3672
        }
3673

3674
        // or if cell is part of a data validation range
3675
        foreach ($this->dataValidationCollection as $key => $dataValidation) {
28✔
3676
            $keyParts = explode(' ', $key);
12✔
3677
            foreach ($keyParts as $keyPart) {
12✔
3678
                if ($keyPart === $cellCoordinate) {
12✔
3679
                    return $dataValidation;
1✔
3680
                }
3681
                if (str_contains($keyPart, ':')) {
12✔
3682
                    if (Coordinate::coordinateIsInsideRange($keyPart, $cellCoordinate)) {
9✔
3683
                        return $dataValidation;
9✔
3684
                    }
3685
                }
3686
            }
3687
        }
3688

3689
        // else create data validation
3690
        $dataValidation = new DataValidation();
20✔
3691
        $dataValidation->setSqref($cellCoordinate);
20✔
3692
        $this->dataValidationCollection[$cellCoordinate] = $dataValidation;
20✔
3693

3694
        return $dataValidation;
20✔
3695
    }
3696

3697
    /**
3698
     * Set data validation.
3699
     *
3700
     * @param string $cellCoordinate Cell coordinate to insert data validation, eg: 'A1'
3701
     *
3702
     * @return $this
3703
     */
3704
    public function setDataValidation(string $cellCoordinate, ?DataValidation $dataValidation = null): static
97✔
3705
    {
3706
        if ($dataValidation === null) {
97✔
3707
            unset($this->dataValidationCollection[$cellCoordinate]);
64✔
3708
        } else {
3709
            $dataValidation->setSqref($cellCoordinate);
40✔
3710
            $this->dataValidationCollection[$cellCoordinate] = $dataValidation;
40✔
3711
        }
3712

3713
        return $this;
97✔
3714
    }
3715

3716
    /**
3717
     * Data validation at a specific coordinate exists?
3718
     *
3719
     * @param string $coordinate eg: 'A1'
3720
     */
3721
    public function dataValidationExists(string $coordinate): bool
25✔
3722
    {
3723
        if (isset($this->dataValidationCollection[$coordinate])) {
25✔
3724
            return true;
23✔
3725
        }
3726
        foreach ($this->dataValidationCollection as $key => $dataValidation) {
8✔
3727
            $keyParts = explode(' ', $key);
7✔
3728
            foreach ($keyParts as $keyPart) {
7✔
3729
                if ($keyPart === $coordinate) {
7✔
3730
                    return true;
1✔
3731
                }
3732
                if (str_contains($keyPart, ':')) {
7✔
3733
                    if (Coordinate::coordinateIsInsideRange($keyPart, $coordinate)) {
2✔
3734
                        return true;
2✔
3735
                    }
3736
                }
3737
            }
3738
        }
3739

3740
        return false;
6✔
3741
    }
3742

3743
    /**
3744
     * Get collection of data validations.
3745
     *
3746
     * @return DataValidation[]
3747
     */
3748
    public function getDataValidationCollection(): array
723✔
3749
    {
3750
        $collectionCells = [];
723✔
3751
        $collectionRanges = [];
723✔
3752
        foreach ($this->dataValidationCollection as $key => $dataValidation) {
723✔
3753
            if (Preg::isMatch('/[: ]/', $key)) {
27✔
3754
                $collectionRanges[$key] = $dataValidation;
15✔
3755
            } else {
3756
                $collectionCells[$key] = $dataValidation;
22✔
3757
            }
3758
        }
3759

3760
        return array_merge($collectionCells, $collectionRanges);
723✔
3761
    }
3762

3763
    /**
3764
     * Accepts a range, returning it as a range that falls within the current highest row and column of the worksheet.
3765
     *
3766
     * @return string Adjusted range value
3767
     */
3768
    public function shrinkRangeToFit(string $range): string
1✔
3769
    {
3770
        $maxCol = $this->getHighestColumn();
1✔
3771
        $maxRow = $this->getHighestRow();
1✔
3772
        $maxCol = Coordinate::columnIndexFromString($maxCol);
1✔
3773

3774
        $rangeBlocks = explode(' ', $range);
1✔
3775
        foreach ($rangeBlocks as &$rangeSet) {
1✔
3776
            $rangeBoundaries = Coordinate::getRangeBoundaries($rangeSet);
1✔
3777

3778
            if (Coordinate::columnIndexFromString($rangeBoundaries[0][0]) > $maxCol) {
1✔
3779
                $rangeBoundaries[0][0] = Coordinate::stringFromColumnIndex($maxCol);
1✔
3780
            }
3781
            if ($rangeBoundaries[0][1] > $maxRow) {
1✔
3782
                $rangeBoundaries[0][1] = $maxRow;
1✔
3783
            }
3784
            if (Coordinate::columnIndexFromString($rangeBoundaries[1][0]) > $maxCol) {
1✔
3785
                $rangeBoundaries[1][0] = Coordinate::stringFromColumnIndex($maxCol);
1✔
3786
            }
3787
            if ($rangeBoundaries[1][1] > $maxRow) {
1✔
3788
                $rangeBoundaries[1][1] = $maxRow;
1✔
3789
            }
3790
            $rangeSet = $rangeBoundaries[0][0] . $rangeBoundaries[0][1] . ':' . $rangeBoundaries[1][0] . $rangeBoundaries[1][1];
1✔
3791
        }
3792
        unset($rangeSet);
1✔
3793

3794
        return implode(' ', $rangeBlocks);
1✔
3795
    }
3796

3797
    /**
3798
     * Get tab color.
3799
     */
3800
    public function getTabColor(): Color
23✔
3801
    {
3802
        if ($this->tabColor === null) {
23✔
3803
            $this->tabColor = new Color();
23✔
3804
        }
3805

3806
        return $this->tabColor;
23✔
3807
    }
3808

3809
    /**
3810
     * Reset tab color.
3811
     *
3812
     * @return $this
3813
     */
3814
    public function resetTabColor(): static
1✔
3815
    {
3816
        $this->tabColor = null;
1✔
3817

3818
        return $this;
1✔
3819
    }
3820

3821
    /**
3822
     * Tab color set?
3823
     */
3824
    public function isTabColorSet(): bool
609✔
3825
    {
3826
        return $this->tabColor !== null;
609✔
3827
    }
3828

3829
    /**
3830
     * Copy worksheet (!= clone!).
3831
     */
3832
    public function copy(): static
1✔
3833
    {
3834
        return clone $this;
1✔
3835
    }
3836

3837
    /**
3838
     * Returns a boolean true if the specified row contains no cells. By default, this means that no cell records
3839
     *          exist in the collection for this row. false will be returned otherwise.
3840
     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
3841
     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
3842
     *                  cells, then the row will be considered empty.
3843
     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
3844
     *                  string value cells, then the row will be considered empty.
3845
     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3846
     *                  If the only cells in the collection are null value or empty string value cells, then the row
3847
     *                  will be considered empty.
3848
     *
3849
     * @param int $definitionOfEmptyFlags
3850
     *              Possible Flag Values are:
3851
     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
3852
     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3853
     */
3854
    public function isEmptyRow(int $rowId, int $definitionOfEmptyFlags = 0): bool
9✔
3855
    {
3856
        try {
3857
            $iterator = new RowIterator($this, $rowId, $rowId);
9✔
3858
            $iterator->seek($rowId);
8✔
3859
            $row = $iterator->current();
8✔
3860
        } catch (Exception) {
1✔
3861
            return true;
1✔
3862
        }
3863

3864
        return $row->isEmpty($definitionOfEmptyFlags);
8✔
3865
    }
3866

3867
    /**
3868
     * Returns a boolean true if the specified column contains no cells. By default, this means that no cell records
3869
     *          exist in the collection for this column. false will be returned otherwise.
3870
     *     This rule can be modified by passing a $definitionOfEmptyFlags value:
3871
     *          1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value
3872
     *                  cells, then the column will be considered empty.
3873
     *          2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty
3874
     *                  string value cells, then the column will be considered empty.
3875
     *          3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3876
     *                  If the only cells in the collection are null value or empty string value cells, then the column
3877
     *                  will be considered empty.
3878
     *
3879
     * @param int $definitionOfEmptyFlags
3880
     *              Possible Flag Values are:
3881
     *                  CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL
3882
     *                  CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL
3883
     */
3884
    public function isEmptyColumn(string $columnId, int $definitionOfEmptyFlags = 0): bool
9✔
3885
    {
3886
        try {
3887
            $iterator = new ColumnIterator($this, $columnId, $columnId);
9✔
3888
            $iterator->seek($columnId);
8✔
3889
            $column = $iterator->current();
8✔
3890
        } catch (Exception) {
1✔
3891
            return true;
1✔
3892
        }
3893

3894
        return $column->isEmpty($definitionOfEmptyFlags);
8✔
3895
    }
3896

3897
    /**
3898
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
3899
     */
3900
    public function __clone()
28✔
3901
    {
3902
        foreach (get_object_vars($this) as $key => $val) {
28✔
3903
            if ($key == 'parent') {
28✔
3904
                continue;
28✔
3905
            }
3906

3907
            if (is_object($val) || (is_array($val))) {
28✔
3908
                if ($key === 'cellCollection') {
28✔
3909
                    $newCollection = $this->cellCollection->cloneCellCollection($this);
28✔
3910
                    $this->cellCollection = $newCollection;
28✔
3911
                } elseif ($key === 'drawingCollection') {
28✔
3912
                    $currentCollection = $this->drawingCollection;
28✔
3913
                    $this->drawingCollection = new ArrayObject();
28✔
3914
                    foreach ($currentCollection as $item) {
28✔
3915
                        $newDrawing = clone $item;
4✔
3916
                        $newDrawing->setWorksheet($this);
4✔
3917
                    }
3918
                } elseif ($key === 'inCellDrawingCollection') {
28✔
3919
                    $currentCollection = $this->inCellDrawingCollection;
28✔
3920
                    $this->inCellDrawingCollection = new ArrayObject();
28✔
3921
                    foreach ($currentCollection as $item) {
28✔
3922
                        $newDrawing = clone $item;
1✔
3923
                        $newDrawing->setWorksheet($this);
1✔
3924
                    }
3925
                } elseif ($key === 'tableCollection') {
28✔
3926
                    $currentCollection = $this->tableCollection;
28✔
3927
                    $this->tableCollection = new ArrayObject();
28✔
3928
                    foreach ($currentCollection as $item) {
28✔
3929
                        $newTable = clone $item;
1✔
3930
                        $newTable->setName($item->getName() . 'clone');
1✔
3931
                        $this->addTable($newTable);
1✔
3932
                    }
3933
                } elseif ($key === 'chartCollection') {
28✔
3934
                    $currentCollection = $this->chartCollection;
28✔
3935
                    $this->chartCollection = new ArrayObject();
28✔
3936
                    foreach ($currentCollection as $item) {
28✔
3937
                        $newChart = clone $item;
5✔
3938
                        $this->addChart($newChart);
5✔
3939
                    }
3940
                } elseif ($key === 'autoFilter') {
28✔
3941
                    $newAutoFilter = clone $this->autoFilter;
28✔
3942
                    $this->autoFilter = $newAutoFilter;
28✔
3943
                    $this->autoFilter->setParent($this);
28✔
3944
                } else {
3945
                    $this->{$key} = unserialize(serialize($val));
28✔
3946
                }
3947
            }
3948
        }
3949
    }
3950

3951
    /**
3952
     * Define the code name of the sheet.
3953
     *
3954
     * @param string $codeName Same rule as Title minus space not allowed (but, like Excel, change
3955
     *                       silently space to underscore)
3956
     * @param bool $validate False to skip validation of new title. WARNING: This should only be set
3957
     *                       at parse time (by Readers), where titles can be assumed to be valid.
3958
     *
3959
     * @return $this
3960
     */
3961
    public function setCodeName(string $codeName, bool $validate = true): static
11,504✔
3962
    {
3963
        // Is this a 'rename' or not?
3964
        if ($this->getCodeName() == $codeName) {
11,504✔
3965
            return $this;
×
3966
        }
3967

3968
        if ($validate) {
11,504✔
3969
            $codeName = str_replace(' ', '_', $codeName); //Excel does this automatically without flinching, we are doing the same
11,504✔
3970

3971
            // Syntax check
3972
            // throw an exception if not valid
3973
            self::checkSheetCodeName($codeName);
11,504✔
3974

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

3977
            if ($this->parent !== null) {
11,504✔
3978
                // Is there already such sheet name?
3979
                if ($this->parent->sheetCodeNameExists($codeName)) {
11,463✔
3980
                    // Use name, but append with lowest possible integer
3981

3982
                    if (StringHelper::countCharacters($codeName) > 29) {
764✔
3983
                        $codeName = StringHelper::substring($codeName, 0, 29);
×
3984
                    }
3985
                    $i = 1;
764✔
3986
                    while ($this->getParentOrThrow()->sheetCodeNameExists($codeName . '_' . $i)) {
764✔
3987
                        ++$i;
301✔
3988
                        if ($i == 10) {
301✔
3989
                            if (StringHelper::countCharacters($codeName) > 28) {
2✔
3990
                                $codeName = StringHelper::substring($codeName, 0, 28);
×
3991
                            }
3992
                        } elseif ($i == 100) {
301✔
3993
                            if (StringHelper::countCharacters($codeName) > 27) {
×
3994
                                $codeName = StringHelper::substring($codeName, 0, 27);
×
3995
                            }
3996
                        }
3997
                    }
3998

3999
                    $codeName .= '_' . $i; // ok, we have a valid name
764✔
4000
                }
4001
            }
4002
        }
4003

4004
        $this->codeName = $codeName;
11,504✔
4005

4006
        return $this;
11,504✔
4007
    }
4008

4009
    /**
4010
     * Return the code name of the sheet.
4011
     */
4012
    public function getCodeName(): ?string
11,504✔
4013
    {
4014
        return $this->codeName;
11,504✔
4015
    }
4016

4017
    /**
4018
     * Sheet has a code name ?
4019
     */
4020
    public function hasCodeName(): bool
2✔
4021
    {
4022
        return $this->codeName !== null;
2✔
4023
    }
4024

4025
    public static function nameRequiresQuotes(string $sheetName): bool
4✔
4026
    {
4027
        return !Preg::isMatch(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName);
4✔
4028
    }
4029

4030
    public function isRowVisible(int $row): bool
136✔
4031
    {
4032
        return !$this->rowDimensionExists($row) || $this->getRowDimension($row)->getVisible();
136✔
4033
    }
4034

4035
    /**
4036
     * Same as Cell->isLocked, but without creating cell if it doesn't exist.
4037
     */
4038
    public function isCellLocked(string $coordinate): bool
1✔
4039
    {
4040
        if ($this->getProtection()->getsheet() !== true) {
1✔
4041
            return false;
1✔
4042
        }
4043
        if ($this->cellExists($coordinate)) {
1✔
4044
            return $this->getCell($coordinate)->isLocked();
1✔
4045
        }
4046
        $spreadsheet = $this->parent;
1✔
4047
        $xfIndex = $this->getXfIndex($coordinate);
1✔
4048
        if ($spreadsheet === null || $xfIndex === null) {
1✔
4049
            return true;
1✔
4050
        }
4051

4052
        return $spreadsheet->getCellXfByIndex($xfIndex)->getProtection()->getLocked() !== StyleProtection::PROTECTION_UNPROTECTED;
×
4053
    }
4054

4055
    /**
4056
     * Same as Cell->isHiddenOnFormulaBar, but without creating cell if it doesn't exist.
4057
     */
4058
    public function isCellHiddenOnFormulaBar(string $coordinate): bool
1✔
4059
    {
4060
        if ($this->cellExists($coordinate)) {
1✔
4061
            return $this->getCell($coordinate)->isHiddenOnFormulaBar();
1✔
4062
        }
4063

4064
        // cell doesn't exist, therefore isn't a formula,
4065
        // therefore isn't hidden on formula bar.
4066
        return false;
1✔
4067
    }
4068

4069
    private function getXfIndex(string $coordinate): ?int
1✔
4070
    {
4071
        [$column, $row] = Coordinate::coordinateFromString($coordinate);
1✔
4072
        $row = (int) $row;
1✔
4073
        $xfIndex = null;
1✔
4074
        if ($this->rowDimensionExists($row)) {
1✔
4075
            $xfIndex = $this->getRowDimension($row)->getXfIndex();
×
4076
        }
4077
        if ($xfIndex === null && $this->ColumnDimensionExists($column)) {
1✔
4078
            $xfIndex = $this->getColumnDimension($column)->getXfIndex();
×
4079
        }
4080

4081
        return $xfIndex;
1✔
4082
    }
4083

4084
    private string $backgroundImage = '';
4085

4086
    private string $backgroundMime = '';
4087

4088
    private string $backgroundExtension = '';
4089

4090
    public function getBackgroundImage(): string
1,125✔
4091
    {
4092
        return $this->backgroundImage;
1,125✔
4093
    }
4094

4095
    public function getBackgroundMime(): string
462✔
4096
    {
4097
        return $this->backgroundMime;
462✔
4098
    }
4099

4100
    public function getBackgroundExtension(): string
462✔
4101
    {
4102
        return $this->backgroundExtension;
462✔
4103
    }
4104

4105
    /**
4106
     * Set background image.
4107
     * Used on read/write for Xlsx.
4108
     * Used on write for Html.
4109
     *
4110
     * @param string $backgroundImage Image represented as a string, e.g. results of file_get_contents
4111
     */
4112
    public function setBackgroundImage(string $backgroundImage): self
4✔
4113
    {
4114
        $imageArray = getimagesizefromstring($backgroundImage) ?: ['mime' => ''];
4✔
4115
        $mime = $imageArray['mime'];
4✔
4116
        if ($mime !== '') {
4✔
4117
            $extension = explode('/', $mime);
3✔
4118
            $extension = $extension[1];
3✔
4119
            $this->backgroundImage = $backgroundImage;
3✔
4120
            $this->backgroundMime = $mime;
3✔
4121
            $this->backgroundExtension = $extension;
3✔
4122
        }
4123

4124
        return $this;
4✔
4125
    }
4126

4127
    /**
4128
     * Copy cells, adjusting relative cell references in formulas.
4129
     * Acts similarly to Excel "fill handle" feature.
4130
     *
4131
     * @param string $fromCell Single source cell, e.g. C3
4132
     * @param string $toCells Single cell or cell range, e.g. C4 or C4:C10
4133
     * @param bool $copyStyle Copy styles as well as values, defaults to true
4134
     */
4135
    public function copyCells(string $fromCell, string $toCells, bool $copyStyle = true): void
1✔
4136
    {
4137
        $toArray = Coordinate::extractAllCellReferencesInRange($toCells);
1✔
4138
        $valueString = $this->getCell($fromCell)->getValueString();
1✔
4139
        /** @var mixed[][] */
4140
        $style = $this->getStyle($fromCell)->exportArray();
1✔
4141
        $fromIndexes = Coordinate::indexesFromString($fromCell);
1✔
4142
        $referenceHelper = ReferenceHelper::getInstance();
1✔
4143
        foreach ($toArray as $destination) {
1✔
4144
            if ($destination !== $fromCell) {
1✔
4145
                $toIndexes = Coordinate::indexesFromString($destination);
1✔
4146
                $this->getCell($destination)->setValue($referenceHelper->updateFormulaReferences($valueString, 'A1', $toIndexes[0] - $fromIndexes[0], $toIndexes[1] - $fromIndexes[1]));
1✔
4147
                if ($copyStyle) {
1✔
4148
                    $this->getCell($destination)->getStyle()->applyFromArray($style);
1✔
4149
                }
4150
            }
4151
        }
4152
    }
4153

4154
    public function calculateArrays(bool $preCalculateFormulas = true): void
1,327✔
4155
    {
4156
        if ($preCalculateFormulas && Calculation::getInstance($this->parent)->getInstanceArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY) {
1,327✔
4157
            $keys = $this->cellCollection->getCoordinates();
52✔
4158
            foreach ($keys as $key) {
52✔
4159
                if ($this->getCell($key)->getDataType() === DataType::TYPE_FORMULA) {
52✔
4160
                    if (!Preg::isMatch(self::FUNCTION_LIKE_GROUPBY, $this->getCell($key)->getValueString())) {
48✔
4161
                        $this->getCell($key)->getCalculatedValue();
47✔
4162
                    }
4163
                }
4164
            }
4165
        }
4166
    }
4167

4168
    public function isCellInSpillRange(string $coordinate): bool
2✔
4169
    {
4170
        if (Calculation::getInstance($this->parent)->getInstanceArrayReturnType() !== Calculation::RETURN_ARRAY_AS_ARRAY) {
2✔
4171
            return false;
1✔
4172
        }
4173
        $this->calculateArrays();
1✔
4174
        $keys = $this->cellCollection->getCoordinates();
1✔
4175
        foreach ($keys as $key) {
1✔
4176
            $attributes = $this->getCell($key)->getFormulaAttributes();
1✔
4177
            if (isset($attributes['ref'])) {
1✔
4178
                if (Coordinate::coordinateIsInsideRange($attributes['ref'], $coordinate)) {
1✔
4179
                    // false for first cell in range, true otherwise
4180
                    return $coordinate !== $key;
1✔
4181
                }
4182
            }
4183
        }
4184

4185
        return false;
1✔
4186
    }
4187

4188
    /** @param mixed[][] $styleArray */
4189
    public function applyStylesFromArray(string $coordinate, array $styleArray): bool
2✔
4190
    {
4191
        $spreadsheet = $this->parent;
2✔
4192
        if ($spreadsheet === null) {
2✔
4193
            return false;
1✔
4194
        }
4195
        $activeSheetIndex = $spreadsheet->getActiveSheetIndex();
1✔
4196
        $originalSelected = $this->selectedCells;
1✔
4197
        $this->getStyle($coordinate)->applyFromArray($styleArray);
1✔
4198
        $this->setSelectedCells($originalSelected);
1✔
4199
        if ($activeSheetIndex >= 0) {
1✔
4200
            $spreadsheet->setActiveSheetIndex($activeSheetIndex);
1✔
4201
        }
4202

4203
        return true;
1✔
4204
    }
4205

4206
    public function copyFormula(string $fromCell, string $toCell): void
1✔
4207
    {
4208
        $formula = $this->getCell($fromCell)->getValue();
1✔
4209
        $newFormula = $formula;
1✔
4210
        if (is_string($formula) && $this->getCell($fromCell)->getDataType() === DataType::TYPE_FORMULA) {
1✔
4211
            [$fromColInt, $fromRow] = Coordinate::indexesFromString($fromCell);
1✔
4212
            [$toColInt, $toRow] = Coordinate::indexesFromString($toCell);
1✔
4213
            $helper = ReferenceHelper::getInstance();
1✔
4214
            $newFormula = $helper->updateFormulaReferences(
1✔
4215
                $formula,
1✔
4216
                'A1',
1✔
4217
                $toColInt - $fromColInt,
1✔
4218
                $toRow - $fromRow
1✔
4219
            );
1✔
4220
        }
4221
        $this->setCellValue($toCell, $newFormula);
1✔
4222
    }
4223
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc