Skip to content

Commit

Permalink
Read conditional styling for cell (#2491)
Browse files Browse the repository at this point in the history
* Allow single-cell checks on conditional styles, even when the style is configured for a range of cells
* Work on the CellMatcher logic to evaluate Conditionals for a cell based on its value, and identify which conditional styles should be applied
* Refactor style merging and cell matching for conditional formatting into separate classes; this should make it easier to test, and easier to extend for other CF expressions subsequently
* Added support for containsErrors and notContainsErrors
* Initial work on a wizard to help simplify created Conditional Formatting rules, to ensure that the correct expressions are set
* Further work on extending the Conditional Formatting rules to cover more of the options that are available in MS Excel
* Prevent phpcs-fixer from removing class @method annotations, used to identify the signature for magic methods used in Wizard classes
* Implement `fromConditional()`` method to allow the creation of a CF Wizard from an existing Conditional
* Ensure that xlsx Reader picks up the timePeriod attribute for DatesOccurring CF Rules
* Allow Duplicates/Uniques CF Rules to be recognised in the Xlsx Reader
* Basic Xlsx reading of CF Rules/Styles from <extLst><ext><ConditinalFormattings> element, and not just the <ConditinalFormatting> element of the worksheet

* Add some validation for operands passed to the CF Wizards
 - remove any leading ``=` from formulae, because they'll be embedded into other formulae
 - unwrap any string literals from quotes, because that's also handled internally

Handle cross-worksheet cell references in cellReferences and Formulae/Expressions

* re-baseline phpstan

* Update Change Log with details of the CF Improvements
Mark Baker authored Jan 22, 2022

Verified

This commit was signed with the committer’s verified signature.
ViBiOh Vincent Boutour
1 parent dee098b commit 4a04499
Showing 66 changed files with 5,967 additions and 185 deletions.
2 changes: 1 addition & 1 deletion .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
@@ -55,7 +55,7 @@
'function_declaration' => true,
'function_to_constant' => true,
'function_typehint_space' => true,
'general_phpdoc_annotation_remove' => ['annotations' => ['access', 'category', 'copyright', 'method', 'throws']],
'general_phpdoc_annotation_remove' => ['annotations' => ['access', 'category', 'copyright', 'throws']],
'global_namespace_import' => true,
'header_comment' => false, // We don't use common header in all our files
'heredoc_indentation' => false, // Requires PHP >= 7.3
19 changes: 17 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -9,7 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org).

### Added

- Nothing
- Improved support for Conditional Formatting Rules [PR #2491](https://github.com/PHPOffice/PhpSpreadsheet/pull/2491)
- Provide support for a wider range of Conditional Formatting Rules for Xlsx Reader/Writer:
- Cells Containing (cellIs)
- Specific Text (containing, notContaining, beginsWith, endsWith)
- Dates Occurring (all supported timePeriods)
- Blanks/NoBlanks
- Errors/NoErrors
- Duplicates/Unique
- Expression
- Provision of CF Wizards (for all the above listed rule types) to help create/modify CF Rules without having to manage all the combinations of types/operators, and the complexities of formula expressions, or the text/timePeriod attributes.

See [documentation](https://phpspreadsheet.readthedocs.io/en/latest/topics/conditional-formatting/) for details

- Full support of the above CF Rules for the Xlsx Reader and Writer; even when the file being loaded has CF rules listed in the `<extLst><ext><ConditionalFormattings>` element for the worksheet rather than the `<ConditionalFormatting>` element.
- Provision of a CellMatcher to identify if rules are matched for a cell, and which matching style will be applied.
- Improved documentation and examples, covering all supported CF rule types.

### Changed

@@ -25,7 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).

### Fixed

- Nothing
- Various bugs related to Conditional Formatting Rules, and errors in the Xlsx Writer for Conditional Formatting [PR #2491](https://github.com/PHPOffice/PhpSpreadsheet/pull/2491)

## 1.21.0 - 2022-01-06

765 changes: 765 additions & 0 deletions docs/topics/conditional-formatting.md

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/topics/images/11-02-CF-Simple-Tab.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/topics/images/11-07-CF-Wizard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/topics/images/11-10-CF-Blanks-Example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/topics/images/11-11-CF-Errors-Example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/topics/images/11-12-CF-Simple-Example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/topics/images/11-17-CF-Text-Contains.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/topics/images/11-20-CF-Rule-Order-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/topics/images/11-21-CF-Rule-Order-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions docs/topics/recipes.md
Original file line number Diff line number Diff line change
@@ -899,6 +899,8 @@ $spreadsheet->getActiveSheet()
);
```

More detailed documentation of the Conditional Formatting options and rules, and the use of Wizards to help create them, can be found in [a dedicated section of the documentation](https://phpspreadsheet.readthedocs.io/en/latest/topics/conditional-formatting/).

### DataBar of Conditional formatting
The basics are the same as conditional formatting.
Additional DataBar object to conditional formatting.
206 changes: 118 additions & 88 deletions phpstan-baseline.neon

Large diffs are not rendered by default.

174 changes: 174 additions & 0 deletions samples/ConditionalFormatting/01_Basic_Comparisons.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<?php

use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Color;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\Style;

require __DIR__ . '/../Header.php';

// Create new Spreadsheet object
$helper->log('Create new Spreadsheet object');
$spreadsheet = new Spreadsheet();

// Set document properties
$helper->log('Set document properties');
$spreadsheet->getProperties()->setCreator('Mark Baker')
->setLastModifiedBy('Mark Baker')
->setTitle('PhpSpreadsheet Test Document')
->setSubject('PhpSpreadsheet Test Document')
->setDescription('Test document for PhpSpreadsheet, generated using PHP classes.')
->setKeywords('office PhpSpreadsheet php')
->setCategory('Test result file');

// Create the worksheet
$helper->log('Add data');
$spreadsheet->setActiveSheetIndex(0);
$spreadsheet->getActiveSheet()
->setCellValue('A1', 'Literal Value Comparison')
->setCellValue('A9', 'Value Comparison with Absolute Cell Reference $H$9')
->setCellValue('A17', 'Value Comparison with Relative Cell References')
->setCellValue('A23', 'Value Comparison with Formula based on AVERAGE() ± STDEV()');

$dataArray = [
[-2, -1, 0, 1, 2],
[-1, 0, 1, 2, 3],
[0, 1, 2, 3, 4],
[1, 2, 3, 4, 5],
];

$betweenDataArray = [
[2, 7, 6],
[9, 5, 1],
[4, 3, 8],
];

$spreadsheet->getActiveSheet()
->fromArray($dataArray, null, 'A2', true)
->fromArray($dataArray, null, 'A10', true)
->fromArray($betweenDataArray, null, 'A18', true)
->fromArray($dataArray, null, 'A24', true)
->setCellValue('H9', 1);

// Set title row bold
$helper->log('Set title row bold');
$spreadsheet->getActiveSheet()->getStyle('A1:E1')->getFont()->setBold(true);
$spreadsheet->getActiveSheet()->getStyle('A9:E9')->getFont()->setBold(true);
$spreadsheet->getActiveSheet()->getStyle('A17:E17')->getFont()->setBold(true);
$spreadsheet->getActiveSheet()->getStyle('A23:E23')->getFont()->setBold(true);

// Define some styles for our Conditionals
$helper->log('Define some styles for our Conditionals');
$yellowStyle = new Style(false, true);
$yellowStyle->getFill()
->setFillType(Fill::FILL_SOLID)
->getEndColor()->setARGB(Color::COLOR_YELLOW);
$greenStyle = new Style(false, true);
$greenStyle->getFill()
->setFillType(Fill::FILL_SOLID)
->getEndColor()->setARGB(Color::COLOR_GREEN);
$redStyle = new Style(false, true);
$redStyle->getFill()
->setFillType(Fill::FILL_SOLID)
->getEndColor()->setARGB(Color::COLOR_RED);

// Set conditional formatting rules and styles
$helper->log('Define conditional formatting and set styles');

// Set rules for Literal Value Comparison
$cellRange = 'A2:E5';
$conditionalStyles = [];
$wizardFactory = new Wizard($cellRange);
/** @var Wizard\CellValue $cellWizard */
$cellWizard = $wizardFactory->newRule(Wizard::CELL_VALUE);

$cellWizard->equals(0)
->setStyle($yellowStyle);
$conditionalStyles[] = $cellWizard->getConditional();

$cellWizard->greaterThan(0)
->setStyle($greenStyle);
$conditionalStyles[] = $cellWizard->getConditional();

$cellWizard->lessThan(0)
->setStyle($redStyle);
$conditionalStyles[] = $cellWizard->getConditional();

$spreadsheet->getActiveSheet()
->getStyle($cellWizard->getCellRange())
->setConditionalStyles($conditionalStyles);

// Set rules for Value Comparison with Absolute Cell Reference $H$9
$cellRange = 'A10:E13';
$conditionalStyles = [];
$wizardFactory = new Wizard($cellRange);
/** @var Wizard\CellValue $cellWizard */
$cellWizard = $wizardFactory->newRule(Wizard::CELL_VALUE);

$cellWizard->equals('$H$9', Wizard::VALUE_TYPE_CELL)
->setStyle($yellowStyle);
$conditionalStyles[] = $cellWizard->getConditional();

$cellWizard->greaterThan('$H$9', Wizard::VALUE_TYPE_CELL)
->setStyle($greenStyle);
$conditionalStyles[] = $cellWizard->getConditional();

$cellWizard->lessThan('$H$9', Wizard::VALUE_TYPE_CELL)
->setStyle($redStyle);
$conditionalStyles[] = $cellWizard->getConditional();

$spreadsheet->getActiveSheet()
->getStyle($cellWizard->getCellRange())
->setConditionalStyles($conditionalStyles);

// Set rules for Value Comparison with Relative Cell References
$cellRange = 'A18:A20';
$conditionalStyles = [];
$wizardFactory = new Wizard($cellRange);
/** @var Wizard\CellValue $cellWizard */
$cellWizard = $wizardFactory->newRule(Wizard::CELL_VALUE);

$cellWizard->between('$B1', Wizard::VALUE_TYPE_CELL)
->and('$C1', Wizard::VALUE_TYPE_CELL)
->setStyle($greenStyle);
$conditionalStyles[] = $cellWizard->getConditional();

$spreadsheet->getActiveSheet()
->getStyle($cellWizard->getCellRange())
->setConditionalStyles($conditionalStyles);

// Set rules for Value Comparison with Formula
$cellRange = 'A24:E27';
$formulaRange = implode(
':',
array_map(
[Coordinate::class, 'absoluteCoordinate'],
Coordinate::splitRange($cellRange)[0]
)
);
$conditionalStyles = [];
$wizardFactory = new Wizard($cellRange);
/** @var Wizard\CellValue $cellWizard */
$cellWizard = $wizardFactory->newRule(Wizard::CELL_VALUE);

$cellWizard->between('AVERAGE(' . $formulaRange . ')-STDEV(' . $formulaRange . ')', Wizard::VALUE_TYPE_FORMULA)
->and('AVERAGE(' . $formulaRange . ')+STDEV(' . $formulaRange . ')', Wizard::VALUE_TYPE_FORMULA)
->setStyle($yellowStyle);
$conditionalStyles[] = $cellWizard->getConditional();

$cellWizard->greaterThan('AVERAGE(' . $formulaRange . ')+STDEV(' . $formulaRange . ')', Wizard::VALUE_TYPE_FORMULA)
->setStyle($greenStyle);
$conditionalStyles[] = $cellWizard->getConditional();

$cellWizard->lessThan('AVERAGE(' . $formulaRange . ')-STDEV(' . $formulaRange . ')', Wizard::VALUE_TYPE_FORMULA)
->setStyle($redStyle);
$conditionalStyles[] = $cellWizard->getConditional();

$spreadsheet->getActiveSheet()
->getStyle($cellWizard->getCellRange())
->setConditionalStyles($conditionalStyles);

// Save
$helper->write($spreadsheet, __FILE__);
227 changes: 227 additions & 0 deletions samples/ConditionalFormatting/02_Text_Comparisons.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
<?php

use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Color;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\Style;

require __DIR__ . '/../Header.php';

// Create new Spreadsheet object
$helper->log('Create new Spreadsheet object');
$spreadsheet = new Spreadsheet();

// Set document properties
$helper->log('Set document properties');
$spreadsheet->getProperties()->setCreator('Mark Baker')
->setLastModifiedBy('Mark Baker')
->setTitle('PhpSpreadsheet Test Document')
->setSubject('PhpSpreadsheet Test Document')
->setDescription('Test document for PhpSpreadsheet, generated using PHP classes.')
->setKeywords('office PhpSpreadsheet php')
->setCategory('Test result file');

// Create the worksheet
$helper->log('Add data');
$spreadsheet->setActiveSheetIndex(0);
$spreadsheet->getActiveSheet()
->setCellValue('A1', 'Value Begins With Literal')
->setCellValue('A7', 'Value Ends With Literal')
->setCellValue('A13', 'Value Contains Literal')
->setCellValue('A19', "Value Doesn't Contain Literal")
->setCellValue('E1', 'Value Begins With using Cell Reference')
->setCellValue('E7', 'Value Ends With using Cell Reference')
->setCellValue('E13', 'Value Contains using Cell Reference')
->setCellValue('E19', "Value Doesn't Contain using Cell Reference")
->setCellValue('A25', 'Simple Comparison using Concatenation Formula');

$dataArray = [
['HELLO', 'WORLD'],
['MELLOW', 'YELLOW'],
['SLEEPY', 'HOLLOW'],
];

$spreadsheet->getActiveSheet()
->fromArray($dataArray, null, 'A2', true)
->fromArray($dataArray, null, 'A8', true)
->fromArray($dataArray, null, 'A14', true)
->fromArray($dataArray, null, 'A20', true)
->fromArray($dataArray, null, 'E2', true)
->fromArray($dataArray, null, 'E8', true)
->fromArray($dataArray, null, 'E14', true)
->fromArray($dataArray, null, 'E20', true)
->fromArray($dataArray, null, 'A26', true)
->setCellValue('D1', 'H')
->setCellValue('D7', 'OW')
->setCellValue('D13', 'LL')
->setCellValue('D19', 'EL')
->setCellValue('C26', 'HELLO WORLD')
->setCellValue('C27', 'SOYLENT GREEN')
->setCellValue('C28', 'SLEEPY HOLLOW');

// Set title row bold
$helper->log('Set title row bold');
$spreadsheet->getActiveSheet()->getStyle('A1:G1')->getFont()->setBold(true);
$spreadsheet->getActiveSheet()->getStyle('A7:G7')->getFont()->setBold(true);
$spreadsheet->getActiveSheet()->getStyle('A13:G13')->getFont()->setBold(true);
$spreadsheet->getActiveSheet()->getStyle('A19:G19')->getFont()->setBold(true);
$spreadsheet->getActiveSheet()->getStyle('A25:C25')->getFont()->setBold(true);

// Define some styles for our Conditionals
$helper->log('Define some styles for our Conditionals');
$yellowStyle = new Style(false, true);
$yellowStyle->getFill()
->setFillType(Fill::FILL_SOLID)
->getEndColor()->setARGB(Color::COLOR_YELLOW);
$greenStyle = new Style(false, true);
$greenStyle->getFill()
->setFillType(Fill::FILL_SOLID)
->getEndColor()->setARGB(Color::COLOR_GREEN);
$redStyle = new Style(false, true);
$redStyle->getFill()
->setFillType(Fill::FILL_SOLID)
->getEndColor()->setARGB(Color::COLOR_RED);

// Set conditional formatting rules and styles
$helper->log('Define conditional formatting and set styles');

// Set rules for Literal Value Begins With
$cellRange = 'A2:B4';
$conditionalStyles = [];
$wizardFactory = new Wizard($cellRange);
/** @var Wizard\TextValue $textWizard */
$textWizard = $wizardFactory->newRule(Wizard::TEXT_VALUE);

$textWizard->beginsWith('H')
->setStyle($yellowStyle);
$conditionalStyles[] = $textWizard->getConditional();

$spreadsheet->getActiveSheet()
->getStyle($textWizard->getCellRange())
->setConditionalStyles($conditionalStyles);

// Set rules for Value Begins With using Cell Reference
$cellRange = 'E2:F4';
$conditionalStyles = [];
$wizardFactory = new Wizard($cellRange);
/** @var Wizard\TextValue $textWizard */
$textWizard = $wizardFactory->newRule(Wizard::TEXT_VALUE);

$textWizard->beginsWith('$D$1', Wizard::VALUE_TYPE_CELL)
->setStyle($yellowStyle);
$conditionalStyles[] = $textWizard->getConditional();

$spreadsheet->getActiveSheet()
->getStyle($textWizard->getCellRange())
->setConditionalStyles($conditionalStyles);

// Set rules for Literal Value Ends With
$cellRange = 'A8:B10';
$conditionalStyles = [];
$wizardFactory = new Wizard($cellRange);
/** @var Wizard\TextValue $textWizard */
$textWizard = $wizardFactory->newRule(Wizard::TEXT_VALUE);

$textWizard->endsWith('OW')
->setStyle($yellowStyle);
$conditionalStyles[] = $textWizard->getConditional();

$spreadsheet->getActiveSheet()
->getStyle($textWizard->getCellRange())
->setConditionalStyles($conditionalStyles);

// Set rules for Value Ends With using Cell Reference
$cellRange = 'E8:F10';
$conditionalStyles = [];
$wizardFactory = new Wizard($cellRange);
/** @var Wizard\TextValue $textWizard */
$textWizard = $wizardFactory->newRule(Wizard::TEXT_VALUE);

$textWizard->endsWith('$D$7', Wizard::VALUE_TYPE_CELL)
->setStyle($yellowStyle);
$conditionalStyles[] = $textWizard->getConditional();

$spreadsheet->getActiveSheet()
->getStyle($textWizard->getCellRange())
->setConditionalStyles($conditionalStyles);

// Set rules for Literal Value Contains
$cellRange = 'A14:B16';
$conditionalStyles = [];
$wizardFactory = new Wizard($cellRange);
/** @var Wizard\TextValue $textWizard */
$textWizard = $wizardFactory->newRule(Wizard::TEXT_VALUE);

$textWizard->contains('LL')
->setStyle($greenStyle);
$conditionalStyles[] = $textWizard->getConditional();

$spreadsheet->getActiveSheet()
->getStyle($textWizard->getCellRange())
->setConditionalStyles($conditionalStyles);

// Set rules for Value Contains using Cell Reference
$cellRange = 'E14:F16';
$conditionalStyles = [];
$wizardFactory = new Wizard($cellRange);
/** @var Wizard\TextValue $textWizard */
$textWizard = $wizardFactory->newRule(Wizard::TEXT_VALUE);

$textWizard->contains('$D$13', Wizard::VALUE_TYPE_CELL)
->setStyle($greenStyle);
$conditionalStyles[] = $textWizard->getConditional();

$spreadsheet->getActiveSheet()
->getStyle($textWizard->getCellRange())
->setConditionalStyles($conditionalStyles);

// Set rules for Literal Value Does Not Contain
$cellRange = 'A20:B22';
$conditionalStyles = [];
$wizardFactory = new Wizard($cellRange);
/** @var Wizard\TextValue $textWizard */
$textWizard = $wizardFactory->newRule(Wizard::TEXT_VALUE);

$textWizard->doesNotContain('EL')
->setStyle($redStyle);
$conditionalStyles[] = $textWizard->getConditional();

$spreadsheet->getActiveSheet()
->getStyle($textWizard->getCellRange())
->setConditionalStyles($conditionalStyles);

// Set rules for Value Contains using Cell Reference
$cellRange = 'E20:F22';
$conditionalStyles = [];
$wizardFactory = new Wizard($cellRange);
/** @var Wizard\TextValue $textWizard */
$textWizard = $wizardFactory->newRule(Wizard::TEXT_VALUE);

$textWizard->doesNotContain('$D$19', Wizard::VALUE_TYPE_CELL)
->setStyle($redStyle);
$conditionalStyles[] = $textWizard->getConditional();

$spreadsheet->getActiveSheet()
->getStyle($textWizard->getCellRange())
->setConditionalStyles($conditionalStyles);

// Set rules for Simple Comparison using Concatenation Formula
$cellRange = 'C26:C28';
$conditionalStyles = [];
$wizardFactory = new Wizard($cellRange);
/** @var Wizard\CellValue $cellWizard */
$cellWizard = $wizardFactory->newRule(Wizard::CELL_VALUE);

$cellWizard->equals('CONCATENATE($A1," ",$B1)', Wizard::VALUE_TYPE_FORMULA)
->setStyle($yellowStyle);
$conditionalStyles[] = $cellWizard->getConditional();

$spreadsheet->getActiveSheet()
->getStyle($cellWizard->getCellRange())
->setConditionalStyles($conditionalStyles);

$spreadsheet->getActiveSheet()->getColumnDimension('C')->setAutoSize(true);

// Save
$helper->write($spreadsheet, __FILE__);
76 changes: 76 additions & 0 deletions samples/ConditionalFormatting/03_Blank_Comparisons.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Color;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\Style;

require __DIR__ . '/../Header.php';

// Create new Spreadsheet object
$helper->log('Create new Spreadsheet object');
$spreadsheet = new Spreadsheet();

// Set document properties
$helper->log('Set document properties');
$spreadsheet->getProperties()->setCreator('Mark Baker')
->setLastModifiedBy('Mark Baker')
->setTitle('PhpSpreadsheet Test Document')
->setSubject('PhpSpreadsheet Test Document')
->setDescription('Test document for PhpSpreadsheet, generated using PHP classes.')
->setKeywords('office PhpSpreadsheet php')
->setCategory('Test result file');

// Create the worksheet
$helper->log('Add data');
$spreadsheet->setActiveSheetIndex(0);
$spreadsheet->getActiveSheet()
->setCellValue('A1', 'Blank Comparison');

$dataArray = [
['HELLO', null],
[null, 'WORLD'],
];

$spreadsheet->getActiveSheet()
->fromArray($dataArray, null, 'A2', true);

// Set title row bold
$helper->log('Set title row bold');
$spreadsheet->getActiveSheet()->getStyle('A1:B1')->getFont()->setBold(true);

// Define some styles for our Conditionals
$helper->log('Define some styles for our Conditionals');
$greenStyle = new Style(false, true);
$greenStyle->getFill()
->setFillType(Fill::FILL_SOLID)
->getEndColor()->setARGB(Color::COLOR_GREEN);
$redStyle = new Style(false, true);
$redStyle->getFill()
->setFillType(Fill::FILL_SOLID)
->getEndColor()->setARGB(Color::COLOR_RED);

// Set conditional formatting rules and styles
$helper->log('Define conditional formatting and set styles');

// Set rules for Blank Comparison
$cellRange = 'A2:B3';
$conditionalStyles = [];
$wizardFactory = new Wizard($cellRange);
/** @var Wizard\Blanks $blanksWizard */
$blanksWizard = $wizardFactory->newRule(Wizard::BLANKS);

$blanksWizard->setStyle($redStyle);
$conditionalStyles[] = $blanksWizard->getConditional();

$blanksWizard->notBlank()
->setStyle($greenStyle);
$conditionalStyles[] = $blanksWizard->getConditional();

$spreadsheet->getActiveSheet()
->getStyle($blanksWizard->getCellRange())
->setConditionalStyles($conditionalStyles);

// Save
$helper->write($spreadsheet, __FILE__);
79 changes: 79 additions & 0 deletions samples/ConditionalFormatting/04_Error_Comparisons.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Color;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\Style;

require __DIR__ . '/../Header.php';

// Create new Spreadsheet object
$helper->log('Create new Spreadsheet object');
$spreadsheet = new Spreadsheet();

// Set document properties
$helper->log('Set document properties');
$spreadsheet->getProperties()->setCreator('Mark Baker')
->setLastModifiedBy('Mark Baker')
->setTitle('PhpSpreadsheet Test Document')
->setSubject('PhpSpreadsheet Test Document')
->setDescription('Test document for PhpSpreadsheet, generated using PHP classes.')
->setKeywords('office PhpSpreadsheet php')
->setCategory('Test result file');

// Create the worksheet
$helper->log('Add data');
$spreadsheet->setActiveSheetIndex(0);
$spreadsheet->getActiveSheet()
->setCellValue('A1', 'Error Comparison');

$dataArray = [
[5, -2, '=A2/B2'],
[5, -1, '=A3/B3'],
[5, 0, '=A4/B4'],
[5, 1, '=A5/B5'],
[5, 2, '=A6/B6'],
];

$spreadsheet->getActiveSheet()
->fromArray($dataArray, null, 'A2', true);

// Set title row bold
$helper->log('Set title row bold');
$spreadsheet->getActiveSheet()->getStyle('A1:C1')->getFont()->setBold(true);

// Define some styles for our Conditionals
$helper->log('Define some styles for our Conditionals');
$greenStyle = new Style(false, true);
$greenStyle->getFill()
->setFillType(Fill::FILL_SOLID)
->getEndColor()->setARGB(Color::COLOR_GREEN);
$redStyle = new Style(false, true);
$redStyle->getFill()
->setFillType(Fill::FILL_SOLID)
->getEndColor()->setARGB(Color::COLOR_RED);

// Set conditional formatting rules and styles
$helper->log('Define conditional formatting and set styles');

// Set rules for Blank Comparison
$cellRange = 'C2:C6';
$conditionalStyles = [];
$wizardFactory = new Wizard($cellRange);
/** @var Wizard\Errors $errorsWizard */
$errorsWizard = $wizardFactory->newRule(Wizard::ERRORS);

$errorsWizard->setStyle($redStyle);
$conditionalStyles[] = $errorsWizard->getConditional();

$errorsWizard->notError()
->setStyle($greenStyle);
$conditionalStyles[] = $errorsWizard->getConditional();

$spreadsheet->getActiveSheet()
->getStyle($errorsWizard->getCellRange())
->setConditionalStyles($conditionalStyles);

// Save
$helper->write($spreadsheet, __FILE__);
152 changes: 152 additions & 0 deletions samples/ConditionalFormatting/05_Date_Comparisons.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?php

use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Color;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\Style;

require __DIR__ . '/../Header.php';

// Create new Spreadsheet object
$helper->log('Create new Spreadsheet object');
$spreadsheet = new Spreadsheet();

// Set document properties
$helper->log('Set document properties');
$spreadsheet->getProperties()->setCreator('Mark Baker')
->setLastModifiedBy('Mark Baker')
->setTitle('PhpSpreadsheet Test Document')
->setSubject('PhpSpreadsheet Test Document')
->setDescription('Test document for PhpSpreadsheet, generated using PHP classes.')
->setKeywords('office PhpSpreadsheet php')
->setCategory('Test result file');

// Create the worksheet
$helper->log('Add data');

$spreadsheet->setActiveSheetIndex(0);
$spreadsheet->getActiveSheet()
->setCellValue('B1', 'yesterday()')
->setCellValue('C1', 'today()')
->setCellValue('D1', 'tomorrow()')
->setCellValue('E1', 'last7Days()')
->setCellValue('F1', 'lastWeek()')
->setCellValue('G1', 'thisWeek()')
->setCellValue('H1', 'nextWeek()')
->setCellValue('I1', 'lastMonth()')
->setCellValue('J1', 'thisMonth()')
->setCellValue('K1', 'nextMonth()');

$dateFunctionArray = [
'yesterday()',
'today()',
'tomorrow()',
'last7Days()',
'lastWeek()',
'thisWeek()',
'nextWeek()',
'lastMonth()',
'thisMonth()',
'nextMonth()',
];
$dateTitleArray = [
['First day of last month'],
['Last day of last month'],
['Last Monday'],
['Last Friday'],
['Monday last week'],
['Wednesday last week'],
['Friday last week'],
['Yesterday'],
['Today'],
['Tomorrow'],
['Monday this week'],
['Wednesday this week'],
['Friday this week'],
['Monday next week'],
['Wednesday next week'],
['Friday next week'],
['First day of next month'],
['Last day of next month'],
];
$dataArray = [
['=EOMONTH(TODAY(),-2)+1'],
['=EOMONTH(TODAY(),-1)'],
['=TODAY()-WEEKDAY(TODAY(),3)'],
['=TODAY()-WEEKDAY(TODAY())-1'],
['=2-WEEKDAY(TODAY())+TODAY()-7'],
['=4-WEEKDAY(TODAY())+TODAY()-7'],
['=6-WEEKDAY(TODAY())+TODAY()-7'],
['=TODAY()-1'],
['=TODAY()'],
['=TODAY()+1'],
['=2-WEEKDAY(TODAY())+TODAY()'],
['=4-WEEKDAY(TODAY())+TODAY()'],
['=6-WEEKDAY(TODAY())+TODAY()'],
['=2-WEEKDAY(TODAY())+TODAY()+7'],
['=4-WEEKDAY(TODAY())+TODAY()+7'],
['=6-WEEKDAY(TODAY())+TODAY()+7'],
['=EOMONTH(TODAY(),0)+1'],
['=EOMONTH(TODAY(),1)'],
];

$spreadsheet->getActiveSheet()
->fromArray($dateFunctionArray, null, 'B1', true);
$spreadsheet->getActiveSheet()
->fromArray($dateTitleArray, null, 'A2', true);
for ($column = 'B'; $column !== 'L'; ++$column) {
$spreadsheet->getActiveSheet()
->fromArray($dataArray, null, "{$column}2", true);
}

// Set title row bold
$helper->log('Set title row bold');
$spreadsheet->getActiveSheet()->getStyle('B1:K1')->getFont()->setBold(true);
$spreadsheet->getActiveSheet()->getStyle('B1:K1')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);

// Define some styles for our Conditionals
$helper->log('Define some styles for our Conditionals');

$yellowStyle = new Style(false, true);
$yellowStyle->getFill()
->setFillType(Fill::FILL_SOLID)
->getEndColor()->setARGB(Color::COLOR_YELLOW);
$yellowStyle->getNumberFormat()->setFormatCode('ddd dd-mmm-yyyy');

// Set conditional formatting rules and styles
$helper->log('Define conditional formatting and set styles');
for ($column = 'B'; $column !== 'L'; ++$column) {
$wizardFactory = new Wizard("{$column}2:{$column}19");
/** @var Wizard\DateValue $dateWizard */
$dateWizard = $wizardFactory->newRule(Wizard::DATES_OCCURRING);
$conditionalStyles = [];

$methodName = trim($spreadsheet->getActiveSheet()->getCell("{$column}1")->getValue(), '()');
$dateWizard->$methodName()
->setStyle($yellowStyle);

$conditionalStyles[] = $dateWizard->getConditional();

$spreadsheet->getActiveSheet()
->getStyle($dateWizard->getCellRange())
->setConditionalStyles($conditionalStyles);
}

// Set conditional formatting rules and styles
$helper->log('Set some additional styling for date formats');

$spreadsheet->getActiveSheet()->getStyle('B:B')->getNumberFormat()->setFormatCode('ddd dd-mmm-yyyy');
for ($column = 'A'; $column !== 'L'; ++$column) {
if ($column !== 'A') {
$spreadsheet->getActiveSheet()->getStyle("{$column}:{$column}")
->getNumberFormat()->setFormatCode('ddd dd-mmm-yyyy');
}
$spreadsheet->getActiveSheet()->getColumnDimension($column)
->setAutoSize(true);
}
$spreadsheet->getActiveSheet()->getStyle('A:A')->getFont()->setBold(true);

// Save
$helper->write($spreadsheet, __FILE__);
85 changes: 85 additions & 0 deletions samples/ConditionalFormatting/06_Duplicate_Comparisons.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Color;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\Style;

require __DIR__ . '/../Header.php';

// Create new Spreadsheet object
$helper->log('Create new Spreadsheet object');
$spreadsheet = new Spreadsheet();

// Set document properties
$helper->log('Set document properties');
$spreadsheet->getProperties()->setCreator('Mark Baker')
->setLastModifiedBy('Mark Baker')
->setTitle('PhpSpreadsheet Test Document')
->setSubject('PhpSpreadsheet Test Document')
->setDescription('Test document for PhpSpreadsheet, generated using PHP classes.')
->setKeywords('office PhpSpreadsheet php')
->setCategory('Test result file');

// Create the worksheet
$helper->log('Add data');
$spreadsheet->setActiveSheetIndex(0);
$spreadsheet->getActiveSheet()
->setCellValue('A1', 'Duplicates Comparison');

$dataArray = [
[1, 0, 3],
[2, 1, 1],
[3, 1, 4],
[4, 2, 1],
[5, 3, 5],
[6, 5, 9],
[7, 8, 2],
[8, 13, 6],
[9, 21, 5],
[10, 34, 3],
[11, 55, 5],
];

$spreadsheet->getActiveSheet()
->fromArray($dataArray, null, 'A2', true);

// Set title row bold
$helper->log('Set title row bold');
$spreadsheet->getActiveSheet()->getStyle('A1:C1')->getFont()->setBold(true);

// Define some styles for our Conditionals
$helper->log('Define some styles for our Conditionals');
$greenStyle = new Style(false, true);
$greenStyle->getFill()
->setFillType(Fill::FILL_SOLID)
->getEndColor()->setARGB(Color::COLOR_GREEN);
$yellowStyle = new Style(false, true);
$yellowStyle->getFill()
->setFillType(Fill::FILL_SOLID)
->getEndColor()->setARGB(Color::COLOR_YELLOW);

// Set conditional formatting rules and styles
$helper->log('Define conditional formatting and set styles');

// Set rules for Duplicates Comparison
$cellRange = 'A2:C12';
$conditionalStyles = [];
$wizardFactory = new Wizard($cellRange);
/** @var Wizard\Duplicates $duplicatesWizard */
$duplicatesWizard = $wizardFactory->newRule(Wizard::DUPLICATES);

$duplicatesWizard->setStyle($yellowStyle);
$conditionalStyles[] = $duplicatesWizard->getConditional();

$duplicatesWizard->unique()
->setStyle($greenStyle);
$conditionalStyles[] = $duplicatesWizard->getConditional();

$spreadsheet->getActiveSheet()
->getStyle($duplicatesWizard->getCellRange())
->setConditionalStyles($conditionalStyles);

// Save
$helper->write($spreadsheet, __FILE__);
147 changes: 147 additions & 0 deletions samples/ConditionalFormatting/07_Expression_Comparisons.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php

use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Color;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Style\Style;

require __DIR__ . '/../Header.php';

// Create new Spreadsheet object
$helper->log('Create new Spreadsheet object');
$spreadsheet = new Spreadsheet();

// Set document properties
$helper->log('Set document properties');
$spreadsheet->getProperties()->setCreator('Mark Baker')
->setLastModifiedBy('Mark Baker')
->setTitle('PhpSpreadsheet Test Document')
->setSubject('PhpSpreadsheet Test Document')
->setDescription('Test document for PhpSpreadsheet, generated using PHP classes.')
->setKeywords('office PhpSpreadsheet php')
->setCategory('Test result file');

// Create the worksheet
$helper->log('Add data');
$spreadsheet->setActiveSheetIndex(0);
$spreadsheet->getActiveSheet()
->setCellValue('A1', 'Odd/Even Expression Comparison')
->setCellValue('A15', 'Sales Grid Expression Comparison')
->setCellValue('A25', 'Sales Grid Multiple Expression Comparison');

$dataArray = [
[1, 0, 3],
[2, 1, 1],
[3, 1, 4],
[4, 2, 1],
[5, 3, 5],
[6, 5, 9],
[7, 8, 2],
[8, 13, 6],
[9, 21, 5],
[10, 34, 4],
];

$salesGrid = [
['Name', 'Sales', 'Country', 'Quarter'],
['Smith', 16753, 'UK', 'Q3'],
['Johnson', 14808, 'USA', 'Q4'],
['Williams', 10644, 'UK', 'Q2'],
['Jones', 1390, 'USA', 'Q3'],
['Brown', 4865, 'USA', 'Q4'],
['Williams', 12438, 'UK', 'Q2'],
];

$spreadsheet->getActiveSheet()
->fromArray($dataArray, null, 'A2', true);
$spreadsheet->getActiveSheet()
->fromArray($salesGrid, null, 'A16', true);
$spreadsheet->getActiveSheet()
->fromArray($salesGrid, null, 'A26', true);

// Set title row bold
$helper->log('Set title row bold');
$spreadsheet->getActiveSheet()->getStyle('A1:B1')->getFont()->setBold(true);
$spreadsheet->getActiveSheet()->getStyle('A15:D16')->getFont()->setBold(true);
$spreadsheet->getActiveSheet()->getStyle('A25:D26')->getFont()->setBold(true);

// Define some styles for our Conditionals
$helper->log('Define some styles for our Conditionals');
$greenStyle = new Style(false, true);
$greenStyle->getFill()
->setFillType(Fill::FILL_SOLID)
->getEndColor()->setARGB(Color::COLOR_GREEN);
$yellowStyle = new Style(false, true);
$yellowStyle->getFill()
->setFillType(Fill::FILL_SOLID)
->getEndColor()->setARGB(Color::COLOR_YELLOW);

$greenStyleMoney = clone $greenStyle;
$greenStyleMoney->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_ACCOUNTING_USD);

// Set conditional formatting rules and styles
$helper->log('Define conditional formatting and set styles');

// Set rules for Odd/Even Expression Comparison
$cellRange = 'A2:C11';
$conditionalStyles = [];
$wizardFactory = new Wizard($cellRange);
/** @var Wizard\Expression $expressionWizard */
$expressionWizard = $wizardFactory->newRule(Wizard::EXPRESSION);

$expressionWizard->expression('ISODD(A1)')
->setStyle($greenStyle);
$conditionalStyles[] = $expressionWizard->getConditional();

$expressionWizard->expression('ISEVEN(A1)')
->setStyle($yellowStyle);
$conditionalStyles[] = $expressionWizard->getConditional();

$spreadsheet->getActiveSheet()
->getStyle($expressionWizard->getCellRange())
->setConditionalStyles($conditionalStyles);

// Set rules for Sales Grid Row match against Country Comparison
$cellRange = 'A17:D22';
$conditionalStyles = [];
$wizardFactory = new Wizard($cellRange);
/** @var Wizard\Expression $expressionWizard */
$expressionWizard = $wizardFactory->newRule(Wizard::EXPRESSION);

$expressionWizard->expression('$C1="USA"')
->setStyle($greenStyleMoney);
$conditionalStyles[] = $expressionWizard->getConditional();

$spreadsheet->getActiveSheet()
->getStyle($expressionWizard->getCellRange())
->setConditionalStyles($conditionalStyles);

// Set rules for Sales Grid Row match against Country and Quarter Comparison
$cellRange = 'A27:D32';
$conditionalStyles = [];
$wizardFactory = new Wizard($cellRange);
/** @var Wizard\Expression $expressionWizard */
$expressionWizard = $wizardFactory->newRule(Wizard::EXPRESSION);

$expressionWizard->expression('AND($C1="USA",$D1="Q4")')
->setStyle($greenStyleMoney);
$conditionalStyles[] = $expressionWizard->getConditional();

$spreadsheet->getActiveSheet()
->getStyle($expressionWizard->getCellRange())
->setConditionalStyles($conditionalStyles);

// Set conditional formatting rules and styles
$helper->log('Set some additional styling for money formats');

$spreadsheet->getActiveSheet()->getStyle('B17:B22')
->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_ACCOUNTING_USD);
$spreadsheet->getActiveSheet()->getStyle('B27:B32')
->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_ACCOUNTING_USD);
$spreadsheet->getActiveSheet()->getColumnDimension('B')
->setAutoSize(true);

// Save
$helper->write($spreadsheet, __FILE__);
2 changes: 1 addition & 1 deletion src/PhpSpreadsheet/Calculation/Calculation.php
Original file line number Diff line number Diff line change
@@ -223,7 +223,7 @@ class Calculation
*
* @var array<string, mixed>
*/
private static $excelConstants = [
public static $excelConstants = [
'TRUE' => true,
'FALSE' => false,
'NULL' => null,
23 changes: 20 additions & 3 deletions src/PhpSpreadsheet/Cell/Cell.php
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\RichText\RichText;
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDate;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\CellStyleAssessor;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Style\Style;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
@@ -559,14 +560,30 @@ public function getMergeRange()

/**
* Get cell style.
*
* @return Style
*/
public function getStyle()
public function getStyle(): Style
{
return $this->getWorksheet()->getStyle($this->getCoordinate());
}

/**
* Get cell style.
*/
public function getAppliedStyle(): Style
{
if ($this->getWorksheet()->conditionalStylesExists($this->getCoordinate()) === false) {
return $this->getStyle();
}
$range = $this->getWorksheet()->getConditionalRange($this->getCoordinate());
if ($range === null) {
return $this->getStyle();
}

$matcher = new CellStyleAssessor($this, $range);

return $matcher->matchConditions($this->getWorksheet()->getConditionalStyles($this->getCoordinate()));
}

/**
* Re-bind parent.
*
5 changes: 4 additions & 1 deletion src/PhpSpreadsheet/Reader/Xlsx.php
Original file line number Diff line number Diff line change
@@ -708,9 +708,12 @@ public function load(string $filename, int $flags = 0): Spreadsheet
$xmlSheetMain = $xmlSheetNS->children($mainNS);
// Setting Conditional Styles adjusts selected cells, so we need to execute this
// before reading the sheet view data to get the actual selected cells
if (!$this->readDataOnly && $xmlSheet->conditionalFormatting) {
if (!$this->readDataOnly && ($xmlSheet->conditionalFormatting)) {
(new ConditionalStyles($docSheet, $xmlSheet, $dxfs))->load();
}
if (!$this->readDataOnly && $xmlSheet->extLst) {
(new ConditionalStyles($docSheet, $xmlSheet, $dxfs))->loadFromExt($this->styleReader);
}
if (isset($xmlSheetMain->sheetViews, $xmlSheetMain->sheetViews->sheetView)) {
$sheetViews = new SheetViews($xmlSheetMain->sheetViews->sheetView, $docSheet);
$sheetViews->load();
124 changes: 120 additions & 4 deletions src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php
Original file line number Diff line number Diff line change
@@ -2,10 +2,12 @@

namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx;

use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Styles as StyleReader;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalDataBar;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormattingRuleExtension;
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormatValueObject;
use PhpOffice\PhpSpreadsheet\Style\Style as Style;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use SimpleXMLElement;

@@ -15,6 +17,11 @@ class ConditionalStyles

private $worksheetXml;

/**
* @var array
*/
private $ns;

private $dxfs;

public function __construct(Worksheet $workSheet, SimpleXMLElement $worksheetXml, array $dxfs = [])
@@ -33,7 +40,113 @@ public function load(): void
);
}

private function readConditionalStyles($xmlSheet)
public function loadFromExt(StyleReader $styleReader): void
{
$this->ns = $this->worksheetXml->getNamespaces(true);
$this->setConditionalsFromExt(
$this->readConditionalsFromExt($this->worksheetXml->extLst, $styleReader)
);
}

private function setConditionalsFromExt(array $conditionals): void
{
foreach ($conditionals as $conditionalRange => $cfRules) {
ksort($cfRules);
// Priority is used as the key for sorting; but may not start at 0,
// so we use array_values to reset the index after sorting.
$this->worksheet->getStyle($conditionalRange)
->setConditionalStyles(array_values($cfRules));
}
}

private function readConditionalsFromExt(SimpleXMLElement $extLst, StyleReader $styleReader): array
{
$conditionals = [];

if (isset($extLst->ext['uri']) && (string) $extLst->ext['uri'] === '{78C0D931-6437-407d-A8EE-F0AAD7539E65}') {
$conditionalFormattingRuleXml = $extLst->ext->children($this->ns['x14']);
if (!$conditionalFormattingRuleXml->conditionalFormattings) {
return [];
}

foreach ($conditionalFormattingRuleXml->children($this->ns['x14']) as $extFormattingXml) {
$extFormattingRangeXml = $extFormattingXml->children($this->ns['xm']);
if (!$extFormattingRangeXml->sqref) {
continue;
}

$sqref = (string) $extFormattingRangeXml->sqref;
$extCfRuleXml = $extFormattingXml->cfRule;

$attributes = $extCfRuleXml->attributes();
if (!$attributes) {
continue;
}
$conditionType = (string) $attributes->type;
if (
!Conditional::isValidConditionType($conditionType) ||
$conditionType === Conditional::CONDITION_DATABAR
) {
continue;
}

$priority = (int) $attributes->priority;

$conditional = $this->readConditionalRuleFromExt($extCfRuleXml, $attributes);
$cfStyle = $this->readStyleFromExt($extCfRuleXml, $styleReader);
$conditional->setStyle($cfStyle);
$conditionals[$sqref][$priority] = $conditional;
}
}

return $conditionals;
}

private function readConditionalRuleFromExt(SimpleXMLElement $cfRuleXml, SimpleXMLElement $attributes): Conditional
{
$conditionType = (string) $attributes->type;
$operatorType = (string) $attributes->operator;

$operands = [];
foreach ($cfRuleXml->children($this->ns['xm']) as $cfRuleOperandsXml) {
$operands[] = (string) $cfRuleOperandsXml;
}

$conditional = new Conditional();
$conditional->setConditionType($conditionType);
$conditional->setOperatorType($operatorType);
if (
$conditionType === Conditional::CONDITION_CONTAINSTEXT ||
$conditionType === Conditional::CONDITION_NOTCONTAINSTEXT ||
$conditionType === Conditional::CONDITION_BEGINSWITH ||
$conditionType === Conditional::CONDITION_ENDSWITH ||
$conditionType === Conditional::CONDITION_TIMEPERIOD
) {
$conditional->setText(array_pop($operands) ?? '');
}
$conditional->setConditions($operands);

return $conditional;
}

private function readStyleFromExt(SimpleXMLElement $extCfRuleXml, StyleReader $styleReader): Style
{
$cfStyle = new Style(false, true);
if ($extCfRuleXml->dxf) {
$styleXML = $extCfRuleXml->dxf->children();

if ($styleXML->borders) {
$styleReader->readBorderStyle($cfStyle->getBorders(), $styleXML->borders);
}
if ($styleXML->fill) {
$styleReader->readFillStyle($cfStyle->getFill(), $styleXML->fill);
}
}

return $cfStyle;
}

private function readConditionalStyles($xmlSheet): array
{
$conditionals = [];
foreach ($xmlSheet->conditionalFormatting as $conditional) {
@@ -51,12 +164,12 @@ private function readConditionalStyles($xmlSheet)

private function setConditionalStyles(Worksheet $worksheet, array $conditionals, $xmlExtLst): void
{
foreach ($conditionals as $ref => $cfRules) {
foreach ($conditionals as $cellRangeReference => $cfRules) {
ksort($cfRules);
$conditionalStyles = $this->readStyleRules($cfRules, $xmlExtLst);

// Extract all cell references in $ref
$cellBlocks = explode(' ', str_replace('$', '', strtoupper($ref)));
// Extract all cell references in $cellRangeReference
$cellBlocks = explode(' ', str_replace('$', '', strtoupper($cellRangeReference)));
foreach ($cellBlocks as $cellBlock) {
$worksheet->getStyle($cellBlock)->setConditionalStyles($conditionalStyles);
}
@@ -67,13 +180,16 @@ private function readStyleRules($cfRules, $extLst)
{
$conditionalFormattingRuleExtensions = ConditionalFormattingRuleExtension::parseExtLstXml($extLst);
$conditionalStyles = [];

foreach ($cfRules as $cfRule) {
$objConditional = new Conditional();
$objConditional->setConditionType((string) $cfRule['type']);
$objConditional->setOperatorType((string) $cfRule['operator']);

if ((string) $cfRule['text'] != '') {
$objConditional->setText((string) $cfRule['text']);
} elseif ((string) $cfRule['timePeriod'] != '') {
$objConditional->setText((string) $cfRule['timePeriod']);
}

if (isset($cfRule['stopIfTrue']) && (int) $cfRule['stopIfTrue'] === 1) {
29 changes: 27 additions & 2 deletions src/PhpSpreadsheet/Style/Conditional.php
Original file line number Diff line number Diff line change
@@ -9,23 +9,37 @@ class Conditional implements IComparable
{
// Condition types
const CONDITION_NONE = 'none';
const CONDITION_BEGINSWITH = 'beginsWith';
const CONDITION_CELLIS = 'cellIs';
const CONDITION_CONTAINSBLANKS = 'containsBlanks';
const CONDITION_CONTAINSERRORS = 'containsErrors';
const CONDITION_CONTAINSTEXT = 'containsText';
const CONDITION_DATABAR = 'dataBar';
const CONDITION_ENDSWITH = 'endsWith';
const CONDITION_EXPRESSION = 'expression';
const CONDITION_CONTAINSBLANKS = 'containsBlanks';
const CONDITION_NOTCONTAINSBLANKS = 'notContainsBlanks';
const CONDITION_DATABAR = 'dataBar';
const CONDITION_NOTCONTAINSERRORS = 'notContainsErrors';
const CONDITION_NOTCONTAINSTEXT = 'notContainsText';
const CONDITION_TIMEPERIOD = 'timePeriod';
const CONDITION_DUPLICATES = 'duplicateValues';
const CONDITION_UNIQUE = 'uniqueValues';

private const CONDITION_TYPES = [
self::CONDITION_BEGINSWITH,
self::CONDITION_CELLIS,
self::CONDITION_CONTAINSBLANKS,
self::CONDITION_CONTAINSERRORS,
self::CONDITION_CONTAINSTEXT,
self::CONDITION_DATABAR,
self::CONDITION_DUPLICATES,
self::CONDITION_ENDSWITH,
self::CONDITION_EXPRESSION,
self::CONDITION_NONE,
self::CONDITION_NOTCONTAINSBLANKS,
self::CONDITION_NOTCONTAINSERRORS,
self::CONDITION_NOTCONTAINSTEXT,
self::CONDITION_TIMEPERIOD,
self::CONDITION_UNIQUE,
];

// Operator types
@@ -43,6 +57,17 @@ class Conditional implements IComparable
const OPERATOR_BETWEEN = 'between';
const OPERATOR_NOTBETWEEN = 'notBetween';

const TIMEPERIOD_TODAY = 'today';
const TIMEPERIOD_YESTERDAY = 'yesterday';
const TIMEPERIOD_TOMORROW = 'tomorrow';
const TIMEPERIOD_LAST_7_DAYS = 'last7Days';
const TIMEPERIOD_LAST_WEEK = 'lastWeek';
const TIMEPERIOD_THIS_WEEK = 'thisWeek';
const TIMEPERIOD_NEXT_WEEK = 'nextWeek';
const TIMEPERIOD_LAST_MONTH = 'lastMonth';
const TIMEPERIOD_THIS_MONTH = 'thisMonth';
const TIMEPERIOD_NEXT_MONTH = 'nextMonth';

/**
* Condition type.
*
312 changes: 312 additions & 0 deletions src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
<?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)
if ($i = !$i) {
$value = 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 = 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;
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
118 changes: 118 additions & 0 deletions src/PhpSpreadsheet/Style/ConditionalFormatting/StyleMerger.php
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
95 changes: 95 additions & 0 deletions src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard.php
Original file line number Diff line number Diff line change
@@ -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');
}
}
}
99 changes: 99 additions & 0 deletions src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Blanks.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
189 changes: 189 additions & 0 deletions src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/CellValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<?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 (array_key_exists($condition, Calculation::$excelConstants)) {
$condition = Calculation::$excelConstants[$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');
}

$this->operand(1, ...$arguments);

return $this;
}

$this->operator(self::MAGIC_OPERATIONS[$methodName]);
$this->operand(0, ...$arguments);

return $this;
}
}
111 changes: 111 additions & 0 deletions src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/DateValue.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
95 changes: 95 additions & 0 deletions src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Errors.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?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($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');
}

$this->expression(...$arguments);

return $this;
}
}
163 changes: 163 additions & 0 deletions src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/TextValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?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
{
if (is_string($operand)) {
$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 (is_string($condition) && array_key_exists($condition, Calculation::$excelConstants)) {
$condition = Calculation::$excelConstants[$condition];
} elseif (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);

return $this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
<?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}";
}

protected 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)
if ($i = !$i) {
$value = 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)
if ($i = !$i) {
$value = 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
);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
23 changes: 16 additions & 7 deletions src/PhpSpreadsheet/Worksheet/Worksheet.php
Original file line number Diff line number Diff line change
@@ -1415,10 +1415,8 @@ public function getStyles()
* Get style for cell.
*
* @param string $cellCoordinate Cell coordinate (or range) to get style for, eg: 'A1'
*
* @return Style
*/
public function getStyle($cellCoordinate)
public function getStyle($cellCoordinate): Style
{
// set this sheet as active
$this->parent->setActiveSheetIndex($this->parent->getIndex($this));
@@ -1440,7 +1438,7 @@ public function getStyle($cellCoordinate)
*
* @return Conditional[]
*/
public function getConditionalStyles($coordinate)
public function getConditionalStyles(string $coordinate): array
{
$coordinate = strtoupper($coordinate);
if (strpos($coordinate, ':') !== false) {
@@ -1457,6 +1455,19 @@ public function getConditionalStyles($coordinate)
return [];
}

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;
}
}

return null;
}

/**
* Do conditional styles exist for this cell?
*
@@ -1465,10 +1476,8 @@ public function getConditionalStyles($coordinate)
* conditional style range.
* If a range of cells is specified, then true will only be returned if the range matches the entire
* range of the conditional.
*
* @return bool
*/
public function conditionalStylesExists($coordinate)
public function conditionalStylesExists($coordinate): bool
{
$coordinate = strtoupper($coordinate);
if (strpos($coordinate, ':') !== false) {
Loading

0 comments on commit 4a04499

Please sign in to comment.