From 56e7422bbad40b7a8941ba0d6203bdc5885cfe3c Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 11 Jan 2025 17:31:52 -0800 Subject: [PATCH] Add "Priority" Property for Conditional Formatting Fix #4311. Excel applies Conditional Formatting rules according to a priority specified in the xml. The priority must be a natural number; the rules are applied in order from lowest priority number to highest. When reading an Xlsx spreadsheet, PhpSpreadsheet has been ignoring the priority, which can result in differences from Excel's behavior, especially when CF cell ranges overlap (note that overlapping ranges are not supported in Xls format). If an application uses PhpSpreadsheet to add new Conditional Formatting to a worksheet and does not change its priority from the default (0), the Xlsx Writer will assign a priority with a higher value than any of the CF objects which have been assigned a priority (either from reading it or explicitly assigning it). --- .../Reader/Xlsx/ConditionalStyles.php | 5 +- src/PhpSpreadsheet/Style/Conditional.php | 14 ++ src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 10 +- .../Reader/Xlsx/ConditionalPriorityTest.php | 129 ++++++++++++++++++ .../Reader/Xlsx/Issue4248Test.php | 4 +- tests/data/Reader/XLSX/issue.4312c.xlsx | Bin 0 -> 16063 bytes 6 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalPriorityTest.php create mode 100644 tests/data/Reader/XLSX/issue.4312c.xlsx diff --git a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php index 8731d642e5..a03fa71b24 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php @@ -125,6 +125,7 @@ private function readConditionalRuleFromExt(SimpleXMLElement $cfRuleXml, SimpleX { $conditionType = (string) $attributes->type; $operatorType = (string) $attributes->operator; + $priority = (int) (string) $attributes->priority; $operands = []; foreach ($cfRuleXml->children($this->ns['xm']) as $cfRuleOperandsXml) { @@ -134,6 +135,7 @@ private function readConditionalRuleFromExt(SimpleXMLElement $cfRuleXml, SimpleX $conditional = new Conditional(); $conditional->setConditionType($conditionType); $conditional->setOperatorType($operatorType); + $conditional->setPriority($priority); if ( $conditionType === Conditional::CONDITION_CONTAINSTEXT || $conditionType === Conditional::CONDITION_NOTCONTAINSTEXT @@ -184,7 +186,7 @@ private function readConditionalStyles(SimpleXMLElement $xmlSheet): array private function setConditionalStyles(Worksheet $worksheet, array $conditionals, SimpleXMLElement $xmlExtLst): void { foreach ($conditionals as $cellRangeReference => $cfRules) { - ksort($cfRules); + ksort($cfRules); // no longer needed for Xlsx, but helps Xls $conditionalStyles = $this->readStyleRules($cfRules, $xmlExtLst); // Extract all cell references in $cellRangeReference @@ -205,6 +207,7 @@ private function readStyleRules(array $cfRules, SimpleXMLElement $extLst): array $objConditional = new Conditional(); $objConditional->setConditionType((string) $cfRule['type']); $objConditional->setOperatorType((string) $cfRule['operator']); + $objConditional->setPriority((int) (string) $cfRule['priority']); $objConditional->setNoFormatSet(!isset($cfRule['dxfId'])); if ((string) $cfRule['text'] != '') { diff --git a/src/PhpSpreadsheet/Style/Conditional.php b/src/PhpSpreadsheet/Style/Conditional.php index 01a4d8a9f3..d476bdffd2 100644 --- a/src/PhpSpreadsheet/Style/Conditional.php +++ b/src/PhpSpreadsheet/Style/Conditional.php @@ -106,6 +106,8 @@ class Conditional implements IComparable private bool $noFormatSet = false; + private int $priority = 0; + /** * Create a new Conditional. */ @@ -115,6 +117,18 @@ public function __construct() $this->style = new Style(false, true); } + public function getPriority(): int + { + return $this->priority; + } + + public function setPriority(int $priority): self + { + $this->priority = $priority; + + return $this; + } + public function getNoFormatSet(): bool { return $this->noFormatSet; diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 28af258297..98ee0bd31e 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -861,7 +861,12 @@ private static function writeColorScaleElements(XMLWriter $objWriter, ?Condition private function writeConditionalFormatting(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void { // Conditional id - $id = 1; + $id = 0; + foreach ($worksheet->getConditionalStylesCollection() as $conditionalStyles) { + foreach ($conditionalStyles as $conditional) { + $id = max($id, $conditional->getPriority()); + } + } // Loop through styles in the current worksheet foreach ($worksheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) { @@ -888,7 +893,8 @@ private function writeConditionalFormatting(XMLWriter $objWriter, Phpspreadsheet 'dxfId', (string) $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode()) ); - $objWriter->writeAttribute('priority', (string) $id++); + $priority = $conditional->getPriority() ?: ++$id; + $objWriter->writeAttribute('priority', (string) $priority); self::writeAttributeif( $objWriter, diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalPriorityTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalPriorityTest.php new file mode 100644 index 0000000000..542b78d865 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalPriorityTest.php @@ -0,0 +1,129 @@ +load($filename); + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $worksheet = $reloadedSpreadsheet->getActiveSheet(); + $priorities = []; + foreach ($worksheet->getConditionalStylesCollection() as $conditionalStyles) { + foreach ($conditionalStyles as $conditional) { + $priorities[] = $conditional->getPriority(); + } + } + $expected = [27, 2, 3, 4, 1, 22, 14, 5, 6, 7, 20]; + self::assertSame($expected, $priorities); + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testZeroPriority(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([ + [1, 1, 1, 1], + [2, 2, 2, 2], + [3, 3, 3, 3], + [4, 4, 4, 4], + [5, 5, 5, 5], + ]); + + $range = 'A1:A5'; + $styles = []; + $new = new Conditional(); + $new->setConditionType(Conditional::CONDITION_CELLIS) + ->setOperatorType(Conditional::OPERATOR_EQUAL) + ->setPriority(30) + ->setConditions(['3']) + ->getStyle() + ->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getStartColor() + ->setArgb('FFC00000'); + $styles[] = $new; + $sheet->setConditionalStyles($range, $styles); + + $range = 'B1:B5'; + $styles = []; + $new = new Conditional(); + $new->setConditionType(Conditional::CONDITION_EXPRESSION) + ->setConditions('=MOD(A1,2)=0') + ->getStyle() + ->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getStartColor() + ->setArgb('FF00B0F0'); + $styles[] = $new; + $new = new Conditional(); + $new->setConditionType(Conditional::CONDITION_CELLIS) + ->setOperatorType(Conditional::OPERATOR_EQUAL) + ->setPriority(40) + ->setConditions(['4']) + ->getStyle() + ->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getStartColor() + ->setArgb('FFFFC000'); + $styles[] = $new; + $sheet->setConditionalStyles($range, $styles); + + $range = 'C1:C5'; + $styles = []; + $new = new Conditional(); + $new->setConditionType(Conditional::CONDITION_CELLIS) + ->setOperatorType(Conditional::OPERATOR_EQUAL) + ->setPriority(20) + ->setConditions(['2']) + ->getStyle() + ->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getStartColor() + ->setArgb('FFFFFF00'); + $styles[] = $new; + $new = new Conditional(); + $new->setConditionType(Conditional::CONDITION_CELLIS) + ->setOperatorType(Conditional::OPERATOR_EQUAL) + ->setConditions(['5']) + ->getStyle() + ->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getStartColor() + ->setArgb('FF008080'); + $styles[] = $new; + $sheet->setConditionalStyles($range, $styles); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $worksheet = $reloadedSpreadsheet->getActiveSheet(); + $priorities = []; + foreach ($worksheet->getConditionalStylesCollection() as $conditionalStyles) { + foreach ($conditionalStyles as $conditional) { + $priorities[] = $conditional->getPriority(); + } + } + // B1:B5 is written in order 41, 40, but Reader sorts them + $expected = [30, 40, 41, 20, 42]; + self::assertSame($expected, $priorities); + $styles = $worksheet->getConditionalStyles('B1:B5'); + self::assertSame(Conditional::CONDITION_CELLIS, $styles[0]->getConditionType()); + self::assertSame(40, $styles[0]->getPriority()); + self::assertSame(Conditional::CONDITION_EXPRESSION, $styles[1]->getConditionType()); + self::assertSame(41, $styles[1]->getPriority()); + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4248Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4248Test.php index 06b0b7f31c..de5ec8f029 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4248Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4248Test.php @@ -66,13 +66,13 @@ public function testStyles(): void $file .= '#xl/worksheets/sheet1.xml'; $data = file_get_contents($file) ?: ''; $expected = '' - . '' + . '' . 'NOT(ISERROR(SEARCH("Oui",C16)))' . '' . ''; self::assertStringContainsString($expected, $data, 'first condition for D18'); $expected = '' - . '' + . '' . 'NOT(ISERROR(SEARCH("Non",C16)))' . '' . ''; diff --git a/tests/data/Reader/XLSX/issue.4312c.xlsx b/tests/data/Reader/XLSX/issue.4312c.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..0446ed6cc0193c80958e77507d5db181e79ff1f9 GIT binary patch literal 16063 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 literal 0 HcmV?d00001