diff --git a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php index b09de2ad6..2892dd122 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php @@ -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 { diff --git a/src/PhpSpreadsheet/Style/Conditional.php b/src/PhpSpreadsheet/Style/Conditional.php index 2ef7cacd7..fedd0aa48 100644 --- a/src/PhpSpreadsheet/Style/Conditional.php +++ b/src/PhpSpreadsheet/Style/Conditional.php @@ -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__ ); } diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php index e6d1035f4..d1dfb5159 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php @@ -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; + } } diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalIconSet.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalIconSet.php new file mode 100644 index 000000000..b7c21d869 --- /dev/null +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalIconSet.php @@ -0,0 +1,96 @@ +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; + } +} diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/IconSetValues.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/IconSetValues.php new file mode 100644 index 000000000..c27d36e6e --- /dev/null +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/IconSetValues.php @@ -0,0 +1,28 @@ +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' diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalIconSetTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalIconSetTest.php new file mode 100644 index 000000000..7189f6ea0 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalIconSetTest.php @@ -0,0 +1,80 @@ +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; + } + } +} diff --git a/tests/data/Reader/XLSX/conditionalFormattingIconSet.xlsx b/tests/data/Reader/XLSX/conditionalFormattingIconSet.xlsx new file mode 100644 index 000000000..6c3f0539d Binary files /dev/null and b/tests/data/Reader/XLSX/conditionalFormattingIconSet.xlsx differ