mirror of
https://github.com/PHPOffice/PhpSpreadsheet.git
synced 2026-09-14 20:16:34 +00:00
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:
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user