Passed
Push — master ( fcb5ef...e65bc8 )
by
unknown
13:57 queued 18s
created

getColumnsForColumnReference()   A

Complexity

Conditions 5
Paths 10

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5

Importance

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