package and depencies

This commit is contained in:
RafficMohammed
2023-01-08 02:57:24 +05:30
parent d5332eb421
commit 1d54b8bc7f
4309 changed files with 193331 additions and 172289 deletions

View File

@@ -0,0 +1,313 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class CellMatcher
{
public const COMPARISON_OPERATORS = [
Conditional::OPERATOR_EQUAL => '=',
Conditional::OPERATOR_GREATERTHAN => '>',
Conditional::OPERATOR_GREATERTHANOREQUAL => '>=',
Conditional::OPERATOR_LESSTHAN => '<',
Conditional::OPERATOR_LESSTHANOREQUAL => '<=',
Conditional::OPERATOR_NOTEQUAL => '<>',
];
public const COMPARISON_RANGE_OPERATORS = [
Conditional::OPERATOR_BETWEEN => 'IF(AND(A1>=%s,A1<=%s),TRUE,FALSE)',
Conditional::OPERATOR_NOTBETWEEN => 'IF(AND(A1>=%s,A1<=%s),FALSE,TRUE)',
];
public const COMPARISON_DUPLICATES_OPERATORS = [
Conditional::CONDITION_DUPLICATES => "COUNTIF('%s'!%s,%s)>1",
Conditional::CONDITION_UNIQUE => "COUNTIF('%s'!%s,%s)=1",
];
/**
* @var Cell
*/
protected $cell;
/**
* @var int
*/
protected $cellRow;
/**
* @var Worksheet
*/
protected $worksheet;
/**
* @var int
*/
protected $cellColumn;
/**
* @var string
*/
protected $conditionalRange;
/**
* @var string
*/
protected $referenceCell;
/**
* @var int
*/
protected $referenceRow;
/**
* @var int
*/
protected $referenceColumn;
/**
* @var Calculation
*/
protected $engine;
public function __construct(Cell $cell, string $conditionalRange)
{
$this->cell = $cell;
$this->worksheet = $cell->getWorksheet();
[$this->cellColumn, $this->cellRow] = Coordinate::indexesFromString($this->cell->getCoordinate());
$this->setReferenceCellForExpressions($conditionalRange);
$this->engine = Calculation::getInstance($this->worksheet->getParent());
}
protected function setReferenceCellForExpressions(string $conditionalRange): void
{
$conditionalRange = Coordinate::splitRange(str_replace('$', '', strtoupper($conditionalRange)));
[$this->referenceCell] = $conditionalRange[0];
[$this->referenceColumn, $this->referenceRow] = Coordinate::indexesFromString($this->referenceCell);
// Convert our conditional range to an absolute conditional range, so it can be used "pinned" in formulae
$rangeSets = [];
foreach ($conditionalRange as $rangeSet) {
$absoluteRangeSet = array_map(
[Coordinate::class, 'absoluteCoordinate'],
$rangeSet
);
$rangeSets[] = implode(':', $absoluteRangeSet);
}
$this->conditionalRange = implode(',', $rangeSets);
}
public function evaluateConditional(Conditional $conditional): bool
{
// Some calculations may modify the stored cell; so reset it before every evaluation.
$cellColumn = Coordinate::stringFromColumnIndex($this->cellColumn);
$cellAddress = "{$cellColumn}{$this->cellRow}";
$this->cell = $this->worksheet->getCell($cellAddress);
switch ($conditional->getConditionType()) {
case Conditional::CONDITION_CELLIS:
return $this->processOperatorComparison($conditional);
case Conditional::CONDITION_DUPLICATES:
case Conditional::CONDITION_UNIQUE:
return $this->processDuplicatesComparison($conditional);
case Conditional::CONDITION_CONTAINSTEXT:
// Expression is NOT(ISERROR(SEARCH("<TEXT>",<Cell Reference>)))
case Conditional::CONDITION_NOTCONTAINSTEXT:
// Expression is ISERROR(SEARCH("<TEXT>",<Cell Reference>))
case Conditional::CONDITION_BEGINSWITH:
// Expression is LEFT(<Cell Reference>,LEN("<TEXT>"))="<TEXT>"
case Conditional::CONDITION_ENDSWITH:
// Expression is RIGHT(<Cell Reference>,LEN("<TEXT>"))="<TEXT>"
case Conditional::CONDITION_CONTAINSBLANKS:
// Expression is LEN(TRIM(<Cell Reference>))=0
case Conditional::CONDITION_NOTCONTAINSBLANKS:
// Expression is LEN(TRIM(<Cell Reference>))>0
case Conditional::CONDITION_CONTAINSERRORS:
// Expression is ISERROR(<Cell Reference>)
case Conditional::CONDITION_NOTCONTAINSERRORS:
// Expression is NOT(ISERROR(<Cell Reference>))
case Conditional::CONDITION_TIMEPERIOD:
// Expression varies, depending on specified timePeriod value, e.g.
// Yesterday FLOOR(<Cell Reference>,1)=TODAY()-1
// Today FLOOR(<Cell Reference>,1)=TODAY()
// Tomorrow FLOOR(<Cell Reference>,1)=TODAY()+1
// Last 7 Days AND(TODAY()-FLOOR(<Cell Reference>,1)<=6,FLOOR(<Cell Reference>,1)<=TODAY())
case Conditional::CONDITION_EXPRESSION:
return $this->processExpression($conditional);
}
return false;
}
/**
* @param mixed $value
*
* @return float|int|string
*/
protected function wrapValue($value)
{
if (!is_numeric($value)) {
if (is_bool($value)) {
return $value ? 'TRUE' : 'FALSE';
} elseif ($value === null) {
return 'NULL';
}
return '"' . $value . '"';
}
return $value;
}
/**
* @return float|int|string
*/
protected function wrapCellValue()
{
return $this->wrapValue($this->cell->getCalculatedValue());
}
/**
* @return float|int|string
*/
protected function conditionCellAdjustment(array $matches)
{
$column = $matches[6];
$row = $matches[7];
if (strpos($column, '$') === false) {
$column = Coordinate::columnIndexFromString($column);
$column += $this->cellColumn - $this->referenceColumn;
$column = Coordinate::stringFromColumnIndex($column);
}
if (strpos($row, '$') === false) {
$row += $this->cellRow - $this->referenceRow;
}
if (!empty($matches[4])) {
$worksheet = $this->worksheet->getParent()->getSheetByName(trim($matches[4], "'"));
if ($worksheet === null) {
return $this->wrapValue(null);
}
return $this->wrapValue(
$worksheet
->getCell(str_replace('$', '', "{$column}{$row}"))
->getCalculatedValue()
);
}
return $this->wrapValue(
$this->worksheet
->getCell(str_replace('$', '', "{$column}{$row}"))
->getCalculatedValue()
);
}
protected function cellConditionCheck(string $condition): string
{
$splitCondition = explode(Calculation::FORMULA_STRING_QUOTE, $condition);
$i = false;
foreach ($splitCondition as &$value) {
// Only count/replace in alternating array entries (ie. not in quoted strings)
$i = $i === false;
if ($i) {
$value = (string) preg_replace_callback(
'/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i',
[$this, 'conditionCellAdjustment'],
$value
);
}
}
unset($value);
// Then rebuild the condition string to return it
return implode(Calculation::FORMULA_STRING_QUOTE, $splitCondition);
}
protected function adjustConditionsForCellReferences(array $conditions): array
{
return array_map(
[$this, 'cellConditionCheck'],
$conditions
);
}
protected function processOperatorComparison(Conditional $conditional): bool
{
if (array_key_exists($conditional->getOperatorType(), self::COMPARISON_RANGE_OPERATORS)) {
return $this->processRangeOperator($conditional);
}
$operator = self::COMPARISON_OPERATORS[$conditional->getOperatorType()];
$conditions = $this->adjustConditionsForCellReferences($conditional->getConditions());
$expression = sprintf('%s%s%s', (string) $this->wrapCellValue(), $operator, (string) array_pop($conditions));
return $this->evaluateExpression($expression);
}
protected function processRangeOperator(Conditional $conditional): bool
{
$conditions = $this->adjustConditionsForCellReferences($conditional->getConditions());
sort($conditions);
$expression = sprintf(
(string) preg_replace(
'/\bA1\b/i',
(string) $this->wrapCellValue(),
self::COMPARISON_RANGE_OPERATORS[$conditional->getOperatorType()]
),
...$conditions
);
return $this->evaluateExpression($expression);
}
protected function processDuplicatesComparison(Conditional $conditional): bool
{
$worksheetName = $this->cell->getWorksheet()->getTitle();
$expression = sprintf(
self::COMPARISON_DUPLICATES_OPERATORS[$conditional->getConditionType()],
$worksheetName,
$this->conditionalRange,
$this->cellConditionCheck($this->cell->getCalculatedValue())
);
return $this->evaluateExpression($expression);
}
protected function processExpression(Conditional $conditional): bool
{
$conditions = $this->adjustConditionsForCellReferences($conditional->getConditions());
$expression = array_pop($conditions);
$expression = (string) preg_replace(
'/\b' . $this->referenceCell . '\b/i',
(string) $this->wrapCellValue(),
$expression
);
return $this->evaluateExpression($expression);
}
protected function evaluateExpression(string $expression): bool
{
$expression = "={$expression}";
try {
$this->engine->flushInstance();
$result = (bool) $this->engine->calculateFormula($expression);
} catch (Exception $e) {
return false;
}
return $result;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
use PhpOffice\PhpSpreadsheet\Style\Style;
class CellStyleAssessor
{
/**
* @var CellMatcher
*/
protected $cellMatcher;
/**
* @var StyleMerger
*/
protected $styleMerger;
public function __construct(Cell $cell, string $conditionalRange)
{
$this->cellMatcher = new CellMatcher($cell, $conditionalRange);
$this->styleMerger = new StyleMerger($cell->getStyle());
}
/**
* @param Conditional[] $conditionalStyles
*/
public function matchConditions(array $conditionalStyles = []): Style
{
foreach ($conditionalStyles as $conditional) {
/** @var Conditional $conditional */
if ($this->cellMatcher->evaluateConditional($conditional) === true) {
// Merging the conditional style into the base style goes in here
$this->styleMerger->mergeStyle($conditional->getStyle());
if ($conditional->getStopIfTrue() === true) {
break;
}
}
}
return $this->styleMerger->getStyle();
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting;
class ConditionalDataBar
{
/** <dataBar> attribute */
/** @var null|bool */
private $showValue;
/** <dataBar> children */
/** @var ?ConditionalFormatValueObject */
private $minimumConditionalFormatValueObject;
/** @var ?ConditionalFormatValueObject */
private $maximumConditionalFormatValueObject;
/** @var string */
private $color;
/** <extLst> */
/** @var ?ConditionalFormattingRuleExtension */
private $conditionalFormattingRuleExt;
/**
* @return null|bool
*/
public function getShowValue()
{
return $this->showValue;
}
/**
* @param bool $showValue
*/
public function setShowValue($showValue)
{
$this->showValue = $showValue;
return $this;
}
public function getMinimumConditionalFormatValueObject(): ?ConditionalFormatValueObject
{
return $this->minimumConditionalFormatValueObject;
}
public function setMinimumConditionalFormatValueObject(ConditionalFormatValueObject $minimumConditionalFormatValueObject)
{
$this->minimumConditionalFormatValueObject = $minimumConditionalFormatValueObject;
return $this;
}
public function getMaximumConditionalFormatValueObject(): ?ConditionalFormatValueObject
{
return $this->maximumConditionalFormatValueObject;
}
public function setMaximumConditionalFormatValueObject(ConditionalFormatValueObject $maximumConditionalFormatValueObject)
{
$this->maximumConditionalFormatValueObject = $maximumConditionalFormatValueObject;
return $this;
}
public function getColor(): string
{
return $this->color;
}
public function setColor(string $color): self
{
$this->color = $color;
return $this;
}
public function getConditionalFormattingRuleExt(): ?ConditionalFormattingRuleExtension
{
return $this->conditionalFormattingRuleExt;
}
public function setConditionalFormattingRuleExt(ConditionalFormattingRuleExtension $conditionalFormattingRuleExt)
{
$this->conditionalFormattingRuleExt = $conditionalFormattingRuleExt;
return $this;
}
}

View File

@@ -0,0 +1,290 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting;
class ConditionalDataBarExtension
{
/** <dataBar> attributes */
/** @var int */
private $minLength;
/** @var int */
private $maxLength;
/** @var null|bool */
private $border;
/** @var null|bool */
private $gradient;
/** @var string */
private $direction;
/** @var null|bool */
private $negativeBarBorderColorSameAsPositive;
/** @var string */
private $axisPosition;
// <dataBar> children
/** @var ConditionalFormatValueObject */
private $maximumConditionalFormatValueObject;
/** @var ConditionalFormatValueObject */
private $minimumConditionalFormatValueObject;
/** @var string */
private $borderColor;
/** @var string */
private $negativeFillColor;
/** @var string */
private $negativeBorderColor;
/** @var array */
private $axisColor = [
'rgb' => null,
'theme' => null,
'tint' => null,
];
public function getXmlAttributes()
{
$ret = [];
foreach (['minLength', 'maxLength', 'direction', 'axisPosition'] as $attrKey) {
if (null !== $this->{$attrKey}) {
$ret[$attrKey] = $this->{$attrKey};
}
}
foreach (['border', 'gradient', 'negativeBarBorderColorSameAsPositive'] as $attrKey) {
if (null !== $this->{$attrKey}) {
$ret[$attrKey] = $this->{$attrKey} ? '1' : '0';
}
}
return $ret;
}
public function getXmlElements()
{
$ret = [];
$elms = ['borderColor', 'negativeFillColor', 'negativeBorderColor'];
foreach ($elms as $elmKey) {
if (null !== $this->{$elmKey}) {
$ret[$elmKey] = ['rgb' => $this->{$elmKey}];
}
}
foreach (array_filter($this->axisColor) as $attrKey => $axisColorAttr) {
if (!isset($ret['axisColor'])) {
$ret['axisColor'] = [];
}
$ret['axisColor'][$attrKey] = $axisColorAttr;
}
return $ret;
}
/**
* @return int
*/
public function getMinLength()
{
return $this->minLength;
}
public function setMinLength(int $minLength): self
{
$this->minLength = $minLength;
return $this;
}
/**
* @return int
*/
public function getMaxLength()
{
return $this->maxLength;
}
public function setMaxLength(int $maxLength): self
{
$this->maxLength = $maxLength;
return $this;
}
/**
* @return null|bool
*/
public function getBorder()
{
return $this->border;
}
public function setBorder(bool $border): self
{
$this->border = $border;
return $this;
}
/**
* @return null|bool
*/
public function getGradient()
{
return $this->gradient;
}
public function setGradient(bool $gradient): self
{
$this->gradient = $gradient;
return $this;
}
/**
* @return string
*/
public function getDirection()
{
return $this->direction;
}
public function setDirection(string $direction): self
{
$this->direction = $direction;
return $this;
}
/**
* @return null|bool
*/
public function getNegativeBarBorderColorSameAsPositive()
{
return $this->negativeBarBorderColorSameAsPositive;
}
public function setNegativeBarBorderColorSameAsPositive(bool $negativeBarBorderColorSameAsPositive): self
{
$this->negativeBarBorderColorSameAsPositive = $negativeBarBorderColorSameAsPositive;
return $this;
}
/**
* @return string
*/
public function getAxisPosition()
{
return $this->axisPosition;
}
public function setAxisPosition(string $axisPosition): self
{
$this->axisPosition = $axisPosition;
return $this;
}
/**
* @return ConditionalFormatValueObject
*/
public function getMaximumConditionalFormatValueObject()
{
return $this->maximumConditionalFormatValueObject;
}
public function setMaximumConditionalFormatValueObject(ConditionalFormatValueObject $maximumConditionalFormatValueObject)
{
$this->maximumConditionalFormatValueObject = $maximumConditionalFormatValueObject;
return $this;
}
/**
* @return ConditionalFormatValueObject
*/
public function getMinimumConditionalFormatValueObject()
{
return $this->minimumConditionalFormatValueObject;
}
public function setMinimumConditionalFormatValueObject(ConditionalFormatValueObject $minimumConditionalFormatValueObject)
{
$this->minimumConditionalFormatValueObject = $minimumConditionalFormatValueObject;
return $this;
}
/**
* @return string
*/
public function getBorderColor()
{
return $this->borderColor;
}
public function setBorderColor(string $borderColor): self
{
$this->borderColor = $borderColor;
return $this;
}
/**
* @return string
*/
public function getNegativeFillColor()
{
return $this->negativeFillColor;
}
public function setNegativeFillColor(string $negativeFillColor): self
{
$this->negativeFillColor = $negativeFillColor;
return $this;
}
/**
* @return string
*/
public function getNegativeBorderColor()
{
return $this->negativeBorderColor;
}
public function setNegativeBorderColor(string $negativeBorderColor): self
{
$this->negativeBorderColor = $negativeBorderColor;
return $this;
}
public function getAxisColor(): array
{
return $this->axisColor;
}
/**
* @param mixed $rgb
* @param null|mixed $theme
* @param null|mixed $tint
*/
public function setAxisColor($rgb, $theme = null, $tint = null): self
{
$this->axisColor = [
'rgb' => $rgb,
'theme' => $theme,
'tint' => $tint,
];
return $this;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting;
class ConditionalFormatValueObject
{
private $type;
private $value;
private $cellFormula;
/**
* ConditionalFormatValueObject constructor.
*
* @param null|mixed $cellFormula
*/
public function __construct($type, $value = null, $cellFormula = null)
{
$this->type = $type;
$this->value = $value;
$this->cellFormula = $cellFormula;
}
/**
* @return mixed
*/
public function getType()
{
return $this->type;
}
/**
* @param mixed $type
*/
public function setType($type)
{
$this->type = $type;
return $this;
}
/**
* @return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* @param mixed $value
*/
public function setValue($value)
{
$this->value = $value;
return $this;
}
/**
* @return mixed
*/
public function getCellFormula()
{
return $this->cellFormula;
}
/**
* @param mixed $cellFormula
*/
public function setCellFormula($cellFormula)
{
$this->cellFormula = $cellFormula;
return $this;
}
}

View File

@@ -0,0 +1,209 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
use SimpleXMLElement;
class ConditionalFormattingRuleExtension
{
const CONDITION_EXTENSION_DATABAR = 'dataBar';
/** <conditionalFormatting> attributes */
private $id;
/** @var string Conditional Formatting Rule */
private $cfRule;
/** <conditionalFormatting> children */
/** @var ConditionalDataBarExtension */
private $dataBar;
/** @var string Sequence of References */
private $sqref;
/**
* ConditionalFormattingRuleExtension constructor.
*/
public function __construct($id = null, string $cfRule = self::CONDITION_EXTENSION_DATABAR)
{
if (null === $id) {
$this->id = '{' . $this->generateUuid() . '}';
} else {
$this->id = $id;
}
$this->cfRule = $cfRule;
}
private function generateUuid()
{
$chars = str_split('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx');
foreach ($chars as $i => $char) {
if ($char === 'x') {
$chars[$i] = dechex(random_int(0, 15));
} elseif ($char === 'y') {
$chars[$i] = dechex(random_int(8, 11));
}
}
return implode('', /** @scrutinizer ignore-type */ $chars);
}
public static function parseExtLstXml($extLstXml)
{
$conditionalFormattingRuleExtensions = [];
$conditionalFormattingRuleExtensionXml = null;
if ($extLstXml instanceof SimpleXMLElement) {
foreach ((count($extLstXml) > 0 ? $extLstXml : [$extLstXml]) as $extLst) {
//this uri is conditionalFormattings
//https://docs.microsoft.com/en-us/openspecs/office_standards/ms-xlsx/07d607af-5618-4ca2-b683-6a78dc0d9627
if (isset($extLst->ext['uri']) && (string) $extLst->ext['uri'] === '{78C0D931-6437-407d-A8EE-F0AAD7539E65}') {
$conditionalFormattingRuleExtensionXml = $extLst->ext;
}
}
if ($conditionalFormattingRuleExtensionXml) {
$ns = $conditionalFormattingRuleExtensionXml->getNamespaces(true);
$extFormattingsXml = $conditionalFormattingRuleExtensionXml->children($ns['x14']);
foreach ($extFormattingsXml->children($ns['x14']) as $extFormattingXml) {
$extCfRuleXml = $extFormattingXml->cfRule;
$attributes = $extCfRuleXml->attributes();
if (!$attributes || ((string) $attributes->type) !== Conditional::CONDITION_DATABAR) {
continue;
}
$extFormattingRuleObj = new self((string) $attributes->id);
$extFormattingRuleObj->setSqref((string) $extFormattingXml->children($ns['xm'])->sqref);
$conditionalFormattingRuleExtensions[$extFormattingRuleObj->getId()] = $extFormattingRuleObj;
$extDataBarObj = new ConditionalDataBarExtension();
$extFormattingRuleObj->setDataBarExt($extDataBarObj);
$dataBarXml = $extCfRuleXml->dataBar;
self::parseExtDataBarAttributesFromXml($extDataBarObj, $dataBarXml);
self::parseExtDataBarElementChildrenFromXml($extDataBarObj, $dataBarXml, $ns);
}
}
}
return $conditionalFormattingRuleExtensions;
}
private static function parseExtDataBarAttributesFromXml(
ConditionalDataBarExtension $extDataBarObj,
SimpleXMLElement $dataBarXml
): void {
$dataBarAttribute = $dataBarXml->attributes();
if ($dataBarAttribute->minLength) {
$extDataBarObj->setMinLength((int) $dataBarAttribute->minLength);
}
if ($dataBarAttribute->maxLength) {
$extDataBarObj->setMaxLength((int) $dataBarAttribute->maxLength);
}
if ($dataBarAttribute->border) {
$extDataBarObj->setBorder((bool) (string) $dataBarAttribute->border);
}
if ($dataBarAttribute->gradient) {
$extDataBarObj->setGradient((bool) (string) $dataBarAttribute->gradient);
}
if ($dataBarAttribute->direction) {
$extDataBarObj->setDirection((string) $dataBarAttribute->direction);
}
if ($dataBarAttribute->negativeBarBorderColorSameAsPositive) {
$extDataBarObj->setNegativeBarBorderColorSameAsPositive((bool) (string) $dataBarAttribute->negativeBarBorderColorSameAsPositive);
}
if ($dataBarAttribute->axisPosition) {
$extDataBarObj->setAxisPosition((string) $dataBarAttribute->axisPosition);
}
}
private static function parseExtDataBarElementChildrenFromXml(ConditionalDataBarExtension $extDataBarObj, SimpleXMLElement $dataBarXml, $ns): void
{
if ($dataBarXml->borderColor) {
$extDataBarObj->setBorderColor((string) $dataBarXml->borderColor->attributes()['rgb']);
}
if ($dataBarXml->negativeFillColor) {
$extDataBarObj->setNegativeFillColor((string) $dataBarXml->negativeFillColor->attributes()['rgb']);
}
if ($dataBarXml->negativeBorderColor) {
$extDataBarObj->setNegativeBorderColor((string) $dataBarXml->negativeBorderColor->attributes()['rgb']);
}
if ($dataBarXml->axisColor) {
$axisColorAttr = $dataBarXml->axisColor->attributes();
$extDataBarObj->setAxisColor((string) $axisColorAttr['rgb'], (string) $axisColorAttr['theme'], (string) $axisColorAttr['tint']);
}
$cfvoIndex = 0;
foreach ($dataBarXml->cfvo as $cfvo) {
$f = (string) $cfvo->/** @scrutinizer ignore-call */ children($ns['xm'])->f;
/** @scrutinizer ignore-call */
$attributes = $cfvo->attributes();
if (!($attributes)) {
continue;
}
if ($cfvoIndex === 0) {
$extDataBarObj->setMinimumConditionalFormatValueObject(new ConditionalFormatValueObject((string) $attributes['type'], null, (empty($f) ? null : $f)));
}
if ($cfvoIndex === 1) {
$extDataBarObj->setMaximumConditionalFormatValueObject(new ConditionalFormatValueObject((string) $attributes['type'], null, (empty($f) ? null : $f)));
}
++$cfvoIndex;
}
}
/**
* @return mixed
*/
public function getId()
{
return $this->id;
}
/**
* @param mixed $id
*/
public function setId($id): self
{
$this->id = $id;
return $this;
}
public function getCfRule(): string
{
return $this->cfRule;
}
public function setCfRule(string $cfRule): self
{
$this->cfRule = $cfRule;
return $this;
}
public function getDataBarExt(): ConditionalDataBarExtension
{
return $this->dataBar;
}
public function setDataBarExt(ConditionalDataBarExtension $dataBar): self
{
$this->dataBar = $dataBar;
return $this;
}
public function getSqref(): string
{
return $this->sqref;
}
public function setSqref(string $sqref): self
{
$this->sqref = $sqref;
return $this;
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Borders;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\Font;
use PhpOffice\PhpSpreadsheet\Style\Style;
class StyleMerger
{
/**
* @var Style
*/
protected $baseStyle;
public function __construct(Style $baseStyle)
{
$this->baseStyle = $baseStyle;
}
public function getStyle(): Style
{
return $this->baseStyle;
}
public function mergeStyle(Style $style): void
{
if ($style->getNumberFormat() !== null && $style->getNumberFormat()->getFormatCode() !== null) {
$this->baseStyle->getNumberFormat()->setFormatCode($style->getNumberFormat()->getFormatCode());
}
if ($style->getFont() !== null) {
$this->mergeFontStyle($this->baseStyle->getFont(), $style->getFont());
}
if ($style->getFill() !== null) {
$this->mergeFillStyle($this->baseStyle->getFill(), $style->getFill());
}
if ($style->getBorders() !== null) {
$this->mergeBordersStyle($this->baseStyle->getBorders(), $style->getBorders());
}
}
protected function mergeFontStyle(Font $baseFontStyle, Font $fontStyle): void
{
if ($fontStyle->getBold() !== null) {
$baseFontStyle->setBold($fontStyle->getBold());
}
if ($fontStyle->getItalic() !== null) {
$baseFontStyle->setItalic($fontStyle->getItalic());
}
if ($fontStyle->getStrikethrough() !== null) {
$baseFontStyle->setStrikethrough($fontStyle->getStrikethrough());
}
if ($fontStyle->getUnderline() !== null) {
$baseFontStyle->setUnderline($fontStyle->getUnderline());
}
if ($fontStyle->getColor() !== null && $fontStyle->getColor()->getARGB() !== null) {
$baseFontStyle->setColor($fontStyle->getColor());
}
}
protected function mergeFillStyle(Fill $baseFillStyle, Fill $fillStyle): void
{
if ($fillStyle->getFillType() !== null) {
$baseFillStyle->setFillType($fillStyle->getFillType());
}
//if ($fillStyle->getRotation() !== null) {
$baseFillStyle->setRotation($fillStyle->getRotation());
//}
if ($fillStyle->getStartColor() !== null && $fillStyle->getStartColor()->getARGB() !== null) {
$baseFillStyle->setStartColor($fillStyle->getStartColor());
}
if ($fillStyle->getEndColor() !== null && $fillStyle->getEndColor()->getARGB() !== null) {
$baseFillStyle->setEndColor($fillStyle->getEndColor());
}
}
protected function mergeBordersStyle(Borders $baseBordersStyle, Borders $bordersStyle): void
{
if ($bordersStyle->getTop() !== null) {
$this->mergeBorderStyle($baseBordersStyle->getTop(), $bordersStyle->getTop());
}
if ($bordersStyle->getBottom() !== null) {
$this->mergeBorderStyle($baseBordersStyle->getBottom(), $bordersStyle->getBottom());
}
if ($bordersStyle->getLeft() !== null) {
$this->mergeBorderStyle($baseBordersStyle->getLeft(), $bordersStyle->getLeft());
}
if ($bordersStyle->getRight() !== null) {
$this->mergeBorderStyle($baseBordersStyle->getRight(), $bordersStyle->getRight());
}
}
protected function mergeBorderStyle(Border $baseBorderStyle, Border $borderStyle): void
{
//if ($borderStyle->getBorderStyle() !== null) {
$baseBorderStyle->setBorderStyle($borderStyle->getBorderStyle());
//}
if ($borderStyle->getColor() !== null && $borderStyle->getColor()->getARGB() !== null) {
$baseBorderStyle->setColor($borderStyle->getColor());
}
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard\WizardInterface;
class Wizard
{
public const CELL_VALUE = 'cellValue';
public const TEXT_VALUE = 'textValue';
public const BLANKS = Conditional::CONDITION_CONTAINSBLANKS;
public const NOT_BLANKS = Conditional::CONDITION_NOTCONTAINSBLANKS;
public const ERRORS = Conditional::CONDITION_CONTAINSERRORS;
public const NOT_ERRORS = Conditional::CONDITION_NOTCONTAINSERRORS;
public const EXPRESSION = Conditional::CONDITION_EXPRESSION;
public const FORMULA = Conditional::CONDITION_EXPRESSION;
public const DATES_OCCURRING = 'DateValue';
public const DUPLICATES = Conditional::CONDITION_DUPLICATES;
public const UNIQUE = Conditional::CONDITION_UNIQUE;
public const VALUE_TYPE_LITERAL = 'value';
public const VALUE_TYPE_CELL = 'cell';
public const VALUE_TYPE_FORMULA = 'formula';
/**
* @var string
*/
protected $cellRange;
public function __construct(string $cellRange)
{
$this->cellRange = $cellRange;
}
public function newRule(string $ruleType): WizardInterface
{
switch ($ruleType) {
case self::CELL_VALUE:
return new Wizard\CellValue($this->cellRange);
case self::TEXT_VALUE:
return new Wizard\TextValue($this->cellRange);
case self::BLANKS:
return new Wizard\Blanks($this->cellRange, true);
case self::NOT_BLANKS:
return new Wizard\Blanks($this->cellRange, false);
case self::ERRORS:
return new Wizard\Errors($this->cellRange, true);
case self::NOT_ERRORS:
return new Wizard\Errors($this->cellRange, false);
case self::EXPRESSION:
case self::FORMULA:
return new Wizard\Expression($this->cellRange);
case self::DATES_OCCURRING:
return new Wizard\DateValue($this->cellRange);
case self::DUPLICATES:
return new Wizard\Duplicates($this->cellRange, false);
case self::UNIQUE:
return new Wizard\Duplicates($this->cellRange, true);
default:
throw new Exception('No wizard exists for this CF rule type');
}
}
public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface
{
$conditionalType = $conditional->getConditionType();
switch ($conditionalType) {
case Conditional::CONDITION_CELLIS:
return Wizard\CellValue::fromConditional($conditional, $cellRange);
case Conditional::CONDITION_CONTAINSTEXT:
case Conditional::CONDITION_NOTCONTAINSTEXT:
case Conditional::CONDITION_BEGINSWITH:
case Conditional::CONDITION_ENDSWITH:
return Wizard\TextValue::fromConditional($conditional, $cellRange);
case Conditional::CONDITION_CONTAINSBLANKS:
case Conditional::CONDITION_NOTCONTAINSBLANKS:
return Wizard\Blanks::fromConditional($conditional, $cellRange);
case Conditional::CONDITION_CONTAINSERRORS:
case Conditional::CONDITION_NOTCONTAINSERRORS:
return Wizard\Errors::fromConditional($conditional, $cellRange);
case Conditional::CONDITION_TIMEPERIOD:
return Wizard\DateValue::fromConditional($conditional, $cellRange);
case Conditional::CONDITION_EXPRESSION:
return Wizard\Expression::fromConditional($conditional, $cellRange);
case Conditional::CONDITION_DUPLICATES:
case Conditional::CONDITION_UNIQUE:
return Wizard\Duplicates::fromConditional($conditional, $cellRange);
default:
throw new Exception('No wizard exists for this CF rule type');
}
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
/**
* @method Blanks notBlank()
* @method Blanks notEmpty()
* @method Blanks isBlank()
* @method Blanks isEmpty()
*/
class Blanks extends WizardAbstract implements WizardInterface
{
protected const OPERATORS = [
'notBlank' => false,
'isBlank' => true,
'notEmpty' => false,
'empty' => true,
];
protected const EXPRESSIONS = [
Wizard::NOT_BLANKS => 'LEN(TRIM(%s))>0',
Wizard::BLANKS => 'LEN(TRIM(%s))=0',
];
/**
* @var bool
*/
protected $inverse;
public function __construct(string $cellRange, bool $inverse = false)
{
parent::__construct($cellRange);
$this->inverse = $inverse;
}
protected function inverse(bool $inverse): void
{
$this->inverse = $inverse;
}
protected function getExpression(): void
{
$this->expression = sprintf(
self::EXPRESSIONS[$this->inverse ? Wizard::BLANKS : Wizard::NOT_BLANKS],
$this->referenceCell
);
}
public function getConditional(): Conditional
{
$this->getExpression();
$conditional = new Conditional();
$conditional->setConditionType(
$this->inverse ? Conditional::CONDITION_CONTAINSBLANKS : Conditional::CONDITION_NOTCONTAINSBLANKS
);
$conditional->setConditions([$this->expression]);
$conditional->setStyle($this->getStyle());
$conditional->setStopIfTrue($this->getStopIfTrue());
return $conditional;
}
public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface
{
if (
$conditional->getConditionType() !== Conditional::CONDITION_CONTAINSBLANKS &&
$conditional->getConditionType() !== Conditional::CONDITION_NOTCONTAINSBLANKS
) {
throw new Exception('Conditional is not a Blanks CF Rule conditional');
}
$wizard = new self($cellRange);
$wizard->style = $conditional->getStyle();
$wizard->stopIfTrue = $conditional->getStopIfTrue();
$wizard->inverse = $conditional->getConditionType() === Conditional::CONDITION_CONTAINSBLANKS;
return $wizard;
}
/**
* @param string $methodName
* @param mixed[] $arguments
*/
public function __call($methodName, $arguments): self
{
if (!array_key_exists($methodName, self::OPERATORS)) {
throw new Exception('Invalid Operation for Blanks CF Rule Wizard');
}
$this->inverse(self::OPERATORS[$methodName]);
return $this;
}
}

View File

@@ -0,0 +1,200 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\CellMatcher;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
/**
* @method CellValue equals($value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
* @method CellValue notEquals($value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
* @method CellValue greaterThan($value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
* @method CellValue greaterThanOrEqual($value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
* @method CellValue lessThan($value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
* @method CellValue lessThanOrEqual($value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
* @method CellValue between($value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
* @method CellValue notBetween($value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
* @method CellValue and($value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
*/
class CellValue extends WizardAbstract implements WizardInterface
{
protected const MAGIC_OPERATIONS = [
'equals' => Conditional::OPERATOR_EQUAL,
'notEquals' => Conditional::OPERATOR_NOTEQUAL,
'greaterThan' => Conditional::OPERATOR_GREATERTHAN,
'greaterThanOrEqual' => Conditional::OPERATOR_GREATERTHANOREQUAL,
'lessThan' => Conditional::OPERATOR_LESSTHAN,
'lessThanOrEqual' => Conditional::OPERATOR_LESSTHANOREQUAL,
'between' => Conditional::OPERATOR_BETWEEN,
'notBetween' => Conditional::OPERATOR_NOTBETWEEN,
];
protected const SINGLE_OPERATORS = CellMatcher::COMPARISON_OPERATORS;
protected const RANGE_OPERATORS = CellMatcher::COMPARISON_RANGE_OPERATORS;
/** @var string */
protected $operator = Conditional::OPERATOR_EQUAL;
/** @var array */
protected $operand = [0];
/**
* @var string[]
*/
protected $operandValueType = [];
public function __construct(string $cellRange)
{
parent::__construct($cellRange);
}
protected function operator(string $operator): void
{
if ((!isset(self::SINGLE_OPERATORS[$operator])) && (!isset(self::RANGE_OPERATORS[$operator]))) {
throw new Exception('Invalid Operator for Cell Value CF Rule Wizard');
}
$this->operator = $operator;
}
/**
* @param mixed $operand
*/
protected function operand(int $index, $operand, string $operandValueType = Wizard::VALUE_TYPE_LITERAL): void
{
if (is_string($operand)) {
$operand = $this->validateOperand($operand, $operandValueType);
}
$this->operand[$index] = $operand;
$this->operandValueType[$index] = $operandValueType;
}
/**
* @param mixed $value
*
* @return float|int|string
*/
protected function wrapValue($value, string $operandValueType)
{
if (!is_numeric($value) && !is_bool($value) && null !== $value) {
if ($operandValueType === Wizard::VALUE_TYPE_LITERAL) {
return '"' . str_replace('"', '""', $value) . '"';
}
return $this->cellConditionCheck($value);
}
if (null === $value) {
$value = 'NULL';
} elseif (is_bool($value)) {
$value = $value ? 'TRUE' : 'FALSE';
}
return $value;
}
public function getConditional(): Conditional
{
if (!isset(self::RANGE_OPERATORS[$this->operator])) {
unset($this->operand[1], $this->operandValueType[1]);
}
$values = array_map([$this, 'wrapValue'], $this->operand, $this->operandValueType);
$conditional = new Conditional();
$conditional->setConditionType(Conditional::CONDITION_CELLIS);
$conditional->setOperatorType($this->operator);
$conditional->setConditions($values);
$conditional->setStyle($this->getStyle());
$conditional->setStopIfTrue($this->getStopIfTrue());
return $conditional;
}
protected static function unwrapString(string $condition): string
{
if ((strpos($condition, '"') === 0) && (strpos(strrev($condition), '"') === 0)) {
$condition = substr($condition, 1, -1);
}
return str_replace('""', '"', $condition);
}
public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface
{
if ($conditional->getConditionType() !== Conditional::CONDITION_CELLIS) {
throw new Exception('Conditional is not a Cell Value CF Rule conditional');
}
$wizard = new self($cellRange);
$wizard->style = $conditional->getStyle();
$wizard->stopIfTrue = $conditional->getStopIfTrue();
$wizard->operator = $conditional->getOperatorType();
$conditions = $conditional->getConditions();
foreach ($conditions as $index => $condition) {
// Best-guess to try and identify if the text is a string literal, a cell reference or a formula?
$operandValueType = Wizard::VALUE_TYPE_LITERAL;
if (is_string($condition)) {
if (Calculation::keyInExcelConstants($condition)) {
$condition = Calculation::getExcelConstants($condition);
} elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '$/i', $condition)) {
$operandValueType = Wizard::VALUE_TYPE_CELL;
$condition = self::reverseAdjustCellRef($condition, $cellRange);
} elseif (
preg_match('/\(\)/', $condition) ||
preg_match('/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i', $condition)
) {
$operandValueType = Wizard::VALUE_TYPE_FORMULA;
$condition = self::reverseAdjustCellRef($condition, $cellRange);
} else {
$condition = self::unwrapString($condition);
}
}
$wizard->operand($index, $condition, $operandValueType);
}
return $wizard;
}
/**
* @param string $methodName
* @param mixed[] $arguments
*/
public function __call($methodName, $arguments): self
{
if (!isset(self::MAGIC_OPERATIONS[$methodName]) && $methodName !== 'and') {
throw new Exception('Invalid Operator for Cell Value CF Rule Wizard');
}
if ($methodName === 'and') {
if (!isset(self::RANGE_OPERATORS[$this->operator])) {
throw new Exception('AND Value is only appropriate for range operators');
}
// Scrutinizer ignores its own suggested workaround.
//$this->operand(1, /** @scrutinizer ignore-type */ ...$arguments);
if (count($arguments) < 2) {
$this->operand(1, $arguments[0]);
} else {
$this->operand(1, $arguments[0], $arguments[1]);
}
return $this;
}
$this->operator(self::MAGIC_OPERATIONS[$methodName]);
//$this->operand(0, ...$arguments);
if (count($arguments) < 2) {
$this->operand(0, $arguments[0]);
} else {
$this->operand(0, $arguments[0], $arguments[1]);
}
return $this;
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
/**
* @method DateValue yesterday()
* @method DateValue today()
* @method DateValue tomorrow()
* @method DateValue lastSevenDays()
* @method DateValue lastWeek()
* @method DateValue thisWeek()
* @method DateValue nextWeek()
* @method DateValue lastMonth()
* @method DateValue thisMonth()
* @method DateValue nextMonth()
*/
class DateValue extends WizardAbstract implements WizardInterface
{
protected const MAGIC_OPERATIONS = [
'yesterday' => Conditional::TIMEPERIOD_YESTERDAY,
'today' => Conditional::TIMEPERIOD_TODAY,
'tomorrow' => Conditional::TIMEPERIOD_TOMORROW,
'lastSevenDays' => Conditional::TIMEPERIOD_LAST_7_DAYS,
'last7Days' => Conditional::TIMEPERIOD_LAST_7_DAYS,
'lastWeek' => Conditional::TIMEPERIOD_LAST_WEEK,
'thisWeek' => Conditional::TIMEPERIOD_THIS_WEEK,
'nextWeek' => Conditional::TIMEPERIOD_NEXT_WEEK,
'lastMonth' => Conditional::TIMEPERIOD_LAST_MONTH,
'thisMonth' => Conditional::TIMEPERIOD_THIS_MONTH,
'nextMonth' => Conditional::TIMEPERIOD_NEXT_MONTH,
];
protected const EXPRESSIONS = [
Conditional::TIMEPERIOD_YESTERDAY => 'FLOOR(%s,1)=TODAY()-1',
Conditional::TIMEPERIOD_TODAY => 'FLOOR(%s,1)=TODAY()',
Conditional::TIMEPERIOD_TOMORROW => 'FLOOR(%s,1)=TODAY()+1',
Conditional::TIMEPERIOD_LAST_7_DAYS => 'AND(TODAY()-FLOOR(%s,1)<=6,FLOOR(%s,1)<=TODAY())',
Conditional::TIMEPERIOD_LAST_WEEK => 'AND(TODAY()-ROUNDDOWN(%s,0)>=(WEEKDAY(TODAY())),TODAY()-ROUNDDOWN(%s,0)<(WEEKDAY(TODAY())+7))',
Conditional::TIMEPERIOD_THIS_WEEK => 'AND(TODAY()-ROUNDDOWN(%s,0)<=WEEKDAY(TODAY())-1,ROUNDDOWN(%s,0)-TODAY()<=7-WEEKDAY(TODAY()))',
Conditional::TIMEPERIOD_NEXT_WEEK => 'AND(ROUNDDOWN(%s,0)-TODAY()>(7-WEEKDAY(TODAY())),ROUNDDOWN(%s,0)-TODAY()<(15-WEEKDAY(TODAY())))',
Conditional::TIMEPERIOD_LAST_MONTH => 'AND(MONTH(%s)=MONTH(EDATE(TODAY(),0-1)),YEAR(%s)=YEAR(EDATE(TODAY(),0-1)))',
Conditional::TIMEPERIOD_THIS_MONTH => 'AND(MONTH(%s)=MONTH(TODAY()),YEAR(%s)=YEAR(TODAY()))',
Conditional::TIMEPERIOD_NEXT_MONTH => 'AND(MONTH(%s)=MONTH(EDATE(TODAY(),0+1)),YEAR(%s)=YEAR(EDATE(TODAY(),0+1)))',
];
/** @var string */
protected $operator;
public function __construct(string $cellRange)
{
parent::__construct($cellRange);
}
protected function operator(string $operator): void
{
$this->operator = $operator;
}
protected function setExpression(): void
{
$referenceCount = substr_count(self::EXPRESSIONS[$this->operator], '%s');
$references = array_fill(0, $referenceCount, $this->referenceCell);
$this->expression = sprintf(self::EXPRESSIONS[$this->operator], ...$references);
}
public function getConditional(): Conditional
{
$this->setExpression();
$conditional = new Conditional();
$conditional->setConditionType(Conditional::CONDITION_TIMEPERIOD);
$conditional->setText($this->operator);
$conditional->setConditions([$this->expression]);
$conditional->setStyle($this->getStyle());
$conditional->setStopIfTrue($this->getStopIfTrue());
return $conditional;
}
public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface
{
if ($conditional->getConditionType() !== Conditional::CONDITION_TIMEPERIOD) {
throw new Exception('Conditional is not a Date Value CF Rule conditional');
}
$wizard = new self($cellRange);
$wizard->style = $conditional->getStyle();
$wizard->stopIfTrue = $conditional->getStopIfTrue();
$wizard->operator = $conditional->getText();
return $wizard;
}
/**
* @param string $methodName
* @param mixed[] $arguments
*/
public function __call($methodName, $arguments): self
{
if (!isset(self::MAGIC_OPERATIONS[$methodName])) {
throw new Exception('Invalid Operation for Date Value CF Rule Wizard');
}
$this->operator(self::MAGIC_OPERATIONS[$methodName]);
return $this;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
/**
* @method Errors duplicates()
* @method Errors unique()
*/
class Duplicates extends WizardAbstract implements WizardInterface
{
protected const OPERATORS = [
'duplicates' => false,
'unique' => true,
];
/**
* @var bool
*/
protected $inverse;
public function __construct(string $cellRange, bool $inverse = false)
{
parent::__construct($cellRange);
$this->inverse = $inverse;
}
protected function inverse(bool $inverse): void
{
$this->inverse = $inverse;
}
public function getConditional(): Conditional
{
$conditional = new Conditional();
$conditional->setConditionType(
$this->inverse ? Conditional::CONDITION_UNIQUE : Conditional::CONDITION_DUPLICATES
);
$conditional->setStyle($this->getStyle());
$conditional->setStopIfTrue($this->getStopIfTrue());
return $conditional;
}
public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface
{
if (
$conditional->getConditionType() !== Conditional::CONDITION_DUPLICATES &&
$conditional->getConditionType() !== Conditional::CONDITION_UNIQUE
) {
throw new Exception('Conditional is not a Duplicates CF Rule conditional');
}
$wizard = new self($cellRange);
$wizard->style = $conditional->getStyle();
$wizard->stopIfTrue = $conditional->getStopIfTrue();
$wizard->inverse = $conditional->getConditionType() === Conditional::CONDITION_UNIQUE;
return $wizard;
}
/**
* @param string $methodName
* @param mixed[] $arguments
*/
public function __call($methodName, $arguments): self
{
if (!array_key_exists($methodName, self::OPERATORS)) {
throw new Exception('Invalid Operation for Errors CF Rule Wizard');
}
$this->inverse(self::OPERATORS[$methodName]);
return $this;
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
/**
* @method Errors notError()
* @method Errors isError()
*/
class Errors extends WizardAbstract implements WizardInterface
{
protected const OPERATORS = [
'notError' => false,
'isError' => true,
];
protected const EXPRESSIONS = [
Wizard::NOT_ERRORS => 'NOT(ISERROR(%s))',
Wizard::ERRORS => 'ISERROR(%s)',
];
/**
* @var bool
*/
protected $inverse;
public function __construct(string $cellRange, bool $inverse = false)
{
parent::__construct($cellRange);
$this->inverse = $inverse;
}
protected function inverse(bool $inverse): void
{
$this->inverse = $inverse;
}
protected function getExpression(): void
{
$this->expression = sprintf(
self::EXPRESSIONS[$this->inverse ? Wizard::ERRORS : Wizard::NOT_ERRORS],
$this->referenceCell
);
}
public function getConditional(): Conditional
{
$this->getExpression();
$conditional = new Conditional();
$conditional->setConditionType(
$this->inverse ? Conditional::CONDITION_CONTAINSERRORS : Conditional::CONDITION_NOTCONTAINSERRORS
);
$conditional->setConditions([$this->expression]);
$conditional->setStyle($this->getStyle());
$conditional->setStopIfTrue($this->getStopIfTrue());
return $conditional;
}
public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface
{
if (
$conditional->getConditionType() !== Conditional::CONDITION_CONTAINSERRORS &&
$conditional->getConditionType() !== Conditional::CONDITION_NOTCONTAINSERRORS
) {
throw new Exception('Conditional is not an Errors CF Rule conditional');
}
$wizard = new self($cellRange);
$wizard->style = $conditional->getStyle();
$wizard->stopIfTrue = $conditional->getStopIfTrue();
$wizard->inverse = $conditional->getConditionType() === Conditional::CONDITION_CONTAINSERRORS;
return $wizard;
}
/**
* @param string $methodName
* @param mixed[] $arguments
*/
public function __call($methodName, $arguments): self
{
if (!array_key_exists($methodName, self::OPERATORS)) {
throw new Exception('Invalid Operation for Errors CF Rule Wizard');
}
$this->inverse(self::OPERATORS[$methodName]);
return $this;
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
/**
* @method Expression formula(string $expression)
*/
class Expression extends WizardAbstract implements WizardInterface
{
/**
* @var string
*/
protected $expression;
public function __construct(string $cellRange)
{
parent::__construct($cellRange);
}
public function expression(string $expression): self
{
$expression = $this->validateOperand($expression, Wizard::VALUE_TYPE_FORMULA);
$this->expression = $expression;
return $this;
}
public function getConditional(): Conditional
{
$expression = $this->adjustConditionsForCellReferences([$this->expression]);
$conditional = new Conditional();
$conditional->setConditionType(Conditional::CONDITION_EXPRESSION);
$conditional->setConditions($expression);
$conditional->setStyle($this->getStyle());
$conditional->setStopIfTrue($this->getStopIfTrue());
return $conditional;
}
public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface
{
if ($conditional->getConditionType() !== Conditional::CONDITION_EXPRESSION) {
throw new Exception('Conditional is not an Expression CF Rule conditional');
}
$wizard = new self($cellRange);
$wizard->style = $conditional->getStyle();
$wizard->stopIfTrue = $conditional->getStopIfTrue();
$wizard->expression = self::reverseAdjustCellRef((string) ($conditional->getConditions()[0]), $cellRange);
return $wizard;
}
/**
* @param string $methodName
* @param mixed[] $arguments
*/
public function __call($methodName, $arguments): self
{
if ($methodName !== 'formula') {
throw new Exception('Invalid Operation for Expression CF Rule Wizard');
}
// Scrutinizer ignores its own recommendation
//$this->expression(/** @scrutinizer ignore-type */ ...$arguments);
$this->expression($arguments[0]);
return $this;
}
}

View File

@@ -0,0 +1,164 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
/**
* @method TextValue contains(string $value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
* @method TextValue doesNotContain(string $value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
* @method TextValue doesntContain(string $value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
* @method TextValue beginsWith(string $value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
* @method TextValue startsWith(string $value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
* @method TextValue endsWith(string $value, string $operandValueType = Wizard::VALUE_TYPE_LITERAL)
*/
class TextValue extends WizardAbstract implements WizardInterface
{
protected const MAGIC_OPERATIONS = [
'contains' => Conditional::OPERATOR_CONTAINSTEXT,
'doesntContain' => Conditional::OPERATOR_NOTCONTAINS,
'doesNotContain' => Conditional::OPERATOR_NOTCONTAINS,
'beginsWith' => Conditional::OPERATOR_BEGINSWITH,
'startsWith' => Conditional::OPERATOR_BEGINSWITH,
'endsWith' => Conditional::OPERATOR_ENDSWITH,
];
protected const OPERATORS = [
Conditional::OPERATOR_CONTAINSTEXT => Conditional::CONDITION_CONTAINSTEXT,
Conditional::OPERATOR_NOTCONTAINS => Conditional::CONDITION_NOTCONTAINSTEXT,
Conditional::OPERATOR_BEGINSWITH => Conditional::CONDITION_BEGINSWITH,
Conditional::OPERATOR_ENDSWITH => Conditional::CONDITION_ENDSWITH,
];
protected const EXPRESSIONS = [
Conditional::OPERATOR_CONTAINSTEXT => 'NOT(ISERROR(SEARCH(%s,%s)))',
Conditional::OPERATOR_NOTCONTAINS => 'ISERROR(SEARCH(%s,%s))',
Conditional::OPERATOR_BEGINSWITH => 'LEFT(%s,LEN(%s))=%s',
Conditional::OPERATOR_ENDSWITH => 'RIGHT(%s,LEN(%s))=%s',
];
/** @var string */
protected $operator;
/** @var string */
protected $operand;
/**
* @var string
*/
protected $operandValueType;
public function __construct(string $cellRange)
{
parent::__construct($cellRange);
}
protected function operator(string $operator): void
{
if (!isset(self::OPERATORS[$operator])) {
throw new Exception('Invalid Operator for Text Value CF Rule Wizard');
}
$this->operator = $operator;
}
protected function operand(string $operand, string $operandValueType = Wizard::VALUE_TYPE_LITERAL): void
{
$operand = $this->validateOperand($operand, $operandValueType);
$this->operand = $operand;
$this->operandValueType = $operandValueType;
}
protected function wrapValue(string $value): string
{
return '"' . $value . '"';
}
protected function setExpression(): void
{
$operand = $this->operandValueType === Wizard::VALUE_TYPE_LITERAL
? $this->wrapValue(str_replace('"', '""', $this->operand))
: $this->cellConditionCheck($this->operand);
if (
$this->operator === Conditional::OPERATOR_CONTAINSTEXT ||
$this->operator === Conditional::OPERATOR_NOTCONTAINS
) {
$this->expression = sprintf(self::EXPRESSIONS[$this->operator], $operand, $this->referenceCell);
} else {
$this->expression = sprintf(self::EXPRESSIONS[$this->operator], $this->referenceCell, $operand, $operand);
}
}
public function getConditional(): Conditional
{
$this->setExpression();
$conditional = new Conditional();
$conditional->setConditionType(self::OPERATORS[$this->operator]);
$conditional->setOperatorType($this->operator);
$conditional->setText(
$this->operandValueType !== Wizard::VALUE_TYPE_LITERAL
? $this->cellConditionCheck($this->operand)
: $this->operand
);
$conditional->setConditions([$this->expression]);
$conditional->setStyle($this->getStyle());
$conditional->setStopIfTrue($this->getStopIfTrue());
return $conditional;
}
public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface
{
if (!in_array($conditional->getConditionType(), self::OPERATORS, true)) {
throw new Exception('Conditional is not a Text Value CF Rule conditional');
}
$wizard = new self($cellRange);
$wizard->operator = (string) array_search($conditional->getConditionType(), self::OPERATORS, true);
$wizard->style = $conditional->getStyle();
$wizard->stopIfTrue = $conditional->getStopIfTrue();
// Best-guess to try and identify if the text is a string literal, a cell reference or a formula?
$wizard->operandValueType = Wizard::VALUE_TYPE_LITERAL;
$condition = $conditional->getText();
if (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '$/i', $condition)) {
$wizard->operandValueType = Wizard::VALUE_TYPE_CELL;
$condition = self::reverseAdjustCellRef($condition, $cellRange);
} elseif (
preg_match('/\(\)/', $condition) ||
preg_match('/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i', $condition)
) {
$wizard->operandValueType = Wizard::VALUE_TYPE_FORMULA;
}
$wizard->operand = $condition;
return $wizard;
}
/**
* @param string $methodName
* @param mixed[] $arguments
*/
public function __call($methodName, $arguments): self
{
if (!isset(self::MAGIC_OPERATIONS[$methodName])) {
throw new Exception('Invalid Operation for Text Value CF Rule Wizard');
}
$this->operator(self::MAGIC_OPERATIONS[$methodName]);
//$this->operand(...$arguments);
if (count($arguments) < 2) {
$this->operand($arguments[0]);
} else {
$this->operand($arguments[0], $arguments[1]);
}
return $this;
}
}

View File

@@ -0,0 +1,199 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
use PhpOffice\PhpSpreadsheet\Style\Style;
abstract class WizardAbstract
{
/**
* @var ?Style
*/
protected $style;
/**
* @var string
*/
protected $expression;
/**
* @var string
*/
protected $cellRange;
/**
* @var string
*/
protected $referenceCell;
/**
* @var int
*/
protected $referenceRow;
/**
* @var bool
*/
protected $stopIfTrue = false;
/**
* @var int
*/
protected $referenceColumn;
public function __construct(string $cellRange)
{
$this->setCellRange($cellRange);
}
public function getCellRange(): string
{
return $this->cellRange;
}
public function setCellRange(string $cellRange): void
{
$this->cellRange = $cellRange;
$this->setReferenceCellForExpressions($cellRange);
}
protected function setReferenceCellForExpressions(string $conditionalRange): void
{
$conditionalRange = Coordinate::splitRange(str_replace('$', '', strtoupper($conditionalRange)));
[$this->referenceCell] = $conditionalRange[0];
[$this->referenceColumn, $this->referenceRow] = Coordinate::indexesFromString($this->referenceCell);
}
public function getStopIfTrue(): bool
{
return $this->stopIfTrue;
}
public function setStopIfTrue(bool $stopIfTrue): void
{
$this->stopIfTrue = $stopIfTrue;
}
public function getStyle(): Style
{
return $this->style ?? new Style(false, true);
}
public function setStyle(Style $style): void
{
$this->style = $style;
}
protected function validateOperand(string $operand, string $operandValueType = Wizard::VALUE_TYPE_LITERAL): string
{
if (
$operandValueType === Wizard::VALUE_TYPE_LITERAL &&
substr($operand, 0, 1) === '"' &&
substr($operand, -1) === '"'
) {
$operand = str_replace('""', '"', substr($operand, 1, -1));
} elseif ($operandValueType === Wizard::VALUE_TYPE_FORMULA && substr($operand, 0, 1) === '=') {
$operand = substr($operand, 1);
}
return $operand;
}
protected static function reverseCellAdjustment(array $matches, int $referenceColumn, int $referenceRow): string
{
$worksheet = $matches[1];
$column = $matches[6];
$row = $matches[7];
if (strpos($column, '$') === false) {
$column = Coordinate::columnIndexFromString($column);
$column -= $referenceColumn - 1;
$column = Coordinate::stringFromColumnIndex($column);
}
if (strpos($row, '$') === false) {
$row -= $referenceRow - 1;
}
return "{$worksheet}{$column}{$row}";
}
public static function reverseAdjustCellRef(string $condition, string $cellRange): string
{
$conditionalRange = Coordinate::splitRange(str_replace('$', '', strtoupper($cellRange)));
[$referenceCell] = $conditionalRange[0];
[$referenceColumnIndex, $referenceRow] = Coordinate::indexesFromString($referenceCell);
$splitCondition = explode(Calculation::FORMULA_STRING_QUOTE, $condition);
$i = false;
foreach ($splitCondition as &$value) {
// Only count/replace in alternating array entries (ie. not in quoted strings)
$i = $i === false;
if ($i) {
$value = (string) preg_replace_callback(
'/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i',
function ($matches) use ($referenceColumnIndex, $referenceRow) {
return self::reverseCellAdjustment($matches, $referenceColumnIndex, $referenceRow);
},
$value
);
}
}
unset($value);
// Then rebuild the condition string to return it
return implode(Calculation::FORMULA_STRING_QUOTE, $splitCondition);
}
protected function conditionCellAdjustment(array $matches): string
{
$worksheet = $matches[1];
$column = $matches[6];
$row = $matches[7];
if (strpos($column, '$') === false) {
$column = Coordinate::columnIndexFromString($column);
$column += $this->referenceColumn - 1;
$column = Coordinate::stringFromColumnIndex($column);
}
if (strpos($row, '$') === false) {
$row += $this->referenceRow - 1;
}
return "{$worksheet}{$column}{$row}";
}
protected function cellConditionCheck(string $condition): string
{
$splitCondition = explode(Calculation::FORMULA_STRING_QUOTE, $condition);
$i = false;
foreach ($splitCondition as &$value) {
// Only count/replace in alternating array entries (ie. not in quoted strings)
$i = $i === false;
if ($i) {
$value = (string) preg_replace_callback(
'/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i',
[$this, 'conditionCellAdjustment'],
$value
);
}
}
unset($value);
// Then rebuild the condition string to return it
return implode(Calculation::FORMULA_STRING_QUOTE, $splitCondition);
}
protected function adjustConditionsForCellReferences(array $conditions): array
{
return array_map(
[$this, 'cellConditionCheck'],
$conditions
);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
use PhpOffice\PhpSpreadsheet\Style\Style;
interface WizardInterface
{
public function getCellRange(): string;
public function setCellRange(string $cellRange): void;
public function getStyle(): Style;
public function setStyle(Style $style): void;
public function getStopIfTrue(): bool;
public function setStopIfTrue(bool $stopIfTrue): void;
public function getConditional(): Conditional;
public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): self;
}