16 public const A1_COORDINATE_REGEX =
'/^(?<absolute_col>\$?)(?<col_ref>[A-Z]{1,3})(?<absolute_row>\$?)(?<row_ref>\d{1,7})$/i';
34 if (preg_match(self::A1_COORDINATE_REGEX, $pCoordinateString, $matches)) {
35 return [$matches[
'absolute_col'] . $matches[
'col_ref'], $matches[
'absolute_row'] . $matches[
'row_ref']];
36 } elseif (self::coordinateIsRange($pCoordinateString)) {
37 throw new Exception(
'Cell coordinate string can not be a range of cells');
38 } elseif ($pCoordinateString ==
'') {
39 throw new Exception(
'Cell coordinate can not be zero-length string');
42 throw new Exception(
'Invalid cell coordinate ' . $pCoordinateString);
54 [$col,
$row] = self::coordinateFromString($coordinates);
57 self::columnIndexFromString(ltrim($col,
'$')),
58 (int) ltrim(
$row,
'$'),
71 return (strpos($coord,
':') !==
false) || (strpos($coord,
',') !==
false);
84 if (self::coordinateIsRange($pCoordinateString)) {
85 throw new Exception(
'Cell coordinate string can not be a range of cells');
90 if ($worksheet >
'') {
95 if (ctype_digit($pCoordinateString)) {
96 return $worksheet .
'$' . $pCoordinateString;
97 } elseif (ctype_alpha($pCoordinateString)) {
98 return $worksheet .
'$' . strtoupper($pCoordinateString);
101 return $worksheet . self::absoluteCoordinate($pCoordinateString);
113 if (self::coordinateIsRange($pCoordinateString)) {
114 throw new Exception(
'Cell coordinate string can not be a range of cells');
119 if ($worksheet >
'') {
124 [$column,
$row] = self::coordinateFromString($pCoordinateString);
125 $column = ltrim($column,
'$');
128 return $worksheet .
'$' . $column .
'$' .
$row;
143 if (empty($pRange)) {
144 $pRange = self::DEFAULT_RANGE;
147 $exploded = explode(
',', $pRange);
148 $counter = count($exploded);
149 for (
$i = 0;
$i < $counter; ++
$i) {
150 $exploded[
$i] = explode(
':', $exploded[
$i]);
166 if (empty($pRange) || !is_array($pRange[0])) {
167 throw new Exception(
'Range does not contain any information');
171 $counter = count($pRange);
172 for (
$i = 0;
$i < $counter; ++
$i) {
173 $pRange[
$i] = implode(
':', $pRange[
$i]);
176 return implode(
',', $pRange);
190 if (empty($pRange)) {
191 $pRange = self::DEFAULT_RANGE;
195 $pRange = strtoupper($pRange);
198 if (strpos($pRange,
':') ===
false) {
199 $rangeA = $rangeB = $pRange;
201 [$rangeA, $rangeB] = explode(
':', $pRange);
205 $rangeStart = self::coordinateFromString($rangeA);
206 $rangeEnd = self::coordinateFromString($rangeB);
209 $rangeStart[0] = self::columnIndexFromString($rangeStart[0]);
210 $rangeEnd[0] = self::columnIndexFromString($rangeEnd[0]);
212 return [$rangeStart, $rangeEnd];
225 [$rangeStart, $rangeEnd] = self::rangeBoundaries($pRange);
227 return [($rangeEnd[0] - $rangeStart[0] + 1), ($rangeEnd[1] - $rangeStart[1] + 1)];
241 if (empty($pRange)) {
242 $pRange = self::DEFAULT_RANGE;
246 $pRange = strtoupper($pRange);
249 if (strpos($pRange,
':') ===
false) {
250 $rangeA = $rangeB = $pRange;
252 [$rangeA, $rangeB] = explode(
':', $pRange);
255 return [self::coordinateFromString($rangeA), self::coordinateFromString($rangeB)];
270 static $indexCache = [];
272 if (isset($indexCache[$pString])) {
273 return $indexCache[$pString];
278 static $columnLookup = [
279 '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,
280 '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,
281 '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,
282 '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,
287 if (isset($pString[0])) {
288 if (!isset($pString[1])) {
289 $indexCache[$pString] = $columnLookup[$pString];
291 return $indexCache[$pString];
292 } elseif (!isset($pString[2])) {
293 $indexCache[$pString] = $columnLookup[$pString[0]] * 26 + $columnLookup[$pString[1]];
295 return $indexCache[$pString];
296 } elseif (!isset($pString[3])) {
297 $indexCache[$pString] = $columnLookup[$pString[0]] * 676 + $columnLookup[$pString[1]] * 26 + $columnLookup[$pString[2]];
299 return $indexCache[$pString];
303 throw new Exception(
'Column string index can not be ' . ((isset($pString[0])) ?
'longer than 3 characters' :
'empty'));
315 static $indexCache = [];
317 if (!isset($indexCache[$columnIndex])) {
318 $indexValue = $columnIndex;
321 $characterValue = ($indexValue % 26) ?: 26;
322 $indexValue = ($indexValue - $characterValue) / 26;
323 $base26 = chr($characterValue + 64) . ($base26 ?:
'');
324 }
while ($indexValue > 0);
325 $indexCache[$columnIndex] = $base26;
328 return $indexCache[$columnIndex];
340 [$ranges, $operators] = self::getCellBlocksFromRangeString($cellRange);
343 foreach ($ranges as $range) {
344 $cells[] = self::getReferencesForCellBlock($range);
347 $cells = self::processRangeSetOperators($operators, $cells);
353 $cellList = array_merge(...$cells);
354 $cellList = self::sortCellReferenceArray($cellList);
361 $operatorCount = count($operators);
362 for ($offset = 0; $offset < $operatorCount; ++$offset) {
363 $operator = $operators[$offset];
364 if ($operator !==
' ') {
368 $cells[$offset] = array_intersect($cells[$offset], $cells[$offset + 1]);
369 unset($operators[$offset], $cells[$offset + 1]);
370 $operators = array_values($operators);
371 $cells = array_values($cells);
383 foreach ($cellList as $coord) {
384 [$column,
$row] = sscanf($coord,
'%[A-Z]%d');
385 $sortKeys[sprintf(
'%3s%09d', $column,
$row)] = $coord;
389 return array_values($sortKeys);
404 if (!self::coordinateIsRange($cellBlock)) {
405 return (array) $cellBlock;
409 $ranges = self::splitRange($cellBlock);
410 foreach ($ranges as $range) {
412 if (!isset($range[1])) {
413 $returnValue[] = $range[0];
419 [$rangeStart, $rangeEnd] = $range;
420 [$startColumn, $startRow] = self::coordinateFromString($rangeStart);
421 [$endColumn, $endRow] = self::coordinateFromString($rangeEnd);
422 $startColumnIndex = self::columnIndexFromString($startColumn);
423 $endColumnIndex = self::columnIndexFromString($endColumn);
427 $currentColumnIndex = $startColumnIndex;
428 $currentRow = $startRow;
430 self::validateRange($cellBlock, $startColumnIndex, $endColumnIndex, $currentRow, $endRow);
433 while ($currentColumnIndex < $endColumnIndex) {
434 while ($currentRow <= $endRow) {
435 $returnValue[] = self::stringFromColumnIndex($currentColumnIndex) . $currentRow;
438 ++$currentColumnIndex;
439 $currentRow = $startRow;
466 $mergedCoordCollection = [];
468 foreach ($pCoordCollection as $coord => $value) {
469 if (self::coordinateIsRange($coord)) {
470 $mergedCoordCollection[$coord] = $value;
475 [$column,
$row] = self::coordinateFromString($coord);
477 $hashCode = $column .
'-' . (is_object($value) ? $value->getHashCode() : $value);
479 if (!isset($hashedValues[$hashCode])) {
480 $hashedValues[$hashCode] = (object) [
486 $hashedValues[$hashCode]->rows[] =
$row;
490 ksort($hashedValues);
492 foreach ($hashedValues as $hashedValue) {
493 sort($hashedValue->rows);
498 foreach ($hashedValue->rows as
$row) {
499 if ($rowStart === null) {
502 } elseif ($rowEnd === $row - 1) {
505 if ($rowStart == $rowEnd) {
506 $ranges[] = $hashedValue->col . $rowStart;
508 $ranges[] = $hashedValue->col . $rowStart .
':' . $hashedValue->col . $rowEnd;
516 if ($rowStart !== null) {
517 if ($rowStart == $rowEnd) {
518 $ranges[] = $hashedValue->col . $rowStart;
520 $ranges[] = $hashedValue->col . $rowStart .
':' . $hashedValue->col . $rowEnd;
524 foreach ($ranges as $range) {
525 $mergedCoordCollection[$range] = $hashedValue->value;
529 return $mergedCoordCollection;
542 $rangeString = str_replace(
'$',
'', strtoupper($rangeString));
545 $tokens = preg_split(
'/([ ,])/', $rangeString, -1, PREG_SPLIT_DELIM_CAPTURE);
547 $split = array_chunk($tokens, 2);
548 $ranges = array_column($split, 0);
549 $operators = array_column($split, 1);
551 return [$ranges, $operators];
564 private static function validateRange($cellBlock, $startColumnIndex, $endColumnIndex, $currentRow, $endRow): void
566 if ($startColumnIndex >= $endColumnIndex || $currentRow > $endRow) {
567 throw new Exception(
'Invalid range: "' . $cellBlock .
'"');
static getCellBlocksFromRangeString($rangeString)
Get the individual cell blocks from a range string, removing any $ characters.
static coordinateIsRange($coord)
Checks if a coordinate represents a range of cells.
static splitRange($pRange)
Split range into coordinate strings.
static absoluteReference($pCoordinateString)
Make string row, column or cell coordinate absolute.
const A1_COORDINATE_REGEX
static extractSheetTitle($pRange, $returnRange=false)
Extract worksheet title from range.
static processRangeSetOperators(array $operators, array $cells)
Helper class to manipulate cell coordinates.
static buildRange(array $pRange)
Build range from coordinate strings.
static indexesFromString(string $coordinates)
Get indexes from a string coordinates.
static getReferencesForCellBlock($cellBlock)
Get all cell references for an individual cell block.
static sortCellReferenceArray(array $cellList)
static coordinateFromString($pCoordinateString)
Coordinate from string.
static mergeRangesInCollection(array $pCoordCollection)
Convert an associative array of single cell coordinates to values to an associative array of cell ran...
static rangeBoundaries($pRange)
Calculate range boundaries.
static getRangeBoundaries($pRange)
Calculate range boundaries.
static extractAllCellReferencesInRange($cellRange)
Extract all cell references in range, which may be comprised of multiple cell ranges.
static absoluteCoordinate($pCoordinateString)
Make string coordinate absolute.
static columnIndexFromString($pString)
Column index from string.
static validateRange($cellBlock, $startColumnIndex, $endColumnIndex, $currentRow, $endRow)
Check that the given range is valid, i.e.
static stringFromColumnIndex($columnIndex)
String from column index.
static rangeDimension($pRange)
Calculate range dimension.