Failed Conditions
Pull Request — master (#4412)
by
unknown
22:45 queued 07:48
created

CellMatcher::adjustConditionsForCellReferences()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
7
use PhpOffice\PhpSpreadsheet\Cell\Cell;
8
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
9
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
10
use PhpOffice\PhpSpreadsheet\Style\Conditional;
11
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
12
13
class CellMatcher
14
{
15
    public const COMPARISON_OPERATORS = [
16
        Conditional::OPERATOR_EQUAL => '=',
17
        Conditional::OPERATOR_GREATERTHAN => '>',
18
        Conditional::OPERATOR_GREATERTHANOREQUAL => '>=',
19
        Conditional::OPERATOR_LESSTHAN => '<',
20
        Conditional::OPERATOR_LESSTHANOREQUAL => '<=',
21
        Conditional::OPERATOR_NOTEQUAL => '<>',
22
    ];
23
24
    public const COMPARISON_RANGE_OPERATORS = [
25
        Conditional::OPERATOR_BETWEEN => 'IF(AND(A1>=%s,A1<=%s),TRUE,FALSE)',
26
        Conditional::OPERATOR_NOTBETWEEN => 'IF(AND(A1>=%s,A1<=%s),FALSE,TRUE)',
27
    ];
28
29
    public const COMPARISON_DUPLICATES_OPERATORS = [
30
        Conditional::CONDITION_DUPLICATES => "COUNTIF('%s'!%s,%s)>1",
31
        Conditional::CONDITION_UNIQUE => "COUNTIF('%s'!%s,%s)=1",
32
    ];
33
34
    protected Cell $cell;
35
36
    protected int $cellRow;
37
38
    protected Worksheet $worksheet;
39
40
    protected int $cellColumn;
41
42
    protected string $conditionalRange;
43
44
    protected string $referenceCell;
45
46
    protected int $referenceRow;
47
48
    protected int $referenceColumn;
49
50
    protected Calculation $engine;
51
52 174
    public function __construct(Cell $cell, string $conditionalRange)
53
    {
54 174
        $this->cell = $cell;
55 174
        $this->worksheet = $cell->getWorksheet();
56 174
        [$this->cellColumn, $this->cellRow] = Coordinate::indexesFromString($this->cell->getCoordinate());
57 174
        $this->setReferenceCellForExpressions($conditionalRange);
58
59 174
        $this->engine = Calculation::getInstance($this->worksheet->getParent());
60
    }
61
62 174
    protected function setReferenceCellForExpressions(string $conditionalRange): void
63
    {
64 174
        $conditionalRange = Coordinate::splitRange(str_replace('$', '', strtoupper($conditionalRange)));
65 174
        [$this->referenceCell] = $conditionalRange[0];
66
67 174
        [$this->referenceColumn, $this->referenceRow] = Coordinate::indexesFromString($this->referenceCell);
68
69
        // Convert our conditional range to an absolute conditional range, so it can be used  "pinned" in formulae
70 174
        $rangeSets = [];
71 174
        foreach ($conditionalRange as $rangeSet) {
72 174
            $absoluteRangeSet = array_map(
73 174
                [Coordinate::class, 'absoluteCoordinate'],
74 174
                $rangeSet
75 174
            );
76 174
            $rangeSets[] = implode(':', $absoluteRangeSet);
77
        }
78 174
        $this->conditionalRange = implode(',', $rangeSets);
79
    }
80
81 174
    public function evaluateConditional(Conditional $conditional): bool
82
    {
83
        // Some calculations may modify the stored cell; so reset it before every evaluation.
84 174
        $cellColumn = Coordinate::stringFromColumnIndex($this->cellColumn);
85 174
        $cellAddress = "{$cellColumn}{$this->cellRow}";
86 174
        $this->cell = $this->worksheet->getCell($cellAddress);
87
88 174
        return match ($conditional->getConditionType()) {
89 44
            Conditional::CONDITION_CELLIS => $this->processOperatorComparison($conditional),
90 7
            Conditional::CONDITION_DUPLICATES, Conditional::CONDITION_UNIQUE => $this->processDuplicatesComparison($conditional),
91
            // Expression is NOT(ISERROR(SEARCH("<TEXT>",<Cell Reference>)))
92 174
            Conditional::CONDITION_CONTAINSTEXT,
93
            // Expression is ISERROR(SEARCH("<TEXT>",<Cell Reference>))
94 174
            Conditional::CONDITION_NOTCONTAINSTEXT,
95
            // Expression is LEFT(<Cell Reference>,LEN("<TEXT>"))="<TEXT>"
96 174
            Conditional::CONDITION_BEGINSWITH,
97
            // Expression is RIGHT(<Cell Reference>,LEN("<TEXT>"))="<TEXT>"
98 174
            Conditional::CONDITION_ENDSWITH,
99
            // Expression is LEN(TRIM(<Cell Reference>))=0
100 174
            Conditional::CONDITION_CONTAINSBLANKS,
101
            // Expression is LEN(TRIM(<Cell Reference>))>0
102 174
            Conditional::CONDITION_NOTCONTAINSBLANKS,
103
            // Expression is ISERROR(<Cell Reference>)
104 174
            Conditional::CONDITION_CONTAINSERRORS,
105
            // Expression is NOT(ISERROR(<Cell Reference>))
106 174
            Conditional::CONDITION_NOTCONTAINSERRORS,
107
            // Expression varies, depending on specified timePeriod value, e.g.
108
            // Yesterday FLOOR(<Cell Reference>,1)=TODAY()-1
109
            // Today FLOOR(<Cell Reference>,1)=TODAY()
110
            // Tomorrow FLOOR(<Cell Reference>,1)=TODAY()+1
111
            // Last 7 Days AND(TODAY()-FLOOR(<Cell Reference>,1)<=6,FLOOR(<Cell Reference>,1)<=TODAY())
112 174
            Conditional::CONDITION_TIMEPERIOD,
113 120
            Conditional::CONDITION_EXPRESSION => $this->processExpression($conditional),
114 4
            Conditional::CONDITION_COLORSCALE => $this->processColorScale($conditional),
115 174
            default => false,
116 174
        };
117
    }
118
119 167
    protected function wrapValue(mixed $value): float|int|string
120
    {
121 167
        if (!is_numeric($value)) {
122 106
            if (is_bool($value)) {
123
                return $value ? 'TRUE' : 'FALSE';
124 106
            } elseif ($value === null) {
125 13
                return 'NULL';
126
            }
127
128 100
            return '"' . StringHelper::convertToString($value) . '"';
129
        }
130
131 74
        return $value;
132
    }
133
134 167
    protected function wrapCellValue(): float|int|string
135
    {
136 167
        $this->cell = $this->worksheet->getCell([$this->cellColumn, $this->cellRow]);
137
138 167
        return $this->wrapValue($this->cell->getCalculatedValue());
139
    }
140
141 145
    protected function conditionCellAdjustment(array $matches): float|int|string
142
    {
143 145
        $column = $matches[6];
144 145
        $row = $matches[7];
145 145
        if (!str_contains($column, '$')) {
146
            //            $column = Coordinate::stringFromColumnIndex($this->cellColumn);
147 102
            $column = Coordinate::columnIndexFromString($column);
148 102
            $column += $this->cellColumn - $this->referenceColumn;
149 102
            $column = Coordinate::stringFromColumnIndex($column);
150
        }
151
152 145
        if (!str_contains($row, '$')) {
153 142
            $row += $this->cellRow - $this->referenceRow;
154
        }
155
156 145
        if (!empty($matches[4])) {
157 5
            $worksheet = $this->worksheet->getParentOrThrow()->getSheetByName(trim($matches[4], "'"));
158 5
            if ($worksheet === null) {
159
                return $this->wrapValue(null);
160
            }
161
162 5
            return $this->wrapValue(
163 5
                $worksheet
164 5
                    ->getCell(str_replace('$', '', "{$column}{$row}"))
165 5
                    ->getCalculatedValue()
166 5
            );
167
        }
168
169 140
        return $this->wrapValue(
170 140
            $this->worksheet
171 140
                ->getCell(str_replace('$', '', "{$column}{$row}"))
172 140
                ->getCalculatedValue()
173 140
        );
174
    }
175
176 170
    protected function cellConditionCheck(string $condition): string
177
    {
178 170
        $splitCondition = explode(Calculation::FORMULA_STRING_QUOTE, $condition);
179 170
        $i = false;
180 170
        foreach ($splitCondition as &$value) {
181
            //    Only count/replace in alternating array entries (ie. not in quoted strings)
182 170
            $i = $i === false;
183 170
            if ($i) {
184 170
                $value = (string) preg_replace_callback(
185 170
                    '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i',
186 170
                    [$this, 'conditionCellAdjustment'],
187 170
                    $value
188 170
                );
189
            }
190
        }
191 170
        unset($value);
192
193
        //    Then rebuild the condition string to return it
194 170
        return implode(Calculation::FORMULA_STRING_QUOTE, $splitCondition);
195
    }
196
197 163
    protected function adjustConditionsForCellReferences(array $conditions): array
198
    {
199 163
        return array_map(
200 163
            [$this, 'cellConditionCheck'],
201 163
            $conditions
202 163
        );
203
    }
204
205 44
    protected function processOperatorComparison(Conditional $conditional): bool
206
    {
207 44
        if (array_key_exists($conditional->getOperatorType(), self::COMPARISON_RANGE_OPERATORS)) {
208 13
            return $this->processRangeOperator($conditional);
209
        }
210
211 31
        $operator = self::COMPARISON_OPERATORS[$conditional->getOperatorType()];
212 31
        $conditions = $this->adjustConditionsForCellReferences($conditional->getConditions());
213 31
        $expression = sprintf('%s%s%s', (string) $this->wrapCellValue(), $operator, (string) array_pop($conditions));
214
215 31
        return $this->evaluateExpression($expression);
216
    }
217
218 4
    protected function processColorScale(Conditional $conditional): bool
219
    {
220 4
        if ($this->wrapCellValue() !== '' && is_float($this->wrapCellValue()) && $conditional->getColorScale() !== null && $conditional->getColorScale()->colorScaleReadyForUse()) {
1 ignored issue
show
introduced by
The condition is_float($this->wrapCellValue()) is always false.
Loading history...
221 4
            return true;
222
        }
223
224 4
        return false;
225
    }
226
227 13
    protected function processRangeOperator(Conditional $conditional): bool
228
    {
229 13
        $conditions = $this->adjustConditionsForCellReferences($conditional->getConditions());
230 13
        sort($conditions);
231 13
        $expression = sprintf(
232 13
            (string) preg_replace(
233 13
                '/\bA1\b/i',
234 13
                (string) $this->wrapCellValue(),
235 13
                self::COMPARISON_RANGE_OPERATORS[$conditional->getOperatorType()]
236 13
            ),
237 13
            ...$conditions
238 13
        );
239
240 13
        return $this->evaluateExpression($expression);
241
    }
242
243 7
    protected function processDuplicatesComparison(Conditional $conditional): bool
244
    {
245 7
        $worksheetName = $this->cell->getWorksheet()->getTitle();
246
247 7
        $expression = sprintf(
248 7
            self::COMPARISON_DUPLICATES_OPERATORS[$conditional->getConditionType()],
249 7
            $worksheetName,
250 7
            $this->conditionalRange,
251 7
            $this->cellConditionCheck($this->cell->getCalculatedValueString())
252 7
        );
253
254 7
        return $this->evaluateExpression($expression);
255
    }
256
257 120
    protected function processExpression(Conditional $conditional): bool
258
    {
259 120
        $conditions = $this->adjustConditionsForCellReferences($conditional->getConditions());
260 120
        $expression = array_pop($conditions);
261
262 120
        $expression = (string) preg_replace(
263 120
            '/\b' . $this->referenceCell . '\b/i',
264 120
            (string) $this->wrapCellValue(),
265 120
            $expression
266 120
        );
267
268 120
        return $this->evaluateExpression($expression);
269
    }
270
271 170
    protected function evaluateExpression(string $expression): bool
272
    {
273 170
        $expression = "={$expression}";
274
275
        try {
276 170
            $this->engine->flushInstance();
277 170
            $result = (bool) $this->engine->calculateFormula($expression);
278
        } catch (Exception) {
279
            return false;
280
        }
281
282 170
        return $result;
283
    }
284
}
285