CellMatcherTest.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. <?php
  2. namespace PhpOffice\PhpSpreadsheetTests\Style\ConditionalFormatting;
  3. use PhpOffice\PhpSpreadsheet\Cell\Cell;
  4. use PhpOffice\PhpSpreadsheet\Exception as ssException;
  5. use PhpOffice\PhpSpreadsheet\IOFactory;
  6. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  7. use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\CellMatcher;
  8. use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
  9. use PHPUnit\Framework\TestCase;
  10. class CellMatcherTest extends TestCase
  11. {
  12. /**
  13. * @var ?Spreadsheet
  14. */
  15. protected $spreadsheet;
  16. protected function loadSpreadsheet(): Spreadsheet
  17. {
  18. $filename = 'tests/data/Style/ConditionalFormatting/CellMatcher.xlsx';
  19. $reader = IOFactory::createReader('Xlsx');
  20. return $reader->load($filename);
  21. }
  22. protected function tearDown(): void
  23. {
  24. if ($this->spreadsheet !== null) {
  25. $this->spreadsheet->disconnectWorksheets();
  26. $this->spreadsheet = null;
  27. }
  28. }
  29. private function confirmString(Worksheet $worksheet, Cell $cell, string $cellAddress): string
  30. {
  31. $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()) ?? '';
  32. if ($cfRange === '') {
  33. self::fail("{$cellAddress} is not in a Conditional Format range");
  34. }
  35. return $cfRange;
  36. }
  37. /**
  38. * @dataProvider basicCellIsComparisonDataProvider
  39. */
  40. public function testBasicCellIsComparison(string $sheetname, string $cellAddress, array $expectedMatches): void
  41. {
  42. $this->spreadsheet = $this->loadSpreadsheet();
  43. $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname);
  44. $cell = $worksheet->getCell($cellAddress);
  45. $cfRange = $this->confirmString($worksheet, $cell, $cellAddress);
  46. $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate());
  47. $matcher = new CellMatcher($cell, $cfRange);
  48. foreach ($cfStyles as $cfIndex => $cfStyle) {
  49. $match = $matcher->evaluateConditional($cfStyle);
  50. self::assertSame($expectedMatches[$cfIndex], $match);
  51. }
  52. }
  53. public function basicCellIsComparisonDataProvider(): array
  54. {
  55. return [
  56. // Less than/Equal/Greater than with Literal
  57. 'A2' => ['cellIs Comparison', 'A2', [false, false, true]],
  58. 'C3' => ['cellIs Comparison', 'C3', [false, true, false]],
  59. 'E6' => ['cellIs Comparison', 'E6', [true, false, false]],
  60. // Less than/Equal/Greater than with Cell Reference
  61. 'A12' => ['cellIs Comparison', 'A12', [false, false, true]],
  62. 'C12' => ['cellIs Comparison', 'C12', [false, true, false]],
  63. 'E12' => ['cellIs Comparison', 'E12', [true, false, false]],
  64. // Compare Text with Cell containing Formula
  65. 'A20' => ['cellIs Comparison', 'A20', [true]],
  66. 'B20' => ['cellIs Comparison', 'B20', [false]],
  67. // Compare Text with Formula referencing relative cells
  68. 'A24' => ['cellIs Comparison', 'A24', [true]],
  69. 'B24' => ['cellIs Comparison', 'B24', [false]],
  70. 'A25' => ['cellIs Comparison', 'A25', [false]],
  71. 'B25' => ['cellIs Comparison', 'B25', [true]],
  72. // Compare Cell Greater/Less with Vertical Cell Reference
  73. 'A30' => ['cellIs Comparison', 'A30', [false, true]],
  74. 'A31' => ['cellIs Comparison', 'A31', [true, false]],
  75. 'A32' => ['cellIs Comparison', 'A32', [false, true]],
  76. 'A33' => ['cellIs Comparison', 'A33', [true, false]],
  77. 'A34' => ['cellIs Comparison', 'A34', [false, false]],
  78. 'A35' => ['cellIs Comparison', 'A35', [false, true]],
  79. 'A36' => ['cellIs Comparison', 'A36', [true, false]],
  80. 'A37' => ['cellIs Comparison', 'A37', [true, false]],
  81. ];
  82. }
  83. public function testNotInRange(): void
  84. {
  85. $this->spreadsheet = $this->loadSpreadsheet();
  86. $sheetname = 'cellIs Comparison';
  87. $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname);
  88. $cell = $worksheet->getCell('J20');
  89. $cfRange = $worksheet->getConditionalRange($cell->getCoordinate());
  90. self::assertNull($cfRange);
  91. }
  92. public function testUnknownSheet(): void
  93. {
  94. $this->expectException(ssException::class);
  95. $this->spreadsheet = $this->loadSpreadsheet();
  96. $sheetname = 'cellIs Comparisonxxx';
  97. $this->spreadsheet->getSheetByNameOrThrow($sheetname);
  98. }
  99. /**
  100. * @dataProvider rangeCellIsComparisonDataProvider
  101. */
  102. public function testRangeCellIsComparison(string $sheetname, string $cellAddress, bool $expectedMatch): void
  103. {
  104. $this->spreadsheet = $this->loadSpreadsheet();
  105. $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname);
  106. $cell = $worksheet->getCell($cellAddress);
  107. $cfRange = $this->confirmString($worksheet, $cell, $cellAddress);
  108. $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate());
  109. $matcher = new CellMatcher($cell, $cfRange);
  110. $match = $matcher->evaluateConditional($cfStyle[0]);
  111. self::assertSame($expectedMatch, $match);
  112. }
  113. public function rangeCellIsComparisonDataProvider(): array
  114. {
  115. return [
  116. // Range between Literals
  117. 'A2' => ['cellIs Range Comparison', 'A2', false],
  118. 'A3' => ['cellIs Range Comparison', 'A3', true],
  119. 'A4' => ['cellIs Range Comparison', 'A4', true],
  120. 'A5' => ['cellIs Range Comparison', 'A5', true],
  121. 'A6' => ['cellIs Range Comparison', 'A6', false],
  122. // Range between Cell References
  123. 'A11' => ['cellIs Range Comparison', 'A11', false],
  124. 'A12' => ['cellIs Range Comparison', 'A12', false],
  125. 'A13' => ['cellIs Range Comparison', 'A13', true],
  126. // Range between unordered Cell References
  127. 'A17' => ['cellIs Range Comparison', 'A17', true],
  128. 'A18' => ['cellIs Range Comparison', 'A18', true],
  129. // Range between with Formula
  130. 'A22' => ['cellIs Range Comparison', 'A22', false],
  131. 'A23' => ['cellIs Range Comparison', 'A23', true],
  132. 'A24' => ['cellIs Range Comparison', 'A24', false],
  133. ];
  134. }
  135. /**
  136. * @dataProvider cellIsExpressionMultipleDataProvider
  137. */
  138. public function testCellIsMultipleExpression(string $sheetname, string $cellAddress, array $expectedMatches): void
  139. {
  140. $this->spreadsheet = $this->loadSpreadsheet();
  141. $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname);
  142. $cell = $worksheet->getCell($cellAddress);
  143. $cfRange = $this->confirmString($worksheet, $cell, $cellAddress);
  144. $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate());
  145. $matcher = new CellMatcher($cell, $cfRange);
  146. foreach ($cfStyles as $cfIndex => $cfStyle) {
  147. $match = $matcher->evaluateConditional($cfStyle);
  148. self::assertSame($expectedMatches[$cfIndex], $match);
  149. }
  150. }
  151. public function cellIsExpressionMultipleDataProvider(): array
  152. {
  153. return [
  154. // Odd/Even
  155. 'A2' => ['cellIs Expression', 'A2', [false, true]],
  156. 'A3' => ['cellIs Expression', 'A3', [true, false]],
  157. 'B3' => ['cellIs Expression', 'B3', [false, true]],
  158. 'C3' => ['cellIs Expression', 'C3', [true, false]],
  159. 'E4' => ['cellIs Expression', 'E4', [false, true]],
  160. 'E5' => ['cellIs Expression', 'E5', [true, false]],
  161. 'E6' => ['cellIs Expression', 'E6', [false, true]],
  162. ];
  163. }
  164. /**
  165. * @dataProvider cellIsExpressionDataProvider
  166. */
  167. public function testCellIsExpression(string $sheetname, string $cellAddress, bool $expectedMatch): void
  168. {
  169. $this->spreadsheet = $this->loadSpreadsheet();
  170. $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname);
  171. $cell = $worksheet->getCell($cellAddress);
  172. $cfRange = $this->confirmString($worksheet, $cell, $cellAddress);
  173. $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate());
  174. $matcher = new CellMatcher($cell, $cfRange);
  175. $match = $matcher->evaluateConditional($cfStyle[0]);
  176. self::assertSame($expectedMatch, $match);
  177. }
  178. public function cellIsExpressionDataProvider(): array
  179. {
  180. return [
  181. // Sales Grid for Country
  182. ['cellIs Expression', 'A12', false],
  183. ['cellIs Expression', 'B12', false],
  184. ['cellIs Expression', 'C12', false],
  185. ['cellIs Expression', 'D12', false],
  186. ['cellIs Expression', 'B13', true],
  187. ['cellIs Expression', 'C13', true],
  188. ['cellIs Expression', 'B15', true],
  189. ['cellIs Expression', 'B16', true],
  190. ['cellIs Expression', 'C17', false],
  191. // Sales Grid for Country and Quarter
  192. ['cellIs Expression', 'A22', false],
  193. ['cellIs Expression', 'B22', false],
  194. ['cellIs Expression', 'C22', false],
  195. ['cellIs Expression', 'D22', false],
  196. ['cellIs Expression', 'B23', true],
  197. ['cellIs Expression', 'C23', true],
  198. ['cellIs Expression', 'B25', false],
  199. ['cellIs Expression', 'B26', true],
  200. ['cellIs Expression', 'C27', false],
  201. ];
  202. }
  203. /**
  204. * @dataProvider textExpressionsDataProvider
  205. */
  206. public function testTextExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void
  207. {
  208. $this->spreadsheet = $this->loadSpreadsheet();
  209. $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname);
  210. $cell = $worksheet->getCell($cellAddress);
  211. $cfRange = $this->confirmString($worksheet, $cell, $cellAddress);
  212. $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate());
  213. $matcher = new CellMatcher($cell, $cfRange);
  214. $match = $matcher->evaluateConditional($cfStyle[0]);
  215. self::assertSame($expectedMatch, $match);
  216. }
  217. public function textExpressionsDataProvider(): array
  218. {
  219. return [
  220. // Text Begins With Literal
  221. ['Text Expressions', 'A2', true],
  222. ['Text Expressions', 'B2', false],
  223. ['Text Expressions', 'A3', false],
  224. ['Text Expressions', 'B3', false],
  225. ['Text Expressions', 'A4', false],
  226. ['Text Expressions', 'B4', true],
  227. // Text Ends With Literal
  228. ['Text Expressions', 'A8', false],
  229. ['Text Expressions', 'B8', false],
  230. ['Text Expressions', 'A9', true],
  231. ['Text Expressions', 'B9', true],
  232. ['Text Expressions', 'A10', false],
  233. ['Text Expressions', 'B10', true],
  234. // Text Contains Literal
  235. ['Text Expressions', 'A14', true],
  236. ['Text Expressions', 'B14', false],
  237. ['Text Expressions', 'A15', true],
  238. ['Text Expressions', 'B15', true],
  239. ['Text Expressions', 'A16', false],
  240. ['Text Expressions', 'B16', true],
  241. // Text Doesn't Contain Literal
  242. ['Text Expressions', 'A20', true],
  243. ['Text Expressions', 'B20', true],
  244. ['Text Expressions', 'A21', true],
  245. ['Text Expressions', 'B21', true],
  246. ['Text Expressions', 'A22', false],
  247. ['Text Expressions', 'B22', true],
  248. // Text Begins With Cell Reference
  249. ['Text Expressions', 'D2', true],
  250. ['Text Expressions', 'E2', false],
  251. ['Text Expressions', 'D3', false],
  252. ['Text Expressions', 'E3', false],
  253. ['Text Expressions', 'D4', false],
  254. ['Text Expressions', 'E4', true],
  255. // Text Ends With Cell Reference
  256. ['Text Expressions', 'D8', false],
  257. ['Text Expressions', 'E8', false],
  258. ['Text Expressions', 'D9', true],
  259. ['Text Expressions', 'E9', true],
  260. ['Text Expressions', 'D10', false],
  261. ['Text Expressions', 'E10', true],
  262. // Text Contains Cell Reference
  263. ['Text Expressions', 'D14', true],
  264. ['Text Expressions', 'E14', false],
  265. ['Text Expressions', 'D15', true],
  266. ['Text Expressions', 'E15', true],
  267. ['Text Expressions', 'D16', false],
  268. ['Text Expressions', 'E16', true],
  269. // Text Doesn't Contain Cell Reference
  270. ['Text Expressions', 'D20', true],
  271. ['Text Expressions', 'E20', true],
  272. ['Text Expressions', 'D21', true],
  273. ['Text Expressions', 'E21', true],
  274. ['Text Expressions', 'D22', false],
  275. ['Text Expressions', 'E22', true],
  276. // Text Begins With Formula
  277. ['Text Expressions', 'G2', true],
  278. ['Text Expressions', 'H2', false],
  279. ['Text Expressions', 'G3', false],
  280. ['Text Expressions', 'H3', false],
  281. ['Text Expressions', 'G4', false],
  282. ['Text Expressions', 'H4', true],
  283. // Text Ends With Formula
  284. ['Text Expressions', 'G8', false],
  285. ['Text Expressions', 'H8', false],
  286. ['Text Expressions', 'G9', true],
  287. ['Text Expressions', 'H9', true],
  288. ['Text Expressions', 'G10', false],
  289. ['Text Expressions', 'H10', true],
  290. // Text Contains Formula
  291. ['Text Expressions', 'G14', true],
  292. ['Text Expressions', 'H14', false],
  293. ['Text Expressions', 'G15', true],
  294. ['Text Expressions', 'H15', true],
  295. ['Text Expressions', 'G16', false],
  296. ['Text Expressions', 'H16', true],
  297. // Text Doesn't Contain Formula
  298. ['Text Expressions', 'G20', true],
  299. ['Text Expressions', 'H20', true],
  300. ['Text Expressions', 'G21', true],
  301. ['Text Expressions', 'H21', true],
  302. ['Text Expressions', 'G22', false],
  303. ['Text Expressions', 'H22', true],
  304. ];
  305. }
  306. /**
  307. * @dataProvider blanksDataProvider
  308. */
  309. public function testBlankExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void
  310. {
  311. $this->spreadsheet = $this->loadSpreadsheet();
  312. $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname);
  313. $cell = $worksheet->getCell($cellAddress);
  314. $cfRange = $this->confirmString($worksheet, $cell, $cellAddress);
  315. $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate());
  316. $matcher = new CellMatcher($cell, $cfRange);
  317. foreach ($cfStyles as $cfIndex => $cfStyle) {
  318. $match = $matcher->evaluateConditional($cfStyle);
  319. self::assertSame($expectedMatches[$cfIndex], $match);
  320. }
  321. }
  322. public function blanksDataProvider(): array
  323. {
  324. return [
  325. // Blank/Not Blank
  326. 'A2' => ['Blank Expressions', 'A2', [false, true]],
  327. 'B2' => ['Blank Expressions', 'B2', [true, false]],
  328. 'A3' => ['Blank Expressions', 'A3', [true, false]],
  329. 'B3' => ['Blank Expressions', 'B3', [false, true]],
  330. ];
  331. }
  332. /**
  333. * @dataProvider errorDataProvider
  334. */
  335. public function testErrorExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void
  336. {
  337. $this->spreadsheet = $this->loadSpreadsheet();
  338. $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname);
  339. $cell = $worksheet->getCell($cellAddress);
  340. $cfRange = $this->confirmString($worksheet, $cell, $cellAddress);
  341. $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate());
  342. $matcher = new CellMatcher($cell, $cfRange);
  343. foreach ($cfStyles as $cfIndex => $cfStyle) {
  344. $match = $matcher->evaluateConditional($cfStyle);
  345. self::assertSame($expectedMatches[$cfIndex], $match);
  346. }
  347. }
  348. public function errorDataProvider(): array
  349. {
  350. return [
  351. // Error/Not Error
  352. 'C2' => ['Error Expressions', 'C2', [false, true]],
  353. 'C4' => ['Error Expressions', 'C4', [true, false]],
  354. 'C5' => ['Error Expressions', 'C5', [false, true]],
  355. ];
  356. }
  357. /**
  358. * @dataProvider dateOccurringDataProvider
  359. */
  360. public function testDateOccurringExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void
  361. {
  362. $this->spreadsheet = $this->loadSpreadsheet();
  363. $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname);
  364. $cell = $worksheet->getCell($cellAddress);
  365. $cfRange = $this->confirmString($worksheet, $cell, $cellAddress);
  366. $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate());
  367. $matcher = new CellMatcher($cell, $cfRange);
  368. $match = $matcher->evaluateConditional($cfStyle[0]);
  369. self::assertSame($expectedMatch, $match);
  370. }
  371. public function dateOccurringDataProvider(): array
  372. {
  373. return [
  374. // Today
  375. ['Date Expressions', 'B9', false],
  376. ['Date Expressions', 'B10', true],
  377. ['Date Expressions', 'B11', false],
  378. // Yesterday
  379. ['Date Expressions', 'C9', true],
  380. ['Date Expressions', 'C10', false],
  381. ['Date Expressions', 'C11', false],
  382. // Tomorrow
  383. ['Date Expressions', 'D9', false],
  384. ['Date Expressions', 'D10', false],
  385. ['Date Expressions', 'D11', true],
  386. // Last Daye
  387. ['Date Expressions', 'E7', false],
  388. ['Date Expressions', 'E8', true],
  389. ['Date Expressions', 'E9', true],
  390. ['Date Expressions', 'E10', true],
  391. ['Date Expressions', 'E11', false],
  392. ];
  393. }
  394. /**
  395. * @dataProvider duplicatesDataProvider
  396. */
  397. public function testDuplicatesExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void
  398. {
  399. $this->spreadsheet = $this->loadSpreadsheet();
  400. $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname);
  401. $cell = $worksheet->getCell($cellAddress);
  402. $cfRange = $this->confirmString($worksheet, $cell, $cellAddress);
  403. $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate());
  404. $matcher = new CellMatcher($cell, $cfRange);
  405. foreach ($cfStyles as $cfIndex => $cfStyle) {
  406. $match = $matcher->evaluateConditional($cfStyle);
  407. self::assertSame($expectedMatches[$cfIndex], $match);
  408. }
  409. }
  410. public function duplicatesDataProvider(): array
  411. {
  412. return [
  413. // Duplicate/Unique
  414. 'A2' => ['Duplicates Expressions', 'A2', [true, false]],
  415. 'B2' => ['Duplicates Expressions', 'B2', [false, true]],
  416. 'A4' => ['Duplicates Expressions', 'A4', [true, false]],
  417. 'A5' => ['Duplicates Expressions', 'A5', [false, true]],
  418. 'B5' => ['Duplicates Expressions', 'B5', [true, false]],
  419. 'A9' => ['Duplicates Expressions', 'A9', [true, false]],
  420. 'B9' => ['Duplicates Expressions', 'B9', [false, true]],
  421. ];
  422. }
  423. /**
  424. * @dataProvider textCrossWorksheetDataProvider
  425. */
  426. public function testCrossWorksheetExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void
  427. {
  428. $this->spreadsheet = $this->loadSpreadsheet();
  429. $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname);
  430. $cell = $worksheet->getCell($cellAddress);
  431. $cfRange = $this->confirmString($worksheet, $cell, $cellAddress);
  432. $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate());
  433. $matcher = new CellMatcher($cell, $cfRange);
  434. $match = $matcher->evaluateConditional($cfStyle[0]);
  435. self::assertSame($expectedMatch, $match);
  436. }
  437. public function textCrossWorksheetDataProvider(): array
  438. {
  439. return [
  440. // Relative Cell References in another Worksheet
  441. 'A1' => ['CrossSheet References', 'A1', false],
  442. 'A2' => ['CrossSheet References', 'A2', false],
  443. 'A3' => ['CrossSheet References', 'A3', true],
  444. 'A4' => ['CrossSheet References', 'A4', false],
  445. 'A5' => ['CrossSheet References', 'A5', false],
  446. ];
  447. }
  448. }