Passed
Push — master ( 46fdc8...4b16f3 )
by Mark
15:13 queued 06:31
created

StructuredReference::getTableByName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 9
rs 10
ccs 5
cts 5
cp 1
cc 2
nc 2
nop 1
crap 2
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);
1 ignored issue
show
Bug introduced by
Are you sure the assignment to $table is correct as $cell->getWorksheet()->g...yName($this->tableName) targeting PhpOffice\PhpSpreadsheet...sheet::getTableByName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
148
149 31
        if ($table === null) {
0 ignored issues
show
introduced by
The condition $table === null is always true.
Loading history...
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
            $cellReference = $columnId . $cell->getRow();
183 11
            $pattern1 = '/\[' . preg_quote($columnName) . '\]/miu';
184 11
            $pattern2 = '/@' . preg_quote($columnName) . '/miu';
185
            /** @var string $reference */
186 11
            if (preg_match($pattern1, $reference) === 1) {
187 10
                $reference = preg_replace($pattern1, $cellReference, $reference);
188 11
            } elseif (preg_match($pattern2, $reference) === 1) {
189 1
                $reference = preg_replace($pattern2, $cellReference, $reference);
190
            }
191
        }
192
193
        /** @var string $reference */
194 11
        return $this->validateParsedReference(trim($reference, '[]@, '));
195
    }
196
197
    /**
198
     * @throws Exception
199
     * @throws \PhpOffice\PhpSpreadsheet\Exception
200
     */
201 21
    private function getColumnReference(): string
202
    {
203 21
        $reference = str_replace("\u{a0}", ' ', $this->reference);
204 21
        $startRow = ($this->totalsRow === null) ? $this->lastDataRow : $this->totalsRow;
205 21
        $endRow = ($this->headersRow === null) ? $this->firstDataRow : $this->headersRow;
206
207 21
        [$startRow, $endRow] = $this->getRowsForColumnReference($reference, $startRow, $endRow);
208 21
        $reference = $this->getColumnsForColumnReference($reference, $startRow, $endRow);
209
210 21
        $reference = trim($reference, '[]@, ');
211 21
        if (substr_count($reference, ':') > 1) {
212 3
            $cells = explode(':', $reference);
213 3
            $firstCell = array_shift($cells);
214 3
            $lastCell = array_pop($cells);
215 3
            $reference = "{$firstCell}:{$lastCell}";
216
        }
217
218 21
        return $this->validateParsedReference($reference);
219
    }
220
221
    /**
222
     * @throws Exception
223
     * @throws \PhpOffice\PhpSpreadsheet\Exception
224
     */
225 31
    private function validateParsedReference(string $reference): string
226
    {
227 31
        if (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . ':' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $reference) !== 1) {
228 14
            if (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $reference) !== 1) {
229
                throw new Exception("Invalid Structured Reference {$this->reference} {$reference}");
230
            }
231
        }
232
233 31
        return $reference;
234
    }
235
236 6
    private function fullData(int $startRow, int $endRow): string
237
    {
238 6
        $columns = array_keys($this->columns);
239 6
        $firstColumn = array_shift($columns);
240 6
        $lastColumn = (empty($columns)) ? $firstColumn : array_pop($columns);
241
242 6
        return "{$firstColumn}{$startRow}:{$lastColumn}{$endRow}";
243
    }
244
245 14
    private function getMinimumRow(string $reference): int
246
    {
247
        switch ($reference) {
248 14
            case self::ITEM_SPECIFIER_ALL:
249 12
            case self::ITEM_SPECIFIER_HEADERS:
250 9
                return $this->headersRow ?? $this->firstDataRow;
251 6
            case self::ITEM_SPECIFIER_DATA:
252 3
                return $this->firstDataRow;
253 3
            case self::ITEM_SPECIFIER_TOTALS:
254 3
                return $this->totalsRow ?? $this->lastDataRow;
255
        }
256
257
        return $this->headersRow ?? $this->firstDataRow;
258
    }
259
260 14
    private function getMaximumRow(string $reference): int
261
    {
262
        switch ($reference) {
263 14
            case self::ITEM_SPECIFIER_HEADERS:
264 7
                return $this->headersRow ?? $this->firstDataRow;
265 8
            case self::ITEM_SPECIFIER_DATA:
266 3
                return $this->lastDataRow;
267 5
            case self::ITEM_SPECIFIER_ALL:
268 3
            case self::ITEM_SPECIFIER_TOTALS:
269 5
                return $this->totalsRow ?? $this->lastDataRow;
270
        }
271
272
        return $this->totalsRow ?? $this->lastDataRow;
273
    }
274
275 20
    public function value(): string
276
    {
277 20
        return $this->value;
278
    }
279
280
    /**
281
     * @return array<int, int>
282
     */
283 21
    private function getRowsForColumnReference(string &$reference, int $startRow, int $endRow): array
284
    {
285 21
        $rowsSelected = false;
286 21
        foreach (self::ITEM_SPECIFIER_ROWS_SET as $rowReference) {
287 21
            $pattern = '/\[' . $rowReference . '\]/mui';
288
            /** @var string $reference */
289 21
            if (preg_match($pattern, $reference) === 1) {
290 14
                if (($rowReference === self::ITEM_SPECIFIER_HEADERS) && ($this->table->getShowHeaderRow() === false)) {
291 2
                    throw new Exception(
292 2
                        'Table Headers are Hidden, and should not be Referenced',
293 2
                        Exception::CALCULATION_ENGINE_PUSH_TO_STACK
294 2
                    );
295
                }
296 14
                $rowsSelected = true;
297 14
                $startRow = min($startRow, $this->getMinimumRow($rowReference));
298 14
                $endRow = max($endRow, $this->getMaximumRow($rowReference));
299 14
                $reference = preg_replace($pattern, '', $reference);
300
            }
301
        }
302 21
        if ($rowsSelected === false) {
303
            // If there isn't any Special Item Identifier specified, then the selection defaults to data rows only.
304 7
            $startRow = $this->firstDataRow;
305 7
            $endRow = $this->lastDataRow;
306
        }
307
308 21
        return [$startRow, $endRow];
309
    }
310
311 21
    private function getColumnsForColumnReference(string $reference, int $startRow, int $endRow): string
312
    {
313 21
        $columnsSelected = false;
314 21
        foreach ($this->columns as $columnId => $columnName) {
315 21
            $columnName = str_replace("\u{a0}", ' ', $columnName);
316 21
            $cellFrom = "{$columnId}{$startRow}";
317 21
            $cellTo = "{$columnId}{$endRow}";
318 21
            $cellReference = ($cellFrom === $cellTo) ? $cellFrom : "{$cellFrom}:{$cellTo}";
319 21
            $pattern = '/\[' . preg_quote($columnName) . '\]/mui';
320 21
            if (preg_match($pattern, $reference) === 1) {
321 15
                $columnsSelected = true;
322 15
                $reference = preg_replace($pattern, $cellReference, $reference);
323
            }
324
            /** @var string $reference */
325
        }
326 21
        if ($columnsSelected === false) {
327 6
            return $this->fullData($startRow, $endRow);
328
        }
329
330 15
        return $reference;
331
    }
332
}
333