Skip to content

Commit 3360608

Browse files
authored
Merge pull request PHPOffice#4042 from oleibman/issue4039
Conditional Range Unions and Intersections
2 parents 1852923 + a6000b6 commit 3360608

File tree

9 files changed

+282
-11
lines changed

9 files changed

+282
-11
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
1313

1414
### Changed
1515

16-
- Nothing
16+
- On read, Xlsx Reader had been breaking up union ranges into separate individual ranges. It will now try to preserve range as it was read in. [PR #4042](https://github.com/PHPOffice/PhpSpreadsheet/pull/4042)
1717

1818
### Deprecated
1919

@@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
3131
- Conditional Color Scale Improvements. [Issue #4049](https://github.com/PHPOffice/PhpSpreadsheet/issues/4049) [PR #4050](https://github.com/PHPOffice/PhpSpreadsheet/pull/4050)
3232
- Mpdf and Tcpdf Borders on Merged Cells. [Issue #3557](https://github.com/PHPOffice/PhpSpreadsheet/issues/3557) [PR #4047](https://github.com/PHPOffice/PhpSpreadsheet/pull/4047)
3333
- Xls Conditional Format Improvements. [PR #4030](https://github.com/PHPOffice/PhpSpreadsheet/pull/4030) [PR #4033](https://github.com/PHPOffice/PhpSpreadsheet/pull/4033)
34+
- Conditional Range Unions and Intersections [Issue #4039](https://github.com/PHPOffice/PhpSpreadsheet/issues/4039) [PR #4042](https://github.com/PHPOffice/PhpSpreadsheet/pull/4042)
3435
- Csv Reader allow use of html mimetype. [Issue #4036](https://github.com/PHPOffice/PhpSpreadsheet/issues/4036) [PR #4049](https://github.com/PHPOffice/PhpSpreadsheet/pull/4040)
3536

3637
## 2024-05-11 - 2.1.0

src/PhpSpreadsheet/Cell/Coordinate.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,44 @@ private static function sortCellReferenceArray(array $cellList): array
504504
return array_values($sortKeys);
505505
}
506506

507+
/**
508+
* Get all cell references applying union and intersection.
509+
*
510+
* @param string $cellBlock A cell range e.g. A1:B5,D1:E5 B2:C4
511+
*
512+
* @return string A string without intersection operator.
513+
* If there was no intersection to begin with, return original argument.
514+
* Otherwise, return cells and/or cell ranges in that range separated by comma.
515+
*/
516+
public static function resolveUnionAndIntersection(string $cellBlock, string $implodeCharacter = ','): string
517+
{
518+
$cellBlock = preg_replace('/ +/', ' ', trim($cellBlock)) ?? $cellBlock;
519+
$cellBlock = preg_replace('/ ,/', ',', $cellBlock) ?? $cellBlock;
520+
$cellBlock = preg_replace('/, /', ',', $cellBlock) ?? $cellBlock;
521+
$array1 = [];
522+
$blocks = explode(',', $cellBlock);
523+
foreach ($blocks as $block) {
524+
$block0 = explode(' ', $block);
525+
if (count($block0) === 1) {
526+
$array1 = array_merge($array1, $block0);
527+
} else {
528+
$blockIdx = -1;
529+
$array2 = [];
530+
foreach ($block0 as $block00) {
531+
++$blockIdx;
532+
if ($blockIdx === 0) {
533+
$array2 = self::getReferencesForCellBlock($block00);
534+
} else {
535+
$array2 = array_intersect($array2, self::getReferencesForCellBlock($block00));
536+
}
537+
}
538+
$array1 = array_merge($array1, $array2);
539+
}
540+
}
541+
542+
return implode($implodeCharacter, $array1);
543+
}
544+
507545
/**
508546
* Get all cell references for an individual cell block.
509547
*

src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,10 @@ private function setConditionalStyles(Worksheet $worksheet, array $conditionals,
188188
$conditionalStyles = $this->readStyleRules($cfRules, $xmlExtLst);
189189

190190
// Extract all cell references in $cellRangeReference
191-
$cellBlocks = explode(' ', str_replace('$', '', strtoupper($cellRangeReference)));
192-
foreach ($cellBlocks as $cellBlock) {
193-
$worksheet->getStyle($cellBlock)->setConditionalStyles($conditionalStyles);
194-
}
191+
// N.B. In Excel UI, intersection is space and union is comma.
192+
// But in Xml, intersection is comma and union is space.
193+
$cellRangeReference = str_replace(['$', ' ', ',', '^'], ['', '^', ' ', ','], strtoupper($cellRangeReference));
194+
$worksheet->getStyle($cellRangeReference)->setConditionalStyles($conditionalStyles);
195195
}
196196
}
197197

src/PhpSpreadsheet/Worksheet/Worksheet.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,8 +1422,11 @@ public function getConditionalStyles(string $coordinate): array
14221422

14231423
$cell = $this->getCell($coordinate);
14241424
foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
1425-
if ($cell->isInRange($conditionalRange)) {
1426-
return $this->conditionalStylesCollection[$conditionalRange];
1425+
$cellBlocks = explode(',', Coordinate::resolveUnionAndIntersection($conditionalRange));
1426+
foreach ($cellBlocks as $cellBlock) {
1427+
if ($cell->isInRange($cellBlock)) {
1428+
return $this->conditionalStylesCollection[$conditionalRange];
1429+
}
14271430
}
14281431
}
14291432

@@ -1435,8 +1438,11 @@ public function getConditionalRange(string $coordinate): ?string
14351438
$coordinate = strtoupper($coordinate);
14361439
$cell = $this->getCell($coordinate);
14371440
foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
1438-
if ($cell->isInRange($conditionalRange)) {
1439-
return $conditionalRange;
1441+
$cellBlocks = explode(',', Coordinate::resolveUnionAndIntersection($conditionalRange));
1442+
foreach ($cellBlocks as $cellBlock) {
1443+
if ($cell->isInRange($cellBlock)) {
1444+
return $conditionalRange;
1445+
}
14401446
}
14411447
}
14421448

src/PhpSpreadsheet/Writer/Xls/Worksheet.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,13 @@ private function writeConditionalFormatting(): void
493493
{
494494
$conditionalFormulaHelper = new ConditionalHelper($this->parser);
495495

496-
$arrConditionalStyles = $this->phpSheet->getConditionalStylesCollection();
496+
$arrConditionalStyles = [];
497+
foreach ($this->phpSheet->getConditionalStylesCollection() as $key => $value) {
498+
$keyExplode = explode(',', Coordinate::resolveUnionAndIntersection($key));
499+
foreach ($keyExplode as $exploded) {
500+
$arrConditionalStyles[$exploded] = $value;
501+
}
502+
}
497503
if (!empty($arrConditionalStyles)) {
498504
// Write ConditionalFormattingTable records
499505
foreach ($arrConditionalStyles as $cellCoordinate => $conditionalStyles) {

src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -860,7 +860,11 @@ private function writeConditionalFormatting(XMLWriter $objWriter, Phpspreadsheet
860860
// Loop through styles in the current worksheet
861861
foreach ($worksheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) {
862862
$objWriter->startElement('conditionalFormatting');
863-
$objWriter->writeAttribute('sqref', $cellCoordinate);
863+
// N.B. In Excel UI, intersection is space and union is comma.
864+
// But in Xml, intersection is comma and union is space.
865+
// Anyhow, I don't think Excel handles intersection correctly when reading.
866+
$outCoordinate = Coordinate::resolveUnionAndIntersection(str_replace('$', '', $cellCoordinate), ' ');
867+
$objWriter->writeAttribute('sqref', $outCoordinate);
864868

865869
foreach ($conditionalStyles as $conditional) {
866870
// WHY was this again?
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx;
6+
7+
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
8+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
9+
use PhpOffice\PhpSpreadsheet\Style\Conditional;
10+
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
11+
12+
class Issue4039Test extends AbstractFunctional
13+
{
14+
private static string $testbook = 'tests/data/Style/ConditionalFormatting/CellMatcher.xlsx';
15+
16+
public function testUnionRange(): void
17+
{
18+
$reader = new Xlsx();
19+
$spreadsheet = $reader->load(self::$testbook);
20+
$sheet = $spreadsheet->getSheetByNameOrThrow('cellIs Expression');
21+
$expected = [
22+
'A12:D17,A20', // split range
23+
'A22:D27',
24+
'A2:E6',
25+
];
26+
self::assertSame($expected, array_keys($sheet->getConditionalStylesCollection()));
27+
self::assertSame($expected[0], $sheet->getConditionalRange('A20'));
28+
self::assertSame($expected[0], $sheet->getConditionalRange('C15'));
29+
self::assertNull($sheet->getConditionalRange('A19'));
30+
self::assertSame($expected[1], $sheet->getConditionalRange('D25'));
31+
$spreadsheet->disconnectWorksheets();
32+
}
33+
34+
public function testIntersectionRange(): void
35+
{
36+
$spreadsheet = new Spreadsheet();
37+
$sheet = $spreadsheet->getActiveSheet();
38+
$sheet->fromArray([
39+
[1, 2, 3, 4, 5],
40+
[2, 3, 4, 5, 6],
41+
[3, 4, 5, 6, 7],
42+
]);
43+
$condition1 = new Conditional();
44+
$condition1->setConditionType(Conditional::CONDITION_CELLIS);
45+
$condition1->setOperatorType(Conditional::OPERATOR_BETWEEN);
46+
$condition1->setConditions([2, 3]);
47+
$condition1->getStyle()->getFont()
48+
->setBold(true);
49+
$conditionalStyles = [$condition1];
50+
// Writer will change this range to equivalent 'B1,B2,B3'
51+
$sheet->setConditionalStyles('A1:C3 B1:B3', $conditionalStyles);
52+
$robj = $this->writeAndReload($spreadsheet, 'Xlsx');
53+
$spreadsheet->disconnectWorksheets();
54+
$sheet0 = $robj->getActiveSheet();
55+
$conditionals = $sheet0->getConditionalStylesCollection();
56+
self::assertSame(['B1,B2,B3'], array_keys($conditionals));
57+
$cond1 = $conditionals['B1,B2,B3'][0];
58+
self::assertSame(Conditional::CONDITION_CELLIS, $cond1->getConditionType());
59+
self::assertSame(Conditional::OPERATOR_BETWEEN, $cond1->getOperatorType());
60+
self::assertSame(['2', '3'], $cond1->getConditions());
61+
$font1 = $cond1->getStyle()->getFont();
62+
self::assertTrue($font1->getBold());
63+
$robj->disconnectWorksheets();
64+
}
65+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Worksheet;
6+
7+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
8+
use PhpOffice\PhpSpreadsheet\Style\Conditional;
9+
use PHPUnit\Framework\TestCase;
10+
11+
class ConditionalIntersectionTest extends TestCase
12+
{
13+
public function testGetConditionalStyles(): void
14+
{
15+
$spreadsheet = new Spreadsheet();
16+
$sheet = $spreadsheet->getActiveSheet();
17+
$sheet->fromArray([
18+
[1, 2, 3, 4, 5],
19+
[2, 3, 4, 5, 6],
20+
[3, 4, 5, 6, 7],
21+
]);
22+
$condition1 = new Conditional();
23+
$condition1->setConditionType(Conditional::CONDITION_CELLIS);
24+
$condition1->setOperatorType(Conditional::OPERATOR_BETWEEN);
25+
$condition1->setConditions([2, 3]);
26+
$condition1->getStyle()->getFont()
27+
->setBold(true);
28+
$conditionalStyles = [$condition1];
29+
$sheet->setConditionalStyles('A1:C3 B1:B3', $conditionalStyles);
30+
self::assertEmpty($sheet->getConditionalStyles('A2'));
31+
$cond = $sheet->getConditionalStyles('B2');
32+
self::assertCount(1, $cond);
33+
self::assertSame(Conditional::CONDITION_CELLIS, $cond[0]->getConditionType());
34+
self::assertSame(Conditional::OPERATOR_BETWEEN, $cond[0]->getOperatorType());
35+
self::assertSame([2, 3], $cond[0]->getConditions());
36+
self::assertTrue($cond[0]->getStyle()->getFont()->getBold());
37+
$spreadsheet->disconnectWorksheets();
38+
}
39+
40+
public function testGetConditionalRange(): void
41+
{
42+
$spreadsheet = new Spreadsheet();
43+
$sheet = $spreadsheet->getActiveSheet();
44+
$sheet->fromArray([
45+
[1, 2, 3, 4, 5],
46+
[2, 3, 4, 5, 6],
47+
[3, 4, 5, 6, 7],
48+
]);
49+
$condition1 = new Conditional();
50+
$condition1->setConditionType(Conditional::CONDITION_CELLIS);
51+
$condition1->setOperatorType(Conditional::OPERATOR_BETWEEN);
52+
$condition1->setConditions([2, 3]);
53+
$condition1->getStyle()->getFont()
54+
->setBold(true);
55+
$conditionalStyles = [$condition1];
56+
$sheet->setConditionalStyles('A1:C3 B1:B3', $conditionalStyles);
57+
self::assertNull($sheet->getConditionalRange('A2'));
58+
self::assertSame('A1:C3 B1:B3', $sheet->getConditionalRange('B2'));
59+
$spreadsheet->disconnectWorksheets();
60+
}
61+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Writer\Xls;
6+
7+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
8+
use PhpOffice\PhpSpreadsheet\Style\Conditional;
9+
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
10+
11+
class ConditionalUnionTest extends AbstractFunctional
12+
{
13+
public function testConditionalUnion(): void
14+
{
15+
$spreadsheet = new Spreadsheet();
16+
$sheet = $spreadsheet->getActiveSheet();
17+
$sheet->fromArray([
18+
[1, 2, 3, 4, 5],
19+
[2, 3, 4, 5, 6],
20+
[3, 4, 5, 6, 7],
21+
]);
22+
$condition1 = new Conditional();
23+
$condition1->setConditionType(Conditional::CONDITION_CELLIS);
24+
$condition1->setOperatorType(Conditional::OPERATOR_BETWEEN);
25+
$condition1->setConditions([2, 4]);
26+
$condition1->getStyle()->getFont()
27+
->setBold(true);
28+
$conditionalStyles = [$condition1];
29+
$sheet->setConditionalStyles('A1:A3,C1:E3', $conditionalStyles);
30+
31+
$robj = $this->writeAndReload($spreadsheet, 'Xls');
32+
$spreadsheet->disconnectWorksheets();
33+
$sheet0 = $robj->getActiveSheet();
34+
$conditionals = $sheet0->getConditionalStylesCollection();
35+
self::assertSame(['A1:A3', 'C1:E3'], array_keys($conditionals));
36+
$cond1 = $conditionals['A1:A3'][0];
37+
self::assertSame(Conditional::CONDITION_CELLIS, $cond1->getConditionType());
38+
self::assertSame(Conditional::OPERATOR_BETWEEN, $cond1->getOperatorType());
39+
self::assertSame([2, 4], $cond1->getConditions());
40+
$font1 = $cond1->getStyle()->getFont();
41+
self::assertTrue($font1->getBold());
42+
43+
$cond2 = $conditionals['C1:E3'][0];
44+
self::assertSame(Conditional::CONDITION_CELLIS, $cond2->getConditionType());
45+
self::assertSame(Conditional::OPERATOR_BETWEEN, $cond2->getOperatorType());
46+
self::assertSame([2, 4], $cond2->getConditions());
47+
$font2 = $cond2->getStyle()->getFont();
48+
self::assertTrue($font2->getBold());
49+
$robj->disconnectWorksheets();
50+
}
51+
52+
public function testIntersectionRange(): void
53+
{
54+
$spreadsheet = new Spreadsheet();
55+
$sheet = $spreadsheet->getActiveSheet();
56+
$sheet->fromArray([
57+
[1, 2, 3, 4, 5],
58+
[2, 3, 4, 5, 6],
59+
[3, 4, 5, 6, 7],
60+
]);
61+
$condition1 = new Conditional();
62+
$condition1->setConditionType(Conditional::CONDITION_CELLIS);
63+
$condition1->setOperatorType(Conditional::OPERATOR_BETWEEN);
64+
$condition1->setConditions([2, 3]);
65+
$condition1->getStyle()->getFont()
66+
->setBold(true);
67+
$conditionalStyles = [$condition1];
68+
$sheet->setConditionalStyles('A1:B5,D1:E5 B2:D4', $conditionalStyles);
69+
$robj = $this->writeAndReload($spreadsheet, 'Xls');
70+
$spreadsheet->disconnectWorksheets();
71+
$sheet0 = $robj->getActiveSheet();
72+
$conditionals = $sheet0->getConditionalStylesCollection();
73+
self::assertSame(['A1:B5', 'D2', 'D3', 'D4'], array_keys($conditionals));
74+
75+
$cond1 = $conditionals['A1:B5'][0];
76+
self::assertSame(Conditional::CONDITION_CELLIS, $cond1->getConditionType());
77+
self::assertSame(Conditional::OPERATOR_BETWEEN, $cond1->getOperatorType());
78+
self::assertSame([2, 3], $cond1->getConditions());
79+
$font1 = $cond1->getStyle()->getFont();
80+
self::assertTrue($font1->getBold());
81+
82+
$cond2 = $conditionals['D2'][0];
83+
self::assertSame(Conditional::CONDITION_CELLIS, $cond2->getConditionType());
84+
self::assertSame(Conditional::OPERATOR_BETWEEN, $cond2->getOperatorType());
85+
self::assertSame([2, 3], $cond2->getConditions());
86+
$font2 = $cond2->getStyle()->getFont();
87+
self::assertTrue($font2->getBold());
88+
$robj->disconnectWorksheets();
89+
}
90+
}

0 commit comments

Comments
 (0)