Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).

### Changed

- Nothing
- 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)

### Deprecated

Expand All @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
- POWER Null/Bool Args. [PR #4031](https://github.com/PHPOffice/PhpSpreadsheet/pull/4031)
- Do Not Output Alignment and Protection for Conditional Format. [Issue #4025](https://github.com/PHPOffice/PhpSpreadsheet/issues/4025) [PR #4027](https://github.com/PHPOffice/PhpSpreadsheet/pull/4027)
- Xls Conditional Format Improvements. [PR #4030](https://github.com/PHPOffice/PhpSpreadsheet/pull/4030) [PR #4033](https://github.com/PHPOffice/PhpSpreadsheet/pull/4033)
- Conditional Range Unions and Intersections [Issue #4039](https://github.com/PHPOffice/PhpSpreadsheet/issues/4039) [PR #4042](https://github.com/PHPOffice/PhpSpreadsheet/pull/4042)
- 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)

## 2024-05-11 - 2.1.0
Expand Down
38 changes: 38 additions & 0 deletions src/PhpSpreadsheet/Cell/Coordinate.php
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,44 @@ private static function sortCellReferenceArray(array $cellList): array
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.
*
Expand Down
8 changes: 4 additions & 4 deletions src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,10 @@ private function setConditionalStyles(Worksheet $worksheet, array $conditionals,
$conditionalStyles = $this->readStyleRules($cfRules, $xmlExtLst);

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

Expand Down
14 changes: 10 additions & 4 deletions src/PhpSpreadsheet/Worksheet/Worksheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -1422,8 +1422,11 @@ public function getConditionalStyles(string $coordinate): array

$cell = $this->getCell($coordinate);
foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
if ($cell->isInRange($conditionalRange)) {
return $this->conditionalStylesCollection[$conditionalRange];
$cellBlocks = explode(',', Coordinate::resolveUnionAndIntersection($conditionalRange));
foreach ($cellBlocks as $cellBlock) {
if ($cell->isInRange($cellBlock)) {
return $this->conditionalStylesCollection[$conditionalRange];
}
}
}

Expand All @@ -1435,8 +1438,11 @@ public function getConditionalRange(string $coordinate): ?string
$coordinate = strtoupper($coordinate);
$cell = $this->getCell($coordinate);
foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) {
if ($cell->isInRange($conditionalRange)) {
return $conditionalRange;
$cellBlocks = explode(',', Coordinate::resolveUnionAndIntersection($conditionalRange));
foreach ($cellBlocks as $cellBlock) {
if ($cell->isInRange($cellBlock)) {
return $conditionalRange;
}
}
}

Expand Down
8 changes: 7 additions & 1 deletion src/PhpSpreadsheet/Writer/Xls/Worksheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,13 @@ private function writeConditionalFormatting(): void
{
$conditionalFormulaHelper = new ConditionalHelper($this->parser);

$arrConditionalStyles = $this->phpSheet->getConditionalStylesCollection();
$arrConditionalStyles = [];
foreach ($this->phpSheet->getConditionalStylesCollection() as $key => $value) {
$keyExplode = explode(',', Coordinate::resolveUnionAndIntersection($key));
foreach ($keyExplode as $exploded) {
$arrConditionalStyles[$exploded] = $value;
}
}
if (!empty($arrConditionalStyles)) {
// Write ConditionalFormattingTable records
foreach ($arrConditionalStyles as $cellCoordinate => $conditionalStyles) {
Expand Down
6 changes: 5 additions & 1 deletion src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -815,7 +815,11 @@ private function writeConditionalFormatting(XMLWriter $objWriter, Phpspreadsheet
// Loop through styles in the current worksheet
foreach ($worksheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) {
$objWriter->startElement('conditionalFormatting');
$objWriter->writeAttribute('sqref', $cellCoordinate);
// N.B. In Excel UI, intersection is space and union is comma.
// But in Xml, intersection is comma and union is space.
// 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?
Expand Down
65 changes: 65 additions & 0 deletions tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4039Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

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 AbstractFunctional
{
private static string $testbook = 'tests/data/Style/ConditionalFormatting/CellMatcher.xlsx';

public function testUnionRange(): void
{
$reader = new Xlsx();
$spreadsheet = $reader->load(self::$testbook);
$sheet = $spreadsheet->getSheetByNameOrThrow('cellIs Expression');
$expected = [
'A12:D17,A20', // split range
'A22:D27',
'A2:E6',
];
self::assertSame($expected, array_keys($sheet->getConditionalStylesCollection()));
self::assertSame($expected[0], $sheet->getConditionalRange('A20'));
self::assertSame($expected[0], $sheet->getConditionalRange('C15'));
self::assertNull($sheet->getConditionalRange('A19'));
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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?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());
$spreadsheet->disconnectWorksheets();
}

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'));
$spreadsheet->disconnectWorksheets();
}
}
90 changes: 90 additions & 0 deletions tests/PhpSpreadsheetTests/Writer/Xls/ConditionalUnionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

namespace PhpOffice\PhpSpreadsheetTests\Writer\Xls;

use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;

class ConditionalUnionTest extends AbstractFunctional
{
public function testConditionalUnion(): 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, 4]);
$condition1->getStyle()->getFont()
->setBold(true);
$conditionalStyles = [$condition1];
$sheet->setConditionalStyles('A1:A3,C1:E3', $conditionalStyles);

$robj = $this->writeAndReload($spreadsheet, 'Xls');
$spreadsheet->disconnectWorksheets();
$sheet0 = $robj->getActiveSheet();
$conditionals = $sheet0->getConditionalStylesCollection();
self::assertSame(['A1:A3', 'C1:E3'], array_keys($conditionals));
$cond1 = $conditionals['A1:A3'][0];
self::assertSame(Conditional::CONDITION_CELLIS, $cond1->getConditionType());
self::assertSame(Conditional::OPERATOR_BETWEEN, $cond1->getOperatorType());
self::assertSame([2, 4], $cond1->getConditions());
$font1 = $cond1->getStyle()->getFont();
self::assertTrue($font1->getBold());

$cond2 = $conditionals['C1:E3'][0];
self::assertSame(Conditional::CONDITION_CELLIS, $cond2->getConditionType());
self::assertSame(Conditional::OPERATOR_BETWEEN, $cond2->getOperatorType());
self::assertSame([2, 4], $cond2->getConditions());
$font2 = $cond2->getStyle()->getFont();
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();
}
}