Skip to content

Commit 2a12587

Browse files
authored
AutoFilter Improvements (#2393)
* AutoFilter Improvements Fix issue #2378. The following changes are made: - NotEqual tests must be part of a custom filter. Documentation has been changed to indicate that. - Method setAndOr was replaced by setJoin some time ago. Documentation now reflects that change. - Documentation to indicate that string filters are not case-sensitive, same as in Excel. - Filters testing against numeric value now include a numeric test (not numeric for not equal, numeric for all others). - String filter had previously treated everything as a test for "equal". It now handles "not equal" and the variants of "greater/less" with or without "equal". - Documentation correctly stated that no more than 2 rules are allowed in a custom filter. Code did not enforce this restriction. It now does, throwing an exception if an attempt is made to add a third rule. - Deleted a lot of comments in Rule.php to make it easier to see what is not yet implemented (between, begins with, etc.). I may take these on in future. - Added a number of tests for the new functionality. * Not Sure Why Phpstan Results Differ Local vs Github Let's see if this change suffices. * Phpstan Still Not sure how to convince it. Let's try this. * Phpstan Solved Figured out the problem on my local machine. Expect this to work.
1 parent 52585a9 commit 2a12587

File tree

7 files changed

+392
-76
lines changed

7 files changed

+392
-76
lines changed

docs/topics/autofilters.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ results are unpredictable.
9999
Other filter expression types (such as cell colour filters) are not yet
100100
supported.
101101

102+
String comparisons in filters are case-insensitive.
103+
102104
### Simple filters
103105

104106
In MS Excel, Simple Filters are a dropdown list of all values used in
@@ -113,6 +115,8 @@ will be hidden.
113115
To create a filter expression, we need to start by identifying the
114116
filter type. In this case, we're just going to specify that this filter
115117
is a standard filter.
118+
*Please note that Excel regards only tests for equal as a standard filter;
119+
all others, including tests for not equal, must be supplied as custom filters.*
116120

117121
```php
118122
$columnFilter->setFilterType(
@@ -255,6 +259,7 @@ MS Excel uses `*` as a wildcard to match any number of characters, and `?`
255259
as a wildcard to match a single character. `U*` equates to "begins with
256260
a 'U'"; `*U` equates to "ends with a 'U'"; and `*U*` equates to
257261
"contains a 'U'".
262+
Note that PhpSpreadsheet recognizes wildcards only for equal/not-equal tests.
258263

259264
If you want to match explicitly against `*` or `?`, you can
260265
escape it with a tilde `~`, so `?~**` would explicitly match for `*`
@@ -290,8 +295,8 @@ This defined two rules, filtering numbers that are `>= -20` OR `<=
290295
than OR.
291296

292297
```php
293-
$columnFilter->setAndOr(
294-
\PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column::AUTOFILTER_COLUMN_ANDOR_AND
298+
$columnFilter->setJoin(
299+
\PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND
295300
);
296301
```
297302

@@ -501,7 +506,7 @@ hiding all other rows within the autofilter area.
501506
### Displaying Filtered Rows
502507

503508
Simply looping through the rows in an autofilter area will still access
504-
ever row, whether it matches the filter criteria or not. To selectively
509+
every row, whether it matches the filter criteria or not. To selectively
505510
access only the filtered rows, you need to test each row’s visibility
506511
settings.
507512

phpstan-baseline.neon

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6052,7 +6052,7 @@ parameters:
60526052

60536053
-
60546054
message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
6055-
count: 2
6055+
count: 1
60566056
path: src/PhpSpreadsheet/Worksheet/AutoFilter.php
60576057

60586058
-
@@ -6065,21 +6065,11 @@ parameters:
60656065
count: 1
60666066
path: src/PhpSpreadsheet/Worksheet/AutoFilter.php
60676067

6068-
-
6069-
message: "#^Cannot access offset 'operator' on mixed\\.$#"
6070-
count: 2
6071-
path: src/PhpSpreadsheet/Worksheet/AutoFilter.php
6072-
60736068
-
60746069
message: "#^Cannot access offset 'time' on mixed\\.$#"
60756070
count: 1
60766071
path: src/PhpSpreadsheet/Worksheet/AutoFilter.php
60776072

6078-
-
6079-
message: "#^Cannot access offset 'value' on mixed\\.$#"
6080-
count: 9
6081-
path: src/PhpSpreadsheet/Worksheet/AutoFilter.php
6082-
60836073
-
60846074
message: "#^Cannot use array destructuring on mixed\\.$#"
60856075
count: 1
@@ -6120,11 +6110,6 @@ parameters:
61206110
count: 1
61216111
path: src/PhpSpreadsheet/Worksheet/AutoFilter.php
61226112

6123-
-
6124-
message: "#^Parameter \\#2 \\$subject of function preg_match expects string, mixed given\\.$#"
6125-
count: 1
6126-
path: src/PhpSpreadsheet/Worksheet/AutoFilter.php
6127-
61286113
-
61296114
message: "#^Parameter \\#3 \\$length of function array_slice expects int\\|null, mixed given\\.$#"
61306115
count: 1

src/PhpSpreadsheet/Worksheet/AutoFilter.php

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ private static function filterTestInDateGroupSet($cellValue, $dataSet)
345345
*/
346346
private static function filterTestInCustomDataSet($cellValue, $ruleSet)
347347
{
348+
/** @var array[] */
348349
$dataSet = $ruleSet['filterRules'];
349350
$join = $ruleSet['join'];
350351
$customRuleForBlanks = $ruleSet['customRuleForBlanks'] ?? false;
@@ -357,38 +358,45 @@ private static function filterTestInCustomDataSet($cellValue, $ruleSet)
357358
}
358359
$returnVal = ($join == AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND);
359360
foreach ($dataSet as $rule) {
361+
/** @var string */
362+
$ruleValue = $rule['value'];
363+
/** @var string */
364+
$ruleOperator = $rule['operator'];
365+
/** @var string */
366+
$cellValueString = $cellValue;
360367
$retVal = false;
361368

362-
if (is_numeric($rule['value'])) {
369+
if (is_numeric($ruleValue)) {
363370
// Numeric values are tested using the appropriate operator
364-
switch ($rule['operator']) {
371+
$numericTest = is_numeric($cellValue);
372+
switch ($ruleOperator) {
365373
case Rule::AUTOFILTER_COLUMN_RULE_EQUAL:
366-
$retVal = ($cellValue == $rule['value']);
374+
$retVal = $numericTest && ($cellValue == $ruleValue);
367375

368376
break;
369377
case Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL:
370-
$retVal = ($cellValue != $rule['value']);
378+
$retVal = !$numericTest || ($cellValue != $ruleValue);
371379

372380
break;
373381
case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN:
374-
$retVal = ($cellValue > $rule['value']);
382+
$retVal = $numericTest && ($cellValue > $ruleValue);
375383

376384
break;
377385
case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL:
378-
$retVal = ($cellValue >= $rule['value']);
386+
$retVal = $numericTest && ($cellValue >= $ruleValue);
379387

380388
break;
381389
case Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN:
382-
$retVal = ($cellValue < $rule['value']);
390+
$retVal = $numericTest && ($cellValue < $ruleValue);
383391

384392
break;
385393
case Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL:
386-
$retVal = ($cellValue <= $rule['value']);
394+
$retVal = $numericTest && ($cellValue <= $ruleValue);
387395

388396
break;
389397
}
390-
} elseif ($rule['value'] == '') {
391-
switch ($rule['operator']) {
398+
} elseif ($ruleValue == '') {
399+
switch ($ruleOperator) {
392400
case Rule::AUTOFILTER_COLUMN_RULE_EQUAL:
393401
$retVal = (($cellValue == '') || ($cellValue === null));
394402

@@ -404,7 +412,32 @@ private static function filterTestInCustomDataSet($cellValue, $ruleSet)
404412
}
405413
} else {
406414
// String values are always tested for equality, factoring in for wildcards (hence a regexp test)
407-
$retVal = preg_match('/^' . $rule['value'] . '$/i', $cellValue);
415+
switch ($ruleOperator) {
416+
case Rule::AUTOFILTER_COLUMN_RULE_EQUAL:
417+
$retVal = (bool) preg_match('/^' . $ruleValue . '$/i', $cellValueString);
418+
419+
break;
420+
case Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL:
421+
$retVal = !((bool) preg_match('/^' . $ruleValue . '$/i', $cellValueString));
422+
423+
break;
424+
case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN:
425+
$retVal = strcasecmp($cellValueString, $ruleValue) > 0;
426+
427+
break;
428+
case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL:
429+
$retVal = strcasecmp($cellValueString, $ruleValue) >= 0;
430+
431+
break;
432+
case Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN:
433+
$retVal = strcasecmp($cellValueString, $ruleValue) < 0;
434+
435+
break;
436+
case Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL:
437+
$retVal = strcasecmp($cellValueString, $ruleValue) <= 0;
438+
439+
break;
440+
}
408441
}
409442
// If there are multiple conditions, then we need to test both using the appropriate join operator
410443
switch ($join) {
@@ -840,7 +873,7 @@ public function showHideRows()
840873

841874
break;
842875
case AutoFilter\Column::AUTOFILTER_FILTERTYPE_CUSTOMFILTER:
843-
$customRuleForBlanks = false;
876+
$customRuleForBlanks = true;
844877
$ruleValues = [];
845878
// Build a list of the filter value selections
846879
foreach ($rules as $rule) {

src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,9 @@ public function setFilterType($filterType)
176176
if (!in_array($filterType, self::$filterTypes)) {
177177
throw new PhpSpreadsheetException('Invalid filter type for column AutoFilter.');
178178
}
179+
if ($filterType === self::AUTOFILTER_FILTERTYPE_CUSTOMFILTER && count($this->ruleset) > 2) {
180+
throw new PhpSpreadsheetException('No more than 2 rules are allowed in a Custom Filter');
181+
}
179182

180183
$this->filterType = $filterType;
181184

@@ -305,6 +308,9 @@ public function getRule($index)
305308
*/
306309
public function createRule()
307310
{
311+
if ($this->filterType === self::AUTOFILTER_FILTERTYPE_CUSTOMFILTER && count($this->ruleset) >= 2) {
312+
throw new PhpSpreadsheetException('No more than 2 rules are allowed in a Custom Filter');
313+
}
308314
$this->ruleset[] = new Column\Rule($this);
309315

310316
return end($this->ruleset);

src/PhpSpreadsheet/Worksheet/AutoFilter/Column/Rule.php

Lines changed: 9 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -125,15 +125,7 @@ class Rule
125125
self::AUTOFILTER_RULETYPE_DYNAMIC_BELOWAVERAGE,
126126
];
127127

128-
/*
129-
* The only valid filter rule operators for filter and customFilter types are:
130-
* <xsd:enumeration value="equal"/>
131-
* <xsd:enumeration value="lessThan"/>
132-
* <xsd:enumeration value="lessThanOrEqual"/>
133-
* <xsd:enumeration value="notEqual"/>
134-
* <xsd:enumeration value="greaterThanOrEqual"/>
135-
* <xsd:enumeration value="greaterThan"/>
136-
*/
128+
// Filter rule operators for filter and customFilter types.
137129
const AUTOFILTER_COLUMN_RULE_EQUAL = 'equal';
138130
const AUTOFILTER_COLUMN_RULE_NOTEQUAL = 'notEqual';
139131
const AUTOFILTER_COLUMN_RULE_GREATERTHAN = 'greaterThan';
@@ -166,39 +158,17 @@ class Rule
166158
self::AUTOFILTER_COLUMN_RULE_TOPTEN_BOTTOM,
167159
];
168160

169-
// Rule Operators (Numeric, Boolean etc)
170-
// const AUTOFILTER_COLUMN_RULE_BETWEEN = 'between'; // greaterThanOrEqual 1 && lessThanOrEqual 2
161+
// Unimplented Rule Operators (Numeric, Boolean etc)
162+
// const AUTOFILTER_COLUMN_RULE_BETWEEN = 'between'; // greaterThanOrEqual 1 && lessThanOrEqual 2
171163
// Rule Operators (Numeric Special) which are translated to standard numeric operators with calculated values
172-
// const AUTOFILTER_COLUMN_RULE_TOPTEN = 'topTen'; // greaterThan calculated value
173-
// const AUTOFILTER_COLUMN_RULE_TOPTENPERCENT = 'topTenPercent'; // greaterThan calculated value
174-
// const AUTOFILTER_COLUMN_RULE_ABOVEAVERAGE = 'aboveAverage'; // Value is calculated as the average
175-
// const AUTOFILTER_COLUMN_RULE_BELOWAVERAGE = 'belowAverage'; // Value is calculated as the average
176164
// Rule Operators (String) which are set as wild-carded values
177-
// const AUTOFILTER_COLUMN_RULE_BEGINSWITH = 'beginsWith'; // A*
178-
// const AUTOFILTER_COLUMN_RULE_ENDSWITH = 'endsWith'; // *Z
179-
// const AUTOFILTER_COLUMN_RULE_CONTAINS = 'contains'; // *B*
180-
// const AUTOFILTER_COLUMN_RULE_DOESNTCONTAIN = 'notEqual'; // notEqual *B*
165+
// const AUTOFILTER_COLUMN_RULE_BEGINSWITH = 'beginsWith'; // A*
166+
// const AUTOFILTER_COLUMN_RULE_ENDSWITH = 'endsWith'; // *Z
167+
// const AUTOFILTER_COLUMN_RULE_CONTAINS = 'contains'; // *B*
168+
// const AUTOFILTER_COLUMN_RULE_DOESNTCONTAIN = 'notEqual'; // notEqual *B*
181169
// Rule Operators (Date Special) which are translated to standard numeric operators with calculated values
182-
// const AUTOFILTER_COLUMN_RULE_BEFORE = 'lessThan';
183-
// const AUTOFILTER_COLUMN_RULE_AFTER = 'greaterThan';
184-
// const AUTOFILTER_COLUMN_RULE_YESTERDAY = 'yesterday';
185-
// const AUTOFILTER_COLUMN_RULE_TODAY = 'today';
186-
// const AUTOFILTER_COLUMN_RULE_TOMORROW = 'tomorrow';
187-
// const AUTOFILTER_COLUMN_RULE_LASTWEEK = 'lastWeek';
188-
// const AUTOFILTER_COLUMN_RULE_THISWEEK = 'thisWeek';
189-
// const AUTOFILTER_COLUMN_RULE_NEXTWEEK = 'nextWeek';
190-
// const AUTOFILTER_COLUMN_RULE_LASTMONTH = 'lastMonth';
191-
// const AUTOFILTER_COLUMN_RULE_THISMONTH = 'thisMonth';
192-
// const AUTOFILTER_COLUMN_RULE_NEXTMONTH = 'nextMonth';
193-
// const AUTOFILTER_COLUMN_RULE_LASTQUARTER = 'lastQuarter';
194-
// const AUTOFILTER_COLUMN_RULE_THISQUARTER = 'thisQuarter';
195-
// const AUTOFILTER_COLUMN_RULE_NEXTQUARTER = 'nextQuarter';
196-
// const AUTOFILTER_COLUMN_RULE_LASTYEAR = 'lastYear';
197-
// const AUTOFILTER_COLUMN_RULE_THISYEAR = 'thisYear';
198-
// const AUTOFILTER_COLUMN_RULE_NEXTYEAR = 'nextYear';
199-
// const AUTOFILTER_COLUMN_RULE_YEARTODATE = 'yearToDate'; // <dynamicFilter val="40909" type="yearToDate" maxVal="41113"/>
200-
// const AUTOFILTER_COLUMN_RULE_ALLDATESINMONTH = 'allDatesInMonth'; // <dynamicFilter type="M2"/> for Month/February
201-
// const AUTOFILTER_COLUMN_RULE_ALLDATESINQUARTER = 'allDatesInQuarter'; // <dynamicFilter type="Q2"/> for Quarter 2
170+
// const AUTOFILTER_COLUMN_RULE_BEFORE = 'lessThan';
171+
// const AUTOFILTER_COLUMN_RULE_AFTER = 'greaterThan';
202172

203173
/**
204174
* Autofilter Column.

0 commit comments

Comments
 (0)