Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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