Test Failed
Push — develop ( 90366f...812a46 )
by Adrien
28:16
created

Coordinate::columnIndexFromString()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 39
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 19
nc 6
nop 1
dl 0
loc 39
ccs 16
cts 16
cp 1
crap 7
rs 6.7272
c 0
b 0
f 0
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Cell;
4
5
use PhpOffice\PhpSpreadsheet\Exception;
6
7
/**
8
 * Helper class to manipulate cell coordinates.
9
 *
10
 * Columns indexes and rows are always based on 1, **not** on 0. This match the behavior
11
 * that Excel users are used to, and also match the Excel functions `COLUMN()` and `ROW()`.
12
 */
13
abstract class Coordinate
14
{
15
    /**
16
     * Default range variable constant.
17
     *
18
     * @var string
19
     */
20
    const DEFAULT_RANGE = 'A1:A1';
21
22
    /**
23
     * Coordinate from string.
24
     *
25
     * @param string $pCoordinateString eg: 'A1'
26
     *
27
     * @throws Exception
28
     *
29
     * @return string[] Array containing column and row (indexes 0 and 1)
30
     */
31 174
    public static function coordinateFromString($pCoordinateString)
32
    {
33 174
        if (preg_match("/^([$]?[A-Z]{1,3})([$]?\d{1,7})$/", $pCoordinateString, $matches)) {
34 171
            return [$matches[1], $matches[2]];
35 4
        } elseif ((strpos($pCoordinateString, ':') !== false) || (strpos($pCoordinateString, ',') !== false)) {
36 1
            throw new Exception('Cell coordinate string can not be a range of cells');
37 3
        } elseif ($pCoordinateString == '') {
38 1
            throw new Exception('Cell coordinate can not be zero-length string');
39
        }
40
41 2
        throw new Exception('Invalid cell coordinate ' . $pCoordinateString);
42
    }
43
44
    /**
45
     * Make string row, column or cell coordinate absolute.
46
     *
47
     * @param string $pCoordinateString e.g. 'A' or '1' or 'A1'
48
     *                    Note that this value can be a row or column reference as well as a cell reference
49
     *
50
     * @throws Exception
51
     *
52
     * @return string Absolute coordinate        e.g. '$A' or '$1' or '$A$1'
53
     */
54 21
    public static function absoluteReference($pCoordinateString)
55
    {
56 21
        if (strpos($pCoordinateString, ':') === false && strpos($pCoordinateString, ',') === false) {
57
            // Split out any worksheet name from the reference
58 20
            $worksheet = '';
59 20
            $cellAddress = explode('!', $pCoordinateString);
60 20
            if (count($cellAddress) > 1) {
61 8
                list($worksheet, $pCoordinateString) = $cellAddress;
62
            }
63 20
            if ($worksheet > '') {
64 8
                $worksheet .= '!';
65
            }
66
67
            // Create absolute coordinate
68 20
            if (ctype_digit($pCoordinateString)) {
69 2
                return $worksheet . '$' . $pCoordinateString;
70 18
            } elseif (ctype_alpha($pCoordinateString)) {
71 2
                return $worksheet . '$' . strtoupper($pCoordinateString);
72
            }
73
74 16
            return $worksheet . self::absoluteCoordinate($pCoordinateString);
75
        }
76
77 1
        throw new Exception('Cell coordinate string can not be a range of cells');
78
    }
79
80
    /**
81
     * Make string coordinate absolute.
82
     *
83
     * @param string $pCoordinateString e.g. 'A1'
84
     *
85
     * @throws Exception
86
     *
87
     * @return string Absolute coordinate        e.g. '$A$1'
88
     */
89 32
    public static function absoluteCoordinate($pCoordinateString)
90
    {
91 32
        if (strpos($pCoordinateString, ':') === false && strpos($pCoordinateString, ',') === false) {
92
            // Split out any worksheet name from the coordinate
93 31
            $worksheet = '';
94 31
            $cellAddress = explode('!', $pCoordinateString);
95 31
            if (count($cellAddress) > 1) {
96 6
                list($worksheet, $pCoordinateString) = $cellAddress;
97
            }
98 31
            if ($worksheet > '') {
99 6
                $worksheet .= '!';
100
            }
101
102
            // Create absolute coordinate
103 31
            list($column, $row) = self::coordinateFromString($pCoordinateString);
104 31
            $column = ltrim($column, '$');
105 31
            $row = ltrim($row, '$');
106
107 31
            return $worksheet . '$' . $column . '$' . $row;
108
        }
109
110 1
        throw new Exception('Cell coordinate string can not be a range of cells');
111
    }
112
113
    /**
114
     * Split range into coordinate strings.
115
     *
116
     * @param string $pRange e.g. 'B4:D9' or 'B4:D9,H2:O11' or 'B4'
117
     *
118
     * @return array Array containg one or more arrays containing one or two coordinate strings
119
     *                                e.g. array('B4','D9') or array(array('B4','D9'),array('H2','O11'))
120
     *                                        or array('B4')
121
     */
122 104
    public static function splitRange($pRange)
123
    {
124
        // Ensure $pRange is a valid range
125 104
        if (empty($pRange)) {
126
            $pRange = self::DEFAULT_RANGE;
127
        }
128
129 104
        $exploded = explode(',', $pRange);
130 104
        $counter = count($exploded);
131 104
        for ($i = 0; $i < $counter; ++$i) {
132 104
            $exploded[$i] = explode(':', $exploded[$i]);
133
        }
134
135 104
        return $exploded;
136
    }
137
138
    /**
139
     * Build range from coordinate strings.
140
     *
141
     * @param array $pRange Array containg one or more arrays containing one or two coordinate strings
142
     *
143
     * @throws Exception
144
     *
145
     * @return string String representation of $pRange
146
     */
147 19
    public static function buildRange(array $pRange)
148
    {
149
        // Verify range
150 19
        if (empty($pRange) || !is_array($pRange[0])) {
151
            throw new Exception('Range does not contain any information');
152
        }
153
154
        // Build range
155 19
        $imploded = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $imploded is dead and can be removed.
Loading history...
156 19
        $counter = count($pRange);
157 19
        for ($i = 0; $i < $counter; ++$i) {
158 19
            $pRange[$i] = implode(':', $pRange[$i]);
159
        }
160 19
        $imploded = implode(',', $pRange);
161
162 19
        return $imploded;
163
    }
164
165
    /**
166
     * Calculate range boundaries.
167
     *
168
     * @param string $pRange Cell range (e.g. A1:A1)
169
     *
170
     * @return array Range coordinates array(Start Cell, End Cell)
171
     *                    where Start Cell and End Cell are arrays (Column Number, Row Number)
172
     */
173 65
    public static function rangeBoundaries($pRange)
174
    {
175
        // Ensure $pRange is a valid range
176 65
        if (empty($pRange)) {
177
            $pRange = self::DEFAULT_RANGE;
178
        }
179
180
        // Uppercase coordinate
181 65
        $pRange = strtoupper($pRange);
182
183
        // Extract range
184 65 View Code Duplication
        if (strpos($pRange, ':') === false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
185 2
            $rangeA = $rangeB = $pRange;
186
        } else {
187 63
            list($rangeA, $rangeB) = explode(':', $pRange);
188
        }
189
190
        // Calculate range outer borders
191 65
        $rangeStart = self::coordinateFromString($rangeA);
192 65
        $rangeEnd = self::coordinateFromString($rangeB);
193
194
        // Translate column into index
195 65
        $rangeStart[0] = self::columnIndexFromString($rangeStart[0]);
196 65
        $rangeEnd[0] = self::columnIndexFromString($rangeEnd[0]);
197
198 65
        return [$rangeStart, $rangeEnd];
199
    }
200
201
    /**
202
     * Calculate range dimension.
203
     *
204
     * @param string $pRange Cell range (e.g. A1:A1)
205
     *
206
     * @return array Range dimension (width, height)
207
     */
208 16
    public static function rangeDimension($pRange)
209
    {
210
        // Calculate range outer borders
211 16
        list($rangeStart, $rangeEnd) = self::rangeBoundaries($pRange);
212
213 16
        return [($rangeEnd[0] - $rangeStart[0] + 1), ($rangeEnd[1] - $rangeStart[1] + 1)];
214
    }
215
216
    /**
217
     * Calculate range boundaries.
218
     *
219
     * @param string $pRange Cell range (e.g. A1:A1)
220
     *
221
     * @return array Range coordinates array(Start Cell, End Cell)
222
     *                    where Start Cell and End Cell are arrays (Column ID, Row Number)
223
     */
224 13
    public static function getRangeBoundaries($pRange)
225
    {
226
        // Ensure $pRange is a valid range
227 13
        if (empty($pRange)) {
228
            $pRange = self::DEFAULT_RANGE;
229
        }
230
231
        // Uppercase coordinate
232 13
        $pRange = strtoupper($pRange);
233
234
        // Extract range
235 13 View Code Duplication
        if (strpos($pRange, ':') === false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
236 1
            $rangeA = $rangeB = $pRange;
237
        } else {
238 12
            list($rangeA, $rangeB) = explode(':', $pRange);
239
        }
240
241 13
        return [self::coordinateFromString($rangeA), self::coordinateFromString($rangeB)];
242
    }
243
244
    /**
245
     * Column index from string.
246
     *
247
     * @param string $pString eg 'A'
248
     *
249
     * @return int Column index (A = 1)
250
     */
251 178
    public static function columnIndexFromString($pString)
252
    {
253
        //    Using a lookup cache adds a slight memory overhead, but boosts speed
254
        //    caching using a static within the method is faster than a class static,
255
        //        though it's additional memory overhead
256 178
        static $indexCache = [];
257
258 178
        if (isset($indexCache[$pString])) {
259 163
            return $indexCache[$pString];
260
        }
261
        //    It's surprising how costly the strtoupper() and ord() calls actually are, so we use a lookup array rather than use ord()
262
        //        and make it case insensitive to get rid of the strtoupper() as well. Because it's a static, there's no significant
263
        //        memory overhead either
264 118
        static $columnLookup = [
265
            'A' => 1, 'B' => 2, 'C' => 3, 'D' => 4, 'E' => 5, 'F' => 6, 'G' => 7, 'H' => 8, 'I' => 9, 'J' => 10, 'K' => 11, 'L' => 12, 'M' => 13,
266
            'N' => 14, 'O' => 15, 'P' => 16, 'Q' => 17, 'R' => 18, 'S' => 19, 'T' => 20, 'U' => 21, 'V' => 22, 'W' => 23, 'X' => 24, 'Y' => 25, 'Z' => 26,
267
            'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6, 'g' => 7, 'h' => 8, 'i' => 9, 'j' => 10, 'k' => 11, 'l' => 12, 'm' => 13,
268
            'n' => 14, 'o' => 15, 'p' => 16, 'q' => 17, 'r' => 18, 's' => 19, 't' => 20, 'u' => 21, 'v' => 22, 'w' => 23, 'x' => 24, 'y' => 25, 'z' => 26,
269
        ];
270
271
        //    We also use the language construct isset() rather than the more costly strlen() function to match the length of $pString
272
        //        for improved performance
273 118
        if (isset($pString[0])) {
274 116
            if (!isset($pString[1])) {
275 105
                $indexCache[$pString] = $columnLookup[$pString];
276
277 105
                return $indexCache[$pString];
278 12
            } elseif (!isset($pString[2])) {
279 9
                $indexCache[$pString] = $columnLookup[$pString[0]] * 26 + $columnLookup[$pString[1]];
280
281 9
                return $indexCache[$pString];
282 3
            } elseif (!isset($pString[3])) {
283 2
                $indexCache[$pString] = $columnLookup[$pString[0]] * 676 + $columnLookup[$pString[1]] * 26 + $columnLookup[$pString[2]];
284
285 2
                return $indexCache[$pString];
286
            }
287
        }
288
289 3
        throw new Exception('Column string index can not be ' . ((isset($pString[0])) ? 'longer than 3 characters' : 'empty'));
290
    }
291
292
    /**
293
     * String from column index.
294
     *
295
     * @param int $columnIndex Column index (A = 1)
296
     *
297
     * @return string
298
     */
299 145
    public static function stringFromColumnIndex($columnIndex)
300
    {
301 145
        static $indexCache = [];
302
303 145
        if (!isset($indexCache[$columnIndex])) {
304 114
            $indexValue = $columnIndex;
305 114
            $base26 = null;
306
            do {
307 114
                $characterValue = ($indexValue % 26) ?: 26;
308 114
                $indexValue = ($indexValue - $characterValue) / 26;
309 114
                $base26 = chr($characterValue + 64) . ($base26 ?: '');
310 114
            } while ($indexValue > 0);
311 114
            $indexCache[$columnIndex] = $base26;
312
        }
313
314 145
        return $indexCache[$columnIndex];
315
    }
316
317
    /**
318
     * Extract all cell references in range.
319
     *
320
     * @param string $pRange Range (e.g. A1 or A1:C10 or A1:E10 A20:E25)
321
     *
322
     * @return array Array containing single cell references
323
     */
324 76
    public static function extractAllCellReferencesInRange($pRange)
325
    {
326
        // Returnvalue
327 76
        $returnValue = [];
328
329
        // Explode spaces
330 76
        $cellBlocks = explode(' ', str_replace('$', '', strtoupper($pRange)));
331 76
        foreach ($cellBlocks as $cellBlock) {
332
            // Single cell?
333 76
            if (strpos($cellBlock, ':') === false && strpos($cellBlock, ',') === false) {
334 61
                $returnValue[] = $cellBlock;
335
336 61
                continue;
337
            }
338
339
            // Range...
340 68
            $ranges = self::splitRange($cellBlock);
341 68
            foreach ($ranges as $range) {
342
                // Single cell?
343 68
                if (!isset($range[1])) {
344
                    $returnValue[] = $range[0];
345
346
                    continue;
347
                }
348
349
                // Range...
350 68
                list($rangeStart, $rangeEnd) = $range;
351 68
                sscanf($rangeStart, '%[A-Z]%d', $startCol, $startRow);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $startRow seems to be never defined.
Loading history...
352 68
                sscanf($rangeEnd, '%[A-Z]%d', $endCol, $endRow);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $endRow seems to be never defined.
Loading history...
353 68
                ++$endCol;
354
355
                // Current data
356 68
                $currentCol = $startCol;
357 68
                $currentRow = $startRow;
358
359
                // Loop cells
360 68
                while ($currentCol != $endCol) {
361 68
                    while ($currentRow <= $endRow) {
362 68
                        $returnValue[] = $currentCol . $currentRow;
363 68
                        ++$currentRow;
364
                    }
365 68
                    ++$currentCol;
366 68
                    $currentRow = $startRow;
367
                }
368
            }
369
        }
370
371
        //    Sort the result by column and row
372 76
        $sortKeys = [];
373 76
        foreach (array_unique($returnValue) as $coord) {
374 76
            sscanf($coord, '%[A-Z]%d', $column, $row);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $row seems to be never defined.
Loading history...
375 76
            $sortKeys[sprintf('%3s%09d', $column, $row)] = $coord;
376
        }
377 76
        ksort($sortKeys);
378
379
        // Return value
380 76
        return array_values($sortKeys);
381
    }
382
383
    /**
384
     * Convert an associative array of single cell coordinates to values to an associative array
385
     * of cell ranges to values.  Only adjacent cell coordinates with the same
386
     * value will be merged.  If the value is an object, it must implement the method getHashCode().
387
     *
388
     * For example, this function converts:
389
     *
390
     *    [ 'A1' => 'x', 'A2' => 'x', 'A3' => 'x', 'A4' => 'y' ]
391
     *
392
     * to:
393
     *
394
     *    [ 'A1:A3' => 'x', 'A4' => 'y' ]
395
     *
396
     * @param array $pCoordCollection associative array mapping coordinates to values
397
     *
398
     * @return array associative array mapping coordinate ranges to valuea
399
     */
400 6
    public static function mergeRangesInCollection(array $pCoordCollection)
401
    {
402 6
        $hashedValues = [];
403
404 6
        foreach ($pCoordCollection as $coord => $value) {
405 6
            list($column, $row) = self::coordinateFromString($coord);
406 6
            $row = (int) (ltrim($row, '$'));
407 6
            $hashCode = $column . '-' . (is_object($value) ? $value->getHashCode() : $value);
408
409 6
            if (!isset($hashedValues[$hashCode])) {
410 6
                $hashedValues[$hashCode] = (object) [
411 6
                    'value' => $value,
412 6
                    'col' => $column,
413 6
                    'rows' => [$row],
414
                ];
415
            } else {
416 6
                $hashedValues[$hashCode]->rows[] = $row;
417
            }
418
        }
419
420 6
        $mergedCoordCollection = [];
421 6
        ksort($hashedValues);
422
423 6
        foreach ($hashedValues as $hashedValue) {
424 6
            sort($hashedValue->rows);
425 6
            $rowStart = null;
426 6
            $rowEnd = null;
427 6
            $ranges = [];
428
429 6
            foreach ($hashedValue->rows as $row) {
430 6
                if ($rowStart === null) {
431 6
                    $rowStart = $row;
432 6
                    $rowEnd = $row;
433 3
                } elseif ($rowEnd === $row - 1) {
434 3
                    $rowEnd = $row;
435
                } else {
436 1 View Code Duplication
                    if ($rowStart == $rowEnd) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
437
                        $ranges[] = $hashedValue->col . $rowStart;
438
                    } else {
439 1
                        $ranges[] = $hashedValue->col . $rowStart . ':' . $hashedValue->col . $rowEnd;
440
                    }
441
442 1
                    $rowStart = $row;
443 6
                    $rowEnd = $row;
444
                }
445
            }
446
447 6 View Code Duplication
            if ($rowStart !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
448 6
                if ($rowStart == $rowEnd) {
449 5
                    $ranges[] = $hashedValue->col . $rowStart;
450
                } else {
451 2
                    $ranges[] = $hashedValue->col . $rowStart . ':' . $hashedValue->col . $rowEnd;
452
                }
453
            }
454
455 6
            foreach ($ranges as $range) {
456 6
                $mergedCoordCollection[$range] = $hashedValue->value;
457
            }
458
        }
459
460 6
        return $mergedCoordCollection;
461
    }
462
}
463