Completed
Push — master ( a907f1...ea97af )
by
unknown
36s queued 25s
created

Spreadsheet::garbageCollect()   F

Complexity

Conditions 17
Paths 4056

Size

Total Lines 74
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 17.289

Importance

Changes 0
Metric Value
cc 17
eloc 35
c 0
b 0
f 0
nc 4056
nop 0
dl 0
loc 74
rs 1.0499
ccs 27
cts 30
cp 0.9
crap 17.289

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet;
4
5
use JsonSerializable;
6
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
7
use PhpOffice\PhpSpreadsheet\Cell\IValueBinder;
8
use PhpOffice\PhpSpreadsheet\Document\Properties;
9
use PhpOffice\PhpSpreadsheet\Document\Security;
10
use PhpOffice\PhpSpreadsheet\Shared\Date;
11
use PhpOffice\PhpSpreadsheet\Shared\Font as SharedFont;
12
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
13
use PhpOffice\PhpSpreadsheet\Style\Style;
14
use PhpOffice\PhpSpreadsheet\Worksheet\Iterator;
15
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
16
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
17
18
class Spreadsheet implements JsonSerializable
19
{
20
    // Allowable values for workbook window visilbity
21
    const VISIBILITY_VISIBLE = 'visible';
22
    const VISIBILITY_HIDDEN = 'hidden';
23
    const VISIBILITY_VERY_HIDDEN = 'veryHidden';
24
25
    private const DEFINED_NAME_IS_RANGE = false;
26
    private const DEFINED_NAME_IS_FORMULA = true;
27
28
    private const WORKBOOK_VIEW_VISIBILITY_VALUES = [
29
        self::VISIBILITY_VISIBLE,
30
        self::VISIBILITY_HIDDEN,
31
        self::VISIBILITY_VERY_HIDDEN,
32
    ];
33
34
    protected int $excelCalendar = Date::CALENDAR_WINDOWS_1900;
35
36
    /**
37
     * Unique ID.
38
     */
39
    private string $uniqueID;
40
41
    /**
42
     * Document properties.
43
     */
44
    private Properties $properties;
45
46
    /**
47
     * Document security.
48
     */
49
    private Security $security;
50
51
    /**
52
     * Collection of Worksheet objects.
53
     *
54
     * @var Worksheet[]
55
     */
56
    private array $workSheetCollection;
57
58
    /**
59
     * Calculation Engine.
60
     */
61
    private Calculation $calculationEngine;
62
63
    /**
64
     * Active sheet index.
65
     */
66
    private int $activeSheetIndex;
67
68
    /**
69
     * Named ranges.
70
     *
71
     * @var DefinedName[]
72
     */
73
    private array $definedNames;
74
75
    /**
76
     * CellXf supervisor.
77
     */
78
    private Style $cellXfSupervisor;
79
80
    /**
81
     * CellXf collection.
82
     *
83
     * @var Style[]
84
     */
85
    private array $cellXfCollection = [];
86
87
    /**
88
     * CellStyleXf collection.
89
     *
90
     * @var Style[]
91
     */
92
    private array $cellStyleXfCollection = [];
93
94
    /**
95
     * hasMacros : this workbook have macros ?
96
     */
97
    private bool $hasMacros = false;
98
99
    /**
100
     * macrosCode : all macros code as binary data (the vbaProject.bin file, this include form, code,  etc.), null if no macro.
101
     */
102
    private ?string $macrosCode = null;
103
104
    /**
105
     * macrosCertificate : if macros are signed, contains binary data vbaProjectSignature.bin file, null if not signed.
106
     */
107
    private ?string $macrosCertificate = null;
108
109
    /**
110
     * ribbonXMLData : null if workbook is'nt Excel 2007 or not contain a customized UI.
111
     *
112
     * @var null|array{target: string, data: string}
113
     */
114
    private ?array $ribbonXMLData = null;
115
116
    /**
117
     * ribbonBinObjects : null if workbook is'nt Excel 2007 or not contain embedded objects (picture(s)) for Ribbon Elements
118
     * ignored if $ribbonXMLData is null.
119
     *
120
     * @var null|mixed[]
121
     */
122
    private ?array $ribbonBinObjects = null;
123
124
    /**
125
     * List of unparsed loaded data for export to same format with better compatibility.
126
     * It has to be minimized when the library start to support currently unparsed data.
127
     *
128
     * @var array<array<array<array<string>|string>>>
129
     */
130
    private array $unparsedLoadedData = [];
131
132
    /**
133
     * Controls visibility of the horizonal scroll bar in the application.
134
     */
135
    private bool $showHorizontalScroll = true;
136
137
    /**
138
     * Controls visibility of the horizonal scroll bar in the application.
139
     */
140
    private bool $showVerticalScroll = true;
141
142
    /**
143
     * Controls visibility of the sheet tabs in the application.
144
     */
145
    private bool $showSheetTabs = true;
146
147
    /**
148
     * Specifies a boolean value that indicates whether the workbook window
149
     * is minimized.
150
     */
151
    private bool $minimized = false;
152
153
    /**
154
     * Specifies a boolean value that indicates whether to group dates
155
     * when presenting the user with filtering optiomd in the user
156
     * interface.
157
     */
158
    private bool $autoFilterDateGrouping = true;
159
160
    /**
161
     * Specifies the index to the first sheet in the book view.
162
     */
163
    private int $firstSheetIndex = 0;
164
165
    /**
166
     * Specifies the visible status of the workbook.
167
     */
168
    private string $visibility = self::VISIBILITY_VISIBLE;
169
170
    /**
171
     * Specifies the ratio between the workbook tabs bar and the horizontal
172
     * scroll bar.  TabRatio is assumed to be out of 1000 of the horizontal
173
     * window width.
174
     */
175
    private int $tabRatio = 600;
176
177
    private Theme $theme;
178
179
    private ?IValueBinder $valueBinder = null;
180
181
    /** @var array<string, int> */
182
    private array $fontCharsets = [
183
        'B Nazanin' => SharedFont::CHARSET_ANSI_ARABIC,
184
    ];
185
186
    /**
187
     * @param int $charset uses any value from Shared\Font,
188
     *    but defaults to ARABIC because that is the only known
189
     *    charset for which this declaration might be needed
190
     */
191 21
    public function addFontCharset(string $fontName, int $charset = SharedFont::CHARSET_ANSI_ARABIC): void
192
    {
193 21
        $this->fontCharsets[$fontName] = $charset;
194
    }
195
196 394
    public function getFontCharset(string $fontName): int
197
    {
198 394
        return $this->fontCharsets[$fontName] ?? -1;
199
    }
200
201
    /**
202
     * Return all fontCharsets.
203
     *
204
     * @return array<string, int>
205
     */
206 1
    public function getFontCharsets(): array
207
    {
208 1
        return $this->fontCharsets;
209
    }
210
211 804
    public function getTheme(): Theme
212
    {
213 804
        return $this->theme;
214
    }
215
216
    /**
217
     * The workbook has macros ?
218
     */
219 436
    public function hasMacros(): bool
220
    {
221 436
        return $this->hasMacros;
222
    }
223
224
    /**
225
     * Define if a workbook has macros.
226
     *
227
     * @param bool $hasMacros true|false
228
     */
229 3
    public function setHasMacros(bool $hasMacros): void
230
    {
231 3
        $this->hasMacros = (bool) $hasMacros;
232
    }
233
234
    /**
235
     * Set the macros code.
236
     */
237 3
    public function setMacrosCode(?string $macroCode): void
238
    {
239 3
        $this->macrosCode = $macroCode;
240 3
        $this->setHasMacros($macroCode !== null);
241
    }
242
243
    /**
244
     * Return the macros code.
245
     */
246 3
    public function getMacrosCode(): ?string
247
    {
248 3
        return $this->macrosCode;
249
    }
250
251
    /**
252
     * Set the macros certificate.
253
     */
254 3
    public function setMacrosCertificate(?string $certificate): void
255
    {
256 3
        $this->macrosCertificate = $certificate;
257
    }
258
259
    /**
260
     * Is the project signed ?
261
     *
262
     * @return bool true|false
263
     */
264 2
    public function hasMacrosCertificate(): bool
265
    {
266 2
        return $this->macrosCertificate !== null;
267
    }
268
269
    /**
270
     * Return the macros certificate.
271
     */
272 2
    public function getMacrosCertificate(): ?string
273
    {
274 2
        return $this->macrosCertificate;
275
    }
276
277
    /**
278
     * Remove all macros, certificate from spreadsheet.
279
     */
280 1
    public function discardMacros(): void
281
    {
282 1
        $this->hasMacros = false;
283 1
        $this->macrosCode = null;
284 1
        $this->macrosCertificate = null;
285
    }
286
287
    /**
288
     * set ribbon XML data.
289
     */
290 2
    public function setRibbonXMLData(mixed $target, mixed $xmlData): void
291
    {
292 2
        if (is_string($target) && is_string($xmlData)) {
293 2
            $this->ribbonXMLData = ['target' => $target, 'data' => $xmlData];
294
        } else {
295
            $this->ribbonXMLData = null;
296
        }
297
    }
298
299
    /**
300
     * retrieve ribbon XML Data.
301
     *
302
     * @return mixed[]
303
     */
304 392
    public function getRibbonXMLData(string $what = 'all'): null|array|string //we need some constants here...
305
    {
306 392
        $returnData = null;
307 392
        $what = strtolower($what);
308
        switch ($what) {
309 392
            case 'all':
310 2
                $returnData = $this->ribbonXMLData;
311
312 2
                break;
313 392
            case 'target':
314 2
            case 'data':
315 392
                if (is_array($this->ribbonXMLData)) {
316 2
                    $returnData = $this->ribbonXMLData[$what];
317
                }
318
319 392
                break;
320
        }
321
322 392
        return $returnData;
323
    }
324
325
    /**
326
     * store binaries ribbon objects (pictures).
327
     */
328 2
    public function setRibbonBinObjects(mixed $binObjectsNames, mixed $binObjectsData): void
329
    {
330 2
        if ($binObjectsNames !== null && $binObjectsData !== null) {
331
            $this->ribbonBinObjects = ['names' => $binObjectsNames, 'data' => $binObjectsData];
332
        } else {
333 2
            $this->ribbonBinObjects = null;
334
        }
335
    }
336
337
    /**
338
     * List of unparsed loaded data for export to same format with better compatibility.
339
     * It has to be minimized when the library start to support currently unparsed data.
340
     *
341
     * @internal
342
     *
343
     * @return mixed[]
344
     */
345 437
    public function getUnparsedLoadedData(): array
346
    {
347 437
        return $this->unparsedLoadedData;
348
    }
349
350
    /**
351
     * List of unparsed loaded data for export to same format with better compatibility.
352
     * It has to be minimized when the library start to support currently unparsed data.
353
     *
354
     * @internal
355
     *
356
     * @param array<array<array<array<string>|string>>> $unparsedLoadedData
357
     */
358 686
    public function setUnparsedLoadedData(array $unparsedLoadedData): void
359
    {
360 686
        $this->unparsedLoadedData = $unparsedLoadedData;
361
    }
362
363
    /**
364
     * retrieve Binaries Ribbon Objects.
365
     *
366
     * @return mixed[]
367
     */
368 2
    public function getRibbonBinObjects(string $what = 'all'): ?array
369
    {
370 2
        $ReturnData = null;
371 2
        $what = strtolower($what);
372
        switch ($what) {
373 2
            case 'all':
374 2
                return $this->ribbonBinObjects;
375 1
            case 'names':
376 1
            case 'data':
377 1
                if (is_array($this->ribbonBinObjects) && is_array($this->ribbonBinObjects[$what] ?? null)) {
378
                    $ReturnData = $this->ribbonBinObjects[$what];
379
                }
380
381 1
                break;
382 1
            case 'types':
383
                if (
384 1
                    is_array($this->ribbonBinObjects)
385 1
                    && isset($this->ribbonBinObjects['data']) && is_array($this->ribbonBinObjects['data'])
386
                ) {
387
                    $tmpTypes = array_keys($this->ribbonBinObjects['data']);
388
                    $ReturnData = array_unique(array_map(fn (string $path): string => pathinfo($path, PATHINFO_EXTENSION), $tmpTypes));
389
                } else {
390 1
                    $ReturnData = []; // the caller want an array... not null if empty
391
                }
392
393 1
                break;
394
        }
395
396 1
        return $ReturnData;
397
    }
398
399
    /**
400
     * This workbook have a custom UI ?
401
     */
402 392
    public function hasRibbon(): bool
403
    {
404 392
        return $this->ribbonXMLData !== null;
405
    }
406
407
    /**
408
     * This workbook have additionnal object for the ribbon ?
409
     */
410 392
    public function hasRibbonBinObjects(): bool
411
    {
412 392
        return $this->ribbonBinObjects !== null;
413
    }
414
415
    /**
416
     * Check if a sheet with a specified code name already exists.
417
     *
418
     * @param string $codeName Name of the worksheet to check
419
     */
420 10613
    public function sheetCodeNameExists(string $codeName): bool
421
    {
422 10613
        return $this->getSheetByCodeName($codeName) !== null;
423
    }
424
425
    /**
426
     * Get sheet by code name. Warning : sheet don't have always a code name !
427
     *
428
     * @param string $codeName Sheet name
429
     */
430 10613
    public function getSheetByCodeName(string $codeName): ?Worksheet
431
    {
432 10613
        $worksheetCount = count($this->workSheetCollection);
433 10613
        for ($i = 0; $i < $worksheetCount; ++$i) {
434 724
            if ($this->workSheetCollection[$i]->getCodeName() == $codeName) {
435 690
                return $this->workSheetCollection[$i];
436
            }
437
        }
438
439 10613
        return null;
440
    }
441
442
    /**
443
     * Create a new PhpSpreadsheet with one Worksheet.
444
     */
445 10613
    public function __construct()
446
    {
447 10613
        $this->uniqueID = uniqid('', true);
448 10613
        $this->calculationEngine = new Calculation($this);
449 10613
        $this->theme = new Theme();
450
451
        // Initialise worksheet collection and add one worksheet
452 10613
        $this->workSheetCollection = [];
453 10613
        $this->workSheetCollection[] = new Worksheet($this);
454 10613
        $this->activeSheetIndex = 0;
455
456
        // Create document properties
457 10613
        $this->properties = new Properties();
458
459
        // Create document security
460 10613
        $this->security = new Security();
461
462
        // Set defined names
463 10613
        $this->definedNames = [];
464
465
        // Create the cellXf supervisor
466 10613
        $this->cellXfSupervisor = new Style(true);
467 10613
        $this->cellXfSupervisor->bindParent($this);
468
469
        // Create the default style
470 10613
        $this->addCellXf(new Style());
471 10613
        $this->addCellStyleXf(new Style());
472
    }
473
474
    /**
475
     * Code to execute when this worksheet is unset().
476
     */
477 121
    public function __destruct()
478
    {
479 121
        $this->disconnectWorksheets();
480 121
        unset($this->calculationEngine);
481 121
        $this->cellXfCollection = [];
482 121
        $this->cellStyleXfCollection = [];
483 121
        $this->definedNames = [];
484
    }
485
486
    /**
487
     * Disconnect all worksheets from this PhpSpreadsheet workbook object,
488
     * typically so that the PhpSpreadsheet object can be unset.
489
     */
490 9666
    public function disconnectWorksheets(): void
491
    {
492 9666
        foreach ($this->workSheetCollection as $worksheet) {
493 9665
            $worksheet->disconnectCells();
494 9665
            unset($worksheet);
495
        }
496 9666
        $this->workSheetCollection = [];
497
    }
498
499
    /**
500
     * Return the calculation engine for this worksheet.
501
     */
502 9692
    public function getCalculationEngine(): Calculation
503
    {
504 9692
        return $this->calculationEngine;
505
    }
506
507
    /**
508
     * Intended for use only via a destructor.
509
     *
510 1647
     * @internal
511
     */
512 1647
    public function getCalculationEngineOrNull(): ?Calculation
513
    {
514
        if (!isset($this->calculationEngine)) { //* @phpstan-ignore-line
515
            return null;
516
        }
517
518 1
        return $this->calculationEngine;
519
    }
520 1
521
    /**
522
     * Get properties.
523
     */
524
    public function getProperties(): Properties
525
    {
526 406
        return $this->properties;
527
    }
528 406
529
    /**
530
     * Set properties.
531
     */
532
    public function setProperties(Properties $documentProperties): void
533
    {
534 1
        $this->properties = $documentProperties;
535
    }
536 1
537
    /**
538
     * Get security.
539
     */
540
    public function getSecurity(): Security
541
    {
542 10539
        return $this->security;
543
    }
544 10539
545
    /**
546
     * Set security.
547
     */
548
    public function setSecurity(Security $documentSecurity): void
549
    {
550
        $this->security = $documentSecurity;
551
    }
552 1264
553
    /**
554 1264
     * Get active sheet.
555 1264
     */
556
    public function getActiveSheet(): Worksheet
557 1264
    {
558
        return $this->getSheet($this->activeSheetIndex);
559
    }
560
561
    /**
562
     * Create sheet and add it to this workbook.
563
     *
564
     * @param null|int $sheetIndex Index where sheet should go (0,1,..., or null for last)
565 1844
     */
566
    public function createSheet(?int $sheetIndex = null): Worksheet
567 1844
    {
568
        $newSheet = new Worksheet($this);
569
        $this->addSheet($newSheet, $sheetIndex, true);
570 1
571
        return $newSheet;
572 1
    }
573 1
574 1
    /**
575
     * Check if a sheet with a specified name already exists.
576 1
     *
577
     * @param string $worksheetName Name of the worksheet to check
578
     */
579
    public function sheetNameExists(string $worksheetName): bool
580
    {
581
        return $this->getSheetByName($worksheetName) !== null;
582
    }
583
584
    public function duplicateWorksheetByTitle(string $title): Worksheet
585 1355
    {
586
        $original = $this->getSheetByNameOrThrow($title);
587 1355
        $index = $this->getIndex($original) + 1;
588 1264
        $clone = clone $original;
589 1264
590 169
        return $this->addSheet($clone, $index, true);
591 169
    }
592 169
593 34
    /**
594 34
     * Add sheet.
595
     *
596 169
     * @param Worksheet $worksheet The worksheet to add
597
     * @param null|int $sheetIndex Index where sheet should go (0,1,..., or null for last)
598
     */
599 1355
    public function addSheet(Worksheet $worksheet, ?int $sheetIndex = null, bool $retitleIfNeeded = false): Worksheet
600 2
    {
601 2
        if ($retitleIfNeeded) {
602 2
            $title = $worksheet->getTitle();
603
            if ($this->sheetNameExists($title)) {
604
                $i = 1;
605 1355
                $newTitle = "$title $i";
606 1322
                while ($this->sheetNameExists($newTitle)) {
607 934
                    ++$i;
608
                    $newTitle = "$title $i";
609 1322
                }
610
                $worksheet->setTitle($newTitle);
611
            }
612 38
        }
613 38
        if ($this->sheetNameExists($worksheet->getTitle())) {
614 38
            throw new Exception(
615 38
                "Workbook already contains a worksheet named '{$worksheet->getTitle()}'. Rename this worksheet first."
616 38
            );
617 38
        }
618
619
        if ($sheetIndex === null) {
620 38
            if ($this->activeSheetIndex < 0) {
621 30
                $this->activeSheetIndex = 0;
622
            }
623 38
            $this->workSheetCollection[] = $worksheet;
624 3
        } else {
625
            // Insert the sheet at the requested index
626
            array_splice(
627
                $this->workSheetCollection,
628 1355
                $sheetIndex,
629 51
                0,
630
                [$worksheet]
631
            );
632 1355
633
            // Adjust active sheet index if necessary
634
            if ($this->activeSheetIndex >= $sheetIndex) {
635
                ++$this->activeSheetIndex;
636
            }
637
            if ($this->activeSheetIndex < 0) {
638
                $this->activeSheetIndex = 0;
639
            }
640 976
        }
641
642 976
        if ($worksheet->getParent() === null) {
643 976
            $worksheet->rebindParent($this);
644 1
        }
645 1
646 1
        return $worksheet;
647
    }
648 975
649
    /**
650
     * Remove sheet by index.
651
     *
652 975
     * @param int $sheetIndex Index position of the worksheet to remove
653 975
     */
654
    public function removeSheetByIndex(int $sheetIndex): void
655 973
    {
656
        $numSheets = count($this->workSheetCollection);
657
        if ($sheetIndex > $numSheets - 1) {
658
            throw new Exception(
659
                "You tried to remove a sheet by the out of bounds index: {$sheetIndex}. The actual number of sheets is {$numSheets}."
660
            );
661
        }
662
        array_splice($this->workSheetCollection, $sheetIndex, 1);
663
664 10545
        // Adjust active sheet index if necessary
665
        if (
666 10545
            ($this->activeSheetIndex >= $sheetIndex)
667 1
            && ($this->activeSheetIndex > 0 || $numSheets <= 1)
668
        ) {
669 1
            --$this->activeSheetIndex;
670 1
        }
671 1
    }
672
673
    /**
674 10545
     * Get sheet by index.
675
     *
676
     * @param int $sheetIndex Sheet index
677
     */
678
    public function getSheet(int $sheetIndex): Worksheet
679
    {
680
        if (!isset($this->workSheetCollection[$sheetIndex])) {
681
            $numSheets = $this->getSheetCount();
682 173
683
            throw new Exception(
684 173
                "Your requested sheet index: {$sheetIndex} is out of bounds. The actual number of sheets is {$numSheets}."
685
            );
686
        }
687
688
        return $this->workSheetCollection[$sheetIndex];
689
    }
690
691
    /**
692 9523
     * Get all sheets.
693
     *
694 9523
     * @return Worksheet[]
695 9523
     */
696 9172
    public function getAllSheets(): array
697 8589
    {
698
        return $this->workSheetCollection;
699
    }
700
701 1860
    /**
702
     * Get sheet by name.
703
     *
704
     * @param string $worksheetName Sheet name
705
     */
706
    public function getSheetByName(string $worksheetName): ?Worksheet
707 236
    {
708
        $trimWorksheetName = trim($worksheetName, "'");
709 236
        foreach ($this->workSheetCollection as $worksheet) {
710 236
            if (strcasecmp($worksheet->getTitle(), $trimWorksheetName) === 0) {
711 1
                return $worksheet;
712
            }
713
        }
714 235
715
        return null;
716
    }
717
718
    /**
719
     * Get sheet by name, throwing exception if not found.
720
     */
721
    public function getSheetByNameOrThrow(string $worksheetName): Worksheet
722 10614
    {
723
        $worksheet = $this->getSheetByName($worksheetName);
724 10614
        if ($worksheet === null) {
725 10614
            throw new Exception("Sheet $worksheetName does not exist.");
726 10386
        }
727 10374
728
        return $worksheet;
729
    }
730 10613
731 10613
    /**
732
     * Get index for sheet.
733
     *
734 3
     * @return int index
735
     */
736
    public function getIndex(Worksheet $worksheet, bool $noThrow = false): int
737
    {
738
        $wsHash = $worksheet->getHashInt();
739
        foreach ($this->workSheetCollection as $key => $value) {
740
            if ($value->getHashInt() === $wsHash) {
741
                return $key;
742
            }
743
        }
744
        if ($noThrow) {
745 1
            return -1;
746
        }
747 1
748 1
        throw new Exception('Sheet does not exist.');
749 1
    }
750 1
751 1
    /**
752 1
     * Set index for sheet by sheet name.
753 1
     *
754 1
     * @param string $worksheetName Sheet name to modify index for
755 1
     * @param int $newIndexPosition New index for the sheet
756 1
     *
757 1
     * @return int New sheet index
758 1
     */
759
    public function setIndexByName(string $worksheetName, int $newIndexPosition): int
760 1
    {
761
        $oldIndex = $this->getIndex($this->getSheetByNameOrThrow($worksheetName));
762
        $worksheet = array_splice(
763
            $this->workSheetCollection,
764
            $oldIndex,
765
            1
766 1626
        );
767
        array_splice(
768 1626
            $this->workSheetCollection,
769
            $newIndexPosition,
770
            0,
771
            $worksheet
772
        );
773
774
        return $newIndexPosition;
775
    }
776 10258
777
    /**
778 10258
     * Get sheet count.
779
     */
780
    public function getSheetCount(): int
781
    {
782
        return count($this->workSheetCollection);
783
    }
784
785
    /**
786 10341
     * Get active sheet index.
787
     *
788 10341
     * @return int Active sheet index
789
     */
790 10341
    public function getActiveSheetIndex(): int
791 6
    {
792 6
        return $this->activeSheetIndex;
793 6
    }
794
795 10335
    /**
796
     * Set active sheet index.
797 10335
     *
798
     * @param int $worksheetIndex Active sheet index
799
     */
800
    public function setActiveSheetIndex(int $worksheetIndex): Worksheet
801
    {
802
        $numSheets = count($this->workSheetCollection);
803
804
        if ($worksheetIndex > $numSheets - 1) {
805 106
            throw new Exception(
806
                "You tried to set a sheet active by the out of bounds index: {$worksheetIndex}. The actual number of sheets is {$numSheets}."
807 106
            );
808 104
        }
809
        $this->activeSheetIndex = $worksheetIndex;
810 104
811
        return $this->getActiveSheet();
812
    }
813 2
814
    /**
815
     * Set active sheet index by name.
816
     *
817
     * @param string $worksheetName Sheet title
818
     */
819
    public function setActiveSheetIndexByName(string $worksheetName): Worksheet
820
    {
821 10
        if (($worksheet = $this->getSheetByName($worksheetName)) instanceof Worksheet) {
822
            $this->setActiveSheetIndex($this->getIndex($worksheet));
823 10
824 10
            return $worksheet;
825 10
        }
826 10
827
        throw new Exception('Workbook does not contain sheet:' . $worksheetName);
828
    }
829 10
830
    /**
831
     * Get sheet names.
832
     *
833
     * @return string[]
834
     */
835
    public function getSheetNames(): array
836
    {
837
        $returnValue = [];
838 5
        $worksheetCount = $this->getSheetCount();
839
        for ($i = 0; $i < $worksheetCount; ++$i) {
840 5
            $returnValue[] = $this->getSheet($i)->getTitle();
841 1
        }
842
843
        return $returnValue;
844
    }
845 4
846
    /**
847
     * Add external sheet.
848 4
     *
849 4
     * @param Worksheet $worksheet External sheet to add
850
     * @param null|int $sheetIndex Index where sheet should go (0,1,..., or null for last)
851
     */
852
    public function addExternalSheet(Worksheet $worksheet, ?int $sheetIndex = null): Worksheet
853 4
    {
854
        if ($this->sheetNameExists($worksheet->getTitle())) {
855
            throw new Exception("Workbook already contains a worksheet named '{$worksheet->getTitle()}'. Rename the external sheet first.");
856 4
        }
857 4
858 4
        // count how many cellXfs there are in this workbook currently, we will need this below
859
        $countCellXfs = count($this->cellXfCollection);
860
861
        // copy all the shared cellXfs from the external workbook and append them to the current
862 4
        foreach ($worksheet->getParentOrThrow()->getCellXfCollection() as $cellXf) {
863 1
            $this->addCellXf(clone $cellXf);
864
        }
865
866
        // move sheet to this workbook
867 4
        $worksheet->rebindParent($this);
868 1
869 1
        // update the cellXfs
870 1
        foreach ($worksheet->getCoordinates(false) as $coordinate) {
871
            $cell = $worksheet->getCell($coordinate);
872
            $cell->setXfIndex($cell->getXfIndex() + $countCellXfs);
873
        }
874 4
875
        // update the column dimensions Xfs
876
        foreach ($worksheet->getColumnDimensions() as $columnDimension) {
877
            $columnDimension->setXfIndex($columnDimension->getXfIndex() + $countCellXfs);
878
        }
879
880
        // update the row dimensions Xfs
881
        foreach ($worksheet->getRowDimensions() as $rowDimension) {
882 9
            $xfIndex = $rowDimension->getXfIndex();
883
            if ($xfIndex !== null) {
884 9
                $rowDimension->setXfIndex($xfIndex + $countCellXfs);
885 9
            }
886 9
        }
887 9
888
        return $this->addSheet($worksheet, $sheetIndex);
889
    }
890
891
    /**
892
     * Get an array of all Named Ranges.
893
     *
894
     * @return DefinedName[]
895 15
     */
896
    public function getNamedRanges(): array
897 15
    {
898 15
        return array_filter(
899 15
            $this->definedNames,
900 15
            fn (DefinedName $definedName): bool => $definedName->isFormula() === self::DEFINED_NAME_IS_RANGE
901
        );
902
    }
903
904
    /**
905
     * Get an array of all Named Formulae.
906
     *
907
     * @return DefinedName[]
908 604
     */
909
    public function getNamedFormulae(): array
910 604
    {
911
        return array_filter(
912
            $this->definedNames,
913
            fn (DefinedName $definedName): bool => $definedName->isFormula() === self::DEFINED_NAME_IS_FORMULA
914
        );
915
    }
916
917 315
    /**
918
     * Get an array of all Defined Names (both named ranges and named formulae).
919 315
     *
920
     * @return DefinedName[]
921
     */
922
    public function getDefinedNames(): array
923
    {
924
        return $this->definedNames;
925
    }
926 12
927
    /**
928 12
     * Add a named range.
929
     * If a named range with this name already exists, then this will replace the existing value.
930
     */
931
    public function addNamedRange(NamedRange $namedRange): void
932
    {
933
        $this->addDefinedName($namedRange);
934
    }
935 453
936
    /**
937 453
     * Add a named formula.
938 453
     * If a named formula with this name already exists, then this will replace the existing value.
939
     */
940 439
    public function addNamedFormula(NamedFormula $namedFormula): void
941
    {
942
        $this->addDefinedName($namedFormula);
943 122
    }
944
945
    /**
946
     * Add a defined name (either a named range or a named formula).
947
     * If a defined named with this name already exists, then this will replace the existing value.
948
     */
949
    public function addDefinedName(DefinedName $definedName): void
950
    {
951
        $upperCaseName = StringHelper::strToUpper($definedName->getName());
952 33
        if ($definedName->getScope() == null) {
953
            // global scope
954 33
            $this->definedNames[$upperCaseName] = $definedName;
955
        } else {
956 33
            // local scope
957 32
            $this->definedNames[$definedName->getScope()->getTitle() . '!' . $upperCaseName] = $definedName;
958
        }
959 32
    }
960
961 32
    /**
962
     * Get named range.
963
     *
964 33
     * @param null|Worksheet $worksheet Scope. Use null for global scope
965
     */
966
    public function getNamedRange(string $namedRange, ?Worksheet $worksheet = null): ?NamedRange
967
    {
968
        $returnValue = null;
969
970
        if ($namedRange !== '') {
971
            $namedRange = StringHelper::strToUpper($namedRange);
972 11
            // first look for global named range
973
            $returnValue = $this->getGlobalDefinedNameByType($namedRange, self::DEFINED_NAME_IS_RANGE);
974 11
            // then look for local named range (has priority over global named range if both names exist)
975
            $returnValue = $this->getLocalDefinedNameByType($namedRange, self::DEFINED_NAME_IS_RANGE, $worksheet) ?: $returnValue;
976 11
        }
977 11
978
        return $returnValue instanceof NamedRange ? $returnValue : null;
979 11
    }
980
981 11
    /**
982
     * Get named formula.
983
     *
984 11
     * @param null|Worksheet $worksheet Scope. Use null for global scope
985
     */
986
    public function getNamedFormula(string $namedFormula, ?Worksheet $worksheet = null): ?NamedFormula
987 43
    {
988
        $returnValue = null;
989 43
990 30
        if ($namedFormula !== '') {
991
            $namedFormula = StringHelper::strToUpper($namedFormula);
992
            // first look for global named formula
993 15
            $returnValue = $this->getGlobalDefinedNameByType($namedFormula, self::DEFINED_NAME_IS_FORMULA);
994
            // then look for local named formula (has priority over global named formula if both names exist)
995
            $returnValue = $this->getLocalDefinedNameByType($namedFormula, self::DEFINED_NAME_IS_FORMULA, $worksheet) ?: $returnValue;
996 43
        }
997
998
        return $returnValue instanceof NamedFormula ? $returnValue : null;
999 43
    }
1000 43
1001
    private function getGlobalDefinedNameByType(string $name, bool $type): ?DefinedName
1002 8
    {
1003
        if (isset($this->definedNames[$name]) && $this->definedNames[$name]->isFormula() === $type) {
1004
            return $this->definedNames[$name];
1005 41
        }
1006
1007
        return null;
1008
    }
1009
1010
    private function getLocalDefinedNameByType(string $name, bool $type, ?Worksheet $worksheet = null): ?DefinedName
1011
    {
1012
        if (
1013 10293
            ($worksheet !== null) && isset($this->definedNames[$worksheet->getTitle() . '!' . $name])
1014
            && $this->definedNames[$worksheet->getTitle() . '!' . $name]->isFormula() === $type
1015 10293
        ) {
1016
            return $this->definedNames[$worksheet->getTitle() . '!' . $name];
1017 10293
        }
1018 10293
1019
        return null;
1020 10293
    }
1021 137
1022
    /**
1023
     * Get named range.
1024
     *
1025 10293
     * @param null|Worksheet $worksheet Scope. Use null for global scope
1026 22
     */
1027
    public function getDefinedName(string $definedName, ?Worksheet $worksheet = null): ?DefinedName
1028
    {
1029
        $returnValue = null;
1030 10293
1031
        if ($definedName !== '') {
1032
            $definedName = StringHelper::strToUpper($definedName);
1033
            // first look for global defined name
1034
            if (isset($this->definedNames[$definedName])) {
1035
                $returnValue = $this->definedNames[$definedName];
1036
            }
1037
1038
            // then look for local defined name (has priority over global defined name if both names exist)
1039
            if (($worksheet !== null) && isset($this->definedNames[$worksheet->getTitle() . '!' . $definedName])) {
1040 5
                $returnValue = $this->definedNames[$worksheet->getTitle() . '!' . $definedName];
1041
            }
1042 5
        }
1043 1
1044
        return $returnValue;
1045
    }
1046 4
1047
    /**
1048
     * Remove named range.
1049
     *
1050
     * @param null|Worksheet $worksheet scope: use null for global scope
1051
     *
1052
     * @return $this
1053
     */
1054
    public function removeNamedRange(string $namedRange, ?Worksheet $worksheet = null): self
1055
    {
1056 4
        if ($this->getNamedRange($namedRange, $worksheet) === null) {
1057
            return $this;
1058 4
        }
1059 1
1060
        return $this->removeDefinedName($namedRange, $worksheet);
1061
    }
1062 3
1063
    /**
1064
     * Remove named formula.
1065
     *
1066
     * @param null|Worksheet $worksheet scope: use null for global scope
1067
     *
1068
     * @return $this
1069
     */
1070
    public function removeNamedFormula(string $namedFormula, ?Worksheet $worksheet = null): self
1071
    {
1072 11
        if ($this->getNamedFormula($namedFormula, $worksheet) === null) {
1073
            return $this;
1074 11
        }
1075
1076 11
        return $this->removeDefinedName($namedFormula, $worksheet);
1077 1
    }
1078 1
1079
    /**
1080
     * Remove defined name.
1081 10
     *
1082 3
     * @param null|Worksheet $worksheet scope: use null for global scope
1083 7
     *
1084 7
     * @return $this
1085
     */
1086
    public function removeDefinedName(string $definedName, ?Worksheet $worksheet = null): self
1087
    {
1088 11
        $definedName = StringHelper::strToUpper($definedName);
1089
1090
        if ($worksheet === null) {
1091
            if (isset($this->definedNames[$definedName])) {
1092
                unset($this->definedNames[$definedName]);
1093
            }
1094 1421
        } else {
1095
            if (isset($this->definedNames[$worksheet->getTitle() . '!' . $definedName])) {
1096 1421
                unset($this->definedNames[$worksheet->getTitle() . '!' . $definedName]);
1097
            } elseif (isset($this->definedNames[$definedName])) {
1098
                unset($this->definedNames[$definedName]);
1099
            }
1100
        }
1101
1102 5
        return $this;
1103
    }
1104 5
1105
    /**
1106
     * Get worksheet iterator.
1107
     */
1108
    public function getWorksheetIterator(): Iterator
1109
    {
1110 5
        return new Iterator($this);
1111
    }
1112 5
1113
    /**
1114 5
     * Copy workbook (!= clone!).
1115
     */
1116 5
    public function copy(): self
1117
    {
1118 5
        return unserialize(serialize($this)); //* @phpstan-ignore-line
1119 5
    }
1120
1121 5
    /**
1122 5
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
1123 5
     */
1124 5
    public function __clone()
1125 5
    {
1126 5
        $this->uniqueID = uniqid('', true);
1127 5
1128 5
        $usedKeys = [];
1129 5
        // I don't now why new Style rather than clone.
1130 5
        $this->cellXfSupervisor = new Style(true);
1131 5
        //$this->cellXfSupervisor = clone $this->cellXfSupervisor;
1132 5
        $this->cellXfSupervisor->bindParent($this);
1133 5
        $usedKeys['cellXfSupervisor'] = true;
1134 5
1135 5
        $oldCalc = $this->calculationEngine;
1136 5
        $this->calculationEngine = new Calculation($this);
1137
        $this->calculationEngine
1138 5
            ->setSuppressFormulaErrors(
1139
                $oldCalc->getSuppressFormulaErrors()
1140 5
            )
1141 5
            ->setCalculationCacheEnabled(
1142 5
                $oldCalc->getCalculationCacheEnabled()
1143 5
            )
1144 5
            ->setBranchPruningEnabled(
1145 5
                $oldCalc->getBranchPruningEnabled()
1146
            )
1147 5
            ->setInstanceArrayReturnType(
1148
                $oldCalc->getInstanceArrayReturnType()
1149 5
            );
1150 5
        $usedKeys['calculationEngine'] = true;
1151 5
1152 5
        $currentCollection = $this->cellStyleXfCollection;
1153 5
        $this->cellStyleXfCollection = [];
1154 5
        foreach ($currentCollection as $item) {
1155
            $clone = $item->exportArray();
1156 5
            $style = (new Style())->applyFromArray($clone);
1157
            $this->addCellStyleXf($style);
1158 5
        }
1159 5
        $usedKeys['cellStyleXfCollection'] = true;
1160 5
1161 5
        $currentCollection = $this->cellXfCollection;
1162 5
        $this->cellXfCollection = [];
1163 5
        foreach ($currentCollection as $item) {
1164
            $clone = $item->exportArray();
1165 5
            $style = (new Style())->applyFromArray($clone);
1166
            $this->addCellXf($style);
1167 5
        }
1168 5
        $usedKeys['cellXfCollection'] = true;
1169 5
1170
        $currentCollection = $this->workSheetCollection;
1171
        $this->workSheetCollection = [];
1172
        foreach ($currentCollection as $item) {
1173 5
            $clone = clone $item;
1174
            $clone->setParent($this);
1175 5
            $this->workSheetCollection[] = $clone;
1176 5
        }
1177 5
        $usedKeys['workSheetCollection'] = true;
1178
1179
        foreach (get_object_vars($this) as $key => $val) {
1180
            if (isset($usedKeys[$key])) {
1181
                continue;
1182 5
            }
1183
            switch ($key) {
1184 5
                // arrays of objects not covered above
1185 5
                case 'definedNames':
1186
                    /** @var DefinedName[] */
1187
                    $currentCollection = $val;
1188
                    $this->$key = [];
1189
                    foreach ($currentCollection as $item) {
1190
                        $clone = clone $item;
1191
                        $this->{$key}[] = $clone;
1192
                    }
1193
1194
                    break;
1195
                default:
1196 1142
                    if (is_object($val)) {
1197
                        $this->$key = clone $val;
1198 1142
                    }
1199
            }
1200
        }
1201
    }
1202
1203
    /**
1204 10200
     * Get the workbook collection of cellXfs.
1205
     *
1206 10200
     * @return Style[]
1207
     */
1208
    public function getCellXfCollection(): array
1209 2
    {
1210
        return $this->cellXfCollection;
1211 2
    }
1212
1213
    /**
1214
     * Get cellXf by index.
1215
     */
1216
    public function getCellXfByIndex(int $cellStyleIndex): Style
1217
    {
1218
        return $this->cellXfCollection[$cellStyleIndex];
1219 914
    }
1220
1221 914
    public function getCellXfByIndexOrNull(?int $cellStyleIndex): ?Style
1222 914
    {
1223 267
        return ($cellStyleIndex === null) ? null : ($this->cellXfCollection[$cellStyleIndex] ?? null);
1224
    }
1225
1226
    /**
1227 851
     * Get cellXf by hash code.
1228
     *
1229
     * @return false|Style
1230
     */
1231
    public function getCellXfByHashCode(string $hashcode): bool|Style
1232
    {
1233 1
        foreach ($this->cellXfCollection as $cellXf) {
1234
            if ($cellXf->getHashCode() === $hashcode) {
1235 1
                return $cellXf;
1236
            }
1237
        }
1238
1239
        return false;
1240
    }
1241 1036
1242
    /**
1243 1036
     * Check if style exists in style collection.
1244 1035
     */
1245
    public function cellXfExists(Style $cellStyleIndex): bool
1246
    {
1247 1
        return in_array($cellStyleIndex, $this->cellXfCollection, true);
1248
    }
1249
1250
    /**
1251
     * Get default style.
1252
     */
1253 10613
    public function getDefaultStyle(): Style
1254
    {
1255 10613
        if (isset($this->cellXfCollection[0])) {
1256 10613
            return $this->cellXfCollection[0];
1257
        }
1258
1259
        throw new Exception('No default style found for this workbook');
1260
    }
1261
1262
    /**
1263
     * Add a cellXf to the workbook.
1264 813
     */
1265
    public function addCellXf(Style $style): void
1266 813
    {
1267 1
        $this->cellXfCollection[] = $style;
1268
        $style->setIndex(count($this->cellXfCollection) - 1);
1269
    }
1270
1271 812
    /**
1272
     * Remove cellXf by index. It is ensured that all cells get their xf index updated.
1273
     *
1274 812
     * @param int $cellStyleIndex Index to cellXf
1275 2
     */
1276 1
    public function removeCellXfByIndex(int $cellStyleIndex): void
1277 1
    {
1278 1
        if ($cellStyleIndex > count($this->cellXfCollection) - 1) {
1279
            throw new Exception('CellXf index is out of bounds.');
1280 1
        }
1281 1
1282
        // first remove the cellXf
1283 1
        array_splice($this->cellXfCollection, $cellStyleIndex, 1);
1284
1285
        // then update cellXf indexes for cells
1286
        foreach ($this->workSheetCollection as $worksheet) {
1287
            foreach ($worksheet->getCoordinates(false) as $coordinate) {
1288
                $cell = $worksheet->getCell($coordinate);
1289
                $xfIndex = $cell->getXfIndex();
1290
                if ($xfIndex > $cellStyleIndex) {
1291
                    // decrease xf index by 1
1292 10227
                    $cell->setXfIndex($xfIndex - 1);
1293
                } elseif ($xfIndex == $cellStyleIndex) {
1294 10227
                    // set to default xf index 0
1295
                    $cell->setXfIndex(0);
1296
                }
1297
            }
1298
        }
1299
    }
1300
1301
    /**
1302 1
     * Get the cellXf supervisor.
1303
     */
1304 1
    public function getCellXfSupervisor(): Style
1305
    {
1306
        return $this->cellXfSupervisor;
1307
    }
1308
1309
    /**
1310
     * Get the workbook collection of cellStyleXfs.
1311
     *
1312 1
     * @return Style[]
1313
     */
1314 1
    public function getCellStyleXfCollection(): array
1315
    {
1316
        return $this->cellStyleXfCollection;
1317
    }
1318
1319
    /**
1320
     * Get cellStyleXf by index.
1321
     *
1322 1
     * @param int $cellStyleIndex Index to cellXf
1323
     */
1324 1
    public function getCellStyleXfByIndex(int $cellStyleIndex): Style
1325 1
    {
1326 1
        return $this->cellStyleXfCollection[$cellStyleIndex];
1327
    }
1328
1329
    /**
1330 1
     * Get cellStyleXf by hash code.
1331
     *
1332
     * @return false|Style
1333
     */
1334
    public function getCellStyleXfByHashCode(string $hashcode): bool|Style
1335
    {
1336 10613
        foreach ($this->cellStyleXfCollection as $cellStyleXf) {
1337
            if ($cellStyleXf->getHashCode() === $hashcode) {
1338 10613
                return $cellStyleXf;
1339 10613
            }
1340
        }
1341
1342
        return false;
1343
    }
1344
1345
    /**
1346
     * Add a cellStyleXf to the workbook.
1347 810
     */
1348
    public function addCellStyleXf(Style $style): void
1349 810
    {
1350 1
        $this->cellStyleXfCollection[] = $style;
1351
        $style->setIndex(count($this->cellStyleXfCollection) - 1);
1352 809
    }
1353
1354
    /**
1355
     * Remove cellStyleXf by index.
1356
     *
1357
     * @param int $cellStyleIndex Index to cellXf
1358
     */
1359 1038
    public function removeCellStyleXfByIndex(int $cellStyleIndex): void
1360
    {
1361
        if ($cellStyleIndex > count($this->cellStyleXfCollection) - 1) {
1362 1038
            throw new Exception('CellStyleXf index is out of bounds.');
1363 1038
        }
1364 1038
        array_splice($this->cellStyleXfCollection, $cellStyleIndex, 1);
1365
    }
1366
1367 1038
    /**
1368
     * Eliminate all unneeded cellXf and afterwards update the xfIndex for all cells
1369 1038
     * and columns in the workbook.
1370 1007
     */
1371 1007
    public function garbageCollect(): void
1372
    {
1373
        // how many references are there to each cellXf ?
1374
        $countReferencesCellXf = [];
1375 1038
        foreach ($this->cellXfCollection as $index => $cellXf) {
1376 92
            $countReferencesCellXf[$index] = 0;
1377 11
        }
1378
1379
        foreach ($this->getWorksheetIterator() as $sheet) {
1380
            // from cells
1381
            foreach ($sheet->getCoordinates(false) as $coordinate) {
1382 1038
                $cell = $sheet->getCell($coordinate);
1383 148
                ++$countReferencesCellXf[$cell->getXfIndex()];
1384
            }
1385
1386
            // from row dimensions
1387
            foreach ($sheet->getRowDimensions() as $rowDimension) {
1388
                if ($rowDimension->getXfIndex() !== null) {
1389 1038
                    ++$countReferencesCellXf[$rowDimension->getXfIndex()];
1390 1038
                }
1391 1038
            }
1392 1038
1393 1038
            // from column dimensions
1394
            foreach ($sheet->getColumnDimensions() as $columnDimension) {
1395 37
                ++$countReferencesCellXf[$columnDimension->getXfIndex()];
1396
            }
1397 1038
        }
1398
1399 1038
        // remove cellXfs without references and create mapping so we can update xfIndex
1400
        // for all cells and columns
1401
        $countNeededCellXfs = 0;
1402 1038
        $map = [];
1403 1038
        foreach ($this->cellXfCollection as $index => $cellXf) {
1404
            if ($countReferencesCellXf[$index] > 0 || $index == 0) { // we must never remove the first cellXf
1405
                ++$countNeededCellXfs;
1406
            } else {
1407 1038
                unset($this->cellXfCollection[$index]);
1408
            }
1409
            $map[$index] = $countNeededCellXfs - 1;
1410
        }
1411
        $this->cellXfCollection = array_values($this->cellXfCollection);
1412 1038
1413
        // update the index for all cellXfs
1414 1038
        foreach ($this->cellXfCollection as $i => $cellXf) {
1415 1007
            $cellXf->setIndex($i);
1416 1007
        }
1417
1418
        // make sure there is always at least one cellXf (there should be)
1419
        if (empty($this->cellXfCollection)) {
1420 1038
            $this->cellXfCollection[] = new Style();
1421 92
        }
1422 11
1423
        // update the xfIndex for all cells, row dimensions, column dimensions
1424
        foreach ($this->getWorksheetIterator() as $sheet) {
1425
            // for all cells
1426
            foreach ($sheet->getCoordinates(false) as $coordinate) {
1427 1038
                $cell = $sheet->getCell($coordinate);
1428 148
                $cell->setXfIndex($map[$cell->getXfIndex()]);
1429
            }
1430
1431
            // for all row dimensions
1432 1038
            foreach ($sheet->getRowDimensions() as $rowDimension) {
1433
                if ($rowDimension->getXfIndex() !== null) {
1434
                    $rowDimension->setXfIndex($map[$rowDimension->getXfIndex()]);
1435
                }
1436
            }
1437
1438
            // for all column dimensions
1439
            foreach ($sheet->getColumnDimensions() as $columnDimension) {
1440
                $columnDimension->setXfIndex($map[$columnDimension->getXfIndex()]);
1441
            }
1442
1443
            // also do garbage collection for all the sheets
1444
            $sheet->garbageCollect();
1445
        }
1446
    }
1447
1448
    /**
1449 393
     * Return the unique ID value assigned to this spreadsheet workbook.
1450
     */
1451 393
    public function getID(): string
1452
    {
1453
        return $this->uniqueID;
1454
    }
1455
1456
    /**
1457
     * Get the visibility of the horizonal scroll bar in the application.
1458
     *
1459 274
     * @return bool True if horizonal scroll bar is visible
1460
     */
1461 274
    public function getShowHorizontalScroll(): bool
1462
    {
1463
        return $this->showHorizontalScroll;
1464
    }
1465
1466
    /**
1467
     * Set the visibility of the horizonal scroll bar in the application.
1468
     *
1469 393
     * @param bool $showHorizontalScroll True if horizonal scroll bar is visible
1470
     */
1471 393
    public function setShowHorizontalScroll(bool $showHorizontalScroll): void
1472
    {
1473
        $this->showHorizontalScroll = (bool) $showHorizontalScroll;
1474
    }
1475
1476
    /**
1477
     * Get the visibility of the vertical scroll bar in the application.
1478
     *
1479 274
     * @return bool True if vertical scroll bar is visible
1480
     */
1481 274
    public function getShowVerticalScroll(): bool
1482
    {
1483
        return $this->showVerticalScroll;
1484
    }
1485
1486
    /**
1487
     * Set the visibility of the vertical scroll bar in the application.
1488
     *
1489 393
     * @param bool $showVerticalScroll True if vertical scroll bar is visible
1490
     */
1491 393
    public function setShowVerticalScroll(bool $showVerticalScroll): void
1492
    {
1493
        $this->showVerticalScroll = (bool) $showVerticalScroll;
1494
    }
1495
1496
    /**
1497
     * Get the visibility of the sheet tabs in the application.
1498
     *
1499 274
     * @return bool True if the sheet tabs are visible
1500
     */
1501 274
    public function getShowSheetTabs(): bool
1502
    {
1503
        return $this->showSheetTabs;
1504
    }
1505
1506
    /**
1507
     * Set the visibility of the sheet tabs  in the application.
1508
     *
1509 393
     * @param bool $showSheetTabs True if sheet tabs are visible
1510
     */
1511 393
    public function setShowSheetTabs(bool $showSheetTabs): void
1512
    {
1513
        $this->showSheetTabs = (bool) $showSheetTabs;
1514
    }
1515
1516
    /**
1517
     * Return whether the workbook window is minimized.
1518
     *
1519 268
     * @return bool true if workbook window is minimized
1520
     */
1521 268
    public function getMinimized(): bool
1522
    {
1523
        return $this->minimized;
1524
    }
1525
1526
    /**
1527
     * Set whether the workbook window is minimized.
1528
     *
1529
     * @param bool $minimized true if workbook window is minimized
1530 393
     */
1531
    public function setMinimized(bool $minimized): void
1532 393
    {
1533
        $this->minimized = (bool) $minimized;
1534
    }
1535
1536
    /**
1537
     * Return whether to group dates when presenting the user with
1538
     * filtering optiomd in the user interface.
1539
     *
1540
     * @return bool true if workbook window is minimized
1541 268
     */
1542
    public function getAutoFilterDateGrouping(): bool
1543 268
    {
1544
        return $this->autoFilterDateGrouping;
1545
    }
1546
1547
    /**
1548
     * Set whether to group dates when presenting the user with
1549
     * filtering optiomd in the user interface.
1550
     *
1551 393
     * @param bool $autoFilterDateGrouping true if workbook window is minimized
1552
     */
1553 393
    public function setAutoFilterDateGrouping(bool $autoFilterDateGrouping): void
1554
    {
1555
        $this->autoFilterDateGrouping = (bool) $autoFilterDateGrouping;
1556
    }
1557
1558
    /**
1559
     * Return the first sheet in the book view.
1560
     *
1561 276
     * @return int First sheet in book view
1562
     */
1563 276
    public function getFirstSheetIndex(): int
1564 275
    {
1565
        return $this->firstSheetIndex;
1566 1
    }
1567
1568
    /**
1569
     * Set the first sheet in the book view.
1570
     *
1571
     * @param int $firstSheetIndex First sheet in book view
1572
     */
1573
    public function setFirstSheetIndex(int $firstSheetIndex): void
1574
    {
1575
        if ($firstSheetIndex >= 0) {
1576
            $this->firstSheetIndex = (int) $firstSheetIndex;
1577
        } else {
1578 394
            throw new Exception('First sheet index must be a positive integer.');
1579
        }
1580 394
    }
1581
1582
    /**
1583
     * Return the visibility status of the workbook.
1584
     *
1585
     * This may be one of the following three values:
1586
     * - visibile
1587
     *
1588
     * @return string Visible status
1589
     */
1590
    public function getVisibility(): string
1591
    {
1592
        return $this->visibility;
1593
    }
1594
1595
    /**
1596
     * Set the visibility status of the workbook.
1597
     *
1598 269
     * Valid values are:
1599
     *  - 'visible' (self::VISIBILITY_VISIBLE):
1600 269
     *       Workbook window is visible
1601 1
     *  - 'hidden' (self::VISIBILITY_HIDDEN):
1602
     *       Workbook window is hidden, but can be shown by the user
1603
     *       via the user interface
1604 269
     *  - 'veryHidden' (self::VISIBILITY_VERY_HIDDEN):
1605 269
     *       Workbook window is hidden and cannot be shown in the
1606
     *       user interface.
1607 1
     *
1608
     * @param null|string $visibility visibility status of the workbook
1609
     */
1610
    public function setVisibility(?string $visibility): void
1611
    {
1612
        if ($visibility === null) {
1613
            $visibility = self::VISIBILITY_VISIBLE;
1614
        }
1615
1616
        if (in_array($visibility, self::WORKBOOK_VIEW_VISIBILITY_VALUES)) {
1617 393
            $this->visibility = $visibility;
1618
        } else {
1619 393
            throw new Exception('Invalid visibility value.');
1620
        }
1621
    }
1622
1623
    /**
1624
     * Get the ratio between the workbook tabs bar and the horizontal scroll bar.
1625
     * TabRatio is assumed to be out of 1000 of the horizontal window width.
1626
     *
1627
     * @return int Ratio between the workbook tabs bar and the horizontal scroll bar
1628 280
     */
1629
    public function getTabRatio(): int
1630 280
    {
1631 279
        return $this->tabRatio;
1632
    }
1633 1
1634
    /**
1635
     * Set the ratio between the workbook tabs bar and the horizontal scroll bar
1636
     * TabRatio is assumed to be out of 1000 of the horizontal window width.
1637 2
     *
1638
     * @param int $tabRatio Ratio between the tabs bar and the horizontal scroll bar
1639 2
     */
1640 2
    public function setTabRatio(int $tabRatio): void
1641 2
    {
1642 2
        if ($tabRatio >= 0 && $tabRatio <= 1000) {
1643 1
            $this->tabRatio = (int) $tabRatio;
1644
        } else {
1645 2
            throw new Exception('Tab ratio must be between 0 and 1000.');
1646
        }
1647
    }
1648
1649
    public function reevaluateAutoFilters(bool $resetToMax): void
1650
    {
1651
        foreach ($this->workSheetCollection as $sheet) {
1652
            $filter = $sheet->getAutoFilter();
1653 1
            if (!empty($filter->getRange())) {
1654
                if ($resetToMax) {
1655 1
                    $filter->setRangeToMaxRow();
1656
                }
1657
                $filter->showHideRows();
1658 1
            }
1659
        }
1660 1
    }
1661 1
1662 1
    /**
1663 1
     * @throws Exception
1664 1
     */
1665 1
    public function jsonSerialize(): mixed
1666 1
    {
1667 1
        throw new Exception('Spreadsheet objects cannot be json encoded');
1668
    }
1669
1670 1
    public function resetThemeFonts(): void
1671 1
    {
1672 1
        $majorFontLatin = $this->theme->getMajorFontLatin();
1673
        $minorFontLatin = $this->theme->getMinorFontLatin();
1674 1
        foreach ($this->cellXfCollection as $cellStyleXf) {
1675 1
            $scheme = $cellStyleXf->getFont()->getScheme();
1676
            if ($scheme === 'major') {
1677
                $cellStyleXf->getFont()->setName($majorFontLatin)->setScheme($scheme);
1678
            } elseif ($scheme === 'minor') {
1679
                $cellStyleXf->getFont()->setName($minorFontLatin)->setScheme($scheme);
1680 39
            }
1681
        }
1682 39
        foreach ($this->cellStyleXfCollection as $cellStyleXf) {
1683 39
            $scheme = $cellStyleXf->getFont()->getScheme();
1684 39
            if ($scheme === 'major') {
1685 39
                $cellStyleXf->getFont()->setName($majorFontLatin)->setScheme($scheme);
1686 5
            } elseif ($scheme === 'minor') {
1687
                $cellStyleXf->getFont()->setName($minorFontLatin)->setScheme($scheme);
1688
            }
1689
        }
1690 39
    }
1691
1692
    public function getTableByName(string $tableName): ?Table
1693
    {
1694
        $table = null;
1695
        foreach ($this->workSheetCollection as $sheet) {
1696 805
            $table = $sheet->getTableByName($tableName);
1697
            if ($table !== null) {
1698 805
                break;
1699 805
            }
1700
        }
1701 805
1702
        return $table;
1703
    }
1704
1705
    /**
1706
     * @return bool Success or failure
1707
     */
1708
    public function setExcelCalendar(int $baseYear): bool
1709
    {
1710 8490
        if (($baseYear === Date::CALENDAR_WINDOWS_1900) || ($baseYear === Date::CALENDAR_MAC_1904)) {
1711
            $this->excelCalendar = $baseYear;
1712 8490
1713
            return true;
1714
        }
1715 2
1716
        return false;
1717 2
    }
1718
1719
    /**
1720 3
     * @return int Excel base date (1900 or 1904)
1721
     */
1722
    public function getExcelCalendar(): int
1723 3
    {
1724
        return $this->excelCalendar;
1725 3
    }
1726
1727
    public function deleteLegacyDrawing(Worksheet $worksheet): void
1728 10065
    {
1729
        unset($this->unparsedLoadedData['sheets'][$worksheet->getCodeName()]['legacyDrawing']);
1730 10065
    }
1731
1732
    public function getLegacyDrawing(Worksheet $worksheet): ?string
1733 1593
    {
1734
        /** @var ?string */
1735 1593
        $temp = $this->unparsedLoadedData['sheets'][$worksheet->getCodeName()]['legacyDrawing'] ?? null;
1736
1737 1593
        return $temp;
1738
    }
1739
1740
    public function getValueBinder(): ?IValueBinder
1741
    {
1742
        return $this->valueBinder;
1743
    }
1744
1745
    public function setValueBinder(?IValueBinder $valueBinder): self
1746
    {
1747
        $this->valueBinder = $valueBinder;
1748 1
1749
        return $this;
1750 1
    }
1751 1
1752 1
    /**
1753 1
     * All the PDF writers treat charts as if they occupy a single cell.
1754 1
     * This will be better most of the time.
1755 1
     * It is not needed for any other output type.
1756 1
     * It changes the contents of the spreadsheet, so you might
1757
     * be better off cloning the spreadsheet and then using
1758 1
     * this method on, and then writing, the clone.
1759
     */
1760
    public function mergeChartCellsForPdf(): void
1761
    {
1762
        foreach ($this->workSheetCollection as $worksheet) {
1763
            foreach ($worksheet->getChartCollection() as $chart) {
1764
                $br = $chart->getBottomRightCell();
1765
                $tl = $chart->getTopLeftCell();
1766
                if ($br !== '' && $br !== $tl) {
1767
                    if (!$worksheet->cellExists($br)) {
1768
                        $worksheet->getCell($br)->setValue(' ');
1769
                    }
1770
                    $worksheet->mergeCells("$tl:$br");
1771
                }
1772 1
            }
1773
        }
1774 1
    }
1775 1
1776 1
    /**
1777 1
     * All the PDF writers do better with drawings than charts.
1778 1
     * This will be better some of the time.
1779 1
     * It is not needed for any other output type.
1780 1
     * It changes the contents of the spreadsheet, so you might
1781
     * be better off cloning the spreadsheet and then using
1782 1
     * this method on, and then writing, the clone.
1783
     */
1784
    public function mergeDrawingCellsForPdf(): void
1785
    {
1786
        foreach ($this->workSheetCollection as $worksheet) {
1787
            foreach ($worksheet->getDrawingCollection() as $drawing) {
1788
                $br = $drawing->getCoordinates2();
1789
                $tl = $drawing->getCoordinates();
1790
                if ($br !== '' && $br !== $tl) {
1791
                    if (!$worksheet->cellExists($br)) {
1792
                        $worksheet->getCell($br)->setValue(' ');
1793
                    }
1794
                    $worksheet->mergeCells("$tl:$br");
1795 1
                }
1796
            }
1797 1
        }
1798 1
    }
1799 1
1800 1
    /**
1801
     * Excel will sometimes replace user's formatting choice
1802
     * with a built-in choice that it thinks is equivalent.
1803
     * Its choice is often not equivalent after all.
1804
     * Such treatment is astonishingly user-hostile.
1805
     * This function will undo such changes.
1806
     */
1807
    public function replaceBuiltinNumberFormat(int $builtinFormatIndex, string $formatCode): void
1808
    {
1809
        foreach ($this->cellXfCollection as $style) {
1810
            $numberFormat = $style->getNumberFormat();
1811
            if ($numberFormat->getBuiltInFormatCode() === $builtinFormatIndex) {
1812
                $numberFormat->setFormatCode($formatCode);
1813
            }
1814
        }
1815
    }
1816
1817
    public function returnArrayAsArray(): void
1818
    {
1819
        $this->calculationEngine->setInstanceArrayReturnType(
1820
            Calculation::RETURN_ARRAY_AS_ARRAY
1821
        );
1822
    }
1823
1824
    public function returnArrayAsValue(): void
1825
    {
1826
        $this->calculationEngine->setInstanceArrayReturnType(
1827
            Calculation::RETURN_ARRAY_AS_VALUE
1828
        );
1829
    }
1830
}
1831