Added Conditional Formatting: IconSet for Xlsx (#4560)

This commit is contained in:
issa
2025-08-07 07:24:22 +09:00
parent ea97afb754
commit 0327b49060
8 changed files with 329 additions and 6 deletions
@@ -9,6 +9,8 @@ use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalColorScale;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalDataBar;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormattingRuleExtension;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormatValueObject;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalIconSet;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\IconSetValues;
use PhpOffice\PhpSpreadsheet\Style\Style as Style;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use SimpleXMLElement;
@@ -265,6 +267,8 @@ class ConditionalStyles
$objConditional->setColorScale(
$this->readColorScale($cfRule)
);
} elseif (isset($cfRule->iconSet)) {
$objConditional->setIconSet($this->readIconSet($cfRule));
} elseif (isset($cfRule['dxfId'])) {
$objConditional->setStyle(clone $this->dxfs[(int) ($cfRule['dxfId'])]);
}
@@ -349,6 +353,40 @@ class ConditionalStyles
return $colorScale;
}
private function readIconSet(SimpleXMLElement $cfRule): ConditionalIconSet
{
$iconSet = new ConditionalIconSet();
if (isset($cfRule->iconSet['iconSet'])) {
$iconSet->setIconSetType(IconSetValues::from($cfRule->iconSet['iconSet']));
}
if (isset($cfRule->iconSet['reverse'])) {
$iconSet->setReverse('1' === (string) $cfRule->iconSet['reverse']);
}
if (isset($cfRule->iconSet['showValue'])) {
$iconSet->setShowValue('1' === (string) $cfRule->iconSet['showValue']);
}
if (isset($cfRule->iconSet['custom'])) {
$iconSet->setCustom('1' === (string) $cfRule->iconSet['custom']);
}
$cfvos = [];
foreach ($cfRule->iconSet->cfvo as $cfvoXml) {
$type = (string) $cfvoXml['type'];
$value = (string) ($cfvoXml['val'] ?? '');
$cfvo = new ConditionalFormatValueObject($type, $value);
if (isset($cfvoXml['gte'])) {
$cfvo->setGreaterThanOrEqual('1' === (string) $cfvoXml['gte']);
}
$cfvos[] = $cfvo;
}
$iconSet->setCfvos($cfvos);
// TODO: The cfIcon element is not implemented yet.
return $iconSet;
}
/** @param ConditionalFormattingRuleExtension[] $conditionalFormattingRuleExtensions */
private function readDataBarExtLstOfConditionalRule(ConditionalDataBar $dataBar, SimpleXMLElement $cfRule, array $conditionalFormattingRuleExtensions): void
{
+21 -4
View File
@@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Style;
use PhpOffice\PhpSpreadsheet\IComparable;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalColorScale;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalDataBar;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalIconSet;
class Conditional implements IComparable
{
@@ -25,6 +26,7 @@ class Conditional implements IComparable
const CONDITION_TIMEPERIOD = 'timePeriod';
const CONDITION_DUPLICATES = 'duplicateValues';
const CONDITION_UNIQUE = 'uniqueValues';
const CONDITION_ICONSET = 'iconSet';
private const CONDITION_TYPES = [
self::CONDITION_BEGINSWITH,
@@ -43,6 +45,7 @@ class Conditional implements IComparable
self::CONDITION_NOTCONTAINSTEXT,
self::CONDITION_TIMEPERIOD,
self::CONDITION_UNIQUE,
self::CONDITION_ICONSET,
];
// Operator types
@@ -102,6 +105,8 @@ class Conditional implements IComparable
private ?ConditionalColorScale $colorScale = null;
private ?ConditionalIconSet $iconSet = null;
private Style $style;
private bool $noFormatSet = false;
@@ -318,6 +323,18 @@ class Conditional implements IComparable
return $this;
}
public function getIconSet(): ?ConditionalIconSet
{
return $this->iconSet;
}
public function setIconSet(ConditionalIconSet $iconSet): static
{
$this->iconSet = $iconSet;
return $this;
}
/**
* Get hash code.
*
@@ -327,10 +344,10 @@ class Conditional implements IComparable
{
return md5(
$this->conditionType
. $this->operatorType
. implode(';', $this->condition)
. $this->style->getHashCode()
. __CLASS__
. $this->operatorType
. implode(';', $this->condition)
. $this->style->getHashCode()
. __CLASS__
);
}
@@ -10,6 +10,13 @@ class ConditionalFormatValueObject
private ?string $cellFormula;
/**
* For icon sets, determines whether this threshold value uses the greater
* than or equal to operator. False indicates 'greater than' is used instead
* of 'greater than or equal to'.
*/
private ?bool $greaterThanOrEqual = null;
public function __construct(string $type, null|float|int|string $value = null, ?string $cellFormula = null)
{
$this->type = $type;
@@ -52,4 +59,16 @@ class ConditionalFormatValueObject
return $this;
}
public function getGreaterThanOrEqual(): ?bool
{
return $this->greaterThanOrEqual;
}
public function setGreaterThanOrEqual(?bool $greaterThanOrEqual): self
{
$this->greaterThanOrEqual = $greaterThanOrEqual;
return $this;
}
}
@@ -0,0 +1,96 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting;
class ConditionalIconSet
{
/** The icon set to display. */
private ?IconSetValues $iconSetType = null;
/** If true, reverses the default order of the icons in this icon set. */
private ?bool $reverse = null;
/** Indicates whether to show the values of the cells on which this icon set is applied. */
private ?bool $showValue = null;
/**
* If true, indicates that the icon set is a custom icon set.
* If this value is "true", there MUST be the same number of cfIcon elements
* as cfvo elements.
* If this value is "false", there MUST be 0 cfIcon elements.
*/
private ?bool $custom = null;
/** @var ConditionalFormatValueObject[] */
private array $cfvos = [];
public function getIconSetType(): ?IconSetValues
{
return $this->iconSetType;
}
public function setIconSetType(IconSetValues $type): self
{
$this->iconSetType = $type;
return $this;
}
public function getReverse(): ?bool
{
return $this->reverse;
}
public function setReverse(bool $reverse): self
{
$this->reverse = $reverse;
return $this;
}
public function getShowValue(): ?bool
{
return $this->showValue;
}
public function setShowValue(bool $showValue): self
{
$this->showValue = $showValue;
return $this;
}
public function getCustom(): ?bool
{
return $this->custom;
}
public function setCustom(bool $custom): self
{
$this->custom = $custom;
return $this;
}
/**
* Get the conditional format value objects.
*
* @return ConditionalFormatValueObject[]
*/
public function getCfvos(): array
{
return $this->cfvos;
}
/**
* Set the conditional format value objects.
*
* @param ConditionalFormatValueObject[] $cfvos
*/
public function setCfvos(array $cfvos): self
{
$this->cfvos = $cfvos;
return $this;
}
}
@@ -0,0 +1,28 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting;
enum IconSetValues: string
{
case ThreeArrows = '3Arrows';
case ThreeArrowsGray = '3ArrowsGray';
case ThreeFlags = '3Flags';
case ThreeTrafficLights1 = '3TrafficLights1';
case ThreeTrafficLights2 = '3TrafficLights2';
case ThreeSigns = '3Signs';
case ThreeSymbols = '3Symbols';
case ThreeSymbols2 = '3Symbols2';
case FourArrows = '4Arrows';
case FourArrowsGray = '4ArrowsGray';
case FourRedToBlack = '4RedToBlack';
case FourRating = '4Rating';
case FourTrafficLights = '4TrafficLights';
case FiveArrows = '5Arrows';
case FiveArrowsGray = '5ArrowsGray';
case FiveRating = '5Rating';
case FiveQuarters = '5Quarters';
// The following icon sets are not implemented yet:
// case ThreeStars = "3Stars";
// case ThreeTriangles = "3Triangles";
// case FiveBoxes = "5Boxes";
}
+47 -2
View File
@@ -16,6 +16,7 @@ use PhpOffice\PhpSpreadsheet\Style\Conditional;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalColorScale;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalDataBar;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormattingRuleExtension;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalIconSet;
use PhpOffice\PhpSpreadsheet\Style\Font;
use PhpOffice\PhpSpreadsheet\Worksheet\RowDimension;
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
@@ -863,6 +864,47 @@ class Worksheet extends WriterPart
}
}
private function writeIconSetElements(XMLWriter $objWriter, ?ConditionalIconSet $iconSet): void
{
if ($iconSet === null) {
return;
}
$objWriter->startElement('iconSet');
if ($iconSet->getIconSetType() !== null) {
$objWriter->writeAttribute('iconSet', $iconSet->getIconSetType()->value);
}
foreach (
[
'reverse' => $iconSet->getReverse(),
'showValue' => $iconSet->getShowValue(),
'custom' => $iconSet->getCustom(),
] as $attr => $value
) {
self::writeAttributeIf($objWriter, $value !== null, $attr, $value ? '1' : '0');
}
foreach ($iconSet->getCfvos() as $cfvo) {
$objWriter->startElement('cfvo');
$objWriter->writeAttribute('type', $cfvo->getType());
self::writeAttributeIf(
$objWriter,
$cfvo->getValue() !== null,
'val',
(string) $cfvo->getValue(),
);
self::writeAttributeIf(
$objWriter,
$cfvo->getGreaterThanOrEqual() !== null,
'gte',
$cfvo->getGreaterThanOrEqual() ? '1' : '0',
);
$objWriter->endElement(); // end cfvo
}
$objWriter->endElement(); // end iconSet
}
/**
* Write ConditionalFormatting.
*/
@@ -897,6 +939,7 @@ class Worksheet extends WriterPart
$objWriter,
($conditional->getConditionType() !== Conditional::CONDITION_COLORSCALE
&& $conditional->getConditionType() !== Conditional::CONDITION_DATABAR
&& $conditional->getConditionType() !== Conditional::CONDITION_ICONSET
&& $conditional->getNoFormatSet() === false),
'dxfId',
(string) $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode())
@@ -933,6 +976,8 @@ class Worksheet extends WriterPart
self::writeTimePeriodCondElements($objWriter, $conditional, $topLeftCell);
} elseif ($conditional->getConditionType() === Conditional::CONDITION_COLORSCALE) {
self::writeColorScaleElements($objWriter, $conditional->getColorScale());
} elseif ($conditional->getConditionType() === Conditional::CONDITION_ICONSET) {
self::writeIconSetElements($objWriter, $conditional->getIconSet());
} else {
self::writeOtherCondElements($objWriter, $conditional, $topLeftCell);
}
@@ -1566,8 +1611,8 @@ class Worksheet extends WriterPart
self::writeElementIf(
$objWriter,
$this->getParentWriter()->getOffice2003Compatibility() === false
&& $this->getParentWriter()->getPreCalculateFormulas()
&& $calculatedValue !== null,
&& $this->getParentWriter()->getPreCalculateFormulas()
&& $calculatedValue !== null,
'v',
(!is_array($calculatedValue) && !str_starts_with($calculatedValueString, '#'))
? StringHelper::formatNumber($calculatedValueString) : '0'
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\IconSetValues;
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
class ConditionalIconSetTest extends AbstractFunctional
{
public function testIconSet(): void
{
$filename = 'tests/data/Reader/XLSX/conditionalFormattingIconSet.xlsx';
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx');
$spreadsheet = $reader->load($filename);
$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx');
$spreadsheet->disconnectWorksheets();
$worksheet = $reloadedSpreadsheet->getActiveSheet();
$columnIndex = 'A';
foreach (IconSetValues::cases() as $iconSetValue) {
// styles
$styles = $worksheet->getConditionalStyles("{$columnIndex}2:{$columnIndex}11");
self::assertCount(1, $styles);
// icon set
$iconSet = $styles[0]->getIconSet();
self::assertNotNull($iconSet);
self::assertSame($iconSetValue, $iconSet->getIconSetType() ?? IconSetValues::ThreeTrafficLights1);
++$columnIndex;
}
// icon set attributes
$columnIndex = 'A';
foreach (
[
['reverse' => false, 'showValue' => false],
['reverse' => true, 'showValue' => false],
['reverse' => false, 'showValue' => true],
] as $expected
) {
$styles = $worksheet->getConditionalStyles("{$columnIndex}2:{$columnIndex}11");
$iconSet = $styles[0]->getIconSet();
self::assertNotNull($iconSet);
self::assertSame($expected['reverse'], $iconSet->getReverse() ?? false);
self::assertSame($expected['showValue'], $iconSet->getShowValue() ?? true);
self::assertFalse($iconSet->getCustom() ?? false);
++$columnIndex;
}
// cfvos
$columnIndex = 'A';
foreach (
[
[['percent', '0', true], ['percent', '33', false], ['percent', '67', true]],
[['percent', '0', true], ['num', '3', false], ['num', '7', true]],
[['percent', '0', true], ['formula', '10/3', false], ['formula', '10/2', true]],
[['percent', '0', true], ['percentile', '33', false], ['percentile', '67', true]],
] as $expected
) {
$styles = $worksheet->getConditionalStyles("{$columnIndex}2:{$columnIndex}11");
$iconSet = $styles[0]->getIconSet();
self::assertNotNull($iconSet);
$cfvos = $iconSet->getCfvos();
self::assertCount(count($expected), $cfvos);
foreach ($expected as $i => [$type, $value, $gte]) {
$cfvo = $cfvos[$i];
self::assertSame($type, $cfvo->getType());
self::assertSame($value, $cfvo->getValue());
self::assertSame($gte, $cfvo->getGreaterThanOrEqual() ?? true);
self::assertNull($cfvo->getCellFormula());
}
++$columnIndex;
}
}
}