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

PHPOffice / PhpSpreadsheet / 18965976143

31 Oct 2025 07:36AM UTC coverage: 95.948% (+0.001%) from 95.947%
18965976143

Pull #4697

github

web-flow
Merge f713b52b6 into 4cb69539a
Pull Request #4697: Unexpected Exception in Php DateTime

27 of 27 new or added lines in 5 files covered. (100.0%)

9 existing lines in 1 file now uncovered.

45371 of 47287 relevant lines covered (95.95%)

373.07 hits per line

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

98.33
/src/PhpSpreadsheet/Worksheet/AutoFilter.php
1
<?php
2

3
namespace PhpOffice\PhpSpreadsheet\Worksheet;
4

5
use DateTime;
6
use DateTimeZone;
7
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
8
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
9
use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch;
10
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
11
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
12
use PhpOffice\PhpSpreadsheet\Cell\CellRange;
13
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
14
use PhpOffice\PhpSpreadsheet\Exception;
15
use PhpOffice\PhpSpreadsheet\Shared\Date;
16
use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule;
17
use Stringable;
18

19
class AutoFilter implements Stringable
20
{
21
    /**
22
     * Autofilter Worksheet.
23
     */
24
    private ?Worksheet $workSheet;
25

26
    /**
27
     * Autofilter Range.
28
     */
29
    private string $range;
30

31
    /**
32
     * Autofilter Column Ruleset.
33
     *
34
     * @var AutoFilter\Column[]
35
     */
36
    private array $columns = [];
37

38
    private bool $evaluated = false;
39

40
    public function getEvaluated(): bool
18✔
41
    {
42
        return $this->evaluated;
18✔
43
    }
44

45
    public function setEvaluated(bool $value): void
155✔
46
    {
47
        $this->evaluated = $value;
155✔
48
    }
49

50
    /**
51
     * Create a new AutoFilter.
52
     *
53
     * @param AddressRange<CellAddress>|AddressRange<int>|AddressRange<string>|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range
54
     *            A simple string containing a Cell range like 'A1:E10' is permitted
55
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
56
     *              or an AddressRange object.
57
     */
58
    public function __construct(AddressRange|string|array $range = '', ?Worksheet $worksheet = null)
10,850✔
59
    {
60
        if ($range !== '') {
10,850✔
61
            [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true);
148✔
62
        }
63

64
        $this->range = $range ?? '';
10,850✔
65
        $this->workSheet = $worksheet;
10,850✔
66
    }
67

68
    public function __destruct()
178✔
69
    {
70
        $this->workSheet = null;
178✔
71
    }
72

73
    /**
74
     * Get AutoFilter Parent Worksheet.
75
     */
76
    public function getParent(): null|Worksheet
2✔
77
    {
78
        return $this->workSheet;
2✔
79
    }
80

81
    /**
82
     * Set AutoFilter Parent Worksheet.
83
     *
84
     * @return $this
85
     */
86
    public function setParent(?Worksheet $worksheet = null): static
125✔
87
    {
88
        $this->evaluated = false;
125✔
89
        $this->workSheet = $worksheet;
125✔
90

91
        return $this;
125✔
92
    }
93

94
    /**
95
     * Get AutoFilter Range.
96
     */
97
    public function getRange(): string
731✔
98
    {
99
        return $this->range;
731✔
100
    }
101

102
    /**
103
     * Set AutoFilter Cell Range.
104
     *
105
     * @param AddressRange<CellRange>|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range
106
     *            A simple string containing a Cell range like 'A1:E10' or a Cell address like 'A1' is permitted
107
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
108
     *              or an AddressRange object.
109
     */
110
    public function setRange(AddressRange|string|array $range = ''): self
316✔
111
    {
112
        $this->evaluated = false;
316✔
113
        // extract coordinate
114
        if ($range !== '') {
316✔
115
            [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true);
316✔
116
        }
117

118
        if (empty($range)) {
316✔
119
            //    Discard all column rules
120
            $this->columns = [];
5✔
121
            $this->range = '';
5✔
122

123
            return $this;
5✔
124
        }
125

126
        if (ctype_digit($range) || ctype_alpha($range)) {
316✔
127
            throw new Exception("{$range} is an invalid range for AutoFilter");
2✔
128
        }
129

130
        $this->range = $range;
314✔
131
        //    Discard any column rules that are no longer valid within this range
132
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
314✔
133
        foreach ($this->columns as $key => $value) {
314✔
134
            $colIndex = Coordinate::columnIndexFromString($key);
4✔
135
            if (($rangeStart[0] > $colIndex) || ($rangeEnd[0] < $colIndex)) {
4✔
136
                unset($this->columns[$key]);
1✔
137
            }
138
        }
139

140
        return $this;
314✔
141
    }
142

143
    public function setRangeToMaxRow(): self
2✔
144
    {
145
        $this->evaluated = false;
2✔
146
        if ($this->workSheet !== null) {
2✔
147
            $thisrange = $this->range;
2✔
148
            $range = (string) preg_replace('/\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange);
2✔
149
            if ($range !== $thisrange) {
2✔
150
                $this->setRange($range);
2✔
151
            }
152
        }
153

154
        return $this;
2✔
155
    }
156

157
    /**
158
     * Get all AutoFilter Columns.
159
     *
160
     * @return AutoFilter\Column[]
161
     */
162
    public function getColumns(): array
23✔
163
    {
164
        return $this->columns;
23✔
165
    }
166

167
    /**
168
     * Validate that the specified column is in the AutoFilter range.
169
     *
170
     * @param string $column Column name (e.g. A)
171
     *
172
     * @return int The column offset within the autofilter range
173
     */
174
    public function testColumnInRange(string $column): int
148✔
175
    {
176
        if (empty($this->range)) {
148✔
177
            throw new Exception('No autofilter range is defined.');
1✔
178
        }
179

180
        $columnIndex = Coordinate::columnIndexFromString($column);
147✔
181
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
147✔
182
        if (($rangeStart[0] > $columnIndex) || ($rangeEnd[0] < $columnIndex)) {
147✔
183
            throw new Exception('Column is outside of current autofilter range.');
4✔
184
        }
185

186
        return $columnIndex - $rangeStart[0];
143✔
187
    }
188

189
    /**
190
     * Get a specified AutoFilter Column Offset within the defined AutoFilter range.
191
     *
192
     * @param string $column Column name (e.g. A)
193
     *
194
     * @return int The offset of the specified column within the autofilter range
195
     */
196
    public function getColumnOffset(string $column): int
11✔
197
    {
198
        return $this->testColumnInRange($column);
11✔
199
    }
200

201
    /**
202
     * Get a specified AutoFilter Column.
203
     *
204
     * @param string $column Column name (e.g. A)
205
     */
206
    public function getColumn(string $column): AutoFilter\Column
137✔
207
    {
208
        $this->testColumnInRange($column);
137✔
209

210
        if (!isset($this->columns[$column])) {
135✔
211
            $this->columns[$column] = new AutoFilter\Column($column, $this);
134✔
212
        }
213

214
        return $this->columns[$column];
135✔
215
    }
216

217
    /**
218
     * Get a specified AutoFilter Column by it's offset.
219
     *
220
     * @param int $columnOffset Column offset within range (starting from 0)
221
     */
222
    public function getColumnByOffset(int $columnOffset): AutoFilter\Column
24✔
223
    {
224
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
24✔
225
        $pColumn = Coordinate::stringFromColumnIndex($rangeStart[0] + $columnOffset);
24✔
226

227
        return $this->getColumn($pColumn);
24✔
228
    }
229

230
    /**
231
     * Set AutoFilter.
232
     *
233
     * @param AutoFilter\Column|string $columnObjectOrString
234
     *            A simple string containing a Column ID like 'A' is permitted
235
     *
236
     * @return $this
237
     */
238
    public function setColumn(AutoFilter\Column|string $columnObjectOrString): static
10✔
239
    {
240
        $this->evaluated = false;
10✔
241
        if ((is_string($columnObjectOrString)) && (!empty($columnObjectOrString))) {
10✔
242
            $column = $columnObjectOrString;
9✔
243
        } elseif ($columnObjectOrString instanceof AutoFilter\Column) {
1✔
244
            $column = $columnObjectOrString->getColumnIndex();
1✔
245
        } else {
UNCOV
246
            throw new Exception('Column is not within the autofilter range.');
×
247
        }
248
        $this->testColumnInRange($column);
10✔
249

250
        if (is_string($columnObjectOrString)) {
8✔
251
            $this->columns[$columnObjectOrString] = new AutoFilter\Column($columnObjectOrString, $this);
7✔
252
        } else {
253
            $columnObjectOrString->setParent($this);
1✔
254
            $this->columns[$column] = $columnObjectOrString;
1✔
255
        }
256
        ksort($this->columns);
8✔
257

258
        return $this;
8✔
259
    }
260

261
    /**
262
     * Clear a specified AutoFilter Column.
263
     *
264
     * @param string $column Column name (e.g. A)
265
     *
266
     * @return $this
267
     */
268
    public function clearColumn(string $column): static
2✔
269
    {
270
        $this->evaluated = false;
2✔
271
        $this->testColumnInRange($column);
2✔
272

273
        if (isset($this->columns[$column])) {
2✔
274
            unset($this->columns[$column]);
2✔
275
        }
276

277
        return $this;
2✔
278
    }
279

280
    /**
281
     * Shift an AutoFilter Column Rule to a different column.
282
     *
283
     * Note: This method bypasses validation of the destination column to ensure it is within this AutoFilter range.
284
     *        Nor does it verify whether any column rule already exists at $toColumn, but will simply override any existing value.
285
     *        Use with caution.
286
     *
287
     * @param string $fromColumn Column name (e.g. A)
288
     * @param string $toColumn Column name (e.g. B)
289
     *
290
     * @return $this
291
     */
292
    public function shiftColumn(string $fromColumn, string $toColumn): static
3✔
293
    {
294
        $this->evaluated = false;
3✔
295
        $fromColumn = strtoupper($fromColumn);
3✔
296
        $toColumn = strtoupper($toColumn);
3✔
297

298
        if (isset($this->columns[$fromColumn])) {
3✔
299
            $this->columns[$fromColumn]->setParent();
2✔
300
            $this->columns[$fromColumn]->setColumnIndex($toColumn);
2✔
301
            $this->columns[$toColumn] = $this->columns[$fromColumn];
2✔
302
            $this->columns[$toColumn]->setParent($this);
2✔
303
            unset($this->columns[$fromColumn]);
2✔
304

305
            ksort($this->columns);
2✔
306
        }
307

308
        return $this;
3✔
309
    }
310

311
    /**
312
     * Test if cell value is in the defined set of values.
313
     *
314
     * @param array{blanks: bool, filterValues: array<string,array<string,string>>} $dataSet
315
     */
316
    protected static function filterTestInSimpleDataSet(mixed $cellValue, array $dataSet): bool
13✔
317
    {
318
        $dataSetValues = $dataSet['filterValues'];
13✔
319
        $blanks = $dataSet['blanks'];
13✔
320
        if (($cellValue === '') || ($cellValue === null)) {
13✔
321
            return $blanks;
4✔
322
        }
323

324
        return in_array($cellValue, $dataSetValues);
13✔
325
    }
326

327
    /**
328
     * Test if cell value is in the defined set of Excel date values.
329
     *
330
     * @param array{blanks: bool, filterValues: array<string,array<string,string>>} $dataSet
331
     */
332
    protected static function filterTestInDateGroupSet(mixed $cellValue, array $dataSet): bool
14✔
333
    {
334
        $dateSet = $dataSet['filterValues'];
14✔
335
        $blanks = $dataSet['blanks'];
14✔
336
        if (($cellValue === '') || ($cellValue === null)) {
14✔
337
            return $blanks;
1✔
338
        }
339
        $timeZone = new DateTimeZone('UTC');
14✔
340

341
        if (is_numeric($cellValue)) {
14✔
342
            $dateTime = Date::excelToDateTimeObject((float) $cellValue, $timeZone);
14✔
343
            $cellValue = (float) $cellValue;
14✔
344
            if ($cellValue < 1) {
14✔
345
                //    Just the time part
346
                $dtVal = $dateTime->format('His');
4✔
347
                $dateSet = $dateSet['time'];
4✔
348
            } elseif ($cellValue == floor($cellValue)) {
10✔
349
                //    Just the date part
350
                $dtVal = $dateTime->format('Ymd');
5✔
351
                $dateSet = $dateSet['date'];
5✔
352
            } else {
353
                //    date and time parts
354
                $dtVal = $dateTime->format('YmdHis');
5✔
355
                $dateSet = $dateSet['dateTime'];
5✔
356
            }
357
            foreach ($dateSet as $dateValue) {
14✔
358
                //    Use of substr to extract value at the appropriate group level
359
                if (str_starts_with($dtVal, $dateValue)) {
13✔
360
                    return true;
11✔
361
                }
362
            }
363
        }
364

365
        return false;
14✔
366
    }
367

368
    /**
369
     * Test if cell value is within a set of values defined by a ruleset.
370
     *
371
     * @param mixed[][] $ruleSet
372
     */
373
    protected static function filterTestInCustomDataSet(mixed $cellValue, array $ruleSet): bool
71✔
374
    {
375
        $dataSet = $ruleSet['filterRules'];
71✔
376
        $join = $ruleSet['join'];
71✔
377
        $customRuleForBlanks = $ruleSet['customRuleForBlanks'] ?? false;
71✔
378

379
        if (!$customRuleForBlanks) {
71✔
380
            //    Blank cells are always ignored, so return a FALSE
381
            if (($cellValue === '') || ($cellValue === null)) {
31✔
382
                return false;
3✔
383
            }
384
        }
385
        $returnVal = ($join == AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND);
71✔
386
        foreach ($dataSet as $rule) {
71✔
387
            /** @var string[] $rule */
388
            $ruleValue = $rule['value'];
71✔
389
            $ruleOperator = $rule['operator'];
71✔
390
            /** @var string */
391
            $cellValueString = $cellValue ?? '';
71✔
392
            $retVal = false;
71✔
393

394
            if (is_numeric($ruleValue)) {
71✔
395
                //    Numeric values are tested using the appropriate operator
396
                $numericTest = is_numeric($cellValue);
47✔
397
                switch ($ruleOperator) {
398
                    case Rule::AUTOFILTER_COLUMN_RULE_EQUAL:
3✔
399
                        $retVal = $numericTest && ($cellValue == $ruleValue);
3✔
400

401
                        break;
3✔
402
                    case Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL:
3✔
403
                        $retVal = !$numericTest || ($cellValue != $ruleValue);
3✔
404

405
                        break;
3✔
406
                    case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN:
3✔
407
                        $retVal = $numericTest && ($cellValue > $ruleValue);
3✔
408

409
                        break;
3✔
410
                    case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL:
3✔
411
                        $retVal = $numericTest && ($cellValue >= $ruleValue);
28✔
412

413
                        break;
28✔
414
                    case Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN:
2✔
415
                        $retVal = $numericTest && ($cellValue < $ruleValue);
21✔
416

417
                        break;
21✔
418
                    case Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL:
1✔
419
                        $retVal = $numericTest && ($cellValue <= $ruleValue);
8✔
420

421
                        break;
8✔
422
                }
423
            } elseif ($ruleValue == '') {
24✔
424
                $retVal = match ($ruleOperator) {
2✔
425
                    Rule::AUTOFILTER_COLUMN_RULE_EQUAL => ($cellValue === '') || ($cellValue === null),
1✔
426
                    Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL => ($cellValue != ''),
1✔
UNCOV
427
                    default => true,
×
428
                };
2✔
429
            } else {
430
                //    String values are always tested for equality, factoring in for wildcards (hence a regexp test)
431
                switch ($ruleOperator) {
432
                    case Rule::AUTOFILTER_COLUMN_RULE_EQUAL:
2✔
433
                        $retVal = (bool) preg_match('/^' . $ruleValue . '$/i', $cellValueString);
15✔
434

435
                        break;
15✔
UNCOV
436
                    case Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL:
×
437
                        $retVal = !((bool) preg_match('/^' . $ruleValue . '$/i', $cellValueString));
3✔
438

439
                        break;
3✔
UNCOV
440
                    case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN:
×
441
                        $retVal = strcasecmp($cellValueString, $ruleValue) > 0;
1✔
442

443
                        break;
1✔
UNCOV
444
                    case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL:
×
445
                        $retVal = strcasecmp($cellValueString, $ruleValue) >= 0;
1✔
446

447
                        break;
1✔
UNCOV
448
                    case Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN:
×
449
                        $retVal = strcasecmp($cellValueString, $ruleValue) < 0;
1✔
450

451
                        break;
1✔
UNCOV
452
                    case Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL:
×
453
                        $retVal = strcasecmp($cellValueString, $ruleValue) <= 0;
1✔
454

455
                        break;
1✔
456
                }
457
            }
458
            //    If there are multiple conditions, then we need to test both using the appropriate join operator
459
            switch ($join) {
460
                case AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR:
71✔
461
                    $returnVal = $returnVal || $retVal;
51✔
462
                    //    Break as soon as we have a TRUE match for OR joins,
463
                    //        to avoid unnecessary additional code execution
464
                    if ($returnVal) {
51✔
465
                        return $returnVal;
51✔
466
                    }
467

468
                    break;
51✔
469
                case AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND:
20✔
470
                    $returnVal = $returnVal && $retVal;
20✔
471

472
                    break;
20✔
473
            }
474
        }
475

476
        return $returnVal;
71✔
477
    }
478

479
    /**
480
     * Test if cell date value is matches a set of values defined by a set of months.
481
     *
482
     * @param mixed[] $monthSet
483
     */
484
    protected static function filterTestInPeriodDateSet(mixed $cellValue, array $monthSet): bool
4✔
485
    {
486
        //    Blank cells are always ignored, so return a FALSE
487
        if (($cellValue === '') || ($cellValue === null)) {
4✔
488
            return false;
3✔
489
        }
490

491
        if (is_numeric($cellValue)) {
4✔
492
            $dateObject = Date::excelToDateTimeObject((float) $cellValue, new DateTimeZone('UTC'));
4✔
493
            $dateValue = (int) $dateObject->format('m');
4✔
494
            if (in_array($dateValue, $monthSet)) {
4✔
495
                return true;
3✔
496
            }
497
        }
498

499
        return false;
4✔
500
    }
501

502
    private static function makeDateObject(int $year, int $month, int $day, int $hour = 0, int $minute = 0, int $second = 0): DateTime
7✔
503
    {
504
        $baseDate = new DateTime();
7✔
505
        $baseDate->setDate($year, $month, $day);
7✔
506
        $baseDate->setTime($hour, $minute, $second);
7✔
507

508
        return $baseDate;
7✔
509
    }
510

511
    private const DATE_FUNCTIONS = [
512
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH => 'dynamicLastMonth',
513
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER => 'dynamicLastQuarter',
514
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK => 'dynamicLastWeek',
515
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR => 'dynamicLastYear',
516
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH => 'dynamicNextMonth',
517
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER => 'dynamicNextQuarter',
518
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK => 'dynamicNextWeek',
519
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR => 'dynamicNextYear',
520
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISMONTH => 'dynamicThisMonth',
521
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISQUARTER => 'dynamicThisQuarter',
522
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISWEEK => 'dynamicThisWeek',
523
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISYEAR => 'dynamicThisYear',
524
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_TODAY => 'dynamicToday',
525
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW => 'dynamicTomorrow',
526
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE => 'dynamicYearToDate',
527
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY => 'dynamicYesterday',
528
    ];
529

530
    /** @return array{DateTime, DateTime} */
531
    private static function dynamicLastMonth(): array
2✔
532
    {
533
        $maxval = new DateTime();
2✔
534
        $year = (int) $maxval->format('Y');
2✔
535
        $month = (int) $maxval->format('m');
2✔
536
        $maxval->setDate($year, $month, 1);
2✔
537
        $maxval->setTime(0, 0, 0);
2✔
538
        $val = clone $maxval;
2✔
539
        $val->modify('-1 month');
2✔
540

541
        return [$val, $maxval];
2✔
542
    }
543

544
    private static function firstDayOfQuarter(): DateTime
4✔
545
    {
546
        $val = new DateTime();
4✔
547
        $year = (int) $val->format('Y');
4✔
548
        $month = (int) $val->format('m');
4✔
549
        $month = 3 * intdiv($month - 1, 3) + 1;
4✔
550
        $val->setDate($year, $month, 1);
4✔
551
        $val->setTime(0, 0, 0);
4✔
552

553
        return $val;
4✔
554
    }
555

556
    /** @return array{DateTime, DateTime} */
557
    private static function dynamicLastQuarter(): array
2✔
558
    {
559
        $maxval = self::firstDayOfQuarter();
2✔
560
        $val = clone $maxval;
2✔
561
        $val->modify('-3 months');
2✔
562

563
        return [$val, $maxval];
2✔
564
    }
565

566
    /** @return array{DateTime, DateTime} */
567
    private static function dynamicLastWeek(): array
2✔
568
    {
569
        $val = new DateTime();
2✔
570
        $val->setTime(0, 0, 0);
2✔
571
        $dayOfWeek = (int) $val->format('w'); // Sunday is 0
2✔
572
        $subtract = $dayOfWeek + 7; // revert to prior Sunday
2✔
573
        $val->modify("-$subtract days");
2✔
574
        $maxval = clone $val;
2✔
575
        $maxval->modify('+7 days');
2✔
576

577
        return [$val, $maxval];
2✔
578
    }
579

580
    /** @return array{DateTime, DateTime} */
581
    private static function dynamicLastYear(): array
2✔
582
    {
583
        $val = new DateTime();
2✔
584
        $year = (int) $val->format('Y');
2✔
585
        $val = self::makeDateObject($year - 1, 1, 1);
2✔
586
        $maxval = self::makeDateObject($year, 1, 1);
2✔
587

588
        return [$val, $maxval];
2✔
589
    }
590

591
    /** @return array{DateTime, DateTime} */
592
    private static function dynamicNextMonth(): array
2✔
593
    {
594
        $val = new DateTime();
2✔
595
        $year = (int) $val->format('Y');
2✔
596
        $month = (int) $val->format('m');
2✔
597
        $val->setDate($year, $month, 1);
2✔
598
        $val->setTime(0, 0, 0);
2✔
599
        $val->modify('+1 month');
2✔
600
        $maxval = clone $val;
2✔
601
        $maxval->modify('+1 month');
2✔
602

603
        return [$val, $maxval];
2✔
604
    }
605

606
    /** @return array{DateTime, DateTime} */
607
    private static function dynamicNextQuarter(): array
2✔
608
    {
609
        $val = self::firstDayOfQuarter();
2✔
610
        $val->modify('+3 months');
2✔
611
        $maxval = clone $val;
2✔
612
        $maxval->modify('+3 months');
2✔
613

614
        return [$val, $maxval];
2✔
615
    }
616

617
    /** @return array{DateTime, DateTime} */
618
    private static function dynamicNextWeek(): array
2✔
619
    {
620
        $val = new DateTime();
2✔
621
        $val->setTime(0, 0, 0);
2✔
622
        $dayOfWeek = (int) $val->format('w'); // Sunday is 0
2✔
623
        $add = 7 - $dayOfWeek; // move to next Sunday
2✔
624
        $val->modify("+$add days");
2✔
625
        $maxval = clone $val;
2✔
626
        $maxval->modify('+7 days');
2✔
627

628
        return [$val, $maxval];
2✔
629
    }
630

631
    /** @return array{DateTime, DateTime} */
632
    private static function dynamicNextYear(): array
2✔
633
    {
634
        $val = new DateTime();
2✔
635
        $year = (int) $val->format('Y');
2✔
636
        $val = self::makeDateObject($year + 1, 1, 1);
2✔
637
        $maxval = self::makeDateObject($year + 2, 1, 1);
2✔
638

639
        return [$val, $maxval];
2✔
640
    }
641

642
    /** @return array{DateTime, DateTime} */
643
    private static function dynamicThisMonth(): array
2✔
644
    {
645
        $baseDate = new DateTime();
2✔
646
        $baseDate->setTime(0, 0, 0);
2✔
647
        $year = (int) $baseDate->format('Y');
2✔
648
        $month = (int) $baseDate->format('m');
2✔
649
        $val = self::makeDateObject($year, $month, 1);
2✔
650
        $maxval = clone $val;
2✔
651
        $maxval->modify('+1 month');
2✔
652

653
        return [$val, $maxval];
2✔
654
    }
655

656
    /** @return array{DateTime, DateTime} */
657
    private static function dynamicThisQuarter(): array
2✔
658
    {
659
        $val = self::firstDayOfQuarter();
2✔
660
        $maxval = clone $val;
2✔
661
        $maxval->modify('+3 months');
2✔
662

663
        return [$val, $maxval];
2✔
664
    }
665

666
    /** @return array{DateTime, DateTime} */
667
    private static function dynamicThisWeek(): array
2✔
668
    {
669
        $val = new DateTime();
2✔
670
        $val->setTime(0, 0, 0);
2✔
671
        $dayOfWeek = (int) $val->format('w'); // Sunday is 0
2✔
672
        $subtract = $dayOfWeek; // revert to Sunday
2✔
673
        $val->modify("-$subtract days");
2✔
674
        $maxval = clone $val;
2✔
675
        $maxval->modify('+7 days');
2✔
676

677
        return [$val, $maxval];
2✔
678
    }
679

680
    /** @return array{DateTime, DateTime} */
681
    private static function dynamicThisYear(): array
2✔
682
    {
683
        $val = new DateTime();
2✔
684
        $year = (int) $val->format('Y');
2✔
685
        $val = self::makeDateObject($year, 1, 1);
2✔
686
        $maxval = self::makeDateObject($year + 1, 1, 1);
2✔
687

688
        return [$val, $maxval];
2✔
689
    }
690

691
    /** @return array{DateTime, DateTime} */
692
    private static function dynamicToday(): array
2✔
693
    {
694
        $val = new DateTime();
2✔
695
        $val->setTime(0, 0, 0);
2✔
696
        $maxval = clone $val;
2✔
697
        $maxval->modify('+1 day');
2✔
698

699
        return [$val, $maxval];
2✔
700
    }
701

702
    /** @return array{DateTime, DateTime} */
703
    private static function dynamicTomorrow(): array
2✔
704
    {
705
        $val = new DateTime();
2✔
706
        $val->setTime(0, 0, 0);
2✔
707
        $val->modify('+1 day');
2✔
708
        $maxval = clone $val;
2✔
709
        $maxval->modify('+1 day');
2✔
710

711
        return [$val, $maxval];
2✔
712
    }
713

714
    /** @return array{DateTime, DateTime} */
715
    private static function dynamicYearToDate(): array
3✔
716
    {
717
        $maxval = new DateTime();
3✔
718
        $maxval->setTime(0, 0, 0);
3✔
719
        $val = self::makeDateObject((int) $maxval->format('Y'), 1, 1);
3✔
720
        $maxval->modify('+1 day');
3✔
721

722
        return [$val, $maxval];
3✔
723
    }
724

725
    /** @return array{DateTime, DateTime} */
726
    private static function dynamicYesterday(): array
2✔
727
    {
728
        $maxval = new DateTime();
2✔
729
        $maxval->setTime(0, 0, 0);
2✔
730
        $val = clone $maxval;
2✔
731
        $val->modify('-1 day');
2✔
732

733
        return [$val, $maxval];
2✔
734
    }
735

736
    /**
737
     * Convert a dynamic rule daterange to a custom filter range expression for ease of calculation.
738
     *
739
     * @return mixed[]
740
     */
741
    private function dynamicFilterDateRange(string $dynamicRuleType, AutoFilter\Column &$filterColumn): array
18✔
742
    {
743
        $ruleValues = [];
18✔
744
        $callBack = [__CLASS__, self::DATE_FUNCTIONS[$dynamicRuleType]]; // What if not found?
18✔
745
        //    Calculate start/end dates for the required date range based on current date
746
        //    Val is lowest permitted value.
747
        //    Maxval is greater than highest permitted value
748
        $val = $maxval = 0;
18✔
749
        if (is_callable($callBack)) { //* @phpstan-ignore-line
18✔
750
            [$val, $maxval] = $callBack();
18✔
751
        }
752
        $val = Date::dateTimeToExcel($val);
18✔
753
        $maxval = Date::dateTimeToExcel($maxval);
18✔
754

755
        //    Set the filter column rule attributes ready for writing
756
        $filterColumn->setAttributes(['val' => $val, 'maxVal' => $maxval]);
18✔
757

758
        //    Set the rules for identifying rows for hide/show
759
        $ruleValues[] = ['operator' => Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL, 'value' => $val];
18✔
760
        $ruleValues[] = ['operator' => Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN, 'value' => $maxval];
18✔
761

762
        return ['method' => 'filterTestInCustomDataSet', 'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND]];
18✔
763
    }
764

765
    /**
766
     * Apply the AutoFilter rules to the AutoFilter Range.
767
     */
768
    private function calculateTopTenValue(string $columnID, int $startRow, int $endRow, ?string $ruleType, mixed $ruleValue): mixed
11✔
769
    {
770
        $range = $columnID . $startRow . ':' . $columnID . $endRow;
11✔
771
        $retVal = null;
11✔
772
        if ($this->workSheet !== null) {
11✔
773
            $dataValues = Functions::flattenArray($this->workSheet->rangeToArray($range, null, true, false));
11✔
774
            $dataValues = array_filter($dataValues);
11✔
775

776
            if ($ruleType == Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) {
11✔
777
                rsort($dataValues);
6✔
778
            } else {
779
                sort($dataValues);
5✔
780
            }
781

782
            if (is_numeric($ruleValue)) {
11✔
783
                $ruleValue = (int) $ruleValue;
11✔
784
            }
785
            if ($ruleValue === null || is_int($ruleValue)) {
11✔
786
                $slice = array_slice($dataValues, 0, $ruleValue);
11✔
787
                $retVal = array_pop($slice);
11✔
788
            }
789
        }
790

791
        return $retVal;
11✔
792
    }
793

794
    /**
795
     * Apply the AutoFilter rules to the AutoFilter Range.
796
     *
797
     * @return $this
798
     */
799
    public function showHideRows(): static
104✔
800
    {
801
        if ($this->workSheet === null) {
104✔
802
            return $this;
1✔
803
        }
804
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
103✔
805

806
        //    The heading row should always be visible
807
        $this->workSheet->getRowDimension($rangeStart[1])->setVisible(true);
103✔
808

809
        $columnFilterTests = [];
103✔
810
        foreach ($this->columns as $columnID => $filterColumn) {
103✔
811
            $rules = $filterColumn->getRules();
96✔
812
            switch ($filterColumn->getFilterType()) {
96✔
813
                case AutoFilter\Column::AUTOFILTER_FILTERTYPE_FILTER:
96✔
814
                    $ruleType = null;
25✔
815
                    $ruleValues = [];
25✔
816
                    //    Build a list of the filter value selections
817
                    foreach ($rules as $rule) {
25✔
818
                        $ruleType = $rule->getRuleType();
25✔
819
                        $ruleValues[] = $rule->getValue();
25✔
820
                    }
821
                    //    Test if we want to include blanks in our filter criteria
822
                    $blanks = false;
25✔
823
                    $ruleDataSet = array_filter($ruleValues);
25✔
824
                    if (count($ruleValues) != count($ruleDataSet)) {
25✔
825
                        $blanks = true;
2✔
826
                    }
827
                    if ($ruleType == Rule::AUTOFILTER_RULETYPE_FILTER) {
25✔
828
                        //    Filter on absolute values
829
                        $columnFilterTests[$columnID] = [
13✔
830
                            'method' => 'filterTestInSimpleDataSet',
13✔
831
                            'arguments' => ['filterValues' => $ruleDataSet, 'blanks' => $blanks],
13✔
832
                        ];
13✔
833
                    } elseif ($ruleType !== null) {
14✔
834
                        //    Filter on date group values
835
                        $arguments = [
14✔
836
                            'date' => [],
14✔
837
                            'time' => [],
14✔
838
                            'dateTime' => [],
14✔
839
                        ];
14✔
840
                        foreach ($ruleDataSet as $ruleValue) {
14✔
841
                            if (!is_array($ruleValue)) {
14✔
842
                                continue;
1✔
843
                            }
844
                            $date = $time = '';
13✔
845
                            if (
846
                                (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR]))
13✔
847
                                && ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR] !== '')
13✔
848
                            ) {
849
                                $date .= sprintf('%04d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR]);
9✔
850
                            }
851
                            if (
852
                                (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH]))
13✔
853
                                && ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH] != '')
13✔
854
                            ) {
855
                                $date .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH]);
7✔
856
                            }
857
                            if (
858
                                (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY]))
13✔
859
                                && ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY] !== '')
13✔
860
                            ) {
861
                                $date .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY]);
6✔
862
                            }
863
                            if (
864
                                (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR]))
13✔
865
                                && ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR] !== '')
13✔
866
                            ) {
867
                                $time .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR]);
6✔
868
                            }
869
                            if (
870
                                (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE]))
13✔
871
                                && ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE] !== '')
13✔
872
                            ) {
873
                                $time .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE]);
5✔
874
                            }
875
                            if (
876
                                (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND]))
13✔
877
                                && ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND] !== '')
13✔
878
                            ) {
879
                                $time .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND]);
4✔
880
                            }
881
                            $dateTime = $date . $time;
13✔
882
                            $arguments['date'][] = $date;
13✔
883
                            $arguments['time'][] = $time;
13✔
884
                            $arguments['dateTime'][] = $dateTime;
13✔
885
                        }
886
                        //    Remove empty elements
887
                        $arguments['date'] = array_filter($arguments['date']);
14✔
888
                        $arguments['time'] = array_filter($arguments['time']);
14✔
889
                        $arguments['dateTime'] = array_filter($arguments['dateTime']);
14✔
890
                        $columnFilterTests[$columnID] = [
14✔
891
                            'method' => 'filterTestInDateGroupSet',
14✔
892
                            'arguments' => ['filterValues' => $arguments, 'blanks' => $blanks],
14✔
893
                        ];
14✔
894
                    }
895

896
                    break;
25✔
897
                case AutoFilter\Column::AUTOFILTER_FILTERTYPE_CUSTOMFILTER:
74✔
898
                    $customRuleForBlanks = true;
41✔
899
                    $ruleValues = [];
41✔
900
                    //    Build a list of the filter value selections
901
                    foreach ($rules as $rule) {
41✔
902
                        $ruleValue = $rule->getValue();
41✔
903
                        if (!is_array($ruleValue) && !is_numeric($ruleValue)) {
41✔
904
                            //    Convert to a regexp allowing for regexp reserved characters, wildcards and escaped wildcards
905
                            $ruleValue = WildcardMatch::wildcard($ruleValue);
24✔
906
                            if (trim($ruleValue) == '') {
24✔
907
                                $customRuleForBlanks = true;
2✔
908
                                $ruleValue = trim($ruleValue);
2✔
909
                            }
910
                        }
911
                        $ruleValues[] = ['operator' => $rule->getOperator(), 'value' => $ruleValue];
41✔
912
                    }
913
                    $join = $filterColumn->getJoin();
41✔
914
                    $columnFilterTests[$columnID] = [
41✔
915
                        'method' => 'filterTestInCustomDataSet',
41✔
916
                        'arguments' => ['filterRules' => $ruleValues, 'join' => $join, 'customRuleForBlanks' => $customRuleForBlanks],
41✔
917
                    ];
41✔
918

919
                    break;
41✔
920
                case AutoFilter\Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER:
34✔
921
                    $ruleValues = [];
23✔
922
                    foreach ($rules as $rule) {
23✔
923
                        //    We should only ever have one Dynamic Filter Rule anyway
924
                        $dynamicRuleType = $rule->getGrouping();
23✔
925
                        if (
926
                            ($dynamicRuleType == Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE)
23✔
927
                            || ($dynamicRuleType == Rule::AUTOFILTER_RULETYPE_DYNAMIC_BELOWAVERAGE)
23✔
928
                        ) {
929
                            //    Number (Average) based
930
                            //    Calculate the average
931
                            $averageFormula = '=AVERAGE(' . $columnID . ($rangeStart[1] + 1) . ':' . $columnID . $rangeEnd[1] . ')';
2✔
932
                            $average = Calculation::getInstance($this->workSheet->getParent())->calculateFormula($averageFormula, null, $this->workSheet->getCell('A1'));
2✔
933
                            while (is_array($average)) {
2✔
UNCOV
934
                                $average = array_pop($average);
×
935
                            }
936
                            //    Set above/below rule based on greaterThan or LessTan
937
                            $operator = ($dynamicRuleType === Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE)
2✔
938
                                ? Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN
1✔
939
                                : Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN;
1✔
940
                            $ruleValues[] = [
2✔
941
                                'operator' => $operator,
2✔
942
                                'value' => $average,
2✔
943
                            ];
2✔
944
                            $columnFilterTests[$columnID] = [
2✔
945
                                'method' => 'filterTestInCustomDataSet',
2✔
946
                                'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR],
2✔
947
                            ];
2✔
948
                        } else {
949
                            //    Date based
950
                            if ($dynamicRuleType[0] == 'M' || $dynamicRuleType[0] == 'Q') {
21✔
951
                                $periodType = '';
4✔
952
                                $period = 0;
4✔
953
                                //    Month or Quarter
954
                                sscanf($dynamicRuleType, '%[A-Z]%d', $periodType, $period);
4✔
955
                                if ($periodType == 'M') {
4✔
956
                                    $ruleValues = [$period];
2✔
957
                                } else {
958
                                    /** @var int $period */
959
                                    --$period;
3✔
960
                                    $periodEnd = (1 + $period) * 3;
3✔
961
                                    $periodStart = 1 + $period * 3;
3✔
962
                                    $ruleValues = range($periodStart, $periodEnd);
3✔
963
                                }
964
                                $columnFilterTests[$columnID] = [
4✔
965
                                    'method' => 'filterTestInPeriodDateSet',
4✔
966
                                    'arguments' => $ruleValues,
4✔
967
                                ];
4✔
968
                                $filterColumn->setAttributes([]);
4✔
969
                            } else {
970
                                //    Date Range
971
                                $columnFilterTests[$columnID] = $this->dynamicFilterDateRange($dynamicRuleType, $filterColumn);
18✔
972

973
                                break;
18✔
974
                            }
975
                        }
976
                    }
977

978
                    break;
23✔
979
                case AutoFilter\Column::AUTOFILTER_FILTERTYPE_TOPTENFILTER:
11✔
980
                    $ruleValues = [];
11✔
981
                    $dataRowCount = $rangeEnd[1] - $rangeStart[1];
11✔
982
                    $toptenRuleType = null;
11✔
983
                    $ruleValue = 0;
11✔
984
                    $ruleOperator = null;
11✔
985
                    foreach ($rules as $rule) {
11✔
986
                        //    We should only ever have one Dynamic Filter Rule anyway
987
                        $toptenRuleType = $rule->getGrouping();
11✔
988
                        $ruleValue = $rule->getValue();
11✔
989
                        $ruleOperator = $rule->getOperator();
11✔
990
                    }
991
                    if (is_numeric($ruleValue) && $ruleOperator === Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT) {
11✔
992
                        $ruleValue = (int) floor((float) $ruleValue * ($dataRowCount / 100));
4✔
993
                    }
994
                    if (!is_array($ruleValue) && $ruleValue < 1) {
11✔
995
                        $ruleValue = 1;
1✔
996
                    }
997
                    if (!is_array($ruleValue) && $ruleValue > 500) {
11✔
998
                        $ruleValue = 500;
1✔
999
                    }
1000

1001
                    /** @var float|int|string */
1002
                    $maxVal = $this->calculateTopTenValue($columnID, $rangeStart[1] + 1, (int) $rangeEnd[1], $toptenRuleType, $ruleValue);
11✔
1003

1004
                    $operator = ($toptenRuleType == Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP)
11✔
1005
                        ? Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL
6✔
1006
                        : Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL;
5✔
1007
                    $ruleValues[] = ['operator' => $operator, 'value' => $maxVal];
11✔
1008
                    $columnFilterTests[$columnID] = [
11✔
1009
                        'method' => 'filterTestInCustomDataSet',
11✔
1010
                        'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR],
11✔
1011
                    ];
11✔
1012
                    $filterColumn->setAttributes(['maxVal' => $maxVal]);
11✔
1013

1014
                    break;
11✔
1015
            }
1016
        }
1017

1018
        $rangeEnd[1] = $this->autoExtendRange($rangeStart[1], $rangeEnd[1]);
103✔
1019

1020
        //    Execute the column tests for each row in the autoFilter range to determine show/hide,
1021
        for ($row = $rangeStart[1] + 1; $row <= $rangeEnd[1]; ++$row) {
103✔
1022
            $result = true;
103✔
1023
            foreach ($columnFilterTests as $columnID => $columnFilterTest) {
103✔
1024
                $cellValue = $this->workSheet->getCell($columnID . $row)->getCalculatedValue();
96✔
1025
                //    Execute the filter test
1026
                /** @var callable */
1027
                $temp = [self::class, $columnFilterTest['method']];
96✔
1028
                /** @var bool */
1029
                $result // $result && // phpstan says $result is always true here
96✔
1030
                    = call_user_func_array($temp, [$cellValue, $columnFilterTest['arguments']]);
96✔
1031
                //    If filter test has resulted in FALSE, exit the loop straightaway rather than running any more tests
1032
                if (!$result) {
96✔
1033
                    break;
96✔
1034
                }
1035
            }
1036
            //    Set show/hide for the row based on the result of the autoFilter result
1037
            //    If the RowDimension object has not been allocated yet and the row should be visible,
1038
            //    then we can avoid any operation since the rows are visible by default (saves a lot of memory)
1039
            if ($result === false || $this->workSheet->rowDimensionExists((int) $row)) {
103✔
1040
                $this->workSheet->getRowDimension((int) $row)->setVisible($result);
98✔
1041
            }
1042
        }
1043
        $this->evaluated = true;
103✔
1044

1045
        return $this;
103✔
1046
    }
1047

1048
    /**
1049
     * Magic Range Auto-sizing.
1050
     * For a single row rangeSet, we follow MS Excel rules, and search for the first empty row to determine our range.
1051
     */
1052
    public function autoExtendRange(int $startRow, int $endRow): int
104✔
1053
    {
1054
        if ($startRow === $endRow && $this->workSheet !== null) {
104✔
1055
            try {
1056
                $rowIterator = $this->workSheet->getRowIterator($startRow + 1);
1✔
1057
            } catch (Exception) {
1✔
1058
                // If there are no rows below $startRow
1059
                return $startRow;
1✔
1060
            }
1061
            foreach ($rowIterator as $row) {
1✔
1062
                if ($row->isEmpty(CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL) === true) {
1✔
1063
                    return $row->getRowIndex() - 1;
1✔
1064
                }
1065
            }
1066
        }
1067

1068
        return $endRow;
104✔
1069
    }
1070

1071
    /**
1072
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
1073
     */
1074
    public function __clone()
22✔
1075
    {
1076
        $vars = get_object_vars($this);
22✔
1077
        foreach ($vars as $key => $value) {
22✔
1078
            if (is_object($value)) {
22✔
1079
                if ($key === 'workSheet') {
22✔
1080
                    //    Detach from worksheet
1081
                    $this->{$key} = null;
22✔
1082
                } else {
UNCOV
1083
                    $this->{$key} = clone $value;
×
1084
                }
1085
            } elseif ((is_array($value)) && ($key == 'columns')) {
22✔
1086
                //    The columns array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\AutoFilter objects
1087
                $this->{$key} = [];
22✔
1088
                foreach ($value as $k => $v) {
22✔
1089
                    $this->{$key}[$k] = clone $v; //* @phpstan-ignore-line
1✔
1090
                    // attach the new cloned Column to this new cloned Autofilter object
1091
                    $this->{$key}[$k]->setParent($this);
1✔
1092
                }
1093
            } else {
1094
                $this->{$key} = $value;
22✔
1095
            }
1096
        }
1097
    }
1098

1099
    /**
1100
     * toString method replicates previous behavior by returning the range if object is
1101
     * referenced as a property of its parent.
1102
     */
1103
    public function __toString(): string
1✔
1104
    {
1105
        return (string) $this->range;
1✔
1106
    }
1107
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc