From b19fcef51f688e7bf573bde0866b0662c0cbffc7 Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Wed, 2 Jun 2021 20:46:14 -0700 Subject: [PATCH] Autofilter Part 1 Most of the remaining 32-bit-unsafe date handling that remains in PhpSpreadsheet is in AutoFilter. Cleaning this up demonstrated that there are a lot of problems with AutoFilter, and I will do it in (probably two) pieces. In this PR: - Dynamic date processing was really wrong. There were no tests nor samples to exercise this code. (If you need details, you can try running the new sample against old code.) It is completely re-written. - ThisYear/Month/Week/Quarter had been omitted. - Rules such as AUTOFILTER_RULETYPE_DYNAMIC_MONTH_2 were almost correct, but showed some off-by-1 errors. I suspect these were timezone-related, and therefore more obvious to those of us far away from Greenwich. - All Autofilter tests are moved to a single directory. - The documentation suggested using null with the Dynamic Date setup, but Phpstan did not like that in my new tests/samples. Rather than change the doc block, I changed the documentation to suggest null string. - I created a new sample to generate sheets using all the dynamic filters. - I have added some new unit tests for each of the dynamic filters. I would love to be able to add some "time travel" tests because the dynamic nature of the filter makes most of the results change from day to day, which presents significant challenges in writing comprehensive unit tests (the same is true for code coverage). I was not able to find a good way to simulate time within PhpUnit, but the Linux 'faketime' package was extraordinarily easy and helpful in allowing me to confirm some edge cases. I had less satisfactory results with some Windows equivalents, but was still able to run some tests. - Code coverage increases from below 60% to above 80%. To be done: - Some 32-bit unsafe dates remain in filterTestInDateGroupSet. - Also in some of the existing AutoFilter samples. - Study existing unit tests for AutoFilter which use mocking to see if they can/should be replaced with 'real' tests. - Improve code coverage in AutoFilter, AutoFilter/Column, and AutoFilter/Common/Rule. --- docs/topics/autofilters.md | 4 +- phpstan-baseline.neon | 42 +- .../10_Autofilter_dynamic_dates.php | 100 +++++ src/PhpSpreadsheet/Worksheet/AutoFilter.php | 416 ++++++++++++------ .../Worksheet/AutoFilter/Column.php | 4 +- .../AutoFilter/AutoFilterMonthTest.php | 91 ++++ .../AutoFilter/AutoFilterQuarterTest.php | 75 ++++ .../{ => AutoFilter}/AutoFilterTest.php | 3 +- .../AutoFilter/AutoFilterTodayTest.php | 65 +++ .../AutoFilter/AutoFilterWeekTest.php | 76 ++++ .../AutoFilter/AutoFilterYearTest.php | 119 +++++ 11 files changed, 805 insertions(+), 190 deletions(-) create mode 100644 samples/Autofilter/10_Autofilter_dynamic_dates.php create mode 100644 tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterMonthTest.php create mode 100644 tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterQuarterTest.php rename tests/PhpSpreadsheetTests/Worksheet/{ => AutoFilter}/AutoFilterTest.php (99%) create mode 100644 tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTodayTest.php create mode 100644 tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterWeekTest.php create mode 100644 tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterYearTest.php diff --git a/docs/topics/autofilters.md b/docs/topics/autofilters.md index d5a07f8b..16efb8ff 100644 --- a/docs/topics/autofilters.md +++ b/docs/topics/autofilters.md @@ -327,14 +327,14 @@ $columnFilter->setFilterType( ``` When defining the rule for a dynamic filter, we don't define a value (we -can simply set that to NULL) but we do specify the dynamic filter +can simply set that to null string) but we do specify the dynamic filter category. ```php $columnFilter->createRule() ->setRule( \PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_EQUAL, - NULL, + '', \PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE ) ->setRuleType( diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4ee0a9ef..a64f3781 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -5417,7 +5417,7 @@ parameters: - message: "#^Parameter \\#1 \\$excelTimestamp of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Date\\:\\:excelToTimestamp\\(\\) expects float\\|int, float\\|int\\|string given\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Worksheet/AutoFilter.php - @@ -5430,36 +5430,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Worksheet/AutoFilter.php - - - message: "#^Parameter \\#2 \\$now of function strtotime expects int, int\\|false given\\.$#" - count: 6 - path: src/PhpSpreadsheet/Worksheet/AutoFilter.php - - - - message: "#^Parameter \\#5 \\$day of function gmmktime expects int, string given\\.$#" - count: 8 - path: src/PhpSpreadsheet/Worksheet/AutoFilter.php - - - - message: "#^Parameter \\#6 \\$year of function gmmktime expects int, string given\\.$#" - count: 13 - path: src/PhpSpreadsheet/Worksheet/AutoFilter.php - - - - message: "#^Parameter \\#4 \\$mon of function gmmktime expects int, string given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Worksheet/AutoFilter.php - - - - message: "#^Parameter \\#5 \\$day of function gmmktime expects int, float given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Worksheet/AutoFilter.php - - - - message: "#^Parameter \\#1 \\$attributes of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\AutoFilter\\\\Column\\:\\:setAttributes\\(\\) expects array\\, array\\ given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/AutoFilter.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\AutoFilter\\:\\:calculateTopTenValue\\(\\) has no return typehint specified\\.$#" count: 1 @@ -7445,21 +7415,11 @@ parameters: count: 1 path: tests/PhpSpreadsheetTests/Worksheet/AutoFilter/Column/RuleTest.php - - - message: "#^Parameter \\#1 \\$attributes of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\AutoFilter\\\\Column\\:\\:setAttributes\\(\\) expects array\\, array\\ given\\.$#" - count: 3 - path: tests/PhpSpreadsheetTests/Worksheet/AutoFilter/ColumnTest.php - - message: "#^Parameter \\#2 \\$pValue of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\AutoFilter\\\\Column\\:\\:setAttribute\\(\\) expects string, int given\\.$#" count: 1 path: tests/PhpSpreadsheetTests/Worksheet/AutoFilter/ColumnTest.php - - - message: "#^Parameter \\#1 \\$pColumn of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\AutoFilter\\:\\:setColumn\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\AutoFilter\\\\Column\\|string, float given\\.$#" - count: 1 - path: tests/PhpSpreadsheetTests/Worksheet/AutoFilterTest.php - - message: "#^Parameter \\#1 \\$im of function imagecolorallocate expects resource, resource\\|false given\\.$#" count: 1 diff --git a/samples/Autofilter/10_Autofilter_dynamic_dates.php b/samples/Autofilter/10_Autofilter_dynamic_dates.php new file mode 100644 index 00000000..959925c1 --- /dev/null +++ b/samples/Autofilter/10_Autofilter_dynamic_dates.php @@ -0,0 +1,100 @@ +createSheet(); + $sheet->setTitle($rule); + $sheet->getCell('A1')->setValue('Date'); + $row = 1; + $date = new DateTime(); + $year = (int) $date->format('Y'); + $month = (int) $date->format('m'); + $day = (int) $date->format('d'); + $yearMinus2 = $year - 2; + $sheet->getCell('B1')->setValue("=DATE($year, $month, $day)"); + // Each day for two weeks before today through 2 weeks after + for ($dayOffset = -14; $dayOffset < 14; ++$dayOffset) { + ++$row; + $sheet->getCell("A$row")->setValue("=B1+($dayOffset)"); + } + // First and last day of each month, starting with January 2 years before, + // through December 2 years after. + for ($monthOffset = 0; $monthOffset < 48; ++$monthOffset) { + ++$row; + $sheet->getCell("A$row")->setValue("=DATE($yearMinus2, $monthOffset, 1)"); + ++$row; + $sheet->getCell("A$row")->setValue("=DATE($yearMinus2, $monthOffset + 1, 0)"); + } + $sheet->getStyle("A2:A$row")->getNumberFormat()->setFormatCode('yyyy-mm-dd'); + $sheet->getStyle('B1')->getNumberFormat()->setFormatCode('yyyy-mm-dd'); + $sheet->getColumnDimension('A')->setAutoSize(true); + $sheet->getColumnDimension('B')->setAutoSize(true); + $autoFilter = $spreadsheet->getActiveSheet()->getAutoFilter(); + $autoFilter->setRange("A1:A$row"); + $columnFilter = $autoFilter->getColumn('A'); + $columnFilter->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER); + $columnFilter->createRule() + ->setRule( + Rule::AUTOFILTER_COLUMN_RULE_EQUAL, + '', + $rule + ) + ->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); + $sheet->setSelectedCell('B1'); +} + +// Create new Spreadsheet object +$helper->log('Create new Spreadsheet object'); +$spreadsheet = new Spreadsheet(); + +// Set document properties +$helper->log('Set document properties'); +$spreadsheet->getProperties()->setCreator('Owen Leibman') + ->setLastModifiedBy('Owen Leibman') + ->setTitle('PhpSpreadsheet Test Document') + ->setSubject('PhpSpreadsheet Test Document') + ->setDescription('Test document for PhpSpreadsheet, generated using PHP classes.') + ->setKeywords('office PhpSpreadsheet php') + ->setCategory('Test result file'); + +$ruleNames = [ + Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISMONTH, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISQUARTER, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISWEEK, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISYEAR, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_TODAY, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_MONTH_2, + Rule::AUTOFILTER_RULETYPE_DYNAMIC_QUARTER_3, +]; + +// Create the worksheets +foreach ($ruleNames as $ruleName) { + $helper->log("Add data and filter for $ruleName"); + createSheet($spreadsheet, $ruleName); +} +$spreadsheet->removeSheetByIndex(0); +$spreadsheet->setActiveSheetIndex(0); +// Save +$helper->write($spreadsheet, __FILE__); diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/src/PhpSpreadsheet/Worksheet/AutoFilter.php index b846f7b6..c75e20ba 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -2,12 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; +use DateTime; +use DateTimeZone; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\Shared\Date; +use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule; class AutoFilter { @@ -358,38 +360,38 @@ class AutoFilter if (is_numeric($rule['value'])) { // Numeric values are tested using the appropriate operator switch ($rule['operator']) { - case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_EQUAL: + case Rule::AUTOFILTER_COLUMN_RULE_EQUAL: $retVal = ($cellValue == $rule['value']); break; - case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL: + case Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL: $retVal = ($cellValue != $rule['value']); break; - case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN: + case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN: $retVal = ($cellValue > $rule['value']); break; - case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL: + case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL: $retVal = ($cellValue >= $rule['value']); break; - case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN: + case Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN: $retVal = ($cellValue < $rule['value']); break; - case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL: + case Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL: $retVal = ($cellValue <= $rule['value']); break; } } elseif ($rule['value'] == '') { switch ($rule['operator']) { - case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_EQUAL: + case Rule::AUTOFILTER_COLUMN_RULE_EQUAL: $retVal = (($cellValue == '') || ($cellValue === null)); break; - case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL: + case Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL: $retVal = (($cellValue != '') && ($cellValue !== null)); break; @@ -439,7 +441,8 @@ class AutoFilter } if (is_numeric($cellValue)) { - $dateValue = date('m', Date::excelToTimestamp($cellValue)); + $dateObject = Date::excelToDateTimeObject((float) $cellValue, new DateTimeZone('UTC')); + $dateValue = (int) $dateObject->format('m'); if (in_array($dateValue, $monthSet)) { return true; } @@ -457,6 +460,224 @@ class AutoFilter private static $toReplace = ['.*', '.', '~', '\*', '\?']; + 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. * @@ -467,118 +688,25 @@ class AutoFilter */ private function dynamicFilterDateRange($dynamicRuleType, &$filterColumn) { - $rDateType = Functions::getReturnDateType(); - Functions::setReturnDateType(Functions::RETURNDATE_PHP_NUMERIC); - $val = $maxVal = null; - $ruleValues = []; - $baseDate = DateTimeExcel\Current::now(); + $val = $maxVal = null; + $callBack = [__CLASS__, self::DATE_FUNCTIONS[$dynamicRuleType]]; // What if not found? // Calculate start/end dates for the required date range based on current date - switch ($dynamicRuleType) { - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK: - $baseDate = strtotime('-7 days', $baseDate); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK: - $baseDate = strtotime('-7 days', $baseDate); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH: - $baseDate = strtotime('-1 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH: - $baseDate = strtotime('+1 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER: - $baseDate = strtotime('-3 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER: - $baseDate = strtotime('+3 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR: - $baseDate = strtotime('-1 year', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR: - $baseDate = strtotime('+1 year', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); - - break; - } - - switch ($dynamicRuleType) { - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_TODAY: - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY: - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW: - $maxVal = (int) Date::PHPtoExcel(strtotime('+1 day', $baseDate)); - $val = (int) Date::PHPToExcel($baseDate); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE: - $maxVal = (int) Date::PHPtoExcel(strtotime('+1 day', $baseDate)); - $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, 1, date('Y', $baseDate))); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISYEAR: - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR: - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR: - $maxVal = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 31, 12, date('Y', $baseDate))); - ++$maxVal; - $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, 1, date('Y', $baseDate))); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISQUARTER: - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER: - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER: - $thisMonth = date('m', $baseDate); - $thisQuarter = floor(--$thisMonth / 3); - $maxVal = (int) Date::PHPtoExcel(gmmktime(0, 0, 0, date('t', $baseDate), (1 + $thisQuarter) * 3, date('Y', $baseDate))); - ++$maxVal; - $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, 1 + $thisQuarter * 3, date('Y', $baseDate))); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISMONTH: - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH: - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH: - $maxVal = (int) Date::PHPtoExcel(gmmktime(0, 0, 0, date('t', $baseDate), date('m', $baseDate), date('Y', $baseDate))); - ++$maxVal; - $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate))); - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISWEEK: - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK: - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK: - $dayOfWeek = date('w', $baseDate); - $val = (int) Date::PHPToExcel($baseDate) - $dayOfWeek; - $maxVal = $val + 7; - - break; - } - - switch ($dynamicRuleType) { - // Adjust Today dates for Yesterday and Tomorrow - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY: - --$maxVal; - --$val; - - break; - case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW: - ++$maxVal; - ++$val; - - break; + // Val is lowest permitted value. + // Maxval is greater than highest permitted value + $val = $maxval = 0; + if (is_callable($callBack)) { + [$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]); + $filterColumn->setAttributes(['val' => $val, 'maxVal' => $maxval]); // Set the rules for identifying rows for hide/show - $ruleValues[] = ['operator' => AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL, 'value' => $val]; - $ruleValues[] = ['operator' => AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN, 'value' => $maxVal]; - Functions::setReturnDateType($rDateType); + $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]]; } @@ -589,7 +717,7 @@ class AutoFilter $dataValues = Functions::flattenArray($this->workSheet->rangeToArray($range, null, true, false)); $dataValues = array_filter($dataValues); - if ($ruleType == AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) { + if ($ruleType == Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) { rsort($dataValues); } else { sort($dataValues); @@ -630,7 +758,7 @@ class AutoFilter if (count($ruleValues) != count($ruleDataSet)) { $blanks = true; } - if ($ruleType == AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_FILTER) { + if ($ruleType == Rule::AUTOFILTER_RULETYPE_FILTER) { // Filter on absolute values $columnFilterTests[$columnID] = [ 'method' => 'filterTestInSimpleDataSet', @@ -646,40 +774,40 @@ class AutoFilter foreach ($ruleDataSet as $ruleValue) { $date = $time = ''; if ( - (isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR])) && - ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR] !== '') + (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR])) && + ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR] !== '') ) { - $date .= sprintf('%04d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR]); + $date .= sprintf('%04d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR]); } if ( - (isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH])) && - ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH] != '') + (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH])) && + ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH] != '') ) { - $date .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH]); + $date .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH]); } if ( - (isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY])) && - ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY] !== '') + (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY])) && + ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY] !== '') ) { - $date .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY]); + $date .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY]); } if ( - (isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR])) && - ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR] !== '') + (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR])) && + ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR] !== '') ) { - $time .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR]); + $time .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR]); } if ( - (isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE])) && - ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE] !== '') + (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE])) && + ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE] !== '') ) { - $time .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE]); + $time .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE]); } if ( - (isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND])) && - ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND] !== '') + (isset($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND])) && + ($ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND] !== '') ) { - $time .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND]); + $time .= sprintf('%02d', $ruleValue[Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND]); } $dateTime = $date . $time; $arguments['date'][] = $date; @@ -727,17 +855,17 @@ class AutoFilter // We should only ever have one Dynamic Filter Rule anyway $dynamicRuleType = $rule->getGrouping(); if ( - ($dynamicRuleType == AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE) || - ($dynamicRuleType == AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_BELOWAVERAGE) + ($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()->calculateFormula($averageFormula, null, $this->workSheet->getCell('A1')); // Set above/below rule based on greaterThan or LessTan - $operator = ($dynamicRuleType === AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE) - ? AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN - : AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN; + $operator = ($dynamicRuleType === Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE) + ? Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN + : Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN; $ruleValues[] = [ 'operator' => $operator, 'value' => $average, @@ -788,7 +916,7 @@ class AutoFilter $ruleValue = $rule->getValue(); $ruleOperator = $rule->getOperator(); } - if ($ruleOperator === AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT) { + if ($ruleOperator === Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT) { $ruleValue = floor($ruleValue * ($dataRowCount / 100)); } if (!is_array($ruleValue) && $ruleValue < 1) { @@ -800,9 +928,9 @@ class AutoFilter $maxVal = $this->calculateTopTenValue($columnID, $rangeStart[1] + 1, $rangeEnd[1], $toptenRuleType, $ruleValue); - $operator = ($toptenRuleType == AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) - ? AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL - : AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL; + $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', diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php b/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php index 8ec0ca3b..1309b30f 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php @@ -215,11 +215,11 @@ class Column /** * Set AutoFilter Attributes. * - * @param string[] $attributes + * @param mixed[] $attributes * * @return $this */ - public function setAttributes(array $attributes) + public function setAttributes($attributes) { $this->attributes = $attributes; diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterMonthTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterMonthTest.php new file mode 100644 index 00000000..064c0dac --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterMonthTest.php @@ -0,0 +1,91 @@ +getCell('A1')->setValue('Date'); + $sheet->getCell('A2')->setValue('=TODAY()'); + $sheet->getCell('A3')->setValue('=DATE(YEAR(A2), MONTH(A2), 1)'); + if ($startMonth === 12) { + $sheet->getCell('A4')->setValue('=DATE(YEAR(A2) + 1, 1, 1)'); + $sheet->getCell('A5')->setValue('=DATE(YEAR(A2) + 1, 2, 1)'); + } elseif ($startMonth === 11) { + $sheet->getCell('A4')->setValue('=DATE(YEAR(A2), MONTH(A2) + 1, 1)'); + $sheet->getCell('A5')->setValue('=DATE(YEAR(A2) + 1, 1, 1)'); + } else { + $sheet->getCell('A4')->setValue('=DATE(YEAR(A2), MONTH(A2) + 1, 1)'); + $sheet->getCell('A5')->setValue('=DATE(YEAR(A2), MONTH(A2) + 2, 1)'); + } + if ($startMonth === 1) { + $sheet->getCell('A6')->setValue('=DATE(YEAR(A2) - 1, 12, 1)'); + $sheet->getCell('A7')->setValue('=DATE(YEAR(A2) - 1, 10, 1)'); + } elseif ($startMonth === 2) { + $sheet->getCell('A6')->setValue('=DATE(YEAR(A2), 1, 1)'); + $sheet->getCell('A7')->setValue('=DATE(YEAR(A2) - 1, 12, 1)'); + } else { + $sheet->getCell('A6')->setValue('=DATE(YEAR(A2), MONTH(A2) - 1, 1)'); + $sheet->getCell('A7')->setValue('=DATE(YEAR(A2), MONTH(A2) - 2, 1)'); + } + $sheet->getCell('A8')->setValue('=DATE(YEAR(A2) + 1, MONTH(A2), 1)'); + $sheet->getCell('A9')->setValue('=DATE(YEAR(A2) - 1, MONTH(A2), 1)'); + } + + /** + * @dataProvider providerMonth + */ + public function testMonths(array $expectedVisible, string $rule): void + { + // Loop to avoid rare edge case where first calculation + // and second do not take place in same day. + do { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $dtStart = new DateTimeImmutable(); + $startDay = (int) $dtStart->format('d'); + $startMonth = (int) $dtStart->format('m'); + self::setCells($sheet, $startMonth); + + $maxRow = 9; + $autoFilter = $spreadsheet->getActiveSheet()->getAutoFilter(); + $autoFilter->setRange("A1:A$maxRow"); + $columnFilter = $autoFilter->getColumn('A'); + $columnFilter->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER); + $columnFilter->createRule() + ->setRule( + Rule::AUTOFILTER_COLUMN_RULE_EQUAL, + '', + $rule + ) + ->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); + $autoFilter->showHideRows(); + $dtEnd = new DateTimeImmutable(); + $endDay = (int) $dtEnd->format('d'); + } while ($startDay !== $endDay); + $actualVisible = []; + for ($row = 2; $row <= $maxRow; ++$row) { + if ($sheet->getRowDimension($row)->getVisible()) { + $actualVisible[] = $row; + } + } + self::assertEquals($expectedVisible, $actualVisible); + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterQuarterTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterQuarterTest.php new file mode 100644 index 00000000..d75f1bc2 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterQuarterTest.php @@ -0,0 +1,75 @@ +getCell('A1')->setValue('Date'); + $sheet->getCell('A2')->setValue('=TODAY()'); + $sheet->getCell('A3')->setValue('=DATE(YEAR(A2), MONTH(A2), 1)'); + $sheet->getCell('A4')->setValue('=DATE(YEAR(A2), MONTH(A2) + 3, 1)'); + $sheet->getCell('A5')->setValue('=DATE(YEAR(A2), MONTH(A2) + 6, 1)'); + $sheet->getCell('A6')->setValue('=DATE(YEAR(A2), MONTH(A2) - 3, 1)'); + $sheet->getCell('A7')->setValue('=DATE(YEAR(A2), MONTH(A2) - 6, 1)'); + $sheet->getCell('A8')->setValue('=DATE(YEAR(A2) + 1, MONTH(A2), 1)'); + $sheet->getCell('A9')->setValue('=DATE(YEAR(A2) - 1, MONTH(A2), 1)'); + } + + /** + * @dataProvider providerQuarter + */ + public function testQuarters(array $expectedVisible, string $rule): void + { + // Loop to avoid rare edge case where first calculation + // and second do not take place in same day. + do { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $dtStart = new DateTimeImmutable(); + $startDay = (int) $dtStart->format('d'); + $startMonth = (int) $dtStart->format('m'); + self::setCells($sheet, $startMonth); + + $maxRow = 9; + $autoFilter = $spreadsheet->getActiveSheet()->getAutoFilter(); + $autoFilter->setRange("A1:A$maxRow"); + $columnFilter = $autoFilter->getColumn('A'); + $columnFilter->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER); + $columnFilter->createRule() + ->setRule( + Rule::AUTOFILTER_COLUMN_RULE_EQUAL, + '', + $rule + ) + ->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); + $autoFilter->showHideRows(); + $dtEnd = new DateTimeImmutable(); + $endDay = (int) $dtEnd->format('d'); + } while ($startDay !== $endDay); + $actualVisible = []; + for ($row = 2; $row <= $maxRow; ++$row) { + if ($sheet->getRowDimension($row)->getVisible()) { + $actualVisible[] = $row; + } + } + self::assertEquals($expectedVisible, $actualVisible); + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilterTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php similarity index 99% rename from tests/PhpSpreadsheetTests/Worksheet/AutoFilterTest.php rename to tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php index 7f218b54..a6693d34 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/AutoFilterTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php @@ -1,6 +1,6 @@ expectException(\PhpOffice\PhpSpreadsheet\Exception::class); $invalidColumn = 123.456; + // @phpstan-ignore-next-line $this->testAutoFilterObject->setColumn($invalidColumn); } diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTodayTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTodayTest.php new file mode 100644 index 00000000..3875eb8d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTodayTest.php @@ -0,0 +1,65 @@ +getActiveSheet(); + $dtStart = new DateTimeImmutable(); + $startDay = $dtStart->format('d'); + $sheet->getCell('A1')->setValue('Date'); + $sheet->getCell('A2')->setValue('=NOW()'); + $sheet->getCell('A3')->setValue('=A2+1'); + $sheet->getCell('A4')->setValue('=A2-1'); + $sheet->getCell('A5')->setValue('=TODAY()'); + $sheet->getCell('A6')->setValue('=A5+1'); + $sheet->getCell('A7')->setValue('=A5-1'); + $maxRow = 7; + $autoFilter = $spreadsheet->getActiveSheet()->getAutoFilter(); + $autoFilter->setRange("A1:A$maxRow"); + $columnFilter = $autoFilter->getColumn('A'); + $columnFilter->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER); + $columnFilter->createRule() + ->setRule( + Rule::AUTOFILTER_COLUMN_RULE_EQUAL, + '', + $rule + ) + ->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); + $autoFilter->showHideRows(); + $dtEnd = new DateTimeImmutable(); + $endDay = $dtEnd->format('d'); + } while ($startDay !== $endDay); + $actualVisible = []; + for ($row = 2; $row <= $maxRow; ++$row) { + if ($sheet->getRowDimension($row)->getVisible()) { + $actualVisible[] = $row; + } + } + self::assertEquals($expectedVisible, $actualVisible); + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterWeekTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterWeekTest.php new file mode 100644 index 00000000..3e8fcdb8 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterWeekTest.php @@ -0,0 +1,76 @@ +getCell('A1')->setValue('Date'); + $sheet->getCell('A2')->setValue('=TODAY()'); + $sheet->getCell('B2')->setValue('=WEEKDAY(A2) - 1'); // subtract to get to Sunday + $sheet->getCell('A3')->setValue('=DATE(YEAR(A2), MONTH(A2), DAY(A2) - B2)'); + $sheet->getCell('A4')->setValue('=DATE(YEAR(A3), MONTH(A3), DAY(A3) + 8)'); + $sheet->getCell('A5')->setValue('=DATE(YEAR(A3), MONTH(A3), DAY(A3) + 19)'); + $sheet->getCell('A6')->setValue('=DATE(YEAR(A3), MONTH(A3), DAY(A3) - 6)'); + $sheet->getCell('A7')->setValue('=DATE(YEAR(A3), MONTH(A3), DAY(A3) - 12)'); + $sheet->getCell('A8')->setValue('=DATE(YEAR(A2) + 1, MONTH(A2), 1)'); + $sheet->getCell('A9')->setValue('=DATE(YEAR(A2) - 1, MONTH(A2), 1)'); + } + + /** + * @dataProvider providerWeek + */ + public function testWeek(array $expectedVisible, string $rule): void + { + // Loop to avoid rare edge case where first calculation + // and second do not take place in same day. + do { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $dtStart = new DateTimeImmutable(); + $startDay = (int) $dtStart->format('d'); + $startMonth = (int) $dtStart->format('m'); + self::setCells($sheet); + + $maxRow = 9; + $autoFilter = $spreadsheet->getActiveSheet()->getAutoFilter(); + $autoFilter->setRange("A1:A$maxRow"); + $columnFilter = $autoFilter->getColumn('A'); + $columnFilter->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER); + $columnFilter->createRule() + ->setRule( + Rule::AUTOFILTER_COLUMN_RULE_EQUAL, + '', + $rule + ) + ->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); + $autoFilter->showHideRows(); + $dtEnd = new DateTimeImmutable(); + $endDay = (int) $dtEnd->format('d'); + } while ($startDay !== $endDay); + $actualVisible = []; + for ($row = 2; $row <= $maxRow; ++$row) { + if ($sheet->getRowDimension($row)->getVisible()) { + $actualVisible[] = $row; + } + } + self::assertEquals($expectedVisible, $actualVisible); + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterYearTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterYearTest.php new file mode 100644 index 00000000..105608ae --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterYearTest.php @@ -0,0 +1,119 @@ +getActiveSheet(); + $dtStart = new DateTimeImmutable(); + $startDay = (int) $dtStart->format('d'); + $sheet->getCell('A1')->setValue('Date'); + $year = (int) $dtStart->format('Y') - 1; + $row = 1; + $iteration = 0; + while ($iteration < 3) { + for ($month = 3; $month < 13; $month += 4) { + ++$row; + $sheet->getCell("A$row")->setValue("=DATE($year, $month, 1)"); + } + ++$year; + ++$iteration; + } + ++$row; + $sheet->getCell("A$row")->setValue('=DATE(2041, 1, 1)'); // beyond epoch + $maxRow = $row; + $autoFilter = $spreadsheet->getActiveSheet()->getAutoFilter(); + $autoFilter->setRange("A1:A$maxRow"); + $columnFilter = $autoFilter->getColumn('A'); + $columnFilter->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER); + $columnFilter->createRule() + ->setRule( + Rule::AUTOFILTER_COLUMN_RULE_EQUAL, + '', + $rule + ) + ->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); + $autoFilter->showHideRows(); + $dtEnd = new DateTimeImmutable(); + $endDay = (int) $dtEnd->format('d'); + } while ($startDay !== $endDay); + $actualVisible = []; + for ($row = 2; $row <= $maxRow; ++$row) { + if ($sheet->getRowDimension($row)->getVisible()) { + $actualVisible[] = $row; + } + } + self::assertEquals($expectedVisible, $actualVisible); + } + + public function testYearToDate(): void + { + // Loop to avoid rare edge case where first calculation + // and second do not take place in same day. + do { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $dtStart = new DateTimeImmutable(); + $startDay = (int) $dtStart->format('d'); + $startMonth = (int) $dtStart->format('m'); + $sheet->getCell('A1')->setValue('Date'); + $year = (int) $dtStart->format('Y'); + $sheet->getCell('A2')->setValue('=TODAY()'); + $sheet->getCell('A3')->setValue('=DATE(YEAR(A2), 12, 31)'); + $sheet->getCell('A4')->setValue('=A3 + 1'); + $sheet->getCell('A5')->setValue('=DATE(YEAR(A2), 1, 1)'); + $sheet->getCell('A6')->setValue('=A5 - 1'); + + $maxRow = 6; + $autoFilter = $spreadsheet->getActiveSheet()->getAutoFilter(); + $autoFilter->setRange("A1:A$maxRow"); + $columnFilter = $autoFilter->getColumn('A'); + $columnFilter->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER); + $columnFilter->createRule() + ->setRule( + Rule::AUTOFILTER_COLUMN_RULE_EQUAL, + '', + Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE + ) + ->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); + $autoFilter->showHideRows(); + $dtEnd = new DateTimeImmutable(); + $endDay = (int) $dtEnd->format('d'); + } while ($startDay !== $endDay); + $actualVisible = []; + for ($row = 2; $row <= $maxRow; ++$row) { + if ($sheet->getRowDimension($row)->getVisible()) { + $actualVisible[] = $row; + } + } + $expected = ($startMonth === 12 && $startDay === 31) ? [2, 3, 5] : [2, 5]; + self::assertEquals($expected, $actualVisible); + } +}