Failed Conditions
Push — master ( bf4629...7712d5 )
by Adrien
27:59 queued 18:08
created

CellMatcher::processExpression()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

265
            $this->cellConditionCheck(/** @scrutinizer ignore-type */ $this->cell->getCalculatedValue())
Loading history...
266 7
        );
267
268 7
        return $this->evaluateExpression($expression);
269
    }
270
271 118
    protected function processExpression(Conditional $conditional): bool
272
    {
273 118
        $conditions = $this->adjustConditionsForCellReferences($conditional->getConditions());
274 118
        $expression = array_pop($conditions);
275
276 118
        $expression = (string) preg_replace(
277 118
            '/\b' . $this->referenceCell . '\b/i',
278 118
            (string) $this->wrapCellValue(),
279 118
            $expression
280 118
        );
281
282 118
        return $this->evaluateExpression($expression);
283
    }
284
285 167
    protected function evaluateExpression(string $expression): bool
286
    {
287 167
        $expression = "={$expression}";
288
289
        try {
290 167
            $this->engine->flushInstance();
291 167
            $result = (bool) $this->engine->calculateFormula($expression);
292
        } catch (Exception) {
293
            return false;
294
        }
295
296 167
        return $result;
297
    }
298
}
299