Failed Conditions
Pull Request — master (#3876)
by Abdul Malik
22:45 queued 13:31
created

StructuredReference::value()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
ccs 2
cts 2
cp 1
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine\Operands;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
0 ignored issues
show
Bug introduced by
The type PhpOffice\PhpSpreadsheet\Calculation\Calculation was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
6
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
7
use PhpOffice\PhpSpreadsheet\Cell\Cell;
8
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
9
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
10
use Stringable;
11
12
final class StructuredReference implements Operand, Stringable
13
{
14
    public const NAME = 'Structured Reference';
15
16
    private const OPEN_BRACE = '[';
17
    private const CLOSE_BRACE = ']';
18
19
    private const ITEM_SPECIFIER_ALL = '#All';
20
    private const ITEM_SPECIFIER_HEADERS = '#Headers';
21
    private const ITEM_SPECIFIER_DATA = '#Data';
22
    private const ITEM_SPECIFIER_TOTALS = '#Totals';
23
    private const ITEM_SPECIFIER_THIS_ROW = '#This Row';
24
25
    private const ITEM_SPECIFIER_ROWS_SET = [
26
        self::ITEM_SPECIFIER_ALL,
27
        self::ITEM_SPECIFIER_HEADERS,
28
        self::ITEM_SPECIFIER_DATA,
29
        self::ITEM_SPECIFIER_TOTALS,
30
    ];
31
32
    private const TABLE_REFERENCE = '/([\p{L}_\\\\][\p{L}\p{N}\._]+)?(\[(?:[^\]\[]+|(?R))*+\])/miu';
33
34
    private string $tableName;
35
36
    private Table $table;
37
38
    private string $reference;
39
40
    private ?int $headersRow = null;
41
42
    private int $firstDataRow;
43
44
    private int $lastDataRow;
45
46
    private ?int $totalsRow = null;
47
48
    private array $columns;
49
50 72
    public function __construct(private string $value)
51
    {
52 72
    }
53
54 26
    public static function fromParser(string $formula, int $index, array $matches): self
55
    {
56 26
        $val = $matches[0];
57
58 26
        $srCount = substr_count($val, self::OPEN_BRACE)
59 26
            - substr_count($val, self::CLOSE_BRACE);
60 26
        while ($srCount > 0) {
61 26
            $srIndex = strlen($val);
62 26
            $srStringRemainder = substr($formula, $index + $srIndex);
63 26
            $closingPos = strpos($srStringRemainder, self::CLOSE_BRACE);
64 26
            if ($closingPos === false) {
65
                throw new Exception("Formula Error: No closing ']' to match opening '['");
66
            }
67 26
            $srStringRemainder = substr($srStringRemainder, 0, $closingPos + 1);
68 26
            --$srCount;
69 26
            if (str_contains($srStringRemainder, self::OPEN_BRACE)) {
70 18
                ++$srCount;
71
            }
72 26
            $val .= $srStringRemainder;
73
        }
74
75 26
        return new self($val);
76
    }
77
78
    /**
79
     * @throws Exception
80
     * @throws \PhpOffice\PhpSpreadsheet\Exception
81
     */
82 61
    public function parse(Cell $cell): string
83
    {
84 61
        $this->getTableStructure($cell);
85 59
        $cellRange = ($this->isRowReference()) ? $this->getRowReference($cell) : $this->getColumnReference();
86 58
        $sheetName = '';
87 58
        $worksheet = $this->table->getWorksheet();
88 58
        if ($worksheet !== null && $worksheet !== $cell->getWorksheet()) {
89 1
            $sheetName = "'" . $worksheet->getTitle() . "'!";
90
        }
91
92 58
        return $sheetName . $cellRange;
93
    }
94
95 59
    private function isRowReference(): bool
96
    {
97 59
        return str_contains($this->value, '[@')
98 59
            || str_contains($this->value, '[' . self::ITEM_SPECIFIER_THIS_ROW . ']');
99
    }
100
101
    /**
102
     * @throws Exception
103
     * @throws \PhpOffice\PhpSpreadsheet\Exception
104
     */
105 61
    private function getTableStructure(Cell $cell): void
106
    {
107 61
        preg_match(self::TABLE_REFERENCE, $this->value, $matches);
108
109 61
        $this->tableName = $matches[1];
110 61
        $this->table = ($this->tableName === '')
111 5
            ? $this->getTableForCell($cell)
112 56
            : $this->getTableByName($cell);
113 59
        $this->reference = $matches[2];
114 59
        $tableRange = Coordinate::getRangeBoundaries($this->table->getRange());
115
116 59
        $this->headersRow = ($this->table->getShowHeaderRow()) ? (int) $tableRange[0][1] : null;
117 59
        $this->firstDataRow = ($this->table->getShowHeaderRow()) ? (int) $tableRange[0][1] + 1 : $tableRange[0][1];
118 59
        $this->totalsRow = ($this->table->getShowTotalsRow()) ? (int) $tableRange[1][1] : null;
119 59
        $this->lastDataRow = ($this->table->getShowTotalsRow()) ? (int) $tableRange[1][1] - 1 : $tableRange[1][1];
120
121 59
        $cellParam = $cell;
122 59
        $worksheet = $this->table->getWorksheet();
123 59
        if ($worksheet !== null && $worksheet !== $cell->getWorksheet()) {
124 1
            $cellParam = $worksheet->getCell('A1');
125
        }
126 59
        $this->columns = $this->getColumns($cellParam, $tableRange);
127
    }
128
129
    /**
130
     * @throws Exception
131
     * @throws \PhpOffice\PhpSpreadsheet\Exception
132
     */
133 5
    private function getTableForCell(Cell $cell): Table
134
    {
135 5
        $tables = $cell->getWorksheet()->getTableCollection();
136 5
        foreach ($tables as $table) {
137
            /** @var Table $table */
138 5
            $range = $table->getRange();
139 5
            if ($cell->isInRange($range) === true) {
140 4
                $this->tableName = $table->getName();
141
142 4
                return $table;
143
            }
144
        }
145
146 1
        throw new Exception('Table for Structured Reference cannot be identified');
147
    }
148
149
    /**
150
     * @throws Exception
151
     * @throws \PhpOffice\PhpSpreadsheet\Exception
152
     */
153 56
    private function getTableByName(Cell $cell): Table
154
    {
155 56
        $table = $cell->getWorksheet()->getTableByName($this->tableName);
156
157 56
        if ($table === null) {
158 2
            $spreadsheet = $cell->getWorksheet()->getParent();
159 2
            if ($spreadsheet !== null) {
160 2
                $table = $spreadsheet->getTableByName($this->tableName);
161
            }
162
        }
163
164 56
        if ($table === null) {
165 1
            throw new Exception("Table {$this->tableName} for Structured Reference cannot be located");
166
        }
167
168 55
        return $table;
169
    }
170
171 59
    private function getColumns(Cell $cell, array $tableRange): array
172
    {
173 59
        $worksheet = $cell->getWorksheet();
174 59
        $cellReference = $cell->getCoordinate();
175
176 59
        $columns = [];
177 59
        $lastColumn = ++$tableRange[1][0];
178 59
        for ($column = $tableRange[0][0]; $column !== $lastColumn; ++$column) {
179 59
            $columns[$column] = $worksheet
180 59
                ->getCell($column . ($this->headersRow ?? ($this->firstDataRow - 1)))
181 59
                ->getCalculatedValue();
182
        }
183
184 59
        $worksheet->getCell($cellReference);
185
186 59
        return $columns;
187
    }
188
189 20
    private function getRowReference(Cell $cell): string
190
    {
191 20
        $reference = str_replace("\u{a0}", ' ', $this->reference);
192
        /** @var string $reference */
193 20
        $reference = str_replace('[' . self::ITEM_SPECIFIER_THIS_ROW . '],', '', $reference);
194
195 20
        foreach ($this->columns as $columnId => $columnName) {
196 20
            $columnName = str_replace("\u{a0}", ' ', $columnName);
197 20
            $reference = $this->adjustRowReference($columnName, $reference, $cell, $columnId);
198
        }
199
200 20
        return $this->validateParsedReference(trim($reference, '[]@, '));
201
    }
202
203 20
    private function adjustRowReference(string $columnName, string $reference, Cell $cell, string $columnId): string
204
    {
205 20
        if ($columnName !== '') {
206 20
            $cellReference = $columnId . $cell->getRow();
207 20
            $pattern1 = '/\[' . preg_quote($columnName, '/') . '\]/miu';
208 20
            $pattern2 = '/@' . preg_quote($columnName, '/') . '/miu';
209 20
            if (preg_match($pattern1, $reference) === 1) {
210 15
                $reference = preg_replace($pattern1, $cellReference, $reference);
211 20
            } elseif (preg_match($pattern2, $reference) === 1) {
212 4
                $reference = preg_replace($pattern2, $cellReference, $reference);
213
            }
214
            /** @var string $reference */
215
        }
216
217 20
        return $reference;
218
    }
219
220
    /**
221
     * @throws Exception
222
     * @throws \PhpOffice\PhpSpreadsheet\Exception
223
     */
224 41
    private function getColumnReference(): string
225
    {
226 41
        $reference = str_replace("\u{a0}", ' ', $this->reference);
227 41
        $startRow = $this->totalsRow ?? $this->lastDataRow;
228 41
        $endRow = $this->headersRow ?? $this->firstDataRow;
229
230 41
        [$startRow, $endRow] = $this->getRowsForColumnReference($reference, $startRow, $endRow);
231 41
        $reference = $this->getColumnsForColumnReference($reference, $startRow, $endRow);
232
233 41
        $reference = trim($reference, '[]@, ');
234 41
        if (substr_count($reference, ':') > 1) {
235 6
            $cells = explode(':', $reference);
236 6
            $firstCell = array_shift($cells);
237 6
            $lastCell = array_pop($cells);
238 6
            $reference = "{$firstCell}:{$lastCell}";
239
        }
240
241 41
        return $this->validateParsedReference($reference);
242
    }
243
244
    /**
245
     * @throws Exception
246
     * @throws \PhpOffice\PhpSpreadsheet\Exception
247
     */
248 59
    private function validateParsedReference(string $reference): string
249
    {
250 59
        if (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . ':' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $reference) !== 1) {
251 22
            if (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $reference) !== 1) {
252 2
                throw new Exception(
253 2
                    "Invalid Structured Reference {$this->reference} {$reference}",
254 2
                    Exception::CALCULATION_ENGINE_PUSH_TO_STACK
255 2
                );
256
            }
257
        }
258
259 58
        return $reference;
260
    }
261
262 13
    private function fullData(int $startRow, int $endRow): string
263
    {
264 13
        $columns = array_keys($this->columns);
265 13
        $firstColumn = array_shift($columns);
266 13
        $lastColumn = (empty($columns)) ? $firstColumn : array_pop($columns);
267
268 13
        return "{$firstColumn}{$startRow}:{$lastColumn}{$endRow}";
269
    }
270
271 26
    private function getMinimumRow(string $reference): int
272
    {
273 26
        return match ($reference) {
274 26
            self::ITEM_SPECIFIER_ALL, self::ITEM_SPECIFIER_HEADERS => $this->headersRow ?? $this->firstDataRow,
275 26
            self::ITEM_SPECIFIER_DATA => $this->firstDataRow,
276 26
            self::ITEM_SPECIFIER_TOTALS => $this->totalsRow ?? $this->lastDataRow,
277 26
            default => $this->headersRow ?? $this->firstDataRow,
278 26
        };
279
    }
280
281 26
    private function getMaximumRow(string $reference): int
282
    {
283 26
        return match ($reference) {
284 26
            self::ITEM_SPECIFIER_HEADERS => $this->headersRow ?? $this->firstDataRow,
285 26
            self::ITEM_SPECIFIER_DATA => $this->lastDataRow,
286 26
            self::ITEM_SPECIFIER_ALL, self::ITEM_SPECIFIER_TOTALS => $this->totalsRow ?? $this->lastDataRow,
287 26
            default => $this->totalsRow ?? $this->lastDataRow,
288 26
        };
289
    }
290
291 26
    public function value(): string
292
    {
293 26
        return $this->value;
294
    }
295
296
    /**
297
     * @return array<int, int>
298
     */
299 41
    private function getRowsForColumnReference(string &$reference, int $startRow, int $endRow): array
300
    {
301 41
        $rowsSelected = false;
302 41
        foreach (self::ITEM_SPECIFIER_ROWS_SET as $rowReference) {
303 41
            $pattern = '/\[' . $rowReference . '\]/mui';
304 41
            if (preg_match($pattern, $reference) === 1) {
305 26
                if (($rowReference === self::ITEM_SPECIFIER_HEADERS) && ($this->table->getShowHeaderRow() === false)) {
306 2
                    throw new Exception(
307 2
                        'Table Headers are Hidden, and should not be Referenced',
308 2
                        Exception::CALCULATION_ENGINE_PUSH_TO_STACK
309 2
                    );
310
                }
311 26
                $rowsSelected = true;
312 26
                $startRow = min($startRow, $this->getMinimumRow($rowReference));
313 26
                $endRow = max($endRow, $this->getMaximumRow($rowReference));
314 26
                $reference = preg_replace($pattern, '', $reference) ?? '';
315
            }
316
        }
317 41
        if ($rowsSelected === false) {
318
            // If there isn't any Special Item Identifier specified, then the selection defaults to data rows only.
319 15
            $startRow = $this->firstDataRow;
320 15
            $endRow = $this->lastDataRow;
321
        }
322
323 41
        return [$startRow, $endRow];
324
    }
325
326 41
    private function getColumnsForColumnReference(string $reference, int $startRow, int $endRow): string
327
    {
328 41
        $columnsSelected = false;
329 41
        foreach ($this->columns as $columnId => $columnName) {
330 41
            $columnName = str_replace("\u{a0}", ' ', $columnName ?? '');
331 41
            $cellFrom = "{$columnId}{$startRow}";
332 41
            $cellTo = "{$columnId}{$endRow}";
333 41
            $cellReference = ($cellFrom === $cellTo) ? $cellFrom : "{$cellFrom}:{$cellTo}";
334 41
            $pattern = '/\[' . preg_quote($columnName, '/') . '\]/mui';
335 41
            if (preg_match($pattern, $reference) === 1) {
336 29
                $columnsSelected = true;
337 29
                $reference = preg_replace($pattern, $cellReference, $reference);
338
            }
339
            /** @var string $reference */
340
        }
341 41
        if ($columnsSelected === false) {
342 13
            return $this->fullData($startRow, $endRow);
343
        }
344
345 29
        return $reference;
346
    }
347
348 1
    public function __toString(): string
349
    {
350 1
        return $this->value;
351
    }
352
}
353