Skip to content

Commit

Permalink
Added several helper functions for working with sequential ranges and…
Browse files Browse the repository at this point in the history
… negative indexes. - refs #47
  • Loading branch information
Luke Visinoni committed Jan 8, 2017
1 parent be9cc37 commit 80b736b
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 13 deletions.
7 changes: 3 additions & 4 deletions src/Collection/Sequence.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public function __invoke($offset = null)
}
if (is_numeric($offset)) {
if ($offset < 0) {
$offset = $count - abs($offset);
$offset = $count + $offset;
}
return $this[$offset];
}
Expand All @@ -122,9 +122,8 @@ private function setData($data)
if (!is_traversable($data)) {
// @todo Maybe create an ImmutableException for this?
throw new BadMethodCallException(sprintf(
'Cannot %s, %s is immutable.',
__METHOD__,
__CLASS__
'Forbidden method call: %s',
__METHOD__
));
}
$data = array_values(to_array($data));
Expand Down
118 changes: 109 additions & 9 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,112 @@ function to_array($data, $strict = true)
return [$data];
}

function get_range_start_end($range, $count)
/**
* Get object count.
*
* This function will accept any data and attempt to return its count.
*
* @param mixed $data The data to count
*
* @return int
*/
function get_count($data)
{
if (is_null($data)) {
return $data;
}

if (is_array($data)) {
return count($data);
}

if (is_numeric($data)) {
return (int) $data;
}

if ($data === '') {
return 0;
}

if ($data === 0) {
return 0;
}

if (is_object($data)) {
if (method_exists($data, 'count')) {
$count = $data->count();
} elseif (method_exists($data, '__toString')) {
$count = (string) $data;
$count = (int) $count;
} elseif (is_traversable($data)) {
$count = 0;
foreach ($data as $item) {
$count++;
}
return (int) $count;
}
if (isset($count)) {
return (int) $count;
}
}

if (is_numeric($data)) {
return (int) $data;
}

throw new RuntimeException('Cannot convert to int.');
}

/**
* Normalize offset to positive integer.
*
* Provided with the requested offset, whether it be a string, an integer (positive or negative), or some type of
* object, this function will normalize it to a positive integer offset or, failing that, it will throw an exception.
* A negative offset will require either the traversable that is being indexed or its total count in order to normalize
* @param int|mixed $offset The offset to normalize
* @param int|array|traversable $count Either the traversable count, or the traversable itself.
* @return int
* @throws RuntimeException If offset cannot be normalized
* @throws InvalidArgumentException If offset is negative and count is not provided
*/
function normalize_offset($offset, $count = null)
{
if (is_object($offset) && method_exists($offset, '__toString')) {
$offset = (string) $offset;
}

if (!is_numeric($offset)) {
throw new RuntimeException('Invalid offset.');
}

$count = get_count($count);

if ($offset < 0) {
if (is_null($count)) {
throw new InvalidArgumentException('Cannot normalize negative offset without a total count.');
}
$offset += $count;
}
return (int) $offset;
}

/**
* Get range start and end from string.
*
* Provided a string in the format of "start:end" and the total items in a collection, this function will return an
* array in the form of [start, length].
*
* @param string $range The range to get (in the format of "start:end"
* @param int|array|traversable $count Either the traversable count, or the traversable itself.
*
* @return array [start, length]
*/
function get_range_start_end($range, $count = null)
{
$count = get_count($count);
if (Str::contains($range, Sequence::SLICE_DELIM)) {
// return slice as a new sequence
list($start, $end) = explode(Sequence::SLICE_DELIM, $range, 2);
Expand All @@ -194,14 +298,10 @@ function get_range_start_end($range, $count)
if ($end == '') {
$end = $count - 1;
}
if (is_numeric($start) && is_numeric($end)) {
if ($start < 0) {
$start = $count - abs($start);
}
if ($end < 0) {
$end = $count - abs($end);
}
$length = $end - $start + 1;
$start = normalize_offset($start, $count);
$end = normalize_offset($end, $count) + 1;
if ($end > $start) {
$length = $end - $start;
return [$start, $length];
}
}
Expand Down
90 changes: 90 additions & 0 deletions tests/FunctionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@
Noz\is_arrayable,
Noz\to_array,
Noz\typeof,
Noz\get_range_start_end,
Noz\normalize_offset,
Noz\_;
use Noz\Collection\Collection;
use Noz\Contracts\CollectionInterface;
use function Noz\get_count;
use RuntimeException;
use SplObjectStorage;
use stdClass;

Expand Down Expand Up @@ -203,4 +207,90 @@ public function testCurryWithUnderscore()
// kinda verbose, but this is the best it's going to get for currying in PHP...
$this->assertEquals('foo -> bar -> baz', _(_(_($arrows, 'foo'), 'bar'), 'baz'));
}

public function testGetCount()
{
$stubNeg5 = $this->getMockBuilder('stdClass')
->setMethods(['__toString'])
->getMock();
$stubNeg5->method('__toString')
->willReturn('-5');

$this->assertSame(-5, get_count($stubNeg5));

$stub10 = [1,2,3,4,5,6,7,8,9,10];

$this->assertSame(10, get_count($stub10));

$stub10 = $this->getMockBuilder(Collection::class)
->setMethods(['count'])
->getMock();
$stub10->method('count')
->willReturn('10');

$this->assertSame(10, get_count($stub10));
}

public function testNormalizeOffset()
{
$stub5 = $this->getMockBuilder('stdClass')
->setMethods(['__toString'])
->getMock();
$stub5->method('__toString')
->willReturn('5');
$stub10 = $this->getMockBuilder('stdClass')
->setMethods(['__toString'])
->getMock();
$stub10->method('__toString')
->willReturn('10');
$stubNeg3 = $this->getMockBuilder('stdClass')
->setMethods(['__toString'])
->getMock();
$stubNeg3->method('__toString')
->willReturn('-3');

$this->assertEquals(0, normalize_offset(0));
$this->assertEquals(2, normalize_offset(2));
$this->assertEquals(6, normalize_offset(-4, 10));
$this->assertEquals(4, normalize_offset('-6', 10));
$this->assertEquals(5, normalize_offset($stub5));
$this->assertEquals(2, normalize_offset($stubNeg3, $stub5));
$this->assertEquals(10, normalize_offset($stub10, 20));
}

public function testGetRangeStartEnd()
{
$stub5 = $this->getMockBuilder('stdClass')
->setMethods(['__toString'])
->getMock();
$stub5->method('__toString')
->willReturn('5');
$stub10 = $this->getMockBuilder('stdClass')
->setMethods(['__toString'])
->getMock();
$stub10->method('__toString')
->willReturn('10');
$stubNeg3 = $this->getMockBuilder('stdClass')
->setMethods(['__toString'])
->getMock();
$stubNeg3->method('__toString')
->willReturn('-3');

$this->assertEquals([3, 4], get_range_start_end('3:6'));
$this->assertEquals([0, 7], get_range_start_end(':6'));
$this->assertEquals([5, 5], get_range_start_end('5:', 10));
$this->assertEquals([0, 10], get_range_start_end(':', 10));
$this->assertEquals([0, 9], get_range_start_end(':-2', 10));
$this->assertEquals([0, 9], get_range_start_end(':-2', $stub10));
$this->assertEquals([5, 4], get_range_start_end('5:-2', $stub10));
$this->assertEquals([3,6], get_range_start_end('-7:-2', $stub10));
}

/**
* @expectedException RuntimeException
*/
public function testNormalizeOffsetThrowsExceptionForInvalidOffset()
{
normalize_offset('chooochooo');
}
}

0 comments on commit 80b736b

Please sign in to comment.