AutoFilter.php 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872
  1. <?php
  2. namespace PhpOffice\PhpSpreadsheet\Worksheet;
  3. use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
  4. use PhpOffice\PhpSpreadsheet\Calculation\DateTime;
  5. use PhpOffice\PhpSpreadsheet\Calculation\Functions;
  6. use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
  7. use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
  8. use PhpOffice\PhpSpreadsheet\Shared\Date;
  9. class AutoFilter
  10. {
  11. /**
  12. * Autofilter Worksheet.
  13. *
  14. * @var Worksheet
  15. */
  16. private $workSheet;
  17. /**
  18. * Autofilter Range.
  19. *
  20. * @var string
  21. */
  22. private $range = '';
  23. /**
  24. * Autofilter Column Ruleset.
  25. *
  26. * @var AutoFilter\Column[]
  27. */
  28. private $columns = [];
  29. /**
  30. * Create a new AutoFilter.
  31. *
  32. * @param string $pRange Cell range (i.e. A1:E10)
  33. * @param Worksheet $pSheet
  34. */
  35. public function __construct($pRange = '', Worksheet $pSheet = null)
  36. {
  37. $this->range = $pRange;
  38. $this->workSheet = $pSheet;
  39. }
  40. /**
  41. * Get AutoFilter Parent Worksheet.
  42. *
  43. * @return Worksheet
  44. */
  45. public function getParent()
  46. {
  47. return $this->workSheet;
  48. }
  49. /**
  50. * Set AutoFilter Parent Worksheet.
  51. *
  52. * @param Worksheet $pSheet
  53. *
  54. * @return AutoFilter
  55. */
  56. public function setParent(Worksheet $pSheet = null)
  57. {
  58. $this->workSheet = $pSheet;
  59. return $this;
  60. }
  61. /**
  62. * Get AutoFilter Range.
  63. *
  64. * @return string
  65. */
  66. public function getRange()
  67. {
  68. return $this->range;
  69. }
  70. /**
  71. * Set AutoFilter Range.
  72. *
  73. * @param string $pRange Cell range (i.e. A1:E10)
  74. *
  75. * @throws PhpSpreadsheetException
  76. *
  77. * @return AutoFilter
  78. */
  79. public function setRange($pRange)
  80. {
  81. // Uppercase coordinate
  82. $cellAddress = explode('!', strtoupper($pRange));
  83. if (count($cellAddress) > 1) {
  84. list($worksheet, $pRange) = $cellAddress;
  85. }
  86. if (strpos($pRange, ':') !== false) {
  87. $this->range = $pRange;
  88. } elseif (empty($pRange)) {
  89. $this->range = '';
  90. } else {
  91. throw new PhpSpreadsheetException('Autofilter must be set on a range of cells.');
  92. }
  93. if (empty($pRange)) {
  94. // Discard all column rules
  95. $this->columns = [];
  96. } else {
  97. // Discard any column rules that are no longer valid within this range
  98. list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($this->range);
  99. foreach ($this->columns as $key => $value) {
  100. $colIndex = Coordinate::columnIndexFromString($key);
  101. if (($rangeStart[0] > $colIndex) || ($rangeEnd[0] < $colIndex)) {
  102. unset($this->columns[$key]);
  103. }
  104. }
  105. }
  106. return $this;
  107. }
  108. /**
  109. * Get all AutoFilter Columns.
  110. *
  111. * @return AutoFilter\Column[]
  112. */
  113. public function getColumns()
  114. {
  115. return $this->columns;
  116. }
  117. /**
  118. * Validate that the specified column is in the AutoFilter range.
  119. *
  120. * @param string $column Column name (e.g. A)
  121. *
  122. * @throws PhpSpreadsheetException
  123. *
  124. * @return int The column offset within the autofilter range
  125. */
  126. public function testColumnInRange($column)
  127. {
  128. if (empty($this->range)) {
  129. throw new PhpSpreadsheetException('No autofilter range is defined.');
  130. }
  131. $columnIndex = Coordinate::columnIndexFromString($column);
  132. list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($this->range);
  133. if (($rangeStart[0] > $columnIndex) || ($rangeEnd[0] < $columnIndex)) {
  134. throw new PhpSpreadsheetException('Column is outside of current autofilter range.');
  135. }
  136. return $columnIndex - $rangeStart[0];
  137. }
  138. /**
  139. * Get a specified AutoFilter Column Offset within the defined AutoFilter range.
  140. *
  141. * @param string $pColumn Column name (e.g. A)
  142. *
  143. * @throws PhpSpreadsheetException
  144. *
  145. * @return int The offset of the specified column within the autofilter range
  146. */
  147. public function getColumnOffset($pColumn)
  148. {
  149. return $this->testColumnInRange($pColumn);
  150. }
  151. /**
  152. * Get a specified AutoFilter Column.
  153. *
  154. * @param string $pColumn Column name (e.g. A)
  155. *
  156. * @throws PhpSpreadsheetException
  157. *
  158. * @return AutoFilter\Column
  159. */
  160. public function getColumn($pColumn)
  161. {
  162. $this->testColumnInRange($pColumn);
  163. if (!isset($this->columns[$pColumn])) {
  164. $this->columns[$pColumn] = new AutoFilter\Column($pColumn, $this);
  165. }
  166. return $this->columns[$pColumn];
  167. }
  168. /**
  169. * Get a specified AutoFilter Column by it's offset.
  170. *
  171. * @param int $pColumnOffset Column offset within range (starting from 0)
  172. *
  173. * @throws PhpSpreadsheetException
  174. *
  175. * @return AutoFilter\Column
  176. */
  177. public function getColumnByOffset($pColumnOffset)
  178. {
  179. list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($this->range);
  180. $pColumn = Coordinate::stringFromColumnIndex($rangeStart[0] + $pColumnOffset);
  181. return $this->getColumn($pColumn);
  182. }
  183. /**
  184. * Set AutoFilter.
  185. *
  186. * @param AutoFilter\Column|string $pColumn
  187. * A simple string containing a Column ID like 'A' is permitted
  188. *
  189. * @throws PhpSpreadsheetException
  190. *
  191. * @return AutoFilter
  192. */
  193. public function setColumn($pColumn)
  194. {
  195. if ((is_string($pColumn)) && (!empty($pColumn))) {
  196. $column = $pColumn;
  197. } elseif (is_object($pColumn) && ($pColumn instanceof AutoFilter\Column)) {
  198. $column = $pColumn->getColumnIndex();
  199. } else {
  200. throw new PhpSpreadsheetException('Column is not within the autofilter range.');
  201. }
  202. $this->testColumnInRange($column);
  203. if (is_string($pColumn)) {
  204. $this->columns[$pColumn] = new AutoFilter\Column($pColumn, $this);
  205. } elseif (is_object($pColumn) && ($pColumn instanceof AutoFilter\Column)) {
  206. $pColumn->setParent($this);
  207. $this->columns[$column] = $pColumn;
  208. }
  209. ksort($this->columns);
  210. return $this;
  211. }
  212. /**
  213. * Clear a specified AutoFilter Column.
  214. *
  215. * @param string $pColumn Column name (e.g. A)
  216. *
  217. * @throws PhpSpreadsheetException
  218. *
  219. * @return AutoFilter
  220. */
  221. public function clearColumn($pColumn)
  222. {
  223. $this->testColumnInRange($pColumn);
  224. if (isset($this->columns[$pColumn])) {
  225. unset($this->columns[$pColumn]);
  226. }
  227. return $this;
  228. }
  229. /**
  230. * Shift an AutoFilter Column Rule to a different column.
  231. *
  232. * Note: This method bypasses validation of the destination column to ensure it is within this AutoFilter range.
  233. * Nor does it verify whether any column rule already exists at $toColumn, but will simply override any existing value.
  234. * Use with caution.
  235. *
  236. * @param string $fromColumn Column name (e.g. A)
  237. * @param string $toColumn Column name (e.g. B)
  238. *
  239. * @return AutoFilter
  240. */
  241. public function shiftColumn($fromColumn, $toColumn)
  242. {
  243. $fromColumn = strtoupper($fromColumn);
  244. $toColumn = strtoupper($toColumn);
  245. if (($fromColumn !== null) && (isset($this->columns[$fromColumn])) && ($toColumn !== null)) {
  246. $this->columns[$fromColumn]->setParent();
  247. $this->columns[$fromColumn]->setColumnIndex($toColumn);
  248. $this->columns[$toColumn] = $this->columns[$fromColumn];
  249. $this->columns[$toColumn]->setParent($this);
  250. unset($this->columns[$fromColumn]);
  251. ksort($this->columns);
  252. }
  253. return $this;
  254. }
  255. /**
  256. * Test if cell value is in the defined set of values.
  257. *
  258. * @param mixed $cellValue
  259. * @param mixed[] $dataSet
  260. *
  261. * @return bool
  262. */
  263. private static function filterTestInSimpleDataSet($cellValue, $dataSet)
  264. {
  265. $dataSetValues = $dataSet['filterValues'];
  266. $blanks = $dataSet['blanks'];
  267. if (($cellValue == '') || ($cellValue === null)) {
  268. return $blanks;
  269. }
  270. return in_array($cellValue, $dataSetValues);
  271. }
  272. /**
  273. * Test if cell value is in the defined set of Excel date values.
  274. *
  275. * @param mixed $cellValue
  276. * @param mixed[] $dataSet
  277. *
  278. * @return bool
  279. */
  280. private static function filterTestInDateGroupSet($cellValue, $dataSet)
  281. {
  282. $dateSet = $dataSet['filterValues'];
  283. $blanks = $dataSet['blanks'];
  284. if (($cellValue == '') || ($cellValue === null)) {
  285. return $blanks;
  286. }
  287. if (is_numeric($cellValue)) {
  288. $dateValue = Date::excelToTimestamp($cellValue);
  289. if ($cellValue < 1) {
  290. // Just the time part
  291. $dtVal = date('His', $dateValue);
  292. $dateSet = $dateSet['time'];
  293. } elseif ($cellValue == floor($cellValue)) {
  294. // Just the date part
  295. $dtVal = date('Ymd', $dateValue);
  296. $dateSet = $dateSet['date'];
  297. } else {
  298. // date and time parts
  299. $dtVal = date('YmdHis', $dateValue);
  300. $dateSet = $dateSet['dateTime'];
  301. }
  302. foreach ($dateSet as $dateValue) {
  303. // Use of substr to extract value at the appropriate group level
  304. if (substr($dtVal, 0, strlen($dateValue)) == $dateValue) {
  305. return true;
  306. }
  307. }
  308. }
  309. return false;
  310. }
  311. /**
  312. * Test if cell value is within a set of values defined by a ruleset.
  313. *
  314. * @param mixed $cellValue
  315. * @param mixed[] $ruleSet
  316. *
  317. * @return bool
  318. */
  319. private static function filterTestInCustomDataSet($cellValue, $ruleSet)
  320. {
  321. $dataSet = $ruleSet['filterRules'];
  322. $join = $ruleSet['join'];
  323. $customRuleForBlanks = isset($ruleSet['customRuleForBlanks']) ? $ruleSet['customRuleForBlanks'] : false;
  324. if (!$customRuleForBlanks) {
  325. // Blank cells are always ignored, so return a FALSE
  326. if (($cellValue == '') || ($cellValue === null)) {
  327. return false;
  328. }
  329. }
  330. $returnVal = ($join == AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND);
  331. foreach ($dataSet as $rule) {
  332. if (is_numeric($rule['value'])) {
  333. // Numeric values are tested using the appropriate operator
  334. switch ($rule['operator']) {
  335. case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_EQUAL:
  336. $retVal = ($cellValue == $rule['value']);
  337. break;
  338. case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL:
  339. $retVal = ($cellValue != $rule['value']);
  340. break;
  341. case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN:
  342. $retVal = ($cellValue > $rule['value']);
  343. break;
  344. case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL:
  345. $retVal = ($cellValue >= $rule['value']);
  346. break;
  347. case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN:
  348. $retVal = ($cellValue < $rule['value']);
  349. break;
  350. case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL:
  351. $retVal = ($cellValue <= $rule['value']);
  352. break;
  353. }
  354. } elseif ($rule['value'] == '') {
  355. switch ($rule['operator']) {
  356. case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_EQUAL:
  357. $retVal = (($cellValue == '') || ($cellValue === null));
  358. break;
  359. case AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL:
  360. $retVal = (($cellValue != '') && ($cellValue !== null));
  361. break;
  362. default:
  363. $retVal = true;
  364. break;
  365. }
  366. } else {
  367. // String values are always tested for equality, factoring in for wildcards (hence a regexp test)
  368. $retVal = preg_match('/^' . $rule['value'] . '$/i', $cellValue);
  369. }
  370. // If there are multiple conditions, then we need to test both using the appropriate join operator
  371. switch ($join) {
  372. case AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR:
  373. $returnVal = $returnVal || $retVal;
  374. // Break as soon as we have a TRUE match for OR joins,
  375. // to avoid unnecessary additional code execution
  376. if ($returnVal) {
  377. return $returnVal;
  378. }
  379. break;
  380. case AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND:
  381. $returnVal = $returnVal && $retVal;
  382. break;
  383. }
  384. }
  385. return $returnVal;
  386. }
  387. /**
  388. * Test if cell date value is matches a set of values defined by a set of months.
  389. *
  390. * @param mixed $cellValue
  391. * @param mixed[] $monthSet
  392. *
  393. * @return bool
  394. */
  395. private static function filterTestInPeriodDateSet($cellValue, $monthSet)
  396. {
  397. // Blank cells are always ignored, so return a FALSE
  398. if (($cellValue == '') || ($cellValue === null)) {
  399. return false;
  400. }
  401. if (is_numeric($cellValue)) {
  402. $dateValue = date('m', Date::excelToTimestamp($cellValue));
  403. if (in_array($dateValue, $monthSet)) {
  404. return true;
  405. }
  406. }
  407. return false;
  408. }
  409. /**
  410. * Search/Replace arrays to convert Excel wildcard syntax to a regexp syntax for preg_matching.
  411. *
  412. * @var array
  413. */
  414. private static $fromReplace = ['\*', '\?', '~~', '~.*', '~.?'];
  415. private static $toReplace = ['.*', '.', '~', '\*', '\?'];
  416. /**
  417. * Convert a dynamic rule daterange to a custom filter range expression for ease of calculation.
  418. *
  419. * @param string $dynamicRuleType
  420. * @param AutoFilter\Column $filterColumn
  421. *
  422. * @return mixed[]
  423. */
  424. private function dynamicFilterDateRange($dynamicRuleType, &$filterColumn)
  425. {
  426. $rDateType = Functions::getReturnDateType();
  427. Functions::setReturnDateType(Functions::RETURNDATE_PHP_NUMERIC);
  428. $val = $maxVal = null;
  429. $ruleValues = [];
  430. $baseDate = DateTime::DATENOW();
  431. // Calculate start/end dates for the required date range based on current date
  432. switch ($dynamicRuleType) {
  433. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK:
  434. $baseDate = strtotime('-7 days', $baseDate);
  435. break;
  436. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK:
  437. $baseDate = strtotime('-7 days', $baseDate);
  438. break;
  439. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH:
  440. $baseDate = strtotime('-1 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));
  441. break;
  442. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH:
  443. $baseDate = strtotime('+1 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));
  444. break;
  445. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER:
  446. $baseDate = strtotime('-3 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));
  447. break;
  448. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER:
  449. $baseDate = strtotime('+3 month', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));
  450. break;
  451. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR:
  452. $baseDate = strtotime('-1 year', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));
  453. break;
  454. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR:
  455. $baseDate = strtotime('+1 year', gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));
  456. break;
  457. }
  458. switch ($dynamicRuleType) {
  459. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_TODAY:
  460. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY:
  461. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW:
  462. $maxVal = (int) Date::PHPtoExcel(strtotime('+1 day', $baseDate));
  463. $val = (int) Date::PHPToExcel($baseDate);
  464. break;
  465. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_YEARTODATE:
  466. $maxVal = (int) Date::PHPtoExcel(strtotime('+1 day', $baseDate));
  467. $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, 1, date('Y', $baseDate)));
  468. break;
  469. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISYEAR:
  470. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTYEAR:
  471. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTYEAR:
  472. $maxVal = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 31, 12, date('Y', $baseDate)));
  473. ++$maxVal;
  474. $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, 1, date('Y', $baseDate)));
  475. break;
  476. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISQUARTER:
  477. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTQUARTER:
  478. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTQUARTER:
  479. $thisMonth = date('m', $baseDate);
  480. $thisQuarter = floor(--$thisMonth / 3);
  481. $maxVal = (int) Date::PHPtoExcel(gmmktime(0, 0, 0, date('t', $baseDate), (1 + $thisQuarter) * 3, date('Y', $baseDate)));
  482. ++$maxVal;
  483. $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, 1 + $thisQuarter * 3, date('Y', $baseDate)));
  484. break;
  485. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISMONTH:
  486. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTMONTH:
  487. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTMONTH:
  488. $maxVal = (int) Date::PHPtoExcel(gmmktime(0, 0, 0, date('t', $baseDate), date('m', $baseDate), date('Y', $baseDate)));
  489. ++$maxVal;
  490. $val = (int) Date::PHPToExcel(gmmktime(0, 0, 0, 1, date('m', $baseDate), date('Y', $baseDate)));
  491. break;
  492. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_THISWEEK:
  493. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK:
  494. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_NEXTWEEK:
  495. $dayOfWeek = date('w', $baseDate);
  496. $val = (int) Date::PHPToExcel($baseDate) - $dayOfWeek;
  497. $maxVal = $val + 7;
  498. break;
  499. }
  500. switch ($dynamicRuleType) {
  501. // Adjust Today dates for Yesterday and Tomorrow
  502. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_YESTERDAY:
  503. --$maxVal;
  504. --$val;
  505. break;
  506. case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_TOMORROW:
  507. ++$maxVal;
  508. ++$val;
  509. break;
  510. }
  511. // Set the filter column rule attributes ready for writing
  512. $filterColumn->setAttributes(['val' => $val, 'maxVal' => $maxVal]);
  513. // Set the rules for identifying rows for hide/show
  514. $ruleValues[] = ['operator' => AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL, 'value' => $val];
  515. $ruleValues[] = ['operator' => AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN, 'value' => $maxVal];
  516. Functions::setReturnDateType($rDateType);
  517. return ['method' => 'filterTestInCustomDataSet', 'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_AND]];
  518. }
  519. private function calculateTopTenValue($columnID, $startRow, $endRow, $ruleType, $ruleValue)
  520. {
  521. $range = $columnID . $startRow . ':' . $columnID . $endRow;
  522. $dataValues = Functions::flattenArray($this->workSheet->rangeToArray($range, null, true, false));
  523. $dataValues = array_filter($dataValues);
  524. if ($ruleType == AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP) {
  525. rsort($dataValues);
  526. } else {
  527. sort($dataValues);
  528. }
  529. return array_pop(array_slice($dataValues, 0, $ruleValue));
  530. }
  531. /**
  532. * Apply the AutoFilter rules to the AutoFilter Range.
  533. *
  534. * @throws PhpSpreadsheetException
  535. *
  536. * @return AutoFilter
  537. */
  538. public function showHideRows()
  539. {
  540. list($rangeStart, $rangeEnd) = Coordinate::rangeBoundaries($this->range);
  541. // The heading row should always be visible
  542. $this->workSheet->getRowDimension($rangeStart[1])->setVisible(true);
  543. $columnFilterTests = [];
  544. foreach ($this->columns as $columnID => $filterColumn) {
  545. $rules = $filterColumn->getRules();
  546. switch ($filterColumn->getFilterType()) {
  547. case AutoFilter\Column::AUTOFILTER_FILTERTYPE_FILTER:
  548. $ruleType = null;
  549. $ruleValues = [];
  550. // Build a list of the filter value selections
  551. foreach ($rules as $rule) {
  552. $ruleType = $rule->getRuleType();
  553. $ruleValues[] = $rule->getValue();
  554. }
  555. // Test if we want to include blanks in our filter criteria
  556. $blanks = false;
  557. $ruleDataSet = array_filter($ruleValues);
  558. if (count($ruleValues) != count($ruleDataSet)) {
  559. $blanks = true;
  560. }
  561. if ($ruleType == AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_FILTER) {
  562. // Filter on absolute values
  563. $columnFilterTests[$columnID] = [
  564. 'method' => 'filterTestInSimpleDataSet',
  565. 'arguments' => ['filterValues' => $ruleDataSet, 'blanks' => $blanks],
  566. ];
  567. } else {
  568. // Filter on date group values
  569. $arguments = [
  570. 'date' => [],
  571. 'time' => [],
  572. 'dateTime' => [],
  573. ];
  574. foreach ($ruleDataSet as $ruleValue) {
  575. $date = $time = '';
  576. if ((isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR])) &&
  577. ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR] !== '')) {
  578. $date .= sprintf('%04d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_YEAR]);
  579. }
  580. if ((isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH])) &&
  581. ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH] != '')) {
  582. $date .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MONTH]);
  583. }
  584. if ((isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY])) &&
  585. ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY] !== '')) {
  586. $date .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_DAY]);
  587. }
  588. if ((isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR])) &&
  589. ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR] !== '')) {
  590. $time .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_HOUR]);
  591. }
  592. if ((isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE])) &&
  593. ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE] !== '')) {
  594. $time .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_MINUTE]);
  595. }
  596. if ((isset($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND])) &&
  597. ($ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND] !== '')) {
  598. $time .= sprintf('%02d', $ruleValue[AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DATEGROUP_SECOND]);
  599. }
  600. $dateTime = $date . $time;
  601. $arguments['date'][] = $date;
  602. $arguments['time'][] = $time;
  603. $arguments['dateTime'][] = $dateTime;
  604. }
  605. // Remove empty elements
  606. $arguments['date'] = array_filter($arguments['date']);
  607. $arguments['time'] = array_filter($arguments['time']);
  608. $arguments['dateTime'] = array_filter($arguments['dateTime']);
  609. $columnFilterTests[$columnID] = [
  610. 'method' => 'filterTestInDateGroupSet',
  611. 'arguments' => ['filterValues' => $arguments, 'blanks' => $blanks],
  612. ];
  613. }
  614. break;
  615. case AutoFilter\Column::AUTOFILTER_FILTERTYPE_CUSTOMFILTER:
  616. $customRuleForBlanks = false;
  617. $ruleValues = [];
  618. // Build a list of the filter value selections
  619. foreach ($rules as $rule) {
  620. $ruleValue = $rule->getValue();
  621. if (!is_numeric($ruleValue)) {
  622. // Convert to a regexp allowing for regexp reserved characters, wildcards and escaped wildcards
  623. $ruleValue = preg_quote($ruleValue);
  624. $ruleValue = str_replace(self::$fromReplace, self::$toReplace, $ruleValue);
  625. if (trim($ruleValue) == '') {
  626. $customRuleForBlanks = true;
  627. $ruleValue = trim($ruleValue);
  628. }
  629. }
  630. $ruleValues[] = ['operator' => $rule->getOperator(), 'value' => $ruleValue];
  631. }
  632. $join = $filterColumn->getJoin();
  633. $columnFilterTests[$columnID] = [
  634. 'method' => 'filterTestInCustomDataSet',
  635. 'arguments' => ['filterRules' => $ruleValues, 'join' => $join, 'customRuleForBlanks' => $customRuleForBlanks],
  636. ];
  637. break;
  638. case AutoFilter\Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER:
  639. $ruleValues = [];
  640. foreach ($rules as $rule) {
  641. // We should only ever have one Dynamic Filter Rule anyway
  642. $dynamicRuleType = $rule->getGrouping();
  643. if (($dynamicRuleType == AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE) ||
  644. ($dynamicRuleType == AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_BELOWAVERAGE)) {
  645. // Number (Average) based
  646. // Calculate the average
  647. $averageFormula = '=AVERAGE(' . $columnID . ($rangeStart[1] + 1) . ':' . $columnID . $rangeEnd[1] . ')';
  648. $average = Calculation::getInstance()->calculateFormula($averageFormula, null, $this->workSheet->getCell('A1'));
  649. // Set above/below rule based on greaterThan or LessTan
  650. $operator = ($dynamicRuleType === AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE)
  651. ? AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN
  652. : AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN;
  653. $ruleValues[] = [
  654. 'operator' => $operator,
  655. 'value' => $average,
  656. ];
  657. $columnFilterTests[$columnID] = [
  658. 'method' => 'filterTestInCustomDataSet',
  659. 'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR],
  660. ];
  661. } else {
  662. // Date based
  663. if ($dynamicRuleType[0] == 'M' || $dynamicRuleType[0] == 'Q') {
  664. // Month or Quarter
  665. sscanf($dynamicRuleType, '%[A-Z]%d', $periodType, $period);
  666. if ($periodType == 'M') {
  667. $ruleValues = [$period];
  668. } else {
  669. --$period;
  670. $periodEnd = (1 + $period) * 3;
  671. $periodStart = 1 + $period * 3;
  672. $ruleValues = range($periodStart, $periodEnd);
  673. }
  674. $columnFilterTests[$columnID] = [
  675. 'method' => 'filterTestInPeriodDateSet',
  676. 'arguments' => $ruleValues,
  677. ];
  678. $filterColumn->setAttributes([]);
  679. } else {
  680. // Date Range
  681. $columnFilterTests[$columnID] = $this->dynamicFilterDateRange($dynamicRuleType, $filterColumn);
  682. break;
  683. }
  684. }
  685. }
  686. break;
  687. case AutoFilter\Column::AUTOFILTER_FILTERTYPE_TOPTENFILTER:
  688. $ruleValues = [];
  689. $dataRowCount = $rangeEnd[1] - $rangeStart[1];
  690. foreach ($rules as $rule) {
  691. // We should only ever have one Dynamic Filter Rule anyway
  692. $toptenRuleType = $rule->getGrouping();
  693. $ruleValue = $rule->getValue();
  694. $ruleOperator = $rule->getOperator();
  695. }
  696. if ($ruleOperator === AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_PERCENT) {
  697. $ruleValue = floor($ruleValue * ($dataRowCount / 100));
  698. }
  699. if ($ruleValue < 1) {
  700. $ruleValue = 1;
  701. }
  702. if ($ruleValue > 500) {
  703. $ruleValue = 500;
  704. }
  705. $maxVal = $this->calculateTopTenValue($columnID, $rangeStart[1] + 1, $rangeEnd[1], $toptenRuleType, $ruleValue);
  706. $operator = ($toptenRuleType == AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_TOPTEN_TOP)
  707. ? AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL
  708. : AutoFilter\Column\Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL;
  709. $ruleValues[] = ['operator' => $operator, 'value' => $maxVal];
  710. $columnFilterTests[$columnID] = [
  711. 'method' => 'filterTestInCustomDataSet',
  712. 'arguments' => ['filterRules' => $ruleValues, 'join' => AutoFilter\Column::AUTOFILTER_COLUMN_JOIN_OR],
  713. ];
  714. $filterColumn->setAttributes(['maxVal' => $maxVal]);
  715. break;
  716. }
  717. }
  718. // Execute the column tests for each row in the autoFilter range to determine show/hide,
  719. for ($row = $rangeStart[1] + 1; $row <= $rangeEnd[1]; ++$row) {
  720. $result = true;
  721. foreach ($columnFilterTests as $columnID => $columnFilterTest) {
  722. $cellValue = $this->workSheet->getCell($columnID . $row)->getCalculatedValue();
  723. // Execute the filter test
  724. $result = $result &&
  725. call_user_func_array(
  726. [self::class, $columnFilterTest['method']],
  727. [$cellValue, $columnFilterTest['arguments']]
  728. );
  729. // If filter test has resulted in FALSE, exit the loop straightaway rather than running any more tests
  730. if (!$result) {
  731. break;
  732. }
  733. }
  734. // Set show/hide for the row based on the result of the autoFilter result
  735. $this->workSheet->getRowDimension($row)->setVisible($result);
  736. }
  737. return $this;
  738. }
  739. /**
  740. * Implement PHP __clone to create a deep clone, not just a shallow copy.
  741. */
  742. public function __clone()
  743. {
  744. $vars = get_object_vars($this);
  745. foreach ($vars as $key => $value) {
  746. if (is_object($value)) {
  747. if ($key == 'workSheet') {
  748. // Detach from worksheet
  749. $this->{$key} = null;
  750. } else {
  751. $this->{$key} = clone $value;
  752. }
  753. } elseif ((is_array($value)) && ($key == 'columns')) {
  754. // The columns array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\AutoFilter objects
  755. $this->{$key} = [];
  756. foreach ($value as $k => $v) {
  757. $this->{$key}[$k] = clone $v;
  758. // attach the new cloned Column to this new cloned Autofilter object
  759. $this->{$key}[$k]->setParent($this);
  760. }
  761. } else {
  762. $this->{$key} = $value;
  763. }
  764. }
  765. }
  766. /**
  767. * toString method replicates previous behavior by returning the range if object is
  768. * referenced as a property of its parent.
  769. */
  770. public function __toString()
  771. {
  772. return (string) $this->range;
  773. }
  774. }