Failed Conditions
Push — master ( 8e3417...f52ae2 )
by
unknown
18:26 queued 07:14
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 175
    public function __construct(Cell $cell, string $conditionalRange)
53
    {
54 175
        $this->cell = $cell;
55 175
        $this->worksheet = $cell->getWorksheet();
56 175
        [$this->cellColumn, $this->cellRow] = Coordinate::indexesFromString($this->cell->getCoordinate());
57 175
        $this->setReferenceCellForExpressions($conditionalRange);
58
59 175
        $this->engine = Calculation::getInstance($this->worksheet->getParent());
60
    }
61
62 175
    protected function setReferenceCellForExpressions(string $conditionalRange): void
63
    {
64 175
        $conditionalRange = Coordinate::splitRange(str_replace('$', '', strtoupper($conditionalRange)));
65 175
        [$this->referenceCell] = $conditionalRange[0];
66
67 175
        [$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 175
        $rangeSets = [];
71 175
        foreach ($conditionalRange as $rangeSet) {
72 175
            $absoluteRangeSet = array_map(
73 175
                [Coordinate::class, 'absoluteCoordinate'],
74 175
                $rangeSet
75 175
            );
76 175
            $rangeSets[] = implode(':', $absoluteRangeSet);
77
        }
78 175
        $this->conditionalRange = implode(',', $rangeSets);
79
    }
80
81 175
    public function evaluateConditional(Conditional $conditional): bool
82
    {
83
        // Some calculations may modify the stored cell; so reset it before every evaluation.
84 175
        $cellColumn = Coordinate::stringFromColumnIndex($this->cellColumn);
85 175
        $cellAddress = "{$cellColumn}{$this->cellRow}";
86 175
        $this->cell = $this->worksheet->getCell($cellAddress);
87
88 175
        return match ($conditional->getConditionType()) {
89 46
            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 175
            Conditional::CONDITION_CONTAINSTEXT,
93
            // Expression is ISERROR(SEARCH("<TEXT>",<Cell Reference>))
94 175
            Conditional::CONDITION_NOTCONTAINSTEXT,
95
            // Expression is LEFT(<Cell Reference>,LEN("<TEXT>"))="<TEXT>"
96 175
            Conditional::CONDITION_BEGINSWITH,
97
            // Expression is RIGHT(<Cell Reference>,LEN("<TEXT>"))="<TEXT>"
98 175
            Conditional::CONDITION_ENDSWITH,
99
            // Expression is LEN(TRIM(<Cell Reference>))=0
100 175
            Conditional::CONDITION_CONTAINSBLANKS,
101
            // Expression is LEN(TRIM(<Cell Reference>))>0
102 175
            Conditional::CONDITION_NOTCONTAINSBLANKS,
103
            // Expression is ISERROR(<Cell Reference>)
104 175
            Conditional::CONDITION_CONTAINSERRORS,
105
            // Expression is NOT(ISERROR(<Cell Reference>))
106 175
            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 175
            Conditional::CONDITION_TIMEPERIOD,
113 122
            Conditional::CONDITION_EXPRESSION => $this->processExpression($conditional),
114 2
            Conditional::CONDITION_COLORSCALE => $this->processColorScale($conditional),
115 175
            default => false,
116 175
        };
117
    }
118
119 168
    protected function wrapValue(mixed $value): float|int|string
120
    {
121 168
        if (!is_numeric($value)) {
122 105
            if (is_bool($value)) {
123
                return $value ? 'TRUE' : 'FALSE';
124 105
            } elseif ($value === null) {
125 12
                return 'NULL';
126
            }
127
128 101
            return '"' . StringHelper::convertToString($value) . '"';
129
        }
130
131 75
        return $value;
132
    }
133
134 168
    protected function wrapCellValue(): float|int|string
135
    {
136 168
        $this->cell = $this->worksheet->getCell([$this->cellColumn, $this->cellRow]);
137
138 168
        return $this->wrapValue($this->cell->getCalculatedValue());
139
    }
140
141 147
    protected function conditionCellAdjustment(array $matches): float|int|string
142
    {
143 147
        $column = $matches[6];
144 147
        $row = $matches[7];
145 147
        if (!str_contains($column, '$')) {
146
            //            $column = Coordinate::stringFromColumnIndex($this->cellColumn);
147 104
            $column = Coordinate::columnIndexFromString($column);
148 104
            $column += $this->cellColumn - $this->referenceColumn;
149 104
            $column = Coordinate::stringFromColumnIndex($column);
150
        }
151
152 147
        if (!str_contains($row, '$')) {
153 144
            $row += $this->cellRow - $this->referenceRow;
154
        }
155
156 147
        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 142
        return $this->wrapValue(
170 142
            $this->worksheet
171 142
                ->getCell(str_replace('$', '', "{$column}{$row}"))
172 142
                ->getCalculatedValue()
173 142
        );
174
    }
175
176 173
    protected function cellConditionCheck(string $condition): string
177
    {
178 173
        $splitCondition = explode(Calculation::FORMULA_STRING_QUOTE, $condition);
179 173
        $i = false;
180 173
        foreach ($splitCondition as &$value) {
181
            //    Only count/replace in alternating array entries (ie. not in quoted strings)
182 173
            $i = $i === false;
183 173
            if ($i) {
184 173
                $value = (string) preg_replace_callback(
185 173
                    '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i',
186 173
                    [$this, 'conditionCellAdjustment'],
187 173
                    $value
188 173
                );
189
            }
190
        }
191 173
        unset($value);
192
193
        //    Then rebuild the condition string to return it
194 173
        return implode(Calculation::FORMULA_STRING_QUOTE, $splitCondition);
195
    }
196
197 166
    protected function adjustConditionsForCellReferences(array $conditions): array
198
    {
199 166
        return array_map(
200 166
            [$this, 'cellConditionCheck'],
201 166
            $conditions
202 166
        );
203
    }
204
205 46
    protected function processOperatorComparison(Conditional $conditional): bool
206
    {
207 46
        if (array_key_exists($conditional->getOperatorType(), self::COMPARISON_RANGE_OPERATORS)) {
208 13
            return $this->processRangeOperator($conditional);
209
        }
210
211 33
        $operator = self::COMPARISON_OPERATORS[$conditional->getOperatorType()];
212 33
        $conditions = $this->adjustConditionsForCellReferences($conditional->getConditions());
213 33
        $expression = sprintf('%s%s%s', (string) $this->wrapCellValue(), $operator, (string) array_pop($conditions));
214
215 33
        return $this->evaluateExpression($expression);
216
    }
217
218 2
    protected function processColorScale(Conditional $conditional): bool
219
    {
220 2
        if (is_numeric($this->wrapCellValue()) && $conditional->getColorScale()?->colorScaleReadyForUse()) {
221 2
            return true;
222
        }
223
224 2
        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 122
    protected function processExpression(Conditional $conditional): bool
258
    {
259 122
        $conditions = $this->adjustConditionsForCellReferences($conditional->getConditions());
260 122
        $expression = array_pop($conditions);
261
262 122
        $expression = (string) preg_replace(
263 122
            '/\b' . $this->referenceCell . '\b/i',
264 122
            (string) $this->wrapCellValue(),
265 122
            $expression
266 122
        );
267
268 122
        return $this->evaluateExpression($expression);
269
    }
270
271 173
    protected function evaluateExpression(string $expression): bool
272
    {
273 173
        $expression = "={$expression}";
274
275
        try {
276 173
            $this->engine->flushInstance();
277 173
            $result = (bool) $this->engine->calculateFormula($expression);
278
        } catch (Exception) {
279
            return false;
280
        }
281
282 173
        return $result;
283
    }
284
}
285