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

PHPOffice / PhpSpreadsheet / 21013444139

14 Jan 2026 11:20PM UTC coverage: 96.199% (+0.2%) from 95.962%
21013444139

Pull #4657

github

web-flow
Merge dcaff346a into 48f2fe37d
Pull Request #4657: Handling Unions as Function Arguments

19 of 20 new or added lines in 1 file covered. (95.0%)

359 existing lines in 16 files now uncovered.

46287 of 48116 relevant lines covered (96.2%)

387.01 hits per line

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

98.35
/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
use Throwable;
19

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

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

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

39
    private bool $evaluated = false;
40

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

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

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

65
        $this->range = $range ?? '';
11,243✔
66
        $this->workSheet = $worksheet;
11,243✔
67
    }
68

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

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

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

92
        return $this;
129✔
93
    }
94

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

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

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

124
            return $this;
5✔
125
        }
126

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

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

141
        return $this;
319✔
142
    }
143

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

155
        return $this;
2✔
156
    }
157

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

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

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

187
        return $columnIndex - $rangeStart[0];
145✔
188
    }
189

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

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

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

215
        return $this->columns[$column];
137✔
216
    }
217

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

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

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

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

259
        return $this;
8✔
260
    }
261

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

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

278
        return $this;
2✔
279
    }
280

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

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

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

309
        return $this;
3✔
310
    }
311

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

325
        return in_array($cellValue, $dataSetValues);
14✔
326
    }
327

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

342
        if (is_numeric($cellValue)) {
15✔
343
            try {
344
                $dateTime = Date::excelToDateTimeObject((float) $cellValue, $timeZone);
15✔
345
            } catch (Throwable) {
1✔
346
                return false;
1✔
347
            }
348

349
            $cellValue = (float) $cellValue;
15✔
350
            if ($cellValue < 1) {
15✔
351
                //    Just the time part
352
                $dtVal = $dateTime->format('His');
4✔
353
                $dateSet = $dateSet['time'];
4✔
354
            } elseif ($cellValue == floor($cellValue)) {
11✔
355
                //    Just the date part
356
                $dtVal = $dateTime->format('Ymd');
5✔
357
                $dateSet = $dateSet['date'];
5✔
358
            } else {
359
                //    date and time parts
360
                $dtVal = $dateTime->format('YmdHis');
6✔
361
                $dateSet = $dateSet['dateTime'];
6✔
362
            }
363
            foreach ($dateSet as $dateValue) {
15✔
364
                //    Use of substr to extract value at the appropriate group level
365
                if (str_starts_with($dtVal, $dateValue)) {
14✔
366
                    return true;
12✔
367
                }
368
            }
369
        }
370

371
        return false;
15✔
372
    }
373

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

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

400
            if (is_numeric($ruleValue)) {
71✔
401
                //    Numeric values are tested using the appropriate operator
402
                $numericTest = is_numeric($cellValue);
47✔
403
                switch ($ruleOperator) {
404
                    case Rule::AUTOFILTER_COLUMN_RULE_EQUAL:
3✔
405
                        $retVal = $numericTest && ($cellValue == $ruleValue);
3✔
406

407
                        break;
3✔
408
                    case Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL:
3✔
409
                        $retVal = !$numericTest || ($cellValue != $ruleValue);
3✔
410

411
                        break;
3✔
412
                    case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN:
3✔
413
                        $retVal = $numericTest && ($cellValue > $ruleValue);
3✔
414

415
                        break;
3✔
416
                    case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL:
3✔
417
                        $retVal = $numericTest && ($cellValue >= $ruleValue);
28✔
418

419
                        break;
28✔
420
                    case Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN:
2✔
421
                        $retVal = $numericTest && ($cellValue < $ruleValue);
21✔
422

423
                        break;
21✔
424
                    case Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL:
1✔
425
                        $retVal = $numericTest && ($cellValue <= $ruleValue);
8✔
426

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

441
                        break;
15✔
442
                    case Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL:
×
443
                        $retVal = !((bool) preg_match('/^' . $ruleValue . '$/i', $cellValueString));
3✔
444

445
                        break;
3✔
446
                    case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN:
×
447
                        $retVal = strcasecmp($cellValueString, $ruleValue) > 0;
1✔
448

449
                        break;
1✔
450
                    case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL:
×
451
                        $retVal = strcasecmp($cellValueString, $ruleValue) >= 0;
1✔
452

453
                        break;
1✔
454
                    case Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN:
×
455
                        $retVal = strcasecmp($cellValueString, $ruleValue) < 0;
1✔
456

457
                        break;
1✔
458
                    case Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL:
×
459
                        $retVal = strcasecmp($cellValueString, $ruleValue) <= 0;
1✔
460

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

474
                    break;
51✔
475
                case AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND:
20✔
476
                    $returnVal = $returnVal && $retVal;
20✔
477

478
                    break;
20✔
479
            }
480
        }
481

482
        return $returnVal;
71✔
483
    }
484

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

497
        if (is_numeric($cellValue)) {
4✔
498
            try {
499
                $dateObject = Date::excelToDateTimeObject((float) $cellValue, new DateTimeZone('UTC'));
4✔
500
            } catch (Throwable) {
3✔
501
                return false;
3✔
502
            }
503

504
            $dateValue = (int) $dateObject->format('m');
4✔
505
            if (in_array($dateValue, $monthSet)) {
4✔
506
                return true;
3✔
507
            }
508
        }
509

510
        return false;
4✔
511
    }
512

513
    private static function makeDateObject(int $year, int $month, int $day, int $hour = 0, int $minute = 0, int $second = 0): DateTime
7✔
514
    {
515
        $baseDate = new DateTime();
7✔
516
        $baseDate->setDate($year, $month, $day);
7✔
517
        $baseDate->setTime($hour, $minute, $second);
7✔
518

519
        return $baseDate;
7✔
520
    }
521

522
    private const DATE_FUNCTIONS = [
523
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH => 'dynamicLastMonth',
524
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER => 'dynamicLastQuarter',
525
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK => 'dynamicLastWeek',
526
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR => 'dynamicLastYear',
527
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH => 'dynamicNextMonth',
528
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER => 'dynamicNextQuarter',
529
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK => 'dynamicNextWeek',
530
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR => 'dynamicNextYear',
531
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISMONTH => 'dynamicThisMonth',
532
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISQUARTER => 'dynamicThisQuarter',
533
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISWEEK => 'dynamicThisWeek',
534
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISYEAR => 'dynamicThisYear',
535
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_TODAY => 'dynamicToday',
536
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW => 'dynamicTomorrow',
537
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE => 'dynamicYearToDate',
538
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY => 'dynamicYesterday',
539
    ];
540

541
    /** @return array{DateTime, DateTime} */
542
    private static function dynamicLastMonth(): array
2✔
543
    {
544
        $maxval = new DateTime();
2✔
545
        $year = (int) $maxval->format('Y');
2✔
546
        $month = (int) $maxval->format('m');
2✔
547
        $maxval->setDate($year, $month, 1);
2✔
548
        $maxval->setTime(0, 0, 0);
2✔
549
        $val = clone $maxval;
2✔
550
        $val->modify('-1 month');
2✔
551

552
        return [$val, $maxval];
2✔
553
    }
554

555
    private static function firstDayOfQuarter(): DateTime
4✔
556
    {
557
        $val = new DateTime();
4✔
558
        $year = (int) $val->format('Y');
4✔
559
        $month = (int) $val->format('m');
4✔
560
        $month = 3 * intdiv($month - 1, 3) + 1;
4✔
561
        $val->setDate($year, $month, 1);
4✔
562
        $val->setTime(0, 0, 0);
4✔
563

564
        return $val;
4✔
565
    }
566

567
    /** @return array{DateTime, DateTime} */
568
    private static function dynamicLastQuarter(): array
2✔
569
    {
570
        $maxval = self::firstDayOfQuarter();
2✔
571
        $val = clone $maxval;
2✔
572
        $val->modify('-3 months');
2✔
573

574
        return [$val, $maxval];
2✔
575
    }
576

577
    /** @return array{DateTime, DateTime} */
578
    private static function dynamicLastWeek(): array
2✔
579
    {
580
        $val = new DateTime();
2✔
581
        $val->setTime(0, 0, 0);
2✔
582
        $dayOfWeek = (int) $val->format('w'); // Sunday is 0
2✔
583
        $subtract = $dayOfWeek + 7; // revert to prior Sunday
2✔
584
        $val->modify("-$subtract days");
2✔
585
        $maxval = clone $val;
2✔
586
        $maxval->modify('+7 days');
2✔
587

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

591
    /** @return array{DateTime, DateTime} */
592
    private static function dynamicLastYear(): array
2✔
593
    {
594
        $val = new DateTime();
2✔
595
        $year = (int) $val->format('Y');
2✔
596
        $val = self::makeDateObject($year - 1, 1, 1);
2✔
597
        $maxval = self::makeDateObject($year, 1, 1);
2✔
598

599
        return [$val, $maxval];
2✔
600
    }
601

602
    /** @return array{DateTime, DateTime} */
603
    private static function dynamicNextMonth(): array
2✔
604
    {
605
        $val = new DateTime();
2✔
606
        $year = (int) $val->format('Y');
2✔
607
        $month = (int) $val->format('m');
2✔
608
        $val->setDate($year, $month, 1);
2✔
609
        $val->setTime(0, 0, 0);
2✔
610
        $val->modify('+1 month');
2✔
611
        $maxval = clone $val;
2✔
612
        $maxval->modify('+1 month');
2✔
613

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

617
    /** @return array{DateTime, DateTime} */
618
    private static function dynamicNextQuarter(): array
2✔
619
    {
620
        $val = self::firstDayOfQuarter();
2✔
621
        $val->modify('+3 months');
2✔
622
        $maxval = clone $val;
2✔
623
        $maxval->modify('+3 months');
2✔
624

625
        return [$val, $maxval];
2✔
626
    }
627

628
    /** @return array{DateTime, DateTime} */
629
    private static function dynamicNextWeek(): array
2✔
630
    {
631
        $val = new DateTime();
2✔
632
        $val->setTime(0, 0, 0);
2✔
633
        $dayOfWeek = (int) $val->format('w'); // Sunday is 0
2✔
634
        $add = 7 - $dayOfWeek; // move to next Sunday
2✔
635
        $val->modify("+$add days");
2✔
636
        $maxval = clone $val;
2✔
637
        $maxval->modify('+7 days');
2✔
638

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

642
    /** @return array{DateTime, DateTime} */
643
    private static function dynamicNextYear(): array
2✔
644
    {
645
        $val = new DateTime();
2✔
646
        $year = (int) $val->format('Y');
2✔
647
        $val = self::makeDateObject($year + 1, 1, 1);
2✔
648
        $maxval = self::makeDateObject($year + 2, 1, 1);
2✔
649

650
        return [$val, $maxval];
2✔
651
    }
652

653
    /** @return array{DateTime, DateTime} */
654
    private static function dynamicThisMonth(): array
2✔
655
    {
656
        $baseDate = new DateTime();
2✔
657
        $baseDate->setTime(0, 0, 0);
2✔
658
        $year = (int) $baseDate->format('Y');
2✔
659
        $month = (int) $baseDate->format('m');
2✔
660
        $val = self::makeDateObject($year, $month, 1);
2✔
661
        $maxval = clone $val;
2✔
662
        $maxval->modify('+1 month');
2✔
663

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

667
    /** @return array{DateTime, DateTime} */
668
    private static function dynamicThisQuarter(): array
2✔
669
    {
670
        $val = self::firstDayOfQuarter();
2✔
671
        $maxval = clone $val;
2✔
672
        $maxval->modify('+3 months');
2✔
673

674
        return [$val, $maxval];
2✔
675
    }
676

677
    /** @return array{DateTime, DateTime} */
678
    private static function dynamicThisWeek(): array
2✔
679
    {
680
        $val = new DateTime();
2✔
681
        $val->setTime(0, 0, 0);
2✔
682
        $dayOfWeek = (int) $val->format('w'); // Sunday is 0
2✔
683
        $subtract = $dayOfWeek; // revert to Sunday
2✔
684
        $val->modify("-$subtract days");
2✔
685
        $maxval = clone $val;
2✔
686
        $maxval->modify('+7 days');
2✔
687

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

691
    /** @return array{DateTime, DateTime} */
692
    private static function dynamicThisYear(): array
2✔
693
    {
694
        $val = new DateTime();
2✔
695
        $year = (int) $val->format('Y');
2✔
696
        $val = self::makeDateObject($year, 1, 1);
2✔
697
        $maxval = self::makeDateObject($year + 1, 1, 1);
2✔
698

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

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

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

713
    /** @return array{DateTime, DateTime} */
714
    private static function dynamicTomorrow(): array
2✔
715
    {
716
        $val = new DateTime();
2✔
717
        $val->setTime(0, 0, 0);
2✔
718
        $val->modify('+1 day');
2✔
719
        $maxval = clone $val;
2✔
720
        $maxval->modify('+1 day');
2✔
721

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

725
    /** @return array{DateTime, DateTime} */
726
    private static function dynamicYearToDate(): array
3✔
727
    {
728
        $maxval = new DateTime();
3✔
729
        $maxval->setTime(0, 0, 0);
3✔
730
        $val = self::makeDateObject((int) $maxval->format('Y'), 1, 1);
3✔
731
        $maxval->modify('+1 day');
3✔
732

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

736
    /** @return array{DateTime, DateTime} */
737
    private static function dynamicYesterday(): array
2✔
738
    {
739
        $maxval = new DateTime();
2✔
740
        $maxval->setTime(0, 0, 0);
2✔
741
        $val = clone $maxval;
2✔
742
        $val->modify('-1 day');
2✔
743

744
        return [$val, $maxval];
2✔
745
    }
746

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

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

769
        //    Set the rules for identifying rows for hide/show
770
        $ruleValues[] = ['operator' => Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL, 'value' => $val];
18✔
771
        $ruleValues[] = ['operator' => Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN, 'value' => $maxval];
18✔
772

773
        return ['method' => 'filterTestInCustomDataSet', 'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND]];
18✔
774
    }
775

776
    /**
777
     * Apply the AutoFilter rules to the AutoFilter Range.
778
     */
779
    private function calculateTopTenValue(string $columnID, int $startRow, int $endRow, ?string $ruleType, mixed $ruleValue): mixed
11✔
780
    {
781
        $range = $columnID . $startRow . ':' . $columnID . $endRow;
11✔
782
        $retVal = null;
11✔
783
        if ($this->workSheet !== null) {
11✔
784
            $dataValues = Functions::flattenArray($this->workSheet->rangeToArray($range, null, true, false));
11✔
785
            $dataValues = array_filter($dataValues);
11✔
786

787
            if ($ruleType == Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) {
11✔
788
                rsort($dataValues);
6✔
789
            } else {
790
                sort($dataValues);
5✔
791
            }
792

793
            if (is_numeric($ruleValue)) {
11✔
794
                $ruleValue = (int) $ruleValue;
11✔
795
            }
796
            if ($ruleValue === null || is_int($ruleValue)) {
11✔
797
                $slice = array_slice($dataValues, 0, $ruleValue);
11✔
798
                $retVal = array_pop($slice);
11✔
799
            }
800
        }
801

802
        return $retVal;
11✔
803
    }
804

805
    /**
806
     * Apply the AutoFilter rules to the AutoFilter Range.
807
     *
808
     * @return $this
809
     */
810
    public function showHideRows(): static
106✔
811
    {
812
        if ($this->workSheet === null) {
106✔
813
            return $this;
1✔
814
        }
815
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
105✔
816

817
        //    The heading row should always be visible
818
        $this->workSheet->getRowDimension($rangeStart[1])->setVisible(true);
105✔
819

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

907
                    break;
27✔
908
                case AutoFilter\Column::AUTOFILTER_FILTERTYPE_CUSTOMFILTER:
74✔
909
                    $customRuleForBlanks = true;
41✔
910
                    $ruleValues = [];
41✔
911
                    //    Build a list of the filter value selections
912
                    foreach ($rules as $rule) {
41✔
913
                        $ruleValue = $rule->getValue();
41✔
914
                        if (!is_array($ruleValue) && !is_numeric($ruleValue)) {
41✔
915
                            //    Convert to a regexp allowing for regexp reserved characters, wildcards and escaped wildcards
916
                            $ruleValue = WildcardMatch::wildcard($ruleValue);
24✔
917
                            if (trim($ruleValue) == '') {
24✔
918
                                $customRuleForBlanks = true;
2✔
919
                                $ruleValue = trim($ruleValue);
2✔
920
                            }
921
                        }
922
                        $ruleValues[] = ['operator' => $rule->getOperator(), 'value' => $ruleValue];
41✔
923
                    }
924
                    $join = $filterColumn->getJoin();
41✔
925
                    $columnFilterTests[$columnID] = [
41✔
926
                        'method' => 'filterTestInCustomDataSet',
41✔
927
                        'arguments' => ['filterRules' => $ruleValues, 'join' => $join, 'customRuleForBlanks' => $customRuleForBlanks],
41✔
928
                    ];
41✔
929

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

984
                                break;
18✔
985
                            }
986
                        }
987
                    }
988

989
                    break;
23✔
990
                case AutoFilter\Column::AUTOFILTER_FILTERTYPE_TOPTENFILTER:
11✔
991
                    $ruleValues = [];
11✔
992
                    $dataRowCount = $rangeEnd[1] - $rangeStart[1];
11✔
993
                    $toptenRuleType = null;
11✔
994
                    $ruleValue = 0;
11✔
995
                    $ruleOperator = null;
11✔
996
                    foreach ($rules as $rule) {
11✔
997
                        //    We should only ever have one Dynamic Filter Rule anyway
998
                        $toptenRuleType = $rule->getGrouping();
11✔
999
                        $ruleValue = $rule->getValue();
11✔
1000
                        $ruleOperator = $rule->getOperator();
11✔
1001
                    }
1002
                    if (is_numeric($ruleValue) && $ruleOperator === Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT) {
11✔
1003
                        $ruleValue = (int) floor((float) $ruleValue * ($dataRowCount / 100));
4✔
1004
                    }
1005
                    if (!is_array($ruleValue) && $ruleValue < 1) {
11✔
1006
                        $ruleValue = 1;
1✔
1007
                    }
1008
                    if (!is_array($ruleValue) && $ruleValue > 500) {
11✔
1009
                        $ruleValue = 500;
1✔
1010
                    }
1011

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

1015
                    $operator = ($toptenRuleType == Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP)
11✔
1016
                        ? Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL
6✔
1017
                        : Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL;
5✔
1018
                    $ruleValues[] = ['operator' => $operator, 'value' => $maxVal];
11✔
1019
                    $columnFilterTests[$columnID] = [
11✔
1020
                        'method' => 'filterTestInCustomDataSet',
11✔
1021
                        'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR],
11✔
1022
                    ];
11✔
1023
                    $filterColumn->setAttributes(['maxVal' => $maxVal]);
11✔
1024

1025
                    break;
11✔
1026
            }
1027
        }
1028

1029
        $rangeEnd[1] = $this->autoExtendRange($rangeStart[1], $rangeEnd[1]);
105✔
1030

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

1059
        return $this;
105✔
1060
    }
1061

1062
    /**
1063
     * Magic Range Auto-sizing.
1064
     * For a single row rangeSet, we follow MS Excel rules, and search for the first empty row to determine our range.
1065
     */
1066
    public function autoExtendRange(int $startRow, int $endRow): int
106✔
1067
    {
1068
        if ($startRow === $endRow && $this->workSheet !== null) {
106✔
1069
            try {
1070
                $rowIterator = $this->workSheet->getRowIterator($startRow + 1);
1✔
1071
            } catch (Exception) {
1✔
1072
                // If there are no rows below $startRow
1073
                return $startRow;
1✔
1074
            }
1075
            foreach ($rowIterator as $row) {
1✔
1076
                if ($row->isEmpty(CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL) === true) {
1✔
1077
                    return $row->getRowIndex() - 1;
1✔
1078
                }
1079
            }
1080
        }
1081

1082
        return $endRow;
106✔
1083
    }
1084

1085
    /**
1086
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
1087
     */
1088
    public function __clone()
24✔
1089
    {
1090
        $vars = get_object_vars($this);
24✔
1091
        foreach ($vars as $key => $value) {
24✔
1092
            if (is_object($value)) {
24✔
1093
                if ($key === 'workSheet') {
24✔
1094
                    //    Detach from worksheet
1095
                    $this->{$key} = null;
24✔
1096
                } else {
UNCOV
1097
                    $this->{$key} = clone $value;
×
1098
                }
1099
            } elseif ((is_array($value)) && ($key == 'columns')) {
24✔
1100
                //    The columns array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\AutoFilter objects
1101
                $this->{$key} = [];
24✔
1102
                foreach ($value as $k => $v) {
24✔
1103
                    $this->{$key}[$k] = clone $v; //* @phpstan-ignore-line
1✔
1104
                    // attach the new cloned Column to this new cloned Autofilter object
1105
                    $this->{$key}[$k]->setParent($this); //* @phpstan-ignore-line
1✔
1106
                }
1107
            } else {
1108
                $this->{$key} = $value;
24✔
1109
            }
1110
        }
1111
    }
1112

1113
    /**
1114
     * toString method replicates previous behavior by returning the range if object is
1115
     * referenced as a property of its parent.
1116
     */
1117
    public function __toString(): string
1✔
1118
    {
1119
        return (string) $this->range;
1✔
1120
    }
1121
}
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