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