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.
This commit is contained in:
oleibman
2024-05-29 08:19:17 -07:00
parent c65674cbcb
commit cf6c8046eb
7 changed files with 179 additions and 6 deletions
+38
View File
@@ -504,6 +504,44 @@ abstract class Coordinate
return array_values($sortKeys);
}
/**
* Get all cell references applying union and intersection.
*
* @param string $cellBlock A cell range e.g. A1:B5,D1:E5 B2:C4
*
* @return string A string without intersection operator.
* If there was no intersection to begin with, return original argument.
* Otherwise, return cells and/or cell ranges in that range separated by comma.
*/
public static function resolveUnionAndIntersection(string $cellBlock, string $implodeCharacter = ','): string
{
$cellBlock = preg_replace('/ +/', ' ', trim($cellBlock)) ?? $cellBlock;
$cellBlock = preg_replace('/ ,/', ',', $cellBlock) ?? $cellBlock;
$cellBlock = preg_replace('/, /', ',', $cellBlock) ?? $cellBlock;
$array1 = [];
$blocks = explode(',', $cellBlock);
foreach ($blocks as $block) {
$block0 = explode(' ', $block);
if (count($block0) === 1) {
$array1 = array_merge($array1, $block0);
} else {
$blockIdx = -1;
$array2 = [];
foreach ($block0 as $block00) {
++$blockIdx;
if ($blockIdx === 0) {
$array2 = self::getReferencesForCellBlock($block00);
} else {
$array2 = array_intersect($array2, self::getReferencesForCellBlock($block00));
}
}
$array1 = array_merge($array1, $array2);
}
}
return implode($implodeCharacter, $array1);
}
/**
* Get all cell references for an individual cell block.
*
+2 -2
View File
@@ -1422,7 +1422,7 @@ class Worksheet implements IComparable
$cell = $this->getCell($coordinate);
foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
$cellBlocks = explode(',', $conditionalRange);
$cellBlocks = explode(',', Coordinate::resolveUnionAndIntersection($conditionalRange));
foreach ($cellBlocks as $cellBlock) {
if ($cell->isInRange($cellBlock)) {
return $this->conditionalStylesCollection[$conditionalRange];
@@ -1438,7 +1438,7 @@ class Worksheet implements IComparable
$coordinate = strtoupper($coordinate);
$cell = $this->getCell($coordinate);
foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
$cellBlocks = explode(',', $conditionalRange);
$cellBlocks = explode(',', Coordinate::resolveUnionAndIntersection($conditionalRange));
foreach ($cellBlocks as $cellBlock) {
if ($cell->isInRange($cellBlock)) {
return $conditionalRange;
+1 -1
View File
@@ -495,7 +495,7 @@ class Worksheet extends BIFFwriter
$arrConditionalStyles = [];
foreach ($this->phpSheet->getConditionalStylesCollection() as $key => $value) {
$keyExplode = explode(',', $key);
$keyExplode = explode(',', Coordinate::resolveUnionAndIntersection($key));
foreach ($keyExplode as $exploded) {
$arrConditionalStyles[$exploded] = $value;
}
+3 -1
View File
@@ -817,7 +817,9 @@ class Worksheet extends WriterPart
$objWriter->startElement('conditionalFormatting');
// N.B. In Excel UI, intersection is space and union is comma.
// But in Xml, intersection is comma and union is space.
$objWriter->writeAttribute('sqref', str_replace(['$', ' ', ',', '^'], ['', '^', ' ', ','], $cellCoordinate));
// Anyhow, I don't think Excel handles intersection correctly when reading.
$outCoordinate = Coordinate::resolveUnionAndIntersection(str_replace('$', '', $cellCoordinate), ' ');
$objWriter->writeAttribute('sqref', $outCoordinate);
foreach ($conditionalStyles as $conditional) {
// WHY was this again?
@@ -5,12 +5,15 @@ declare(strict_types=1);
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
class Issue4039Test extends \PHPUnit\Framework\TestCase
class Issue4039Test extends AbstractFunctional
{
private static string $testbook = 'tests/data/Style/ConditionalFormatting/CellMatcher.xlsx';
public function testSplitRange(): void
public function testUnionRange(): void
{
$reader = new Xlsx();
$spreadsheet = $reader->load(self::$testbook);
@@ -27,4 +30,36 @@ class Issue4039Test extends \PHPUnit\Framework\TestCase
self::assertSame($expected[1], $sheet->getConditionalRange('D25'));
$spreadsheet->disconnectWorksheets();
}
public function testIntersectionRange(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->fromArray([
[1, 2, 3, 4, 5],
[2, 3, 4, 5, 6],
[3, 4, 5, 6, 7],
]);
$condition1 = new Conditional();
$condition1->setConditionType(Conditional::CONDITION_CELLIS);
$condition1->setOperatorType(Conditional::OPERATOR_BETWEEN);
$condition1->setConditions([2, 3]);
$condition1->getStyle()->getFont()
->setBold(true);
$conditionalStyles = [$condition1];
// Writer will change this range to equivalent 'B1,B2,B3'
$sheet->setConditionalStyles('A1:C3 B1:B3', $conditionalStyles);
$robj = $this->writeAndReload($spreadsheet, 'Xlsx');
$spreadsheet->disconnectWorksheets();
$sheet0 = $robj->getActiveSheet();
$conditionals = $sheet0->getConditionalStylesCollection();
self::assertSame(['B1,B2,B3'], array_keys($conditionals));
$cond1 = $conditionals['B1,B2,B3'][0];
self::assertSame(Conditional::CONDITION_CELLIS, $cond1->getConditionType());
self::assertSame(Conditional::OPERATOR_BETWEEN, $cond1->getOperatorType());
self::assertSame(['2', '3'], $cond1->getConditions());
$font1 = $cond1->getStyle()->getFont();
self::assertTrue($font1->getBold());
$robj->disconnectWorksheets();
}
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace PhpOffice\PhpSpreadsheetTests\Worksheet;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
use PHPUnit\Framework\TestCase;
class ConditionalIntersectionTest extends TestCase
{
public function testGetConditionalStyles(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->fromArray([
[1, 2, 3, 4, 5],
[2, 3, 4, 5, 6],
[3, 4, 5, 6, 7],
]);
$condition1 = new Conditional();
$condition1->setConditionType(Conditional::CONDITION_CELLIS);
$condition1->setOperatorType(Conditional::OPERATOR_BETWEEN);
$condition1->setConditions([2, 3]);
$condition1->getStyle()->getFont()
->setBold(true);
$conditionalStyles = [$condition1];
$sheet->setConditionalStyles('A1:C3 B1:B3', $conditionalStyles);
self::assertEmpty($sheet->getConditionalStyles('A2'));
$cond = $sheet->getConditionalStyles('B2');
self::assertCount(1, $cond);
self::assertSame(Conditional::CONDITION_CELLIS, $cond[0]->getConditionType());
self::assertSame(Conditional::OPERATOR_BETWEEN, $cond[0]->getOperatorType());
self::assertSame([2, 3], $cond[0]->getConditions());
self::assertTrue($cond[0]->getStyle()->getFont()->getBold());
}
public function testGetConditionalRange(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->fromArray([
[1, 2, 3, 4, 5],
[2, 3, 4, 5, 6],
[3, 4, 5, 6, 7],
]);
$condition1 = new Conditional();
$condition1->setConditionType(Conditional::CONDITION_CELLIS);
$condition1->setOperatorType(Conditional::OPERATOR_BETWEEN);
$condition1->setConditions([2, 3]);
$condition1->getStyle()->getFont()
->setBold(true);
$conditionalStyles = [$condition1];
$sheet->setConditionalStyles('A1:C3 B1:B3', $conditionalStyles);
self::assertNull($sheet->getConditionalRange('A2'));
self::assertSame('A1:C3 B1:B3', $sheet->getConditionalRange('B2'));
}
}
@@ -48,4 +48,43 @@ class ConditionalUnionTest extends AbstractFunctional
self::assertTrue($font2->getBold());
$robj->disconnectWorksheets();
}
public function testIntersectionRange(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->fromArray([
[1, 2, 3, 4, 5],
[2, 3, 4, 5, 6],
[3, 4, 5, 6, 7],
]);
$condition1 = new Conditional();
$condition1->setConditionType(Conditional::CONDITION_CELLIS);
$condition1->setOperatorType(Conditional::OPERATOR_BETWEEN);
$condition1->setConditions([2, 3]);
$condition1->getStyle()->getFont()
->setBold(true);
$conditionalStyles = [$condition1];
$sheet->setConditionalStyles('A1:B5,D1:E5 B2:D4', $conditionalStyles);
$robj = $this->writeAndReload($spreadsheet, 'Xls');
$spreadsheet->disconnectWorksheets();
$sheet0 = $robj->getActiveSheet();
$conditionals = $sheet0->getConditionalStylesCollection();
self::assertSame(['A1:B5', 'D2', 'D3', 'D4'], array_keys($conditionals));
$cond1 = $conditionals['A1:B5'][0];
self::assertSame(Conditional::CONDITION_CELLIS, $cond1->getConditionType());
self::assertSame(Conditional::OPERATOR_BETWEEN, $cond1->getOperatorType());
self::assertSame([2, 3], $cond1->getConditions());
$font1 = $cond1->getStyle()->getFont();
self::assertTrue($font1->getBold());
$cond2 = $conditionals['D2'][0];
self::assertSame(Conditional::CONDITION_CELLIS, $cond2->getConditionType());
self::assertSame(Conditional::OPERATOR_BETWEEN, $cond2->getOperatorType());
self::assertSame([2, 3], $cond2->getConditions());
$font2 = $cond2->getStyle()->getFont();
self::assertTrue($font2->getBold());
$robj->disconnectWorksheets();
}
}