AutorÃa | Ultima modificación | Ver Log |
<?phpnamespace PhpOffice\PhpSpreadsheet\Worksheet;use DateTime;use DateTimeZone;use PhpOffice\PhpSpreadsheet\Calculation\Calculation;use PhpOffice\PhpSpreadsheet\Calculation\Functions;use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch;use PhpOffice\PhpSpreadsheet\Cell\AddressRange;use PhpOffice\PhpSpreadsheet\Cell\CellAddress;use PhpOffice\PhpSpreadsheet\Cell\CellRange;use PhpOffice\PhpSpreadsheet\Cell\Coordinate;use PhpOffice\PhpSpreadsheet\Exception;use PhpOffice\PhpSpreadsheet\Shared\Date;use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule;use Stringable;class AutoFilter implements Stringable{/*** Autofilter Worksheet.*/private ?Worksheet $workSheet;/*** Autofilter Range.*/private string $range;/*** Autofilter Column Ruleset.** @var AutoFilter\Column[]*/private array $columns = [];private bool $evaluated = false;public function getEvaluated(): bool{return $this->evaluated;}public function setEvaluated(bool $value): void{$this->evaluated = $value;}/*** Create a new AutoFilter.** @param AddressRange<CellAddress>|AddressRange<int>|AddressRange<string>|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range* A simple string containing a Cell range like 'A1:E10' is permitted* or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),* or an AddressRange object.*/public function __construct(AddressRange|string|array $range = '', ?Worksheet $worksheet = null){if ($range !== '') {[, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true);}$this->range = $range ?? '';$this->workSheet = $worksheet;}public function __destruct(){$this->workSheet = null;}/*** Get AutoFilter Parent Worksheet.*/public function getParent(): null|Worksheet{return $this->workSheet;}/*** Set AutoFilter Parent Worksheet.** @return $this*/public function setParent(?Worksheet $worksheet = null): static{$this->evaluated = false;$this->workSheet = $worksheet;return $this;}/*** Get AutoFilter Range.*/public function getRange(): string{return $this->range;}/*** Set AutoFilter Cell Range.** @param AddressRange<CellRange>|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range* A simple string containing a Cell range like 'A1:E10' or a Cell address like 'A1' is permitted* or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),* or an AddressRange object.*/public function setRange(AddressRange|string|array $range = ''): self{$this->evaluated = false;// extract coordinateif ($range !== '') {[, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true);}if (empty($range)) {// Discard all column rules$this->columns = [];$this->range = '';return $this;}if (ctype_digit($range) || ctype_alpha($range)) {throw new Exception("{$range} is an invalid range for AutoFilter");}$this->range = $range;// Discard any column rules that are no longer valid within this range[$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);foreach ($this->columns as $key => $value) {$colIndex = Coordinate::columnIndexFromString($key);if (($rangeStart[0] > $colIndex) || ($rangeEnd[0] < $colIndex)) {unset($this->columns[$key]);}}return $this;}public function setRangeToMaxRow(): self{$this->evaluated = false;if ($this->workSheet !== null) {$thisrange = $this->range;$range = (string) preg_replace('/\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange);if ($range !== $thisrange) {$this->setRange($range);}}return $this;}/*** Get all AutoFilter Columns.** @return AutoFilter\Column[]*/public function getColumns(): array{return $this->columns;}/*** Validate that the specified column is in the AutoFilter range.** @param string $column Column name (e.g. A)** @return int The column offset within the autofilter range*/public function testColumnInRange(string $column): int{if (empty($this->range)) {throw new Exception('No autofilter range is defined.');}$columnIndex = Coordinate::columnIndexFromString($column);[$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);if (($rangeStart[0] > $columnIndex) || ($rangeEnd[0] < $columnIndex)) {throw new Exception('Column is outside of current autofilter range.');}return $columnIndex - $rangeStart[0];}/*** Get a specified AutoFilter Column Offset within the defined AutoFilter range.** @param string $column Column name (e.g. A)** @return int The offset of the specified column within the autofilter range*/public function getColumnOffset(string $column): int{return $this->testColumnInRange($column);}/*** Get a specified AutoFilter Column.** @param string $column Column name (e.g. A)*/public function getColumn(string $column): AutoFilter\Column{$this->testColumnInRange($column);if (!isset($this->columns[$column])) {$this->columns[$column] = new AutoFilter\Column($column, $this);}return $this->columns[$column];}/*** Get a specified AutoFilter Column by it's offset.** @param int $columnOffset Column offset within range (starting from 0)*/public function getColumnByOffset(int $columnOffset): AutoFilter\Column{[$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);$pColumn = Coordinate::stringFromColumnIndex($rangeStart[0] + $columnOffset);return $this->getColumn($pColumn);}/*** Set AutoFilter.** @param AutoFilter\Column|string $columnObjectOrString* A simple string containing a Column ID like 'A' is permitted** @return $this*/public function setColumn(AutoFilter\Column|string $columnObjectOrString): static{$this->evaluated = false;if ((is_string($columnObjectOrString)) && (!empty($columnObjectOrString))) {$column = $columnObjectOrString;} elseif ($columnObjectOrString instanceof AutoFilter\Column) {$column = $columnObjectOrString->getColumnIndex();} else {throw new Exception('Column is not within the autofilter range.');}$this->testColumnInRange($column);if (is_string($columnObjectOrString)) {$this->columns[$columnObjectOrString] = new AutoFilter\Column($columnObjectOrString, $this);} else {$columnObjectOrString->setParent($this);$this->columns[$column] = $columnObjectOrString;}ksort($this->columns);return $this;}/*** Clear a specified AutoFilter Column.** @param string $column Column name (e.g. A)** @return $this*/public function clearColumn(string $column): static{$this->evaluated = false;$this->testColumnInRange($column);if (isset($this->columns[$column])) {unset($this->columns[$column]);}return $this;}/*** Shift an AutoFilter Column Rule to a different column.** Note: This method bypasses validation of the destination column to ensure it is within this AutoFilter range.* Nor does it verify whether any column rule already exists at $toColumn, but will simply override any existing value.* Use with caution.** @param string $fromColumn Column name (e.g. A)* @param string $toColumn Column name (e.g. B)** @return $this*/public function shiftColumn(string $fromColumn, string $toColumn): static{$this->evaluated = false;$fromColumn = strtoupper($fromColumn);$toColumn = strtoupper($toColumn);if (isset($this->columns[$fromColumn])) {$this->columns[$fromColumn]->setParent();$this->columns[$fromColumn]->setColumnIndex($toColumn);$this->columns[$toColumn] = $this->columns[$fromColumn];$this->columns[$toColumn]->setParent($this);unset($this->columns[$fromColumn]);ksort($this->columns);}return $this;}/*** Test if cell value is in the defined set of values.** @param array{blanks: bool, filterValues: array<string,array<string,string>>} $dataSet*/protected static function filterTestInSimpleDataSet(mixed $cellValue, array $dataSet): bool{$dataSetValues = $dataSet['filterValues'];$blanks = $dataSet['blanks'];if (($cellValue === '') || ($cellValue === null)) {return $blanks;}return in_array($cellValue, $dataSetValues);}/*** Test if cell value is in the defined set of Excel date values.** @param array{blanks: bool, filterValues: array<string,array<string,string>>} $dataSet*/protected static function filterTestInDateGroupSet(mixed $cellValue, array $dataSet): bool{$dateSet = $dataSet['filterValues'];$blanks = $dataSet['blanks'];if (($cellValue === '') || ($cellValue === null)) {return $blanks;}$timeZone = new DateTimeZone('UTC');if (is_numeric($cellValue)) {$dateTime = Date::excelToDateTimeObject((float) $cellValue, $timeZone);$cellValue = (float) $cellValue;if ($cellValue < 1) {// Just the time part$dtVal = $dateTime->format('His');$dateSet = $dateSet['time'];} elseif ($cellValue == floor($cellValue)) {// Just the date part$dtVal = $dateTime->format('Ymd');$dateSet = $dateSet['date'];} else {// date and time parts$dtVal = $dateTime->format('YmdHis');$dateSet = $dateSet['dateTime'];}foreach ($dateSet as $dateValue) {// Use of substr to extract value at the appropriate group levelif (str_starts_with($dtVal, $dateValue)) {return true;}}}return false;}/*** Test if cell value is within a set of values defined by a ruleset.** @param mixed[] $ruleSet*/protected static function filterTestInCustomDataSet(mixed $cellValue, array $ruleSet): bool{/** @var array[] $dataSet */$dataSet = $ruleSet['filterRules'];$join = $ruleSet['join'];$customRuleForBlanks = $ruleSet['customRuleForBlanks'] ?? false;if (!$customRuleForBlanks) {// Blank cells are always ignored, so return a FALSEif (($cellValue === '') || ($cellValue === null)) {return false;}}$returnVal = ($join == AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND);foreach ($dataSet as $rule) {/** @var string $ruleValue */$ruleValue = $rule['value'];/** @var string $ruleOperator */$ruleOperator = $rule['operator'];/** @var string $cellValueString */$cellValueString = $cellValue ?? '';$retVal = false;if (is_numeric($ruleValue)) {// Numeric values are tested using the appropriate operator$numericTest = is_numeric($cellValue);switch ($ruleOperator) {case Rule::AUTOFILTER_COLUMN_RULE_EQUAL:$retVal = $numericTest && ($cellValue == $ruleValue);break;case Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL:$retVal = !$numericTest || ($cellValue != $ruleValue);break;case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN:$retVal = $numericTest && ($cellValue > $ruleValue);break;case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL:$retVal = $numericTest && ($cellValue >= $ruleValue);break;case Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN:$retVal = $numericTest && ($cellValue < $ruleValue);break;case Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL:$retVal = $numericTest && ($cellValue <= $ruleValue);break;}} elseif ($ruleValue == '') {$retVal = match ($ruleOperator) {Rule::AUTOFILTER_COLUMN_RULE_EQUAL => ($cellValue === '') || ($cellValue === null),Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL => ($cellValue != ''),default => true,};} else {// String values are always tested for equality, factoring in for wildcards (hence a regexp test)switch ($ruleOperator) {case Rule::AUTOFILTER_COLUMN_RULE_EQUAL:$retVal = (bool) preg_match('/^' . $ruleValue . '$/i', $cellValueString);break;case Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL:$retVal = !((bool) preg_match('/^' . $ruleValue . '$/i', $cellValueString));break;case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN:$retVal = strcasecmp($cellValueString, $ruleValue) > 0;break;case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL:$retVal = strcasecmp($cellValueString, $ruleValue) >= 0;break;case Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN:$retVal = strcasecmp($cellValueString, $ruleValue) < 0;break;case Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL:$retVal = strcasecmp($cellValueString, $ruleValue) <= 0;break;}}// If there are multiple conditions, then we need to test both using the appropriate join operatorswitch ($join) {case AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR:$returnVal = $returnVal || $retVal;// Break as soon as we have a TRUE match for OR joins,// to avoid unnecessary additional code executionif ($returnVal) {return $returnVal;}break;case AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND:$returnVal = $returnVal && $retVal;break;}}return $returnVal;}/*** Test if cell date value is matches a set of values defined by a set of months.** @param mixed[] $monthSet*/protected static function filterTestInPeriodDateSet(mixed $cellValue, array $monthSet): bool{// Blank cells are always ignored, so return a FALSEif (($cellValue === '') || ($cellValue === null)) {return false;}if (is_numeric($cellValue)) {$dateObject = Date::excelToDateTimeObject((float) $cellValue, new DateTimeZone('UTC'));$dateValue = (int) $dateObject->format('m');if (in_array($dateValue, $monthSet)) {return true;}}return false;}private static function makeDateObject(int $year, int $month, int $day, int $hour = 0, int $minute = 0, int $second = 0): DateTime{$baseDate = new DateTime();$baseDate->setDate($year, $month, $day);$baseDate->setTime($hour, $minute, $second);return $baseDate;}private const DATE_FUNCTIONS = [Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH => 'dynamicLastMonth',Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER => 'dynamicLastQuarter',Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK => 'dynamicLastWeek',Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR => 'dynamicLastYear',Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH => 'dynamicNextMonth',Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER => 'dynamicNextQuarter',Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK => 'dynamicNextWeek',Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR => 'dynamicNextYear',Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISMONTH => 'dynamicThisMonth',Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISQUARTER => 'dynamicThisQuarter',Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISWEEK => 'dynamicThisWeek',Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISYEAR => 'dynamicThisYear',Rule::AUTOFILTER_RULETYPE_DYNAMIC_TODAY => 'dynamicToday',Rule::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW => 'dynamicTomorrow',Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE => 'dynamicYearToDate',Rule::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY => 'dynamicYesterday',];private static function dynamicLastMonth(): array{$maxval = new DateTime();$year = (int) $maxval->format('Y');$month = (int) $maxval->format('m');$maxval->setDate($year, $month, 1);$maxval->setTime(0, 0, 0);$val = clone $maxval;$val->modify('-1 month');return [$val, $maxval];}private static function firstDayOfQuarter(): DateTime{$val = new DateTime();$year = (int) $val->format('Y');$month = (int) $val->format('m');$month = 3 * intdiv($month - 1, 3) + 1;$val->setDate($year, $month, 1);$val->setTime(0, 0, 0);return $val;}private static function dynamicLastQuarter(): array{$maxval = self::firstDayOfQuarter();$val = clone $maxval;$val->modify('-3 months');return [$val, $maxval];}private static function dynamicLastWeek(): array{$val = new DateTime();$val->setTime(0, 0, 0);$dayOfWeek = (int) $val->format('w'); // Sunday is 0$subtract = $dayOfWeek + 7; // revert to prior Sunday$val->modify("-$subtract days");$maxval = clone $val;$maxval->modify('+7 days');return [$val, $maxval];}private static function dynamicLastYear(): array{$val = new DateTime();$year = (int) $val->format('Y');$val = self::makeDateObject($year - 1, 1, 1);$maxval = self::makeDateObject($year, 1, 1);return [$val, $maxval];}private static function dynamicNextMonth(): array{$val = new DateTime();$year = (int) $val->format('Y');$month = (int) $val->format('m');$val->setDate($year, $month, 1);$val->setTime(0, 0, 0);$val->modify('+1 month');$maxval = clone $val;$maxval->modify('+1 month');return [$val, $maxval];}private static function dynamicNextQuarter(): array{$val = self::firstDayOfQuarter();$val->modify('+3 months');$maxval = clone $val;$maxval->modify('+3 months');return [$val, $maxval];}private static function dynamicNextWeek(): array{$val = new DateTime();$val->setTime(0, 0, 0);$dayOfWeek = (int) $val->format('w'); // Sunday is 0$add = 7 - $dayOfWeek; // move to next Sunday$val->modify("+$add days");$maxval = clone $val;$maxval->modify('+7 days');return [$val, $maxval];}private static function dynamicNextYear(): array{$val = new DateTime();$year = (int) $val->format('Y');$val = self::makeDateObject($year + 1, 1, 1);$maxval = self::makeDateObject($year + 2, 1, 1);return [$val, $maxval];}private static function dynamicThisMonth(): array{$baseDate = new DateTime();$baseDate->setTime(0, 0, 0);$year = (int) $baseDate->format('Y');$month = (int) $baseDate->format('m');$val = self::makeDateObject($year, $month, 1);$maxval = clone $val;$maxval->modify('+1 month');return [$val, $maxval];}private static function dynamicThisQuarter(): array{$val = self::firstDayOfQuarter();$maxval = clone $val;$maxval->modify('+3 months');return [$val, $maxval];}private static function dynamicThisWeek(): array{$val = new DateTime();$val->setTime(0, 0, 0);$dayOfWeek = (int) $val->format('w'); // Sunday is 0$subtract = $dayOfWeek; // revert to Sunday$val->modify("-$subtract days");$maxval = clone $val;$maxval->modify('+7 days');return [$val, $maxval];}private static function dynamicThisYear(): array{$val = new DateTime();$year = (int) $val->format('Y');$val = self::makeDateObject($year, 1, 1);$maxval = self::makeDateObject($year + 1, 1, 1);return [$val, $maxval];}private static function dynamicToday(): array{$val = new DateTime();$val->setTime(0, 0, 0);$maxval = clone $val;$maxval->modify('+1 day');return [$val, $maxval];}private static function dynamicTomorrow(): array{$val = new DateTime();$val->setTime(0, 0, 0);$val->modify('+1 day');$maxval = clone $val;$maxval->modify('+1 day');return [$val, $maxval];}private static function dynamicYearToDate(): array{$maxval = new DateTime();$maxval->setTime(0, 0, 0);$val = self::makeDateObject((int) $maxval->format('Y'), 1, 1);$maxval->modify('+1 day');return [$val, $maxval];}private static function dynamicYesterday(): array{$maxval = new DateTime();$maxval->setTime(0, 0, 0);$val = clone $maxval;$val->modify('-1 day');return [$val, $maxval];}/*** Convert a dynamic rule daterange to a custom filter range expression for ease of calculation.** @return mixed[]*/private function dynamicFilterDateRange(string $dynamicRuleType, AutoFilter\Column &$filterColumn): array{$ruleValues = [];$callBack = [__CLASS__, self::DATE_FUNCTIONS[$dynamicRuleType]]; // What if not found?// Calculate start/end dates for the required date range based on current date// Val is lowest permitted value.// Maxval is greater than highest permitted value$val = $maxval = 0;if (is_callable($callBack)) { //* @phpstan-ignore-line[$val, $maxval] = $callBack();}$val = Date::dateTimeToExcel($val);$maxval = Date::dateTimeToExcel($maxval);// Set the filter column rule attributes ready for writing$filterColumn->setAttributes(['val' => $val, 'maxVal' => $maxval]);// Set the rules for identifying rows for hide/show$ruleValues[] = ['operator' => Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL, 'value' => $val];$ruleValues[] = ['operator' => Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN, 'value' => $maxval];return ['method' => 'filterTestInCustomDataSet', 'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND]];}/*** Apply the AutoFilter rules to the AutoFilter Range.*/private function calculateTopTenValue(string $columnID, int $startRow, int $endRow, ?string $ruleType, mixed $ruleValue): mixed{$range = $columnID . $startRow . ':' . $columnID . $endRow;$retVal = null;if ($this->workSheet !== null) {$dataValues = Functions::flattenArray($this->workSheet->rangeToArray($range, null, true, false));$dataValues = array_filter($dataValues);if ($ruleType == Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) {rsort($dataValues);} else {sort($dataValues);}if (is_numeric($ruleValue)) {$ruleValue = (int) $ruleValue;}if ($ruleValue === null || is_int($ruleValue)) {$slice = array_slice($dataValues, 0, $ruleValue);$retVal = array_pop($slice);}}return $retVal;}/*** Apply the AutoFilter rules to the AutoFilter Range.** @return $this*/public function showHideRows(): static{if ($this->workSheet === null) {return $this;}[$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);// The heading row should always be visible$this->workSheet->getRowDimension($rangeStart[1])->setVisible(true);$columnFilterTests = [];foreach ($this->columns as $columnID => $filterColumn) {$rules = $filterColumn->getRules();switch ($filterColumn->getFilterType()) {case AutoFilter\Column::AUTOFILTER_FILTERTYPE_FILTER:$ruleType = null;$ruleValues = [];// Build a list of the filter value selectionsforeach ($rules as $rule) {$ruleType = $rule->getRuleType();$ruleValues[] = $rule->getValue();}// Test if we want to include blanks in our filter criteria$blanks = false;$ruleDataSet = array_filter($ruleValues);if (count($ruleValues) != count($ruleDataSet)) {$blanks = true;}if ($ruleType == Rule::AUTOFILTER_RULETYPE_FILTER) {// Filter on absolute values$columnFilterTests[$columnID] = ['method' => 'filterTestInSimpleDataSet','arguments' => ['filterValues' => $ruleDataSet, 'blanks' => $blanks],];} elseif ($ruleType !== null) {// Filter on date group values$arguments = ['date' => [],'time' => [],'dateTime' => [],];foreach ($ruleDataSet as $ruleValue) {if (!is_array($ruleValue)) {continue;}$date = $time = '';if ((isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR]))&& ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR] !== '')) {$date .= sprintf('%04d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR]);}if ((isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH]))&& ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH] != '')) {$date .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH]);}if ((isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY]))&& ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY] !== '')) {$date .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY]);}if ((isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR]))&& ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR] !== '')) {$time .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR]);}if ((isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE]))&& ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE] !== '')) {$time .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE]);}if ((isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND]))&& ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND] !== '')) {$time .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND]);}$dateTime = $date . $time;$arguments['date'][] = $date;$arguments['time'][] = $time;$arguments['dateTime'][] = $dateTime;}// Remove empty elements$arguments['date'] = array_filter($arguments['date']);$arguments['time'] = array_filter($arguments['time']);$arguments['dateTime'] = array_filter($arguments['dateTime']);$columnFilterTests[$columnID] = ['method' => 'filterTestInDateGroupSet','arguments' => ['filterValues' => $arguments, 'blanks' => $blanks],];}break;case AutoFilter\Column::AUTOFILTER_FILTERTYPE_CUSTOMFILTER:$customRuleForBlanks = true;$ruleValues = [];// Build a list of the filter value selectionsforeach ($rules as $rule) {$ruleValue = $rule->getValue();if (!is_array($ruleValue) && !is_numeric($ruleValue)) {// Convert to a regexp allowing for regexp reserved characters, wildcards and escaped wildcards$ruleValue = WildcardMatch::wildcard($ruleValue);if (trim($ruleValue) == '') {$customRuleForBlanks = true;$ruleValue = trim($ruleValue);}}$ruleValues[] = ['operator' => $rule->getOperator(), 'value' => $ruleValue];}$join = $filterColumn->getJoin();$columnFilterTests[$columnID] = ['method' => 'filterTestInCustomDataSet','arguments' => ['filterRules' => $ruleValues, 'join' => $join, 'customRuleForBlanks' => $customRuleForBlanks],];break;case AutoFilter\Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER:$ruleValues = [];foreach ($rules as $rule) {// We should only ever have one Dynamic Filter Rule anyway$dynamicRuleType = $rule->getGrouping();if (($dynamicRuleType == Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE)|| ($dynamicRuleType == Rule::AUTOFILTER_RULETYPE_DYNAMIC_BELOWAVERAGE)) {// Number (Average) based// Calculate the average$averageFormula = '=AVERAGE(' . $columnID . ($rangeStart[1] + 1) . ':' . $columnID . $rangeEnd[1] . ')';$average = Calculation::getInstance($this->workSheet->getParent())->calculateFormula($averageFormula, null, $this->workSheet->getCell('A1'));while (is_array($average)) {$average = array_pop($average);}// Set above/below rule based on greaterThan or LessTan$operator = ($dynamicRuleType === Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE)? Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN: Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN;$ruleValues[] = ['operator' => $operator,'value' => $average,];$columnFilterTests[$columnID] = ['method' => 'filterTestInCustomDataSet','arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR],];} else {// Date basedif ($dynamicRuleType[0] == 'M' || $dynamicRuleType[0] == 'Q') {$periodType = '';$period = 0;// Month or Quartersscanf($dynamicRuleType, '%[A-Z]%d', $periodType, $period);if ($periodType == 'M') {$ruleValues = [$period];} else {--$period;$periodEnd = (1 + $period) * 3;$periodStart = 1 + $period * 3;$ruleValues = range($periodStart, $periodEnd);}$columnFilterTests[$columnID] = ['method' => 'filterTestInPeriodDateSet','arguments' => $ruleValues,];$filterColumn->setAttributes([]);} else {// Date Range$columnFilterTests[$columnID] = $this->dynamicFilterDateRange($dynamicRuleType, $filterColumn);break;}}}break;case AutoFilter\Column::AUTOFILTER_FILTERTYPE_TOPTENFILTER:$ruleValues = [];$dataRowCount = $rangeEnd[1] - $rangeStart[1];$toptenRuleType = null;$ruleValue = 0;$ruleOperator = null;foreach ($rules as $rule) {// We should only ever have one Dynamic Filter Rule anyway$toptenRuleType = $rule->getGrouping();$ruleValue = $rule->getValue();$ruleOperator = $rule->getOperator();}if (is_numeric($ruleValue) && $ruleOperator === Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT) {$ruleValue = (int) floor((float) $ruleValue * ($dataRowCount / 100));}if (!is_array($ruleValue) && $ruleValue < 1) {$ruleValue = 1;}if (!is_array($ruleValue) && $ruleValue > 500) {$ruleValue = 500;}/** @var float|int|string */$maxVal = $this->calculateTopTenValue($columnID, $rangeStart[1] + 1, (int) $rangeEnd[1], $toptenRuleType, $ruleValue);$operator = ($toptenRuleType == Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP)? Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL: Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL;$ruleValues[] = ['operator' => $operator, 'value' => $maxVal];$columnFilterTests[$columnID] = ['method' => 'filterTestInCustomDataSet','arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR],];$filterColumn->setAttributes(['maxVal' => $maxVal]);break;}}$rangeEnd[1] = $this->autoExtendRange($rangeStart[1], $rangeEnd[1]);// Execute the column tests for each row in the autoFilter range to determine show/hide,for ($row = $rangeStart[1] + 1; $row <= $rangeEnd[1]; ++$row) {$result = true;foreach ($columnFilterTests as $columnID => $columnFilterTest) {$cellValue = $this->workSheet->getCell($columnID . $row)->getCalculatedValue();// Execute the filter test/** @var callable */$temp = [self::class, $columnFilterTest['method']];/** @var bool */$result // $result && // phpstan says $result is always true here= call_user_func_array($temp, [$cellValue, $columnFilterTest['arguments']]);// If filter test has resulted in FALSE, exit the loop straightaway rather than running any more testsif (!$result) {break;}}// Set show/hide for the row based on the result of the autoFilter result// If the RowDimension object has not been allocated yet and the row should be visible,// then we can avoid any operation since the rows are visible by default (saves a lot of memory)if ($result === false || $this->workSheet->rowDimensionExists((int) $row)) {$this->workSheet->getRowDimension((int) $row)->setVisible($result);}}$this->evaluated = true;return $this;}/*** Magic Range Auto-sizing.* For a single row rangeSet, we follow MS Excel rules, and search for the first empty row to determine our range.*/public function autoExtendRange(int $startRow, int $endRow): int{if ($startRow === $endRow && $this->workSheet !== null) {try {$rowIterator = $this->workSheet->getRowIterator($startRow + 1);} catch (Exception) {// If there are no rows below $startRowreturn $startRow;}foreach ($rowIterator as $row) {if ($row->isEmpty(CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL) === true) {return $row->getRowIndex() - 1;}}}return $endRow;}/*** Implement PHP __clone to create a deep clone, not just a shallow copy.*/public function __clone(){$vars = get_object_vars($this);foreach ($vars as $key => $value) {if (is_object($value)) {if ($key === 'workSheet') {// Detach from worksheet$this->{$key} = null;} else {$this->{$key} = clone $value;}} elseif ((is_array($value)) && ($key == 'columns')) {// The columns array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\AutoFilter objects$this->{$key} = [];foreach ($value as $k => $v) {$this->{$key}[$k] = clone $v;// attach the new cloned Column to this new cloned Autofilter object$this->{$key}[$k]->setParent($this);}} else {$this->{$key} = $value;}}}/*** toString method replicates previous behavior by returning the range if object is* referenced as a property of its parent.*/public function __toString(): string{return (string) $this->range;}}