Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
<?php
2
 
3
namespace PhpOffice\PhpSpreadsheet\Cell;
4
 
5
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException;
7
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
8
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
9
use PhpOffice\PhpSpreadsheet\Collection\Cells;
10
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
11
use PhpOffice\PhpSpreadsheet\RichText\RichText;
12
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDate;
13
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
14
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\CellStyleAssessor;
15
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
16
use PhpOffice\PhpSpreadsheet\Style\Protection;
17
use PhpOffice\PhpSpreadsheet\Style\Style;
18
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
19
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
20
use Stringable;
21
 
22
class Cell implements Stringable
23
{
24
    /**
25
     * Value binder to use.
26
     */
27
    private static ?IValueBinder $valueBinder = null;
28
 
29
    /**
30
     * Value of the cell.
31
     */
32
    private mixed $value;
33
 
34
    /**
35
     *    Calculated value of the cell (used for caching)
36
     *    This returns the value last calculated by MS Excel or whichever spreadsheet program was used to
37
     *        create the original spreadsheet file.
38
     *    Note that this value is not guaranteed to reflect the actual calculated value because it is
39
     *        possible that auto-calculation was disabled in the original spreadsheet, and underlying data
40
     *        values used by the formula have changed since it was last calculated.
41
     *
42
     * @var mixed
43
     */
44
    private $calculatedValue;
45
 
46
    /**
47
     * Type of the cell data.
48
     */
49
    private string $dataType;
50
 
51
    /**
52
     * The collection of cells that this cell belongs to (i.e. The Cell Collection for the parent Worksheet).
53
     */
54
    private ?Cells $parent;
55
 
56
    /**
57
     * Index to the cellXf reference for the styling of this cell.
58
     */
59
    private int $xfIndex = 0;
60
 
61
    /**
62
     * Attributes of the formula.
63
     *
64
     * @var null|array<string, string>
65
     */
66
    private ?array $formulaAttributes = null;
67
 
68
    private IgnoredErrors $ignoredErrors;
69
 
70
    /**
71
     * Update the cell into the cell collection.
72
     *
73
     * @throws SpreadsheetException
74
     */
75
    public function updateInCollection(): self
76
    {
77
        $parent = $this->parent;
78
        if ($parent === null) {
79
            throw new SpreadsheetException('Cannot update when cell is not bound to a worksheet');
80
        }
81
        $parent->update($this);
82
 
83
        return $this;
84
    }
85
 
86
    public function detach(): void
87
    {
88
        $this->parent = null;
89
    }
90
 
91
    public function attach(Cells $parent): void
92
    {
93
        $this->parent = $parent;
94
    }
95
 
96
    /**
97
     * Create a new Cell.
98
     *
99
     * @throws SpreadsheetException
100
     */
101
    public function __construct(mixed $value, ?string $dataType, Worksheet $worksheet)
102
    {
103
        // Initialise cell value
104
        $this->value = $value;
105
 
106
        // Set worksheet cache
107
        $this->parent = $worksheet->getCellCollection();
108
 
109
        // Set datatype?
110
        if ($dataType !== null) {
111
            if ($dataType == DataType::TYPE_STRING2) {
112
                $dataType = DataType::TYPE_STRING;
113
            }
114
            $this->dataType = $dataType;
115
        } else {
116
            $valueBinder = $worksheet->getParent()?->getValueBinder() ?? self::getValueBinder();
117
            if ($valueBinder->bindValue($this, $value) === false) {
118
                throw new SpreadsheetException('Value could not be bound to cell.');
119
            }
120
        }
121
        $this->ignoredErrors = new IgnoredErrors();
122
    }
123
 
124
    /**
125
     * Get cell coordinate column.
126
     *
127
     * @throws SpreadsheetException
128
     */
129
    public function getColumn(): string
130
    {
131
        $parent = $this->parent;
132
        if ($parent === null) {
133
            throw new SpreadsheetException('Cannot get column when cell is not bound to a worksheet');
134
        }
135
 
136
        return $parent->getCurrentColumn();
137
    }
138
 
139
    /**
140
     * Get cell coordinate row.
141
     *
142
     * @throws SpreadsheetException
143
     */
144
    public function getRow(): int
145
    {
146
        $parent = $this->parent;
147
        if ($parent === null) {
148
            throw new SpreadsheetException('Cannot get row when cell is not bound to a worksheet');
149
        }
150
 
151
        return $parent->getCurrentRow();
152
    }
153
 
154
    /**
155
     * Get cell coordinate.
156
     *
157
     * @throws SpreadsheetException
158
     */
159
    public function getCoordinate(): string
160
    {
161
        $parent = $this->parent;
162
        if ($parent !== null) {
163
            $coordinate = $parent->getCurrentCoordinate();
164
        } else {
165
            $coordinate = null;
166
        }
167
        if ($coordinate === null) {
168
            throw new SpreadsheetException('Coordinate no longer exists');
169
        }
170
 
171
        return $coordinate;
172
    }
173
 
174
    /**
175
     * Get cell value.
176
     */
177
    public function getValue(): mixed
178
    {
179
        return $this->value;
180
    }
181
 
182
    public function getValueString(): string
183
    {
184
        $value = $this->value;
185
 
186
        return ($value === '' || is_scalar($value) || $value instanceof Stringable) ? "$value" : '';
187
    }
188
 
189
    /**
190
     * Get cell value with formatting.
191
     */
192
    public function getFormattedValue(): string
193
    {
194
        $currentCalendar = SharedDate::getExcelCalendar();
195
        SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar());
196
        $formattedValue = (string) NumberFormat::toFormattedString(
197
            $this->getCalculatedValueString(),
198
            (string) $this->getStyle()->getNumberFormat()->getFormatCode(true)
199
        );
200
        SharedDate::setExcelCalendar($currentCalendar);
201
 
202
        return $formattedValue;
203
    }
204
 
205
    protected static function updateIfCellIsTableHeader(?Worksheet $workSheet, self $cell, mixed $oldValue, mixed $newValue): void
206
    {
207
        $oldValue = (is_scalar($oldValue) || $oldValue instanceof Stringable) ? ((string) $oldValue) : null;
208
        $newValue = (is_scalar($newValue) || $newValue instanceof Stringable) ? ((string) $newValue) : null;
209
        if (StringHelper::strToLower($oldValue ?? '') === StringHelper::strToLower($newValue ?? '') || $workSheet === null) {
210
            return;
211
        }
212
 
213
        foreach ($workSheet->getTableCollection() as $table) {
214
            /** @var Table $table */
215
            if ($cell->isInRange($table->getRange())) {
216
                $rangeRowsColumns = Coordinate::getRangeBoundaries($table->getRange());
217
                if ($cell->getRow() === (int) $rangeRowsColumns[0][1]) {
218
                    Table\Column::updateStructuredReferences($workSheet, $oldValue, $newValue);
219
                }
220
 
221
                return;
222
            }
223
        }
224
    }
225
 
226
    /**
227
     * Set cell value.
228
     *
229
     *    Sets the value for a cell, automatically determining the datatype using the value binder
230
     *
231
     * @param mixed $value Value
232
     * @param null|IValueBinder $binder Value Binder to override the currently set Value Binder
233
     *
234
     * @throws SpreadsheetException
235
     */
236
    public function setValue(mixed $value, ?IValueBinder $binder = null): self
237
    {
238
        // Cells?->Worksheet?->Spreadsheet
239
        $binder ??= $this->parent?->getParent()?->getParent()?->getValueBinder() ?? self::getValueBinder();
240
        if (!$binder->bindValue($this, $value)) {
241
            throw new SpreadsheetException('Value could not be bound to cell.');
242
        }
243
 
244
        return $this;
245
    }
246
 
247
    /**
248
     * Set the value for a cell, with the explicit data type passed to the method (bypassing any use of the value binder).
249
     *
250
     * @param mixed $value Value
251
     * @param string $dataType Explicit data type, see DataType::TYPE_*
252
     *        Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this
253
     *             method, then it is your responsibility as an end-user developer to validate that the value and
254
     *             the datatype match.
255
     *       If you do mismatch value and datatype, then the value you enter may be changed to match the datatype
256
     *          that you specify.
257
     *
258
     * @throws SpreadsheetException
259
     */
260
    public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE_STRING): self
261
    {
262
        $oldValue = $this->value;
263
        $quotePrefix = false;
264
 
265
        // set the value according to data type
266
        switch ($dataType) {
267
            case DataType::TYPE_NULL:
268
                $this->value = null;
269
 
270
                break;
271
            case DataType::TYPE_STRING2:
272
                $dataType = DataType::TYPE_STRING;
273
                // no break
274
            case DataType::TYPE_STRING:
275
                // Synonym for string
276
                if (is_string($value) && strlen($value) > 1 && $value[0] === '=') {
277
                    $quotePrefix = true;
278
                }
279
                // no break
280
            case DataType::TYPE_INLINE:
281
                // Rich text
282
                if ($value !== null && !is_scalar($value) && !($value instanceof Stringable)) {
283
                    throw new SpreadsheetException('Invalid unstringable value for datatype Inline/String/String2');
284
                }
285
                $this->value = DataType::checkString(($value instanceof RichText) ? $value : ((string) $value));
286
 
287
                break;
288
            case DataType::TYPE_NUMERIC:
289
                if (is_string($value) && !is_numeric($value)) {
290
                    throw new SpreadsheetException('Invalid numeric value for datatype Numeric');
291
                }
292
                $this->value = 0 + $value;
293
 
294
                break;
295
            case DataType::TYPE_FORMULA:
296
                if ($value !== null && !is_scalar($value) && !($value instanceof Stringable)) {
297
                    throw new SpreadsheetException('Invalid unstringable value for datatype Formula');
298
                }
299
                $this->value = (string) $value;
300
 
301
                break;
302
            case DataType::TYPE_BOOL:
303
                $this->value = (bool) $value;
304
 
305
                break;
306
            case DataType::TYPE_ISO_DATE:
307
                $this->value = SharedDate::convertIsoDate($value);
308
                $dataType = DataType::TYPE_NUMERIC;
309
 
310
                break;
311
            case DataType::TYPE_ERROR:
312
                $this->value = DataType::checkErrorCode($value);
313
 
314
                break;
315
            default:
316
                throw new SpreadsheetException('Invalid datatype: ' . $dataType);
317
        }
318
 
319
        // set the datatype
320
        $this->dataType = $dataType;
321
 
322
        $this->updateInCollection();
323
        $cellCoordinate = $this->getCoordinate();
324
        self::updateIfCellIsTableHeader($this->getParent()?->getParent(), $this, $oldValue, $value);
325
        $worksheet = $this->getWorksheet();
326
        $spreadsheet = $worksheet->getParent();
327
        if (isset($spreadsheet) && $spreadsheet->getIndex($worksheet, true) >= 0) {
328
            $originalSelected = $worksheet->getSelectedCells();
329
            $activeSheetIndex = $spreadsheet->getActiveSheetIndex();
330
            $style = $this->getStyle();
331
            $oldQuotePrefix = $style->getQuotePrefix();
332
            if ($oldQuotePrefix !== $quotePrefix) {
333
                $style->setQuotePrefix($quotePrefix);
334
            }
335
            $worksheet->setSelectedCells($originalSelected);
336
            if ($activeSheetIndex >= 0) {
337
                $spreadsheet->setActiveSheetIndex($activeSheetIndex);
338
            }
339
        }
340
 
341
        return $this->getParent()?->get($cellCoordinate) ?? $this;
342
    }
343
 
344
    public const CALCULATE_DATE_TIME_ASIS = 0;
345
    public const CALCULATE_DATE_TIME_FLOAT = 1;
346
    public const CALCULATE_TIME_FLOAT = 2;
347
 
348
    private static int $calculateDateTimeType = self::CALCULATE_DATE_TIME_ASIS;
349
 
350
    public static function getCalculateDateTimeType(): int
351
    {
352
        return self::$calculateDateTimeType;
353
    }
354
 
355
    /** @throws CalculationException */
356
    public static function setCalculateDateTimeType(int $calculateDateTimeType): void
357
    {
358
        self::$calculateDateTimeType = match ($calculateDateTimeType) {
359
            self::CALCULATE_DATE_TIME_ASIS, self::CALCULATE_DATE_TIME_FLOAT, self::CALCULATE_TIME_FLOAT => $calculateDateTimeType,
360
            default => throw new CalculationException("Invalid value $calculateDateTimeType for calculated date time type"),
361
        };
362
    }
363
 
364
    /**
365
     * Convert date, time, or datetime from int to float if desired.
366
     */
367
    private function convertDateTimeInt(mixed $result): mixed
368
    {
369
        if (is_int($result)) {
370
            if (self::$calculateDateTimeType === self::CALCULATE_TIME_FLOAT) {
371
                if (SharedDate::isDateTime($this, $result, false)) {
372
                    $result = (float) $result;
373
                }
374
            } elseif (self::$calculateDateTimeType === self::CALCULATE_DATE_TIME_FLOAT) {
375
                if (SharedDate::isDateTime($this, $result, true)) {
376
                    $result = (float) $result;
377
                }
378
            }
379
        }
380
 
381
        return $result;
382
    }
383
 
384
    /**
385
     * Get calculated cell value converted to string.
386
     */
387
    public function getCalculatedValueString(): string
388
    {
389
        $value = $this->getCalculatedValue();
390
        while (is_array($value)) {
391
            $value = array_shift($value);
392
        }
393
 
394
        return ($value === '' || is_scalar($value) || $value instanceof Stringable) ? "$value" : '';
395
    }
396
 
397
    /**
398
     * Get calculated cell value.
399
     *
400
     * @param bool $resetLog Whether the calculation engine logger should be reset or not
401
     *
402
     * @throws CalculationException
403
     */
404
    public function getCalculatedValue(bool $resetLog = true): mixed
405
    {
406
        $title = 'unknown';
407
        $oldAttributes = $this->formulaAttributes;
408
        $oldAttributesT = $oldAttributes['t'] ?? '';
409
        $coordinate = $this->getCoordinate();
410
        $oldAttributesRef = $oldAttributes['ref'] ?? $coordinate;
411
        $originalValue = $this->value;
412
        $originalDataType = $this->dataType;
413
        $this->formulaAttributes = [];
414
        $spill = false;
415
 
416
        if ($this->dataType === DataType::TYPE_FORMULA) {
417
            try {
418
                $currentCalendar = SharedDate::getExcelCalendar();
419
                SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar());
420
                $thisworksheet = $this->getWorksheet();
421
                $index = $thisworksheet->getParentOrThrow()->getActiveSheetIndex();
422
                $selected = $thisworksheet->getSelectedCells();
423
                $title = $thisworksheet->getTitle();
424
                $calculation = Calculation::getInstance($thisworksheet->getParent());
425
                $result = $calculation->calculateCellValue($this, $resetLog);
426
                $result = $this->convertDateTimeInt($result);
427
                $thisworksheet->setSelectedCells($selected);
428
                $thisworksheet->getParentOrThrow()->setActiveSheetIndex($index);
429
                if (is_array($result) && $calculation->getInstanceArrayReturnType() !== Calculation::RETURN_ARRAY_AS_ARRAY) {
430
                    while (is_array($result)) {
431
                        $result = array_shift($result);
432
                    }
433
                }
434
                if (
435
                    !is_array($result)
436
                    && $calculation->getInstanceArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY
437
                    && $oldAttributesT === 'array'
438
                    && ($oldAttributesRef === $coordinate || $oldAttributesRef === "$coordinate:$coordinate")
439
                ) {
440
                    $result = [$result];
441
                }
442
                // if return_as_array for formula like '=sheet!cell'
443
                if (is_array($result) && count($result) === 1) {
444
                    $resultKey = array_keys($result)[0];
445
                    $resultValue = $result[$resultKey];
446
                    if (is_int($resultKey) && is_array($resultValue) && count($resultValue) === 1) {
447
                        $resultKey2 = array_keys($resultValue)[0];
448
                        $resultValue2 = $resultValue[$resultKey2];
449
                        if (is_string($resultKey2) && !is_array($resultValue2) && preg_match('/[a-zA-Z]{1,3}/', $resultKey2) === 1) {
450
                            $result = $resultValue2;
451
                        }
452
                    }
453
                }
454
                $newColumn = $this->getColumn();
455
                if (is_array($result)) {
456
                    $this->formulaAttributes['t'] = 'array';
457
                    $this->formulaAttributes['ref'] = $maxCoordinate = $coordinate;
458
                    $newRow = $row = $this->getRow();
459
                    $column = $this->getColumn();
460
                    foreach ($result as $resultRow) {
461
                        if (is_array($resultRow)) {
462
                            $newColumn = $column;
463
                            foreach ($resultRow as $resultValue) {
464
                                if ($row !== $newRow || $column !== $newColumn) {
465
                                    $maxCoordinate = $newColumn . $newRow;
466
                                    if ($thisworksheet->getCell($newColumn . $newRow)->getValue() !== null) {
467
                                        if (!Coordinate::coordinateIsInsideRange($oldAttributesRef, $newColumn . $newRow)) {
468
                                            $spill = true;
469
 
470
                                            break;
471
                                        }
472
                                    }
473
                                }
474
                                ++$newColumn;
475
                            }
476
                            ++$newRow;
477
                        } else {
478
                            if ($row !== $newRow || $column !== $newColumn) {
479
                                $maxCoordinate = $newColumn . $newRow;
480
                                if ($thisworksheet->getCell($newColumn . $newRow)->getValue() !== null) {
481
                                    if (!Coordinate::coordinateIsInsideRange($oldAttributesRef, $newColumn . $newRow)) {
482
                                        $spill = true;
483
                                    }
484
                                }
485
                            }
486
                            ++$newColumn;
487
                        }
488
                        if ($spill) {
489
                            break;
490
                        }
491
                    }
492
                    if (!$spill) {
493
                        $this->formulaAttributes['ref'] .= ":$maxCoordinate";
494
                    }
495
                    $thisworksheet->getCell($column . $row);
496
                }
497
                if (is_array($result)) {
498
                    if ($oldAttributes !== null && $calculation->getInstanceArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY) {
499
                        if (($oldAttributesT) === 'array') {
500
                            $thisworksheet = $this->getWorksheet();
501
                            $coordinate = $this->getCoordinate();
502
                            $ref = $oldAttributesRef;
503
                            if (preg_match('/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/', $ref, $matches) === 1) {
504
                                if (isset($matches[3])) {
505
                                    $minCol = $matches[1];
506
                                    $minRow = (int) $matches[2];
507
                                    // https://github.com/phpstan/phpstan/issues/11602
508
                                    $maxCol = $matches[4]; // @phpstan-ignore-line
509
                                    ++$maxCol;
510
                                    $maxRow = (int) $matches[5]; // @phpstan-ignore-line
511
                                    for ($row = $minRow; $row <= $maxRow; ++$row) {
512
                                        for ($col = $minCol; $col !== $maxCol; ++$col) {
513
                                            if ("$col$row" !== $coordinate) {
514
                                                $thisworksheet->getCell("$col$row")->setValue(null);
515
                                            }
516
                                        }
517
                                    }
518
                                }
519
                            }
520
                            $thisworksheet->getCell($coordinate);
521
                        }
522
                    }
523
                }
524
                if ($spill) {
525
                    $result = ExcelError::SPILL();
526
                }
527
                if (is_array($result)) {
528
                    $newRow = $row = $this->getRow();
529
                    $newColumn = $column = $this->getColumn();
530
                    foreach ($result as $resultRow) {
531
                        if (is_array($resultRow)) {
532
                            $newColumn = $column;
533
                            foreach ($resultRow as $resultValue) {
534
                                if ($row !== $newRow || $column !== $newColumn) {
535
                                    $thisworksheet->getCell($newColumn . $newRow)->setValue($resultValue);
536
                                }
537
                                ++$newColumn;
538
                            }
539
                            ++$newRow;
540
                        } else {
541
                            if ($row !== $newRow || $column !== $newColumn) {
542
                                $thisworksheet->getCell($newColumn . $newRow)->setValue($resultRow);
543
                            }
544
                            ++$newColumn;
545
                        }
546
                    }
547
                    $thisworksheet->getCell($column . $row);
548
                    $this->value = $originalValue;
549
                    $this->dataType = $originalDataType;
550
                }
551
            } catch (SpreadsheetException $ex) {
552
                SharedDate::setExcelCalendar($currentCalendar);
553
                if (($ex->getMessage() === 'Unable to access External Workbook') && ($this->calculatedValue !== null)) {
554
                    return $this->calculatedValue; // Fallback for calculations referencing external files.
555
                } elseif (preg_match('/[Uu]ndefined (name|offset: 2|array key 2)/', $ex->getMessage()) === 1) {
556
                    return ExcelError::NAME();
557
                }
558
 
559
                throw new CalculationException(
560
                    $title . '!' . $this->getCoordinate() . ' -> ' . $ex->getMessage(),
561
                    $ex->getCode(),
562
                    $ex
563
                );
564
            }
565
            SharedDate::setExcelCalendar($currentCalendar);
566
 
567
            if ($result === Functions::NOT_YET_IMPLEMENTED) {
568
                $this->formulaAttributes = $oldAttributes;
569
 
570
                return $this->calculatedValue; // Fallback if calculation engine does not support the formula.
571
            }
572
 
573
            return $result;
574
        } elseif ($this->value instanceof RichText) {
575
            return $this->value->getPlainText();
576
        }
577
 
578
        return $this->convertDateTimeInt($this->value);
579
    }
580
 
581
    /**
582
     * Set old calculated value (cached).
583
     *
584
     * @param mixed $originalValue Value
585
     */
586
    public function setCalculatedValue(mixed $originalValue, bool $tryNumeric = true): self
587
    {
588
        if ($originalValue !== null) {
589
            $this->calculatedValue = ($tryNumeric && is_numeric($originalValue)) ? (0 + $originalValue) : $originalValue;
590
        }
591
 
592
        return $this->updateInCollection();
593
    }
594
 
595
    /**
596
     *    Get old calculated value (cached)
597
     *    This returns the value last calculated by MS Excel or whichever spreadsheet program was used to
598
     *        create the original spreadsheet file.
599
     *    Note that this value is not guaranteed to reflect the actual calculated value because it is
600
     *        possible that auto-calculation was disabled in the original spreadsheet, and underlying data
601
     *        values used by the formula have changed since it was last calculated.
602
     */
603
    public function getOldCalculatedValue(): mixed
604
    {
605
        return $this->calculatedValue;
606
    }
607
 
608
    /**
609
     * Get cell data type.
610
     */
611
    public function getDataType(): string
612
    {
613
        return $this->dataType;
614
    }
615
 
616
    /**
617
     * Set cell data type.
618
     *
619
     * @param string $dataType see DataType::TYPE_*
620
     */
621
    public function setDataType(string $dataType): self
622
    {
623
        $this->setValueExplicit($this->value, $dataType);
624
 
625
        return $this;
626
    }
627
 
628
    /**
629
     * Identify if the cell contains a formula.
630
     */
631
    public function isFormula(): bool
632
    {
633
        return $this->dataType === DataType::TYPE_FORMULA && $this->getStyle()->getQuotePrefix() === false;
634
    }
635
 
636
    /**
637
     *    Does this cell contain Data validation rules?
638
     *
639
     * @throws SpreadsheetException
640
     */
641
    public function hasDataValidation(): bool
642
    {
643
        if (!isset($this->parent)) {
644
            throw new SpreadsheetException('Cannot check for data validation when cell is not bound to a worksheet');
645
        }
646
 
647
        return $this->getWorksheet()->dataValidationExists($this->getCoordinate());
648
    }
649
 
650
    /**
651
     * Get Data validation rules.
652
     *
653
     * @throws SpreadsheetException
654
     */
655
    public function getDataValidation(): DataValidation
656
    {
657
        if (!isset($this->parent)) {
658
            throw new SpreadsheetException('Cannot get data validation for cell that is not bound to a worksheet');
659
        }
660
 
661
        return $this->getWorksheet()->getDataValidation($this->getCoordinate());
662
    }
663
 
664
    /**
665
     * Set Data validation rules.
666
     *
667
     * @throws SpreadsheetException
668
     */
669
    public function setDataValidation(?DataValidation $dataValidation = null): self
670
    {
671
        if (!isset($this->parent)) {
672
            throw new SpreadsheetException('Cannot set data validation for cell that is not bound to a worksheet');
673
        }
674
 
675
        $this->getWorksheet()->setDataValidation($this->getCoordinate(), $dataValidation);
676
 
677
        return $this->updateInCollection();
678
    }
679
 
680
    /**
681
     * Does this cell contain valid value?
682
     */
683
    public function hasValidValue(): bool
684
    {
685
        $validator = new DataValidator();
686
 
687
        return $validator->isValid($this);
688
    }
689
 
690
    /**
691
     * Does this cell contain a Hyperlink?
692
     *
693
     * @throws SpreadsheetException
694
     */
695
    public function hasHyperlink(): bool
696
    {
697
        if (!isset($this->parent)) {
698
            throw new SpreadsheetException('Cannot check for hyperlink when cell is not bound to a worksheet');
699
        }
700
 
701
        return $this->getWorksheet()->hyperlinkExists($this->getCoordinate());
702
    }
703
 
704
    /**
705
     * Get Hyperlink.
706
     *
707
     * @throws SpreadsheetException
708
     */
709
    public function getHyperlink(): Hyperlink
710
    {
711
        if (!isset($this->parent)) {
712
            throw new SpreadsheetException('Cannot get hyperlink for cell that is not bound to a worksheet');
713
        }
714
 
715
        return $this->getWorksheet()->getHyperlink($this->getCoordinate());
716
    }
717
 
718
    /**
719
     * Set Hyperlink.
720
     *
721
     * @throws SpreadsheetException
722
     */
723
    public function setHyperlink(?Hyperlink $hyperlink = null): self
724
    {
725
        if (!isset($this->parent)) {
726
            throw new SpreadsheetException('Cannot set hyperlink for cell that is not bound to a worksheet');
727
        }
728
 
729
        $this->getWorksheet()->setHyperlink($this->getCoordinate(), $hyperlink);
730
 
731
        return $this->updateInCollection();
732
    }
733
 
734
    /**
735
     * Get cell collection.
736
     */
737
    public function getParent(): ?Cells
738
    {
739
        return $this->parent;
740
    }
741
 
742
    /**
743
     * Get parent worksheet.
744
     *
745
     * @throws SpreadsheetException
746
     */
747
    public function getWorksheet(): Worksheet
748
    {
749
        $parent = $this->parent;
750
        if ($parent !== null) {
751
            $worksheet = $parent->getParent();
752
        } else {
753
            $worksheet = null;
754
        }
755
 
756
        if ($worksheet === null) {
757
            throw new SpreadsheetException('Worksheet no longer exists');
758
        }
759
 
760
        return $worksheet;
761
    }
762
 
763
    public function getWorksheetOrNull(): ?Worksheet
764
    {
765
        $parent = $this->parent;
766
        if ($parent !== null) {
767
            $worksheet = $parent->getParent();
768
        } else {
769
            $worksheet = null;
770
        }
771
 
772
        return $worksheet;
773
    }
774
 
775
    /**
776
     * Is this cell in a merge range.
777
     */
778
    public function isInMergeRange(): bool
779
    {
780
        return (bool) $this->getMergeRange();
781
    }
782
 
783
    /**
784
     * Is this cell the master (top left cell) in a merge range (that holds the actual data value).
785
     */
786
    public function isMergeRangeValueCell(): bool
787
    {
788
        if ($mergeRange = $this->getMergeRange()) {
789
            $mergeRange = Coordinate::splitRange($mergeRange);
790
            [$startCell] = $mergeRange[0];
791
 
792
            return $this->getCoordinate() === $startCell;
793
        }
794
 
795
        return false;
796
    }
797
 
798
    /**
799
     * If this cell is in a merge range, then return the range.
800
     *
801
     * @return false|string
802
     */
803
    public function getMergeRange()
804
    {
805
        foreach ($this->getWorksheet()->getMergeCells() as $mergeRange) {
806
            if ($this->isInRange($mergeRange)) {
807
                return $mergeRange;
808
            }
809
        }
810
 
811
        return false;
812
    }
813
 
814
    /**
815
     * Get cell style.
816
     */
817
    public function getStyle(): Style
818
    {
819
        return $this->getWorksheet()->getStyle($this->getCoordinate());
820
    }
821
 
822
    /**
823
     * Get cell style.
824
     */
825
    public function getAppliedStyle(): Style
826
    {
827
        if ($this->getWorksheet()->conditionalStylesExists($this->getCoordinate()) === false) {
828
            return $this->getStyle();
829
        }
830
        $range = $this->getWorksheet()->getConditionalRange($this->getCoordinate());
831
        if ($range === null) {
832
            return $this->getStyle();
833
        }
834
 
835
        $matcher = new CellStyleAssessor($this, $range);
836
 
837
        return $matcher->matchConditions($this->getWorksheet()->getConditionalStyles($this->getCoordinate()));
838
    }
839
 
840
    /**
841
     * Re-bind parent.
842
     */
843
    public function rebindParent(Worksheet $parent): self
844
    {
845
        $this->parent = $parent->getCellCollection();
846
 
847
        return $this->updateInCollection();
848
    }
849
 
850
    /**
851
     *    Is cell in a specific range?
852
     *
853
     * @param string $range Cell range (e.g. A1:A1)
854
     */
855
    public function isInRange(string $range): bool
856
    {
857
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range);
858
 
859
        // Translate properties
860
        $myColumn = Coordinate::columnIndexFromString($this->getColumn());
861
        $myRow = $this->getRow();
862
 
863
        // Verify if cell is in range
864
        return ($rangeStart[0] <= $myColumn) && ($rangeEnd[0] >= $myColumn)
865
            && ($rangeStart[1] <= $myRow) && ($rangeEnd[1] >= $myRow);
866
    }
867
 
868
    /**
869
     * Compare 2 cells.
870
     *
871
     * @param Cell $a Cell a
872
     * @param Cell $b Cell b
873
     *
874
     * @return int Result of comparison (always -1 or 1, never zero!)
875
     */
876
    public static function compareCells(self $a, self $b): int
877
    {
878
        if ($a->getRow() < $b->getRow()) {
879
            return -1;
880
        } elseif ($a->getRow() > $b->getRow()) {
881
            return 1;
882
        } elseif (Coordinate::columnIndexFromString($a->getColumn()) < Coordinate::columnIndexFromString($b->getColumn())) {
883
            return -1;
884
        }
885
 
886
        return 1;
887
    }
888
 
889
    /**
890
     * Get value binder to use.
891
     */
892
    public static function getValueBinder(): IValueBinder
893
    {
894
        if (self::$valueBinder === null) {
895
            self::$valueBinder = new DefaultValueBinder();
896
        }
897
 
898
        return self::$valueBinder;
899
    }
900
 
901
    /**
902
     * Set value binder to use.
903
     */
904
    public static function setValueBinder(IValueBinder $binder): void
905
    {
906
        self::$valueBinder = $binder;
907
    }
908
 
909
    /**
910
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
911
     */
912
    public function __clone()
913
    {
914
        $vars = get_object_vars($this);
915
        foreach ($vars as $propertyName => $propertyValue) {
916
            if ((is_object($propertyValue)) && ($propertyName !== 'parent')) {
917
                $this->$propertyName = clone $propertyValue;
918
            } else {
919
                $this->$propertyName = $propertyValue;
920
            }
921
        }
922
    }
923
 
924
    /**
925
     * Get index to cellXf.
926
     */
927
    public function getXfIndex(): int
928
    {
929
        return $this->xfIndex;
930
    }
931
 
932
    /**
933
     * Set index to cellXf.
934
     */
935
    public function setXfIndex(int $indexValue): self
936
    {
937
        $this->xfIndex = $indexValue;
938
 
939
        return $this->updateInCollection();
940
    }
941
 
942
    /**
943
     * Set the formula attributes.
944
     *
945
     * @param $attributes null|array<string, string>
946
     *
947
     * @return $this
948
     */
949
    public function setFormulaAttributes(?array $attributes): self
950
    {
951
        $this->formulaAttributes = $attributes;
952
 
953
        return $this;
954
    }
955
 
956
    /**
957
     * Get the formula attributes.
958
     *
959
     * @return null|array<string, string>
960
     */
961
    public function getFormulaAttributes(): mixed
962
    {
963
        return $this->formulaAttributes;
964
    }
965
 
966
    /**
967
     * Convert to string.
968
     */
969
    public function __toString(): string
970
    {
971
        $retVal = $this->value;
972
 
973
        return ($retVal === null || is_scalar($retVal) || $retVal instanceof Stringable) ? ((string) $retVal) : '';
974
    }
975
 
976
    public function getIgnoredErrors(): IgnoredErrors
977
    {
978
        return $this->ignoredErrors;
979
    }
980
 
981
    public function isLocked(): bool
982
    {
983
        $protected = $this->parent?->getParent()?->getProtection()?->getSheet();
984
        if ($protected !== true) {
985
            return false;
986
        }
987
        $locked = $this->getStyle()->getProtection()->getLocked();
988
 
989
        return $locked !== Protection::PROTECTION_UNPROTECTED;
990
    }
991
 
992
    public function isHiddenOnFormulaBar(): bool
993
    {
994
        if ($this->getDataType() !== DataType::TYPE_FORMULA) {
995
            return false;
996
        }
997
        $protected = $this->parent?->getParent()?->getProtection()?->getSheet();
998
        if ($protected !== true) {
999
            return false;
1000
        }
1001
        $hidden = $this->getStyle()->getProtection()->getHidden();
1002
 
1003
        return $hidden !== Protection::PROTECTION_UNPROTECTED;
1004
    }
1005
}