Skip to content

Commit

Permalink
check if coordinate is inside range (#3779)
Browse files Browse the repository at this point in the history
* check if coordinate is inside range

* Added coordinateIsInsideRange tests

* fix coordinateIsInsideRange error throwing

* fix coordinateIsInsideRange error throwing

* add support to worksheet name

* validateReferenceAndGetData type

* change validate and add tests data

* fix tests data reference

* fix absolute reference error

* fix boundaries to get range

* fix scrutinizer erros

* Additional Test Cases

---------

Co-authored-by: oleibman <[email protected]>
  • Loading branch information
AdrianBatista and oleibman authored Nov 17, 2023
1 parent f9eb35d commit 276f781
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 0 deletions.
86 changes: 86 additions & 0 deletions src/PhpSpreadsheet/Cell/Coordinate.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
abstract class Coordinate
{
public const A1_COORDINATE_REGEX = '/^(?<col>\$?[A-Z]{1,3})(?<row>\$?\d{1,7})$/i';
public const FULL_REFERENCE_REGEX = '/^(?:(?<worksheet>[^!]*)!)?(?<localReference>(?<firstCoordinate>[$]?[A-Z]{1,3}[$]?\d{1,7})(?:\:(?<secondCoordinate>[$]?[A-Z]{1,3}[$]?\d{1,7}))?)$/i';

/**
* Default range variable constant.
Expand Down Expand Up @@ -258,6 +259,91 @@ public static function getRangeBoundaries(string $range)
];
}

/**
* Check if cell or range reference is valid and return an array with type of reference (cell or range), worksheet (if it was given)
* and the coordinate or the first coordinate and second coordinate if it is a range.
*
* @param string $reference Coordinate or Range (e.g. A1:A1, B2, B:C, 2:3)
*
* @return array reference data
*/
private static function validateReferenceAndGetData($reference): array
{
$data = [];
preg_match(self::FULL_REFERENCE_REGEX, $reference, $matches);
if (count($matches) === 0) {
return ['type' => 'invalid'];
}

if (isset($matches['secondCoordinate'])) {
$data['type'] = 'range';
$data['firstCoordinate'] = str_replace('$', '', $matches['firstCoordinate']);
$data['secondCoordinate'] = str_replace('$', '', $matches['secondCoordinate']);
} else {
$data['type'] = 'coordinate';
$data['coordinate'] = str_replace('$', '', $matches['firstCoordinate']);
}

$worksheet = $matches['worksheet'];
if ($worksheet !== '') {
if (substr($worksheet, 0, 1) === "'" && substr($worksheet, -1, 1) === "'") {
$worksheet = substr($worksheet, 1, -1);
}
$data['worksheet'] = strtolower($worksheet);
}
$data['localReference'] = str_replace('$', '', $matches['localReference']);

return $data;
}

/**
* Check if coordinate is inside a range.
*
* @param string $range Cell range, Single Cell, Row/Column Range (e.g. A1:A1, B2, B:C, 2:3)
* @param string $coordinate Cell coordinate (e.g. A1)
*
* @return bool true if coordinate is inside range
*/
public static function coordinateIsInsideRange(string $range, string $coordinate): bool
{
$rangeData = self::validateReferenceAndGetData($range);
if ($rangeData['type'] === 'invalid') {
throw new Exception('First argument needs to be a range');
}

$coordinateData = self::validateReferenceAndGetData($coordinate);
if ($coordinateData['type'] === 'invalid') {
throw new Exception('Second argument needs to be a single coordinate');
}

if (isset($coordinateData['worksheet']) && !isset($rangeData['worksheet'])) {
return false;
}
if (!isset($coordinateData['worksheet']) && isset($rangeData['worksheet'])) {
return false;
}

if (isset($coordinateData['worksheet'], $rangeData['worksheet'])) {
if ($coordinateData['worksheet'] !== $rangeData['worksheet']) {
return false;
}
}

$boundaries = self::rangeBoundaries($rangeData['localReference']);
$coordinates = self::indexesFromString($coordinateData['localReference']);

$columnIsInside = $boundaries[0][0] <= $coordinates[0] && $coordinates[0] <= $boundaries[1][0];
if (!$columnIsInside) {
return false;
}
$rowIsInside = $boundaries[0][1] <= $coordinates[1] && $coordinates[1] <= $boundaries[1][1];
if (!$rowIsInside) {
return false;
}

return true;
}

/**
* Column index from string.
*
Expand Down
35 changes: 35 additions & 0 deletions tests/PhpSpreadsheetTests/Cell/CoordinateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,41 @@ public static function providerGetRangeBoundaries(): array
return require 'tests/data/CellGetRangeBoundaries.php';
}

/**
* @dataProvider providerCoordinateIsInsideRange
*/
public static function testCoordinateIsInsideRange(bool $expectedResult, string $range, string $coordinate): void
{
$result = Coordinate::coordinateIsInsideRange($range, $coordinate);
self::assertEquals($result, $expectedResult);
}

public static function providerCoordinateIsInsideRange(): array
{
return require 'tests/data/Cell/CoordinateIsInsideRange.php';
}

/**
* @dataProvider providerCoordinateIsInsideRangeException
*/
public static function testCoordinateIsInsideRangeException(string $expectedResult, string $range, string $coordinate): void
{
try {
Coordinate::coordinateIsInsideRange($range, $coordinate);
} catch (\Exception $e) {
self::assertInstanceOf(Exception::class, $e);
self::assertEquals($e->getMessage(), $expectedResult);

return;
}
self::fail('An expected exception has not been raised.');
}

public static function providerCoordinateIsInsideRangeException(): array
{
return require 'tests/data/Cell/CoordinateIsInsideRangeException.php';
}

/**
* @dataProvider providerExtractAllCellReferencesInRange
*/
Expand Down
33 changes: 33 additions & 0 deletions tests/data/Cell/CoordinateIsInsideRange.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

return [
[true, 'A1:E20', 'B4'],
[false, 'A1:E20', 'F36'],
[true, '$A$1:$E$20', '$B$4'],
[false, '$A$1:$E$20', '$F$36'],
[true, '$A$1:$E$20', 'B4'],
[false, '$A$1:$E$20', 'F36'],
[true, 'A1:E20', '$B$4'],
[false, 'A1:E20', '$F$36'],
[true, 'Sheet!A1:E20', 'Sheet!B4'],
'case insensitive' => [true, 'Sheet!A1:E20', 'sheet!B4'],
'apostrophes 1st sheetname not 2nd' => [true, '\'Sheet\'!A1:E20', 'sheet!B4'],
'apostrophes 2nd sheetname not 1st' => [true, 'Sheet!A1:E20', '\'sheet\'!B4'],
[false, 'Sheet!A1:E20', 'Sheet!F36'],
[true, 'Sheet!$A$1:$E$20', 'Sheet!$B$4'],
[false, 'Sheet!$A$1:$E$20', 'Sheet!$F$36'],
[false, 'Sheet!A1:E20', 'B4'],
[false, 'Sheet!A1:E20', 'F36'],
[false, 'Sheet!$A$1:$E$20', '$B$4'],
[false, 'Sheet!$A$1:$E$20', '$F$36'],
[false, 'A1:E20', 'Sheet!B4'],
[false, 'A1:E20', 'Sheet!F36'],
[false, '$A$1:$E$20', 'Sheet!$B$4'],
[false, '$A$1:$E$20', 'Sheet!$F$36'],
[true, '\'Sheet space\'!A1:E20', '\'Sheet space\'!B4'],
[false, '\'Sheet space\'!A1:E20', '\'Sheet space\'!F36'],
[true, '\'Sheet space\'!$A$1:$E$20', '\'Sheet space\'!$B$4'],
[false, '\'Sheet space\'!$A$1:$E$20', '\'Sheet space\'!$F$36'],
];
8 changes: 8 additions & 0 deletions tests/data/Cell/CoordinateIsInsideRangeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

declare(strict_types=1);

return [
['First argument needs to be a range', 'invalidRange', 'B4'],
['Second argument needs to be a single coordinate', 'A1:E20', 'invalidCoordinate'],
];

0 comments on commit 276f781

Please sign in to comment.