Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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\Coordinate;
12
use PhpOffice\PhpSpreadsheet\Exception;
13
use PhpOffice\PhpSpreadsheet\Shared\Date;
14
use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule;
15
 
16
class AutoFilter
17
{
18
    /**
19
     * Autofilter Worksheet.
20
     *
21
     * @var null|Worksheet
22
     */
23
    private $workSheet;
24
 
25
    /**
26
     * Autofilter Range.
27
     *
28
     * @var string
29
     */
30
    private $range = '';
31
 
32
    /**
33
     * Autofilter Column Ruleset.
34
     *
35
     * @var AutoFilter\Column[]
36
     */
37
    private $columns = [];
38
 
39
    /** @var bool */
40
    private $evaluated = false;
41
 
42
    public function getEvaluated(): bool
43
    {
44
        return $this->evaluated;
45
    }
46
 
47
    public function setEvaluated(bool $value): void
48
    {
49
        $this->evaluated = $value;
50
    }
51
 
52
    /**
53
     * Create a new AutoFilter.
54
     *
55
     * @param AddressRange|array<int>|string $range
56
     *            A simple string containing a Cell range like 'A1:E10' is permitted
57
     *              or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
58
     *              or an AddressRange object.
59
     */
60
    public function __construct($range = '', ?Worksheet $worksheet = null)
61
    {
62
        if ($range !== '') {
63
            [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true);
64
        }
65
 
66
        $this->range = $range;
67
        $this->workSheet = $worksheet;
68
    }
69
 
70
    /**
71
     * Get AutoFilter Parent Worksheet.
72
     *
73
     * @return null|Worksheet
74
     */
75
    public function getParent()
76
    {
77
        return $this->workSheet;
78
    }
79
 
80
    /**
81
     * Set AutoFilter Parent Worksheet.
82
     *
83
     * @return $this
84
     */
85
    public function setParent(?Worksheet $worksheet = null)
86
    {
87
        $this->evaluated = false;
88
        $this->workSheet = $worksheet;
89
 
90
        return $this;
91
    }
92
 
93
    /**
94
     * Get AutoFilter Range.
95
     *
96
     * @return string
97
     */
98
    public function getRange()
99
    {
100
        return $this->range;
101
    }
102
 
103
    /**
104
     * Set AutoFilter Cell Range.
105
     *
106
     * @param AddressRange|array<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($range = ''): self
112
    {
113
        $this->evaluated = false;
114
        // extract coordinate
115
        if ($range !== '') {
116
            [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true);
117
        }
118
 
119
        if (empty($range)) {
120
            //    Discard all column rules
121
            $this->columns = [];
122
            $this->range = '';
123
 
124
            return $this;
125
        }
126
 
127
        if (ctype_digit($range) || ctype_alpha($range)) {
128
            throw new Exception("{$range} is an invalid range for AutoFilter");
129
        }
130
 
131
        $this->range = $range;
132
        //    Discard any column rules that are no longer valid within this range
133
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
134
        foreach ($this->columns as $key => $value) {
135
            $colIndex = Coordinate::columnIndexFromString($key);
136
            if (($rangeStart[0] > $colIndex) || ($rangeEnd[0] < $colIndex)) {
137
                unset($this->columns[$key]);
138
            }
139
        }
140
 
141
        return $this;
142
    }
143
 
144
    public function setRangeToMaxRow(): self
145
    {
146
        $this->evaluated = false;
147
        if ($this->workSheet !== null) {
148
            $thisrange = $this->range;
149
            $range = (string) preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange);
150
            if ($range !== $thisrange) {
151
                $this->setRange($range);
152
            }
153
        }
154
 
155
        return $this;
156
    }
157
 
158
    /**
159
     * Get all AutoFilter Columns.
160
     *
161
     * @return AutoFilter\Column[]
162
     */
163
    public function getColumns()
164
    {
165
        return $this->columns;
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($column)
176
    {
177
        if (empty($this->range)) {
178
            throw new Exception('No autofilter range is defined.');
179
        }
180
 
181
        $columnIndex = Coordinate::columnIndexFromString($column);
182
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
183
        if (($rangeStart[0] > $columnIndex) || ($rangeEnd[0] < $columnIndex)) {
184
            throw new Exception('Column is outside of current autofilter range.');
185
        }
186
 
187
        return $columnIndex - $rangeStart[0];
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($column)
198
    {
199
        return $this->testColumnInRange($column);
200
    }
201
 
202
    /**
203
     * Get a specified AutoFilter Column.
204
     *
205
     * @param string $column Column name (e.g. A)
206
     *
207
     * @return AutoFilter\Column
208
     */
209
    public function getColumn($column)
210
    {
211
        $this->testColumnInRange($column);
212
 
213
        if (!isset($this->columns[$column])) {
214
            $this->columns[$column] = new AutoFilter\Column($column, $this);
215
        }
216
 
217
        return $this->columns[$column];
218
    }
219
 
220
    /**
221
     * Get a specified AutoFilter Column by it's offset.
222
     *
223
     * @param int $columnOffset Column offset within range (starting from 0)
224
     *
225
     * @return AutoFilter\Column
226
     */
227
    public function getColumnByOffset($columnOffset)
228
    {
229
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
230
        $pColumn = Coordinate::stringFromColumnIndex($rangeStart[0] + $columnOffset);
231
 
232
        return $this->getColumn($pColumn);
233
    }
234
 
235
    /**
236
     * Set AutoFilter.
237
     *
238
     * @param AutoFilter\Column|string $columnObjectOrString
239
     *            A simple string containing a Column ID like 'A' is permitted
240
     *
241
     * @return $this
242
     */
243
    public function setColumn($columnObjectOrString)
244
    {
245
        $this->evaluated = false;
246
        if ((is_string($columnObjectOrString)) && (!empty($columnObjectOrString))) {
247
            $column = $columnObjectOrString;
248
        } elseif (is_object($columnObjectOrString) && ($columnObjectOrString instanceof AutoFilter\Column)) {
249
            $column = $columnObjectOrString->getColumnIndex();
250
        } else {
251
            throw new Exception('Column is not within the autofilter range.');
252
        }
253
        $this->testColumnInRange($column);
254
 
255
        if (is_string($columnObjectOrString)) {
256
            $this->columns[$columnObjectOrString] = new AutoFilter\Column($columnObjectOrString, $this);
257
        } else {
258
            $columnObjectOrString->setParent($this);
259
            $this->columns[$column] = $columnObjectOrString;
260
        }
261
        ksort($this->columns);
262
 
263
        return $this;
264
    }
265
 
266
    /**
267
     * Clear a specified AutoFilter Column.
268
     *
269
     * @param string $column Column name (e.g. A)
270
     *
271
     * @return $this
272
     */
273
    public function clearColumn($column)
274
    {
275
        $this->evaluated = false;
276
        $this->testColumnInRange($column);
277
 
278
        if (isset($this->columns[$column])) {
279
            unset($this->columns[$column]);
280
        }
281
 
282
        return $this;
283
    }
284
 
285
    /**
286
     * Shift an AutoFilter Column Rule to a different column.
287
     *
288
     * Note: This method bypasses validation of the destination column to ensure it is within this AutoFilter range.
289
     *        Nor does it verify whether any column rule already exists at $toColumn, but will simply override any existing value.
290
     *        Use with caution.
291
     *
292
     * @param string $fromColumn Column name (e.g. A)
293
     * @param string $toColumn Column name (e.g. B)
294
     *
295
     * @return $this
296
     */
297
    public function shiftColumn($fromColumn, $toColumn)
298
    {
299
        $this->evaluated = false;
300
        $fromColumn = strtoupper($fromColumn);
301
        $toColumn = strtoupper($toColumn);
302
 
303
        if (($fromColumn !== null) && (isset($this->columns[$fromColumn])) && ($toColumn !== null)) {
304
            $this->columns[$fromColumn]->setParent();
305
            $this->columns[$fromColumn]->setColumnIndex($toColumn);
306
            $this->columns[$toColumn] = $this->columns[$fromColumn];
307
            $this->columns[$toColumn]->setParent($this);
308
            unset($this->columns[$fromColumn]);
309
 
310
            ksort($this->columns);
311
        }
312
 
313
        return $this;
314
    }
315
 
316
    /**
317
     * Test if cell value is in the defined set of values.
318
     *
319
     * @param mixed $cellValue
320
     * @param mixed[] $dataSet
321
     *
322
     * @return bool
323
     */
324
    protected static function filterTestInSimpleDataSet($cellValue, $dataSet)
325
    {
326
        $dataSetValues = $dataSet['filterValues'];
327
        $blanks = $dataSet['blanks'];
328
        if (($cellValue == '') || ($cellValue === null)) {
329
            return $blanks;
330
        }
331
 
332
        return in_array($cellValue, $dataSetValues);
333
    }
334
 
335
    /**
336
     * Test if cell value is in the defined set of Excel date values.
337
     *
338
     * @param mixed $cellValue
339
     * @param mixed[] $dataSet
340
     *
341
     * @return bool
342
     */
343
    protected static function filterTestInDateGroupSet($cellValue, $dataSet)
344
    {
345
        $dateSet = $dataSet['filterValues'];
346
        $blanks = $dataSet['blanks'];
347
        if (($cellValue == '') || ($cellValue === null)) {
348
            return $blanks;
349
        }
350
        $timeZone = new DateTimeZone('UTC');
351
 
352
        if (is_numeric($cellValue)) {
353
            $dateTime = Date::excelToDateTimeObject((float) $cellValue, $timeZone);
354
            $cellValue = (float) $cellValue;
355
            if ($cellValue < 1) {
356
                //    Just the time part
357
                $dtVal = $dateTime->format('His');
358
                $dateSet = $dateSet['time'];
359
            } elseif ($cellValue == floor($cellValue)) {
360
                //    Just the date part
361
                $dtVal = $dateTime->format('Ymd');
362
                $dateSet = $dateSet['date'];
363
            } else {
364
                //    date and time parts
365
                $dtVal = $dateTime->format('YmdHis');
366
                $dateSet = $dateSet['dateTime'];
367
            }
368
            foreach ($dateSet as $dateValue) {
369
                //    Use of substr to extract value at the appropriate group level
370
                if (substr($dtVal, 0, strlen($dateValue)) == $dateValue) {
371
                    return true;
372
                }
373
            }
374
        }
375
 
376
        return false;
377
    }
378
 
379
    /**
380
     * Test if cell value is within a set of values defined by a ruleset.
381
     *
382
     * @param mixed $cellValue
383
     * @param mixed[] $ruleSet
384
     *
385
     * @return bool
386
     */
387
    protected static function filterTestInCustomDataSet($cellValue, $ruleSet)
388
    {
389
        /** @var array[] */
390
        $dataSet = $ruleSet['filterRules'];
391
        $join = $ruleSet['join'];
392
        $customRuleForBlanks = $ruleSet['customRuleForBlanks'] ?? false;
393
 
394
        if (!$customRuleForBlanks) {
395
            //    Blank cells are always ignored, so return a FALSE
396
            if (($cellValue == '') || ($cellValue === null)) {
397
                return false;
398
            }
399
        }
400
        $returnVal = ($join == AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND);
401
        foreach ($dataSet as $rule) {
402
            /** @var string */
403
            $ruleValue = $rule['value'];
404
            /** @var string */
405
            $ruleOperator = $rule['operator'];
406
            /** @var string */
407
            $cellValueString = $cellValue ?? '';
408
            $retVal = false;
409
 
410
            if (is_numeric($ruleValue)) {
411
                //    Numeric values are tested using the appropriate operator
412
                $numericTest = is_numeric($cellValue);
413
                switch ($ruleOperator) {
414
                    case Rule::AUTOFILTER_COLUMN_RULE_EQUAL:
415
                        $retVal = $numericTest && ($cellValue == $ruleValue);
416
 
417
                        break;
418
                    case Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL:
419
                        $retVal = !$numericTest || ($cellValue != $ruleValue);
420
 
421
                        break;
422
                    case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN:
423
                        $retVal = $numericTest && ($cellValue > $ruleValue);
424
 
425
                        break;
426
                    case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL:
427
                        $retVal = $numericTest && ($cellValue >= $ruleValue);
428
 
429
                        break;
430
                    case Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN:
431
                        $retVal = $numericTest && ($cellValue < $ruleValue);
432
 
433
                        break;
434
                    case Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL:
435
                        $retVal = $numericTest && ($cellValue <= $ruleValue);
436
 
437
                        break;
438
                }
439
            } elseif ($ruleValue == '') {
440
                switch ($ruleOperator) {
441
                    case Rule::AUTOFILTER_COLUMN_RULE_EQUAL:
442
                        $retVal = (($cellValue == '') || ($cellValue === null));
443
 
444
                        break;
445
                    case Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL:
446
                        $retVal = (($cellValue != '') && ($cellValue !== null));
447
 
448
                        break;
449
                    default:
450
                        $retVal = true;
451
 
452
                        break;
453
                }
454
            } else {
455
                //    String values are always tested for equality, factoring in for wildcards (hence a regexp test)
456
                switch ($ruleOperator) {
457
                    case Rule::AUTOFILTER_COLUMN_RULE_EQUAL:
458
                        $retVal = (bool) preg_match('/^' . $ruleValue . '$/i', $cellValueString);
459
 
460
                        break;
461
                    case Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL:
462
                        $retVal = !((bool) preg_match('/^' . $ruleValue . '$/i', $cellValueString));
463
 
464
                        break;
465
                    case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN:
466
                        $retVal = strcasecmp($cellValueString, $ruleValue) > 0;
467
 
468
                        break;
469
                    case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL:
470
                        $retVal = strcasecmp($cellValueString, $ruleValue) >= 0;
471
 
472
                        break;
473
                    case Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN:
474
                        $retVal = strcasecmp($cellValueString, $ruleValue) < 0;
475
 
476
                        break;
477
                    case Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL:
478
                        $retVal = strcasecmp($cellValueString, $ruleValue) <= 0;
479
 
480
                        break;
481
                }
482
            }
483
            //    If there are multiple conditions, then we need to test both using the appropriate join operator
484
            switch ($join) {
485
                case AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR:
486
                    $returnVal = $returnVal || $retVal;
487
                    //    Break as soon as we have a TRUE match for OR joins,
488
                    //        to avoid unnecessary additional code execution
489
                    if ($returnVal) {
490
                        return $returnVal;
491
                    }
492
 
493
                    break;
494
                case AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND:
495
                    $returnVal = $returnVal && $retVal;
496
 
497
                    break;
498
            }
499
        }
500
 
501
        return $returnVal;
502
    }
503
 
504
    /**
505
     * Test if cell date value is matches a set of values defined by a set of months.
506
     *
507
     * @param mixed $cellValue
508
     * @param mixed[] $monthSet
509
     *
510
     * @return bool
511
     */
512
    protected static function filterTestInPeriodDateSet($cellValue, $monthSet)
513
    {
514
        //    Blank cells are always ignored, so return a FALSE
515
        if (($cellValue == '') || ($cellValue === null)) {
516
            return false;
517
        }
518
 
519
        if (is_numeric($cellValue)) {
520
            $dateObject = Date::excelToDateTimeObject((float) $cellValue, new DateTimeZone('UTC'));
521
            $dateValue = (int) $dateObject->format('m');
522
            if (in_array($dateValue, $monthSet)) {
523
                return true;
524
            }
525
        }
526
 
527
        return false;
528
    }
529
 
530
    private static function makeDateObject(int $year, int $month, int $day, int $hour = 0, int $minute = 0, int $second = 0): DateTime
531
    {
532
        $baseDate = new DateTime();
533
        $baseDate->setDate($year, $month, $day);
534
        $baseDate->setTime($hour, $minute, $second);
535
 
536
        return $baseDate;
537
    }
538
 
539
    private const DATE_FUNCTIONS = [
540
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH => 'dynamicLastMonth',
541
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER => 'dynamicLastQuarter',
542
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK => 'dynamicLastWeek',
543
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR => 'dynamicLastYear',
544
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH => 'dynamicNextMonth',
545
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER => 'dynamicNextQuarter',
546
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK => 'dynamicNextWeek',
547
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR => 'dynamicNextYear',
548
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISMONTH => 'dynamicThisMonth',
549
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISQUARTER => 'dynamicThisQuarter',
550
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISWEEK => 'dynamicThisWeek',
551
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISYEAR => 'dynamicThisYear',
552
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_TODAY => 'dynamicToday',
553
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW => 'dynamicTomorrow',
554
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE => 'dynamicYearToDate',
555
        Rule::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY => 'dynamicYesterday',
556
    ];
557
 
558
    private static function dynamicLastMonth(): array
559
    {
560
        $maxval = new DateTime();
561
        $year = (int) $maxval->format('Y');
562
        $month = (int) $maxval->format('m');
563
        $maxval->setDate($year, $month, 1);
564
        $maxval->setTime(0, 0, 0);
565
        $val = clone $maxval;
566
        $val->modify('-1 month');
567
 
568
        return [$val, $maxval];
569
    }
570
 
571
    private static function firstDayOfQuarter(): DateTime
572
    {
573
        $val = new DateTime();
574
        $year = (int) $val->format('Y');
575
        $month = (int) $val->format('m');
576
        $month = 3 * intdiv($month - 1, 3) + 1;
577
        $val->setDate($year, $month, 1);
578
        $val->setTime(0, 0, 0);
579
 
580
        return $val;
581
    }
582
 
583
    private static function dynamicLastQuarter(): array
584
    {
585
        $maxval = self::firstDayOfQuarter();
586
        $val = clone $maxval;
587
        $val->modify('-3 months');
588
 
589
        return [$val, $maxval];
590
    }
591
 
592
    private static function dynamicLastWeek(): array
593
    {
594
        $val = new DateTime();
595
        $val->setTime(0, 0, 0);
596
        $dayOfWeek = (int) $val->format('w'); // Sunday is 0
597
        $subtract = $dayOfWeek + 7; // revert to prior Sunday
598
        $val->modify("-$subtract days");
599
        $maxval = clone $val;
600
        $maxval->modify('+7 days');
601
 
602
        return [$val, $maxval];
603
    }
604
 
605
    private static function dynamicLastYear(): array
606
    {
607
        $val = new DateTime();
608
        $year = (int) $val->format('Y');
609
        $val = self::makeDateObject($year - 1, 1, 1);
610
        $maxval = self::makeDateObject($year, 1, 1);
611
 
612
        return [$val, $maxval];
613
    }
614
 
615
    private static function dynamicNextMonth(): array
616
    {
617
        $val = new DateTime();
618
        $year = (int) $val->format('Y');
619
        $month = (int) $val->format('m');
620
        $val->setDate($year, $month, 1);
621
        $val->setTime(0, 0, 0);
622
        $val->modify('+1 month');
623
        $maxval = clone $val;
624
        $maxval->modify('+1 month');
625
 
626
        return [$val, $maxval];
627
    }
628
 
629
    private static function dynamicNextQuarter(): array
630
    {
631
        $val = self::firstDayOfQuarter();
632
        $val->modify('+3 months');
633
        $maxval = clone $val;
634
        $maxval->modify('+3 months');
635
 
636
        return [$val, $maxval];
637
    }
638
 
639
    private static function dynamicNextWeek(): array
640
    {
641
        $val = new DateTime();
642
        $val->setTime(0, 0, 0);
643
        $dayOfWeek = (int) $val->format('w'); // Sunday is 0
644
        $add = 7 - $dayOfWeek; // move to next Sunday
645
        $val->modify("+$add days");
646
        $maxval = clone $val;
647
        $maxval->modify('+7 days');
648
 
649
        return [$val, $maxval];
650
    }
651
 
652
    private static function dynamicNextYear(): array
653
    {
654
        $val = new DateTime();
655
        $year = (int) $val->format('Y');
656
        $val = self::makeDateObject($year + 1, 1, 1);
657
        $maxval = self::makeDateObject($year + 2, 1, 1);
658
 
659
        return [$val, $maxval];
660
    }
661
 
662
    private static function dynamicThisMonth(): array
663
    {
664
        $baseDate = new DateTime();
665
        $baseDate->setTime(0, 0, 0);
666
        $year = (int) $baseDate->format('Y');
667
        $month = (int) $baseDate->format('m');
668
        $val = self::makeDateObject($year, $month, 1);
669
        $maxval = clone $val;
670
        $maxval->modify('+1 month');
671
 
672
        return [$val, $maxval];
673
    }
674
 
675
    private static function dynamicThisQuarter(): array
676
    {
677
        $val = self::firstDayOfQuarter();
678
        $maxval = clone $val;
679
        $maxval->modify('+3 months');
680
 
681
        return [$val, $maxval];
682
    }
683
 
684
    private static function dynamicThisWeek(): array
685
    {
686
        $val = new DateTime();
687
        $val->setTime(0, 0, 0);
688
        $dayOfWeek = (int) $val->format('w'); // Sunday is 0
689
        $subtract = $dayOfWeek; // revert to Sunday
690
        $val->modify("-$subtract days");
691
        $maxval = clone $val;
692
        $maxval->modify('+7 days');
693
 
694
        return [$val, $maxval];
695
    }
696
 
697
    private static function dynamicThisYear(): array
698
    {
699
        $val = new DateTime();
700
        $year = (int) $val->format('Y');
701
        $val = self::makeDateObject($year, 1, 1);
702
        $maxval = self::makeDateObject($year + 1, 1, 1);
703
 
704
        return [$val, $maxval];
705
    }
706
 
707
    private static function dynamicToday(): array
708
    {
709
        $val = new DateTime();
710
        $val->setTime(0, 0, 0);
711
        $maxval = clone $val;
712
        $maxval->modify('+1 day');
713
 
714
        return [$val, $maxval];
715
    }
716
 
717
    private static function dynamicTomorrow(): array
718
    {
719
        $val = new DateTime();
720
        $val->setTime(0, 0, 0);
721
        $val->modify('+1 day');
722
        $maxval = clone $val;
723
        $maxval->modify('+1 day');
724
 
725
        return [$val, $maxval];
726
    }
727
 
728
    private static function dynamicYearToDate(): array
729
    {
730
        $maxval = new DateTime();
731
        $maxval->setTime(0, 0, 0);
732
        $val = self::makeDateObject((int) $maxval->format('Y'), 1, 1);
733
        $maxval->modify('+1 day');
734
 
735
        return [$val, $maxval];
736
    }
737
 
738
    private static function dynamicYesterday(): array
739
    {
740
        $maxval = new DateTime();
741
        $maxval->setTime(0, 0, 0);
742
        $val = clone $maxval;
743
        $val->modify('-1 day');
744
 
745
        return [$val, $maxval];
746
    }
747
 
748
    /**
749
     * Convert a dynamic rule daterange to a custom filter range expression for ease of calculation.
750
     *
751
     * @param string $dynamicRuleType
752
     *
753
     * @return mixed[]
754
     */
755
    private function dynamicFilterDateRange($dynamicRuleType, AutoFilter\Column &$filterColumn)
756
    {
757
        $ruleValues = [];
758
        $callBack = [__CLASS__, self::DATE_FUNCTIONS[$dynamicRuleType]]; // What if not found?
759
        //    Calculate start/end dates for the required date range based on current date
760
        //    Val is lowest permitted value.
761
        //    Maxval is greater than highest permitted value
762
        $val = $maxval = 0;
763
        if (is_callable($callBack)) {
764
            [$val, $maxval] = $callBack();
765
        }
766
        $val = Date::dateTimeToExcel($val);
767
        $maxval = Date::dateTimeToExcel($maxval);
768
 
769
        //    Set the filter column rule attributes ready for writing
770
        $filterColumn->setAttributes(['val' => $val, 'maxVal' => $maxval]);
771
 
772
        //    Set the rules for identifying rows for hide/show
773
        $ruleValues[] = ['operator' => Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL, 'value' => $val];
774
        $ruleValues[] = ['operator' => Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN, 'value' => $maxval];
775
 
776
        return ['method' => 'filterTestInCustomDataSet', 'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND]];
777
    }
778
 
779
    /**
780
     * Apply the AutoFilter rules to the AutoFilter Range.
781
     *
782
     * @param string $columnID
783
     * @param int $startRow
784
     * @param int $endRow
785
     * @param ?string $ruleType
786
     * @param mixed $ruleValue
787
     *
788
     * @return mixed
789
     */
790
    private function calculateTopTenValue($columnID, $startRow, $endRow, $ruleType, $ruleValue)
791
    {
792
        $range = $columnID . $startRow . ':' . $columnID . $endRow;
793
        $retVal = null;
794
        if ($this->workSheet !== null) {
795
            $dataValues = Functions::flattenArray($this->workSheet->rangeToArray($range, null, true, false));
796
            $dataValues = array_filter($dataValues);
797
 
798
            if ($ruleType == Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) {
799
                rsort($dataValues);
800
            } else {
801
                sort($dataValues);
802
            }
803
 
804
            $slice = array_slice($dataValues, 0, $ruleValue);
805
 
806
            $retVal = array_pop($slice);
807
        }
808
 
809
        return $retVal;
810
    }
811
 
812
    /**
813
     * Apply the AutoFilter rules to the AutoFilter Range.
814
     *
815
     * @return $this
816
     */
817
    public function showHideRows()
818
    {
819
        if ($this->workSheet === null) {
820
            return $this;
821
        }
822
        [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
823
 
824
        //    The heading row should always be visible
825
        $this->workSheet->getRowDimension($rangeStart[1])->setVisible(true);
826
 
827
        $columnFilterTests = [];
828
        foreach ($this->columns as $columnID => $filterColumn) {
829
            $rules = $filterColumn->getRules();
830
            switch ($filterColumn->getFilterType()) {
831
                case AutoFilter\Column::AUTOFILTER_FILTERTYPE_FILTER:
832
                    $ruleType = null;
833
                    $ruleValues = [];
834
                    //    Build a list of the filter value selections
835
                    foreach ($rules as $rule) {
836
                        $ruleType = $rule->getRuleType();
837
                        $ruleValues[] = $rule->getValue();
838
                    }
839
                    //    Test if we want to include blanks in our filter criteria
840
                    $blanks = false;
841
                    $ruleDataSet = array_filter($ruleValues);
842
                    if (count($ruleValues) != count($ruleDataSet)) {
843
                        $blanks = true;
844
                    }
845
                    if ($ruleType == Rule::AUTOFILTER_RULETYPE_FILTER) {
846
                        //    Filter on absolute values
847
                        $columnFilterTests[$columnID] = [
848
                            'method' => 'filterTestInSimpleDataSet',
849
                            'arguments' => ['filterValues' => $ruleDataSet, 'blanks' => $blanks],
850
                        ];
851
                    } else {
852
                        //    Filter on date group values
853
                        $arguments = [
854
                            'date' => [],
855
                            'time' => [],
856
                            'dateTime' => [],
857
                        ];
858
                        foreach ($ruleDataSet as $ruleValue) {
859
                            if (!is_array($ruleValue)) {
860
                                continue;
861
                            }
862
                            $date = $time = '';
863
                            if (
864
                                (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR])) &&
865
                                ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR] !== '')
866
                            ) {
867
                                $date .= sprintf('%04d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR]);
868
                            }
869
                            if (
870
                                (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH])) &&
871
                                ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH] != '')
872
                            ) {
873
                                $date .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH]);
874
                            }
875
                            if (
876
                                (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY])) &&
877
                                ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY] !== '')
878
                            ) {
879
                                $date .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY]);
880
                            }
881
                            if (
882
                                (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR])) &&
883
                                ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR] !== '')
884
                            ) {
885
                                $time .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR]);
886
                            }
887
                            if (
888
                                (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE])) &&
889
                                ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE] !== '')
890
                            ) {
891
                                $time .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE]);
892
                            }
893
                            if (
894
                                (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND])) &&
895
                                ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND] !== '')
896
                            ) {
897
                                $time .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND]);
898
                            }
899
                            $dateTime = $date . $time;
900
                            $arguments['date'][] = $date;
901
                            $arguments['time'][] = $time;
902
                            $arguments['dateTime'][] = $dateTime;
903
                        }
904
                        //    Remove empty elements
905
                        $arguments['date'] = array_filter($arguments['date']);
906
                        $arguments['time'] = array_filter($arguments['time']);
907
                        $arguments['dateTime'] = array_filter($arguments['dateTime']);
908
                        $columnFilterTests[$columnID] = [
909
                            'method' => 'filterTestInDateGroupSet',
910
                            'arguments' => ['filterValues' => $arguments, 'blanks' => $blanks],
911
                        ];
912
                    }
913
 
914
                    break;
915
                case AutoFilter\Column::AUTOFILTER_FILTERTYPE_CUSTOMFILTER:
916
                    $customRuleForBlanks = true;
917
                    $ruleValues = [];
918
                    //    Build a list of the filter value selections
919
                    foreach ($rules as $rule) {
920
                        $ruleValue = $rule->getValue();
921
                        if (!is_array($ruleValue) && !is_numeric($ruleValue)) {
922
                            //    Convert to a regexp allowing for regexp reserved characters, wildcards and escaped wildcards
923
                            $ruleValue = WildcardMatch::wildcard($ruleValue);
924
                            if (trim($ruleValue) == '') {
925
                                $customRuleForBlanks = true;
926
                                $ruleValue = trim($ruleValue);
927
                            }
928
                        }
929
                        $ruleValues[] = ['operator' => $rule->getOperator(), 'value' => $ruleValue];
930
                    }
931
                    $join = $filterColumn->getJoin();
932
                    $columnFilterTests[$columnID] = [
933
                        'method' => 'filterTestInCustomDataSet',
934
                        'arguments' => ['filterRules' => $ruleValues, 'join' => $join, 'customRuleForBlanks' => $customRuleForBlanks],
935
                    ];
936
 
937
                    break;
938
                case AutoFilter\Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER:
939
                    $ruleValues = [];
940
                    foreach ($rules as $rule) {
941
                        //    We should only ever have one Dynamic Filter Rule anyway
942
                        $dynamicRuleType = $rule->getGrouping();
943
                        if (
944
                            ($dynamicRuleType == Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE) ||
945
                            ($dynamicRuleType == Rule::AUTOFILTER_RULETYPE_DYNAMIC_BELOWAVERAGE)
946
                        ) {
947
                            //    Number (Average) based
948
                            //    Calculate the average
949
                            $averageFormula = '=AVERAGE(' . $columnID . ($rangeStart[1] + 1) . ':' . $columnID . $rangeEnd[1] . ')';
950
                            $spreadsheet = ($this->workSheet === null) ? null : $this->workSheet->getParent();
951
                            $average = Calculation::getInstance($spreadsheet)->calculateFormula($averageFormula, null, $this->workSheet->getCell('A1'));
952
                            while (is_array($average)) {
953
                                $average = array_pop($average);
954
                            }
955
                            //    Set above/below rule based on greaterThan or LessTan
956
                            $operator = ($dynamicRuleType === Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE)
957
                                ? Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN
958
                                : Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN;
959
                            $ruleValues[] = [
960
                                'operator' => $operator,
961
                                'value' => $average,
962
                            ];
963
                            $columnFilterTests[$columnID] = [
964
                                'method' => 'filterTestInCustomDataSet',
965
                                'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR],
966
                            ];
967
                        } else {
968
                            //    Date based
969
                            if ($dynamicRuleType[0] == 'M' || $dynamicRuleType[0] == 'Q') {
970
                                $periodType = '';
971
                                $period = 0;
972
                                //    Month or Quarter
973
                                sscanf($dynamicRuleType, '%[A-Z]%d', $periodType, $period);
974
                                if ($periodType == 'M') {
975
                                    $ruleValues = [$period];
976
                                } else {
977
                                    --$period;
978
                                    $periodEnd = (1 + $period) * 3;
979
                                    $periodStart = 1 + $period * 3;
980
                                    $ruleValues = range($periodStart, $periodEnd);
981
                                }
982
                                $columnFilterTests[$columnID] = [
983
                                    'method' => 'filterTestInPeriodDateSet',
984
                                    'arguments' => $ruleValues,
985
                                ];
986
                                $filterColumn->setAttributes([]);
987
                            } else {
988
                                //    Date Range
989
                                $columnFilterTests[$columnID] = $this->dynamicFilterDateRange($dynamicRuleType, $filterColumn);
990
 
991
                                break;
992
                            }
993
                        }
994
                    }
995
 
996
                    break;
997
                case AutoFilter\Column::AUTOFILTER_FILTERTYPE_TOPTENFILTER:
998
                    $ruleValues = [];
999
                    $dataRowCount = $rangeEnd[1] - $rangeStart[1];
1000
                    $toptenRuleType = null;
1001
                    $ruleValue = 0;
1002
                    $ruleOperator = null;
1003
                    foreach ($rules as $rule) {
1004
                        //    We should only ever have one Dynamic Filter Rule anyway
1005
                        $toptenRuleType = $rule->getGrouping();
1006
                        $ruleValue = $rule->getValue();
1007
                        $ruleOperator = $rule->getOperator();
1008
                    }
1009
                    if (is_numeric($ruleValue) && $ruleOperator === Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT) {
1010
                        $ruleValue = floor((float) $ruleValue * ($dataRowCount / 100));
1011
                    }
1012
                    if (!is_array($ruleValue) && $ruleValue < 1) {
1013
                        $ruleValue = 1;
1014
                    }
1015
                    if (!is_array($ruleValue) && $ruleValue > 500) {
1016
                        $ruleValue = 500;
1017
                    }
1018
 
1019
                    $maxVal = $this->calculateTopTenValue($columnID, $rangeStart[1] + 1, (int) $rangeEnd[1], $toptenRuleType, $ruleValue);
1020
 
1021
                    $operator = ($toptenRuleType == Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP)
1022
                        ? Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL
1023
                        : Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL;
1024
                    $ruleValues[] = ['operator' => $operator, 'value' => $maxVal];
1025
                    $columnFilterTests[$columnID] = [
1026
                        'method' => 'filterTestInCustomDataSet',
1027
                        'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR],
1028
                    ];
1029
                    $filterColumn->setAttributes(['maxVal' => $maxVal]);
1030
 
1031
                    break;
1032
            }
1033
        }
1034
 
1035
        $rangeEnd[1] = $this->autoExtendRange($rangeStart[1], $rangeEnd[1]);
1036
 
1037
        //    Execute the column tests for each row in the autoFilter range to determine show/hide,
1038
        for ($row = $rangeStart[1] + 1; $row <= $rangeEnd[1]; ++$row) {
1039
            $result = true;
1040
            foreach ($columnFilterTests as $columnID => $columnFilterTest) {
1041
                $cellValue = $this->workSheet->getCell($columnID . $row)->getCalculatedValue();
1042
                //    Execute the filter test
1043
                $result = // $result && // phpstan says $result is always true here
1044
                    // @phpstan-ignore-next-line
1045
                    call_user_func_array([self::class, $columnFilterTest['method']], [$cellValue, $columnFilterTest['arguments']]);
1046
                //    If filter test has resulted in FALSE, exit the loop straightaway rather than running any more tests
1047
                if (!$result) {
1048
                    break;
1049
                }
1050
            }
1051
            //    Set show/hide for the row based on the result of the autoFilter result
1052
            $this->workSheet->getRowDimension((int) $row)->setVisible($result);
1053
        }
1054
        $this->evaluated = true;
1055
 
1056
        return $this;
1057
    }
1058
 
1059
    /**
1060
     * Magic Range Auto-sizing.
1061
     * For a single row rangeSet, we follow MS Excel rules, and search for the first empty row to determine our range.
1062
     */
1063
    public function autoExtendRange(int $startRow, int $endRow): int
1064
    {
1065
        if ($startRow === $endRow && $this->workSheet !== null) {
1066
            try {
1067
                $rowIterator = $this->workSheet->getRowIterator($startRow + 1);
1068
            } catch (Exception $e) {
1069
                // If there are no rows below $startRow
1070
                return $startRow;
1071
            }
1072
            foreach ($rowIterator as $row) {
1073
                if ($row->isEmpty(CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL) === true) {
1074
                    return $row->getRowIndex() - 1;
1075
                }
1076
            }
1077
        }
1078
 
1079
        return $endRow;
1080
    }
1081
 
1082
    /**
1083
     * Implement PHP __clone to create a deep clone, not just a shallow copy.
1084
     */
1085
    public function __clone()
1086
    {
1087
        $vars = get_object_vars($this);
1088
        foreach ($vars as $key => $value) {
1089
            if (is_object($value)) {
1090
                if ($key === 'workSheet') {
1091
                    //    Detach from worksheet
1092
                    $this->{$key} = null;
1093
                } else {
1094
                    $this->{$key} = clone $value;
1095
                }
1096
            } elseif ((is_array($value)) && ($key == 'columns')) {
1097
                //    The columns array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\AutoFilter objects
1098
                $this->{$key} = [];
1099
                foreach ($value as $k => $v) {
1100
                    $this->{$key}[$k] = clone $v;
1101
                    // attach the new cloned Column to this new cloned Autofilter object
1102
                    $this->{$key}[$k]->setParent($this);
1103
                }
1104
            } else {
1105
                $this->{$key} = $value;
1106
            }
1107
        }
1108
    }
1109
 
1110
    /**
1111
     * toString method replicates previous behavior by returning the range if object is
1112
     * referenced as a property of its parent.
1113
     */
1114
    public function __toString()
1115
    {
1116
        return (string) $this->range;
1117
    }
1118
}