Skip to content

Commit eafbed6

Browse files
authored
Merge pull request #4314 from oleibman/issue4312
CF Priority Property and Overlapping Ranges
2 parents e1ae687 + 8d7500b commit eafbed6

File tree

9 files changed

+240
-27
lines changed

9 files changed

+240
-27
lines changed

src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ private function readConditionalRuleFromExt(SimpleXMLElement $cfRuleXml, SimpleX
125125
{
126126
$conditionType = (string) $attributes->type;
127127
$operatorType = (string) $attributes->operator;
128+
$priority = (int) (string) $attributes->priority;
128129

129130
$operands = [];
130131
foreach ($cfRuleXml->children($this->ns['xm']) as $cfRuleOperandsXml) {
@@ -134,6 +135,7 @@ private function readConditionalRuleFromExt(SimpleXMLElement $cfRuleXml, SimpleX
134135
$conditional = new Conditional();
135136
$conditional->setConditionType($conditionType);
136137
$conditional->setOperatorType($operatorType);
138+
$conditional->setPriority($priority);
137139
if (
138140
$conditionType === Conditional::CONDITION_CONTAINSTEXT
139141
|| $conditionType === Conditional::CONDITION_NOTCONTAINSTEXT
@@ -184,7 +186,7 @@ private function readConditionalStyles(SimpleXMLElement $xmlSheet): array
184186
private function setConditionalStyles(Worksheet $worksheet, array $conditionals, SimpleXMLElement $xmlExtLst): void
185187
{
186188
foreach ($conditionals as $cellRangeReference => $cfRules) {
187-
ksort($cfRules);
189+
ksort($cfRules); // no longer needed for Xlsx, but helps Xls
188190
$conditionalStyles = $this->readStyleRules($cfRules, $xmlExtLst);
189191

190192
// Extract all cell references in $cellRangeReference
@@ -205,6 +207,7 @@ private function readStyleRules(array $cfRules, SimpleXMLElement $extLst): array
205207
$objConditional = new Conditional();
206208
$objConditional->setConditionType((string) $cfRule['type']);
207209
$objConditional->setOperatorType((string) $cfRule['operator']);
210+
$objConditional->setPriority((int) (string) $cfRule['priority']);
208211
$objConditional->setNoFormatSet(!isset($cfRule['dxfId']));
209212

210213
if ((string) $cfRule['text'] != '') {

src/PhpSpreadsheet/Style/Conditional.php

+14
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ class Conditional implements IComparable
106106

107107
private bool $noFormatSet = false;
108108

109+
private int $priority = 0;
110+
109111
/**
110112
* Create a new Conditional.
111113
*/
@@ -115,6 +117,18 @@ public function __construct()
115117
$this->style = new Style(false, true);
116118
}
117119

120+
public function getPriority(): int
121+
{
122+
return $this->priority;
123+
}
124+
125+
public function setPriority(int $priority): self
126+
{
127+
$this->priority = $priority;
128+
129+
return $this;
130+
}
131+
118132
public function getNoFormatSet(): bool
119133
{
120134
return $this->noFormatSet;

src/PhpSpreadsheet/Worksheet/Worksheet.php

+51-22
Original file line numberDiff line numberDiff line change
@@ -1432,27 +1432,68 @@ public function getStyle(AddressRange|CellAddress|int|string|array $cellCoordina
14321432
* included in a conditional style range.
14331433
* If a range of cells is specified, then the styles will only be returned if the range matches the entire
14341434
* range of the conditional.
1435+
* @param bool $firstOnly default true, return all matching
1436+
* conditionals ordered by priority if false, first only if true
14351437
*
14361438
* @return Conditional[]
14371439
*/
1438-
public function getConditionalStyles(string $coordinate): array
1440+
public function getConditionalStyles(string $coordinate, bool $firstOnly = true): array
14391441
{
14401442
$coordinate = strtoupper($coordinate);
1441-
if (str_contains($coordinate, ':')) {
1443+
if (preg_match('/[: ,]/', $coordinate) === 1) {
14421444
return $this->conditionalStylesCollection[$coordinate] ?? [];
14431445
}
14441446

1445-
$cell = $this->getCell($coordinate);
1446-
foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
1447-
$cellBlocks = explode(',', Coordinate::resolveUnionAndIntersection($conditionalRange));
1448-
foreach ($cellBlocks as $cellBlock) {
1449-
if ($cell->isInRange($cellBlock)) {
1450-
return $this->conditionalStylesCollection[$conditionalRange];
1447+
$conditionalStyles = [];
1448+
foreach ($this->conditionalStylesCollection as $keyStylesOrig => $conditionalRange) {
1449+
$keyStyles = Coordinate::resolveUnionAndIntersection($keyStylesOrig);
1450+
$keyParts = explode(',', $keyStyles);
1451+
foreach ($keyParts as $keyPart) {
1452+
if ($keyPart === $coordinate) {
1453+
if ($firstOnly) {
1454+
return $conditionalRange;
1455+
}
1456+
$conditionalStyles[$keyStylesOrig] = $conditionalRange;
1457+
1458+
break;
1459+
} elseif (str_contains($keyPart, ':')) {
1460+
if (Coordinate::coordinateIsInsideRange($keyPart, $coordinate)) {
1461+
if ($firstOnly) {
1462+
return $conditionalRange;
1463+
}
1464+
$conditionalStyles[$keyStylesOrig] = $conditionalRange;
1465+
1466+
break;
1467+
}
14511468
}
14521469
}
14531470
}
1471+
$outArray = [];
1472+
foreach ($conditionalStyles as $conditionalArray) {
1473+
foreach ($conditionalArray as $conditional) {
1474+
$outArray[] = $conditional;
1475+
}
1476+
}
1477+
usort($outArray, [self::class, 'comparePriority']);
14541478

1455-
return [];
1479+
return $outArray;
1480+
}
1481+
1482+
private static function comparePriority(Conditional $condA, Conditional $condB): int
1483+
{
1484+
$a = $condA->getPriority();
1485+
$b = $condB->getPriority();
1486+
if ($a === $b) {
1487+
return 0;
1488+
}
1489+
if ($a === 0) {
1490+
return 1;
1491+
}
1492+
if ($b === 0) {
1493+
return -1;
1494+
}
1495+
1496+
return ($a < $b) ? -1 : 1;
14561497
}
14571498

14581499
public function getConditionalRange(string $coordinate): ?string
@@ -1482,19 +1523,7 @@ public function getConditionalRange(string $coordinate): ?string
14821523
*/
14831524
public function conditionalStylesExists(string $coordinate): bool
14841525
{
1485-
$coordinate = strtoupper($coordinate);
1486-
if (str_contains($coordinate, ':')) {
1487-
return isset($this->conditionalStylesCollection[$coordinate]);
1488-
}
1489-
1490-
$cell = $this->getCell($coordinate);
1491-
foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
1492-
if ($cell->isInRange($conditionalRange)) {
1493-
return true;
1494-
}
1495-
}
1496-
1497-
return false;
1526+
return !empty($this->getConditionalStyles($coordinate));
14981527
}
14991528

15001529
/**

src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php

+8-2
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,12 @@ private static function writeColorScaleElements(XMLWriter $objWriter, ?Condition
862862
private function writeConditionalFormatting(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
863863
{
864864
// Conditional id
865-
$id = 1;
865+
$id = 0;
866+
foreach ($worksheet->getConditionalStylesCollection() as $conditionalStyles) {
867+
foreach ($conditionalStyles as $conditional) {
868+
$id = max($id, $conditional->getPriority());
869+
}
870+
}
866871

867872
// Loop through styles in the current worksheet
868873
foreach ($worksheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) {
@@ -889,7 +894,8 @@ private function writeConditionalFormatting(XMLWriter $objWriter, Phpspreadsheet
889894
'dxfId',
890895
(string) $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode())
891896
);
892-
$objWriter->writeAttribute('priority', (string) $id++);
897+
$priority = $conditional->getPriority() ?: ++$id;
898+
$objWriter->writeAttribute('priority', (string) $priority);
893899

894900
self::writeAttributeif(
895901
$objWriter,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx;
6+
7+
use PhpOffice\PhpSpreadsheet\IOFactory;
8+
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
9+
10+
class ConditionalPriority2Test extends AbstractFunctional
11+
{
12+
public function testConditionalPriority(): void
13+
{
14+
$filename = 'tests/data/Reader/XLSX/issue.4318.xlsx';
15+
$reader = IOFactory::createReader('Xlsx');
16+
$spreadsheet = $reader->load($filename);
17+
$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx');
18+
$spreadsheet->disconnectWorksheets();
19+
$sheet = $reloadedSpreadsheet->getActiveSheet();
20+
$rules = $sheet->getConditionalStyles('E5', false);
21+
self::assertCount(4, $rules);
22+
self::assertSame(2, $rules[0]->getPriority());
23+
self::assertSame(['$B$2<2'], $rules[0]->getConditions());
24+
self::assertSame(3, $rules[1]->getPriority());
25+
self::assertSame(['$B$2=""'], $rules[1]->getConditions());
26+
self::assertSame(4, $rules[2]->getPriority());
27+
self::assertSame(['$B$2=3'], $rules[2]->getConditions());
28+
self::assertSame(5, $rules[3]->getPriority());
29+
self::assertSame(['$B$2=2'], $rules[3]->getConditions());
30+
$reloadedSpreadsheet->disconnectWorksheets();
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx;
6+
7+
use PhpOffice\PhpSpreadsheet\IOFactory;
8+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
9+
use PhpOffice\PhpSpreadsheet\Style\Conditional;
10+
use PhpOffice\PhpSpreadsheet\Style\Fill;
11+
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
12+
13+
class ConditionalPriorityTest extends AbstractFunctional
14+
{
15+
public function testConditionalPriority(): void
16+
{
17+
$filename = 'tests/data/Reader/XLSX/issue.4312c.xlsx';
18+
$reader = IOFactory::createReader('Xlsx');
19+
$spreadsheet = $reader->load($filename);
20+
$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx');
21+
$spreadsheet->disconnectWorksheets();
22+
$worksheet = $reloadedSpreadsheet->getActiveSheet();
23+
$priorities = [];
24+
foreach ($worksheet->getConditionalStylesCollection() as $conditionalStyles) {
25+
foreach ($conditionalStyles as $conditional) {
26+
$priorities[] = $conditional->getPriority();
27+
}
28+
}
29+
$expected = [27, 2, 3, 4, 1, 22, 14, 5, 6, 7, 20];
30+
self::assertSame($expected, $priorities);
31+
$reloadedSpreadsheet->disconnectWorksheets();
32+
}
33+
34+
public function testZeroPriority(): void
35+
{
36+
$spreadsheet = new Spreadsheet();
37+
$sheet = $spreadsheet->getActiveSheet();
38+
$sheet->fromArray([
39+
[1, 1, 1, 1],
40+
[2, 2, 2, 2],
41+
[3, 3, 3, 3],
42+
[4, 4, 4, 4],
43+
[5, 5, 5, 5],
44+
]);
45+
46+
$range = 'A1:A5';
47+
$styles = [];
48+
$new = new Conditional();
49+
$new->setConditionType(Conditional::CONDITION_CELLIS)
50+
->setOperatorType(Conditional::OPERATOR_EQUAL)
51+
->setPriority(30)
52+
->setConditions(['3'])
53+
->getStyle()
54+
->getFill()
55+
->setFillType(Fill::FILL_SOLID)
56+
->getStartColor()
57+
->setArgb('FFC00000');
58+
$styles[] = $new;
59+
$sheet->setConditionalStyles($range, $styles);
60+
61+
$range = 'B1:B5';
62+
$styles = [];
63+
$new = new Conditional();
64+
$new->setConditionType(Conditional::CONDITION_EXPRESSION)
65+
->setConditions('=MOD(A1,2)=0')
66+
->getStyle()
67+
->getFill()
68+
->setFillType(Fill::FILL_SOLID)
69+
->getStartColor()
70+
->setArgb('FF00B0F0');
71+
$styles[] = $new;
72+
$new = new Conditional();
73+
$new->setConditionType(Conditional::CONDITION_CELLIS)
74+
->setOperatorType(Conditional::OPERATOR_EQUAL)
75+
->setPriority(40)
76+
->setConditions(['4'])
77+
->getStyle()
78+
->getFill()
79+
->setFillType(Fill::FILL_SOLID)
80+
->getStartColor()
81+
->setArgb('FFFFC000');
82+
$styles[] = $new;
83+
$sheet->setConditionalStyles($range, $styles);
84+
85+
$range = 'C1:C5';
86+
$styles = [];
87+
$new = new Conditional();
88+
$new->setConditionType(Conditional::CONDITION_CELLIS)
89+
->setOperatorType(Conditional::OPERATOR_EQUAL)
90+
->setPriority(20)
91+
->setConditions(['2'])
92+
->getStyle()
93+
->getFill()
94+
->setFillType(Fill::FILL_SOLID)
95+
->getStartColor()
96+
->setArgb('FFFFFF00');
97+
$styles[] = $new;
98+
$new = new Conditional();
99+
$new->setConditionType(Conditional::CONDITION_CELLIS)
100+
->setOperatorType(Conditional::OPERATOR_EQUAL)
101+
->setConditions(['5'])
102+
->getStyle()
103+
->getFill()
104+
->setFillType(Fill::FILL_SOLID)
105+
->getStartColor()
106+
->setArgb('FF008080');
107+
$styles[] = $new;
108+
$sheet->setConditionalStyles($range, $styles);
109+
110+
$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx');
111+
$spreadsheet->disconnectWorksheets();
112+
$worksheet = $reloadedSpreadsheet->getActiveSheet();
113+
$priorities = [];
114+
foreach ($worksheet->getConditionalStylesCollection() as $conditionalStyles) {
115+
foreach ($conditionalStyles as $conditional) {
116+
$priorities[] = $conditional->getPriority();
117+
}
118+
}
119+
// B1:B5 is written in order 41, 40, but Reader sorts them
120+
$expected = [30, 40, 41, 20, 42];
121+
self::assertSame($expected, $priorities);
122+
$styles = $worksheet->getConditionalStyles('B1:B5');
123+
self::assertSame(Conditional::CONDITION_CELLIS, $styles[0]->getConditionType());
124+
self::assertSame(40, $styles[0]->getPriority());
125+
self::assertSame(Conditional::CONDITION_EXPRESSION, $styles[1]->getConditionType());
126+
self::assertSame(41, $styles[1]->getPriority());
127+
$reloadedSpreadsheet->disconnectWorksheets();
128+
}
129+
}

tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4248Test.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,13 @@ public function testStyles(): void
6666
$file .= '#xl/worksheets/sheet1.xml';
6767
$data = file_get_contents($file) ?: '';
6868
$expected = '<conditionalFormatting sqref="C16:C38 E17:H18 I17:J37 D18 J23:J38 E38 I38">'
69-
. '<cfRule type="containsText" dxfId="0" priority="1" operator="containsText" text="Oui">'
69+
. '<cfRule type="containsText" dxfId="0" priority="15" operator="containsText" text="Oui">'
7070
. '<formula>NOT(ISERROR(SEARCH(&quot;Oui&quot;,C16)))</formula>'
7171
. '</cfRule>'
7272
. '</conditionalFormatting>';
7373
self::assertStringContainsString($expected, $data, 'first condition for D18');
7474
$expected = '<conditionalFormatting sqref="C16:C38 I17:J37 E17:H18 D18 J23:J38 E38 I38">'
75-
. '<cfRule type="containsText" dxfId="1" priority="2" operator="containsText" text="Non">'
75+
. '<cfRule type="containsText" dxfId="1" priority="14" operator="containsText" text="Non">'
7676
. '<formula>NOT(ISERROR(SEARCH(&quot;Non&quot;,C16)))</formula>'
7777
. '</cfRule>'
7878
. '</conditionalFormatting>';
15.7 KB
Binary file not shown.
8.73 KB
Binary file not shown.

0 commit comments

Comments
 (0)