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