Passed
Push — master ( 8fc7d9...eeb858 )
by Mark
14:21
created

StructuredReference::getMaximumRow()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5.0342

Importance

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