Passed
Pull Request — master (#3311)
by Mark
13:26
created

StructuredReference::getColumnReference()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

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