Skip to content

Commit cf6c804

Browse files
committed
Full(?) Conditional Range Union and Intersection Support
Provide a means to convert a range, possibly with unions and possibly with intersections, into something that both Excel and PhpSpreadsheet can handle. Intersections are changed into unions of the individual cells which they comprise. With this change, Xls Writer now handles intersections (previously it would have thrown an Exception or created a corrupt worksheet if this was attempted), and Xlsx Writer works correctly (it seemed to before, but Excel didn't understand what it wrote). Worksheet::getConditionalRange and ::getConditionalStyles would previously have thrown an Exception when presented with an intersection, and will no longer do so. **NOTE:** Intersection support is limited to Conditional ranges. Use of intersections in other contexts will usually not achieve the desired result.
1 parent c65674c commit cf6c804

File tree

7 files changed

+179
-6
lines changed

7 files changed

+179
-6
lines changed

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/Worksheet/Worksheet.php

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

14231423
$cell = $this->getCell($coordinate);
14241424
foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
1425-
$cellBlocks = explode(',', $conditionalRange);
1425+
$cellBlocks = explode(',', Coordinate::resolveUnionAndIntersection($conditionalRange));
14261426
foreach ($cellBlocks as $cellBlock) {
14271427
if ($cell->isInRange($cellBlock)) {
14281428
return $this->conditionalStylesCollection[$conditionalRange];
@@ -1438,7 +1438,7 @@ public function getConditionalRange(string $coordinate): ?string
14381438
$coordinate = strtoupper($coordinate);
14391439
$cell = $this->getCell($coordinate);
14401440
foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
1441-
$cellBlocks = explode(',', $conditionalRange);
1441+
$cellBlocks = explode(',', Coordinate::resolveUnionAndIntersection($conditionalRange));
14421442
foreach ($cellBlocks as $cellBlock) {
14431443
if ($cell->isInRange($cellBlock)) {
14441444
return $conditionalRange;

src/PhpSpreadsheet/Writer/Xls/Worksheet.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ private function writeConditionalFormatting(): void
495495

496496
$arrConditionalStyles = [];
497497
foreach ($this->phpSheet->getConditionalStylesCollection() as $key => $value) {
498-
$keyExplode = explode(',', $key);
498+
$keyExplode = explode(',', Coordinate::resolveUnionAndIntersection($key));
499499
foreach ($keyExplode as $exploded) {
500500
$arrConditionalStyles[$exploded] = $value;
501501
}

src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,9 @@ private function writeConditionalFormatting(XMLWriter $objWriter, Phpspreadsheet
817817
$objWriter->startElement('conditionalFormatting');
818818
// N.B. In Excel UI, intersection is space and union is comma.
819819
// But in Xml, intersection is comma and union is space.
820-
$objWriter->writeAttribute('sqref', str_replace(['$', ' ', ',', '^'], ['', '^', ' ', ','], $cellCoordinate));
820+
// Anyhow, I don't think Excel handles intersection correctly when reading.
821+
$outCoordinate = Coordinate::resolveUnionAndIntersection(str_replace('$', '', $cellCoordinate), ' ');
822+
$objWriter->writeAttribute('sqref', $outCoordinate);
821823

822824
foreach ($conditionalStyles as $conditional) {
823825
// WHY was this again?

tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4039Test.php

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx;
66

77
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
8+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
9+
use PhpOffice\PhpSpreadsheet\Style\Conditional;
10+
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
811

9-
class Issue4039Test extends \PHPUnit\Framework\TestCase
12+
class Issue4039Test extends AbstractFunctional
1013
{
1114
private static string $testbook = 'tests/data/Style/ConditionalFormatting/CellMatcher.xlsx';
1215

13-
public function testSplitRange(): void
16+
public function testUnionRange(): void
1417
{
1518
$reader = new Xlsx();
1619
$spreadsheet = $reader->load(self::$testbook);
@@ -27,4 +30,36 @@ public function testSplitRange(): void
2730
self::assertSame($expected[1], $sheet->getConditionalRange('D25'));
2831
$spreadsheet->disconnectWorksheets();
2932
}
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+
}
3065
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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+
}
38+
39+
public function testGetConditionalRange(): void
40+
{
41+
$spreadsheet = new Spreadsheet();
42+
$sheet = $spreadsheet->getActiveSheet();
43+
$sheet->fromArray([
44+
[1, 2, 3, 4, 5],
45+
[2, 3, 4, 5, 6],
46+
[3, 4, 5, 6, 7],
47+
]);
48+
$condition1 = new Conditional();
49+
$condition1->setConditionType(Conditional::CONDITION_CELLIS);
50+
$condition1->setOperatorType(Conditional::OPERATOR_BETWEEN);
51+
$condition1->setConditions([2, 3]);
52+
$condition1->getStyle()->getFont()
53+
->setBold(true);
54+
$conditionalStyles = [$condition1];
55+
$sheet->setConditionalStyles('A1:C3 B1:B3', $conditionalStyles);
56+
self::assertNull($sheet->getConditionalRange('A2'));
57+
self::assertSame('A1:C3 B1:B3', $sheet->getConditionalRange('B2'));
58+
}
59+
}

tests/PhpSpreadsheetTests/Writer/Xls/ConditionalUnionTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,43 @@ public function testConditionalUnion(): void
4848
self::assertTrue($font2->getBold());
4949
$robj->disconnectWorksheets();
5050
}
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+
}
5190
}

0 commit comments

Comments
 (0)