Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implements slice method to Collection for extracting a portion of the collection based on given start and length. #15

Merged
merged 1 commit into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,16 @@ elements, or finding elements that match a specific condition.
$collection->count();
```

#### Retrieve by condition

- `findBy`: Finds the first element that matches one or more predicates.

```php
$collection->findBy(predicates: fn(CryptoCurrency $crypto): bool => $crypto->symbol === 'ETH');
```

<div id='comparing'></div>

#### Retrieve single elements

- `first`: Retrieves the first element from the Collection or returns a default value if the Collection is empty.
Expand All @@ -208,16 +218,17 @@ elements, or finding elements that match a specific condition.
$collection->last(defaultValueIfNotFound: 'default');
```

#### Retrieve by condition
#### Retrieve collection elements

- `findBy`: Finds the first element that matches one or more predicates.
- `slice`: Extracts a portion of the collection, starting at the specified index and retrieving the specified number of
elements.
If length is negative, it excludes many elements from the end of the collection.
If length is not provided or set to -1, it returns all elements from the specified index to the end of the collection.

```php
$collection->findBy(predicates: fn(CryptoCurrency $crypto): bool => $crypto->symbol === 'ETH');
$collection->slice(index: 1, length: 2);
```

<div id='comparing'></div>

### Comparing

These methods enable comparing collections to check for equality or to apply other comparison logic.
Expand Down Expand Up @@ -267,7 +278,7 @@ These methods allow the Collection's elements to be transformed or converted int
```php
$collection->each(actions: fn(Invoice $invoice): void => $collectionB->add(elements: new InvoiceSummary(amount: $invoice->amount, customer: $invoice->customer)));
```

#### Grouping elements

- `groupBy`: Groups the elements in the Collection based on the provided grouping criterion.
Expand Down
14 changes: 14 additions & 0 deletions src/Collectible.php
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,20 @@ public function reduce(Closure $aggregator, mixed $initial): mixed;
*/
public function sort(Order $order = Order::ASCENDING_KEY, ?Closure $predicate = null): Collectible;

/**
* Returns a subset of the collection starting at the specified index and containing the specified number of
* elements.
*
* If the `length` is negative, it will exclude that many elements from the end of the collection.
* If the `length` is not provided or set to `-1`, it returns all elements starting from the index until the end.
*
* @param int $index The zero-based index at which to start the slice.
* @param int $length The number of elements to include in the slice. If negative, removes that many from the end.
* Default is `-1`, meaning all elements from the index onward will be included.
* @return Collectible<Element> A new collection containing the sliced elements.
*/
public function slice(int $index, int $length = -1): Collectible;

/**
* Converts the collection to an array.
*
Expand Down
18 changes: 14 additions & 4 deletions src/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace TinyBlocks\Collection;

use Closure;
use TinyBlocks\Collection\Internal\Iterators\InternalIterator;
use TinyBlocks\Collection\Internal\Iterators\LazyIterator;
use TinyBlocks\Collection\Internal\Operations\Aggregate\Reduce;
use TinyBlocks\Collection\Internal\Operations\Compare\Contains;
use TinyBlocks\Collection\Internal\Operations\Compare\Equals;
Expand All @@ -16,6 +16,7 @@
use TinyBlocks\Collection\Internal\Operations\Retrieve\First;
use TinyBlocks\Collection\Internal\Operations\Retrieve\Get;
use TinyBlocks\Collection\Internal\Operations\Retrieve\Last;
use TinyBlocks\Collection\Internal\Operations\Retrieve\Slice;
use TinyBlocks\Collection\Internal\Operations\Transform\Each;
use TinyBlocks\Collection\Internal\Operations\Transform\GroupBy;
use TinyBlocks\Collection\Internal\Operations\Transform\Map;
Expand All @@ -35,16 +36,16 @@
*/
class Collection implements Collectible
{
private InternalIterator $iterator;
private LazyIterator $iterator;

private function __construct(InternalIterator $iterator)
private function __construct(LazyIterator $iterator)
{
$this->iterator = $iterator;
}

public static function createFrom(iterable $elements): static
{
return new static(iterator: InternalIterator::from(elements: $elements, operation: Create::fromEmpty()));
return new static(iterator: LazyIterator::from(elements: $elements, operation: Create::fromEmpty()));
}

public static function createFromEmpty(): static
Expand Down Expand Up @@ -150,6 +151,15 @@ public function sort(Order $order = Order::ASCENDING_KEY, ?Closure $predicate =
);
}

public function slice(int $index, int $length = -1): static
{
return new static(
iterator: $this->iterator->add(
operation: Slice::from(index: $index, length: $length)
)
);
}

public function toArray(PreserveKeys $preserveKeys = PreserveKeys::PRESERVE): array
{
return MapToArray::from(elements: $this->iterator->getIterator(), preserveKeys: $preserveKeys)->toArray();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* @template Value of mixed
* @implements IteratorAggregate<Key, Value>
*/
final class InternalIterator implements IteratorAggregate
final class LazyIterator implements IteratorAggregate
{
/**
* @var LazyOperation[]
Expand All @@ -35,18 +35,18 @@ private function __construct(private readonly iterable $elements, LazyOperation
/**
* @param iterable $elements
* @param LazyOperation $operation
* @return InternalIterator
* @return LazyIterator
*/
public static function from(iterable $elements, LazyOperation $operation): InternalIterator
public static function from(iterable $elements, LazyOperation $operation): LazyIterator
{
return new InternalIterator(elements: $elements, operation: $operation);
return new LazyIterator(elements: $elements, operation: $operation);
}

/**
* @param LazyOperation $operation
* @return InternalIterator
* @return LazyIterator
*/
public function add(LazyOperation $operation): InternalIterator
public function add(LazyOperation $operation): LazyIterator
{
$this->operations[] = $operation;
return $this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,22 @@
* @template Value
* @implements IteratorAggregate<Key, Value>
*/
final readonly class IterableIteratorAggregate implements IteratorAggregate
final readonly class LazyIteratorAggregate implements IteratorAggregate
{
public function __construct(private iterable $elements)
/**
* @param iterable<Key, Value> $elements
*/
private function __construct(private iterable $elements)
{
}

/**
* @param iterable $elements
* @return LazyIteratorAggregate
*/
public static function from(iterable $elements): LazyIteratorAggregate
{
return new LazyIteratorAggregate(elements: $elements);
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/Internal/Operations/Compare/Equals.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace TinyBlocks\Collection\Internal\Operations\Compare;

use TinyBlocks\Collection\Collectible;
use TinyBlocks\Collection\Internal\Iterators\IterableIteratorAggregate;
use TinyBlocks\Collection\Internal\Iterators\LazyIteratorAggregate;
use TinyBlocks\Collection\Internal\Operations\ImmediateOperation;

final readonly class Equals implements ImmediateOperation
Expand All @@ -26,8 +26,8 @@ public static function build(): Equals

public function compareAll(Collectible $other): bool
{
$currentIterator = (new IterableIteratorAggregate(elements: $other))->getIterator();
$targetIterator = (new IterableIteratorAggregate(elements: $this->elements))->getIterator();
$currentIterator = LazyIteratorAggregate::from(elements: $other)->getIterator();
$targetIterator = LazyIteratorAggregate::from(elements: $this->elements)->getIterator();

while ($currentIterator->valid() || $targetIterator->valid()) {
if (!$currentIterator->valid() || !$targetIterator->valid()) {
Expand Down
42 changes: 42 additions & 0 deletions src/Internal/Operations/Retrieve/Slice.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Collection\Internal\Operations\Retrieve;

use Generator;
use TinyBlocks\Collection\Internal\Operations\LazyOperation;

final readonly class Slice implements LazyOperation
{
private function __construct(private int $index, private int $length)
{
}

public static function from(int $index, int $length): Slice
{
return new Slice(index: $index, length: $length);
}

public function apply(iterable $elements): Generator
{
$collected = [];
$currentIndex = 0;

foreach ($elements as $key => $value) {
if ($currentIndex++ < $this->index) {
continue;
}

$collected[] = [$key, $value];
}

if ($this->length !== -1) {
$collected = array_slice($collected, 0, $this->length);
}

foreach ($collected as [$key, $value]) {
yield $key => $value;
}
}
}
2 changes: 1 addition & 1 deletion src/Internal/Operations/Transform/Each.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public static function from(Closure ...$actions): Each

public function execute(iterable $elements): void
{
$runActions = static function ($actions) use ($elements): void {
$runActions = static function (iterable $actions) use ($elements): void {
foreach ($elements as $key => $value) {
foreach ($actions as $action) {
$action($value, $key);
Expand Down
124 changes: 124 additions & 0 deletions tests/Operations/Retrieve/CollectionSliceOperationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Collection\Operations\Retrieve;

use PHPUnit\Framework\TestCase;
use TinyBlocks\Collection\Collection;
use TinyBlocks\Collection\Models\CryptoCurrency;

final class CollectionSliceOperationTest extends TestCase
{
public function testSliceReturnsSubsetOfElements(): void
{
/** @Given a collection of CryptoCurrency objects */
$elements = [
new CryptoCurrency(name: 'Bitcoin', price: 60000.0, symbol: 'BTC'),
new CryptoCurrency(name: 'Ethereum', price: 40000.0, symbol: 'ETH'),
new CryptoCurrency(name: 'Binance Coin', price: 1500.0, symbol: 'BNB'),
new CryptoCurrency(name: 'Cardano', price: 2.0, symbol: 'ADA')
];
$collection = Collection::createFrom(elements: $elements);

/** @When slicing the collection */
$actual = $collection->slice(index: 1, length: 2);

/** @Then the result should contain the sliced elements */
self::assertSame([
1 => $elements[1]->toArray(),
2 => $elements[2]->toArray()
], $actual->toArray());
}

public function testSliceReturnsEmptyWhenIndexExceedsCollectionSize(): void
{
/** @Given a collection of CryptoCurrency objects */
$elements = [
new CryptoCurrency(name: 'Bitcoin', price: 60000.0, symbol: 'BTC'),
new CryptoCurrency(name: 'Ethereum', price: 40000.0, symbol: 'ETH'),
new CryptoCurrency(name: 'Binance Coin', price: 1500.0, symbol: 'BNB')
];
$collection = Collection::createFrom(elements: $elements);

/** @When slicing the collection with an index that exceeds the collection size */
$actual = $collection->slice(index: 5, length: 2);

/** @Then the result should be an empty array */
self::assertEmpty($actual->toArray());
}

public function testSliceWithZeroLengthReturnsEmpty(): void
{
/** @Given a collection of CryptoCurrency objects */
$elements = [
new CryptoCurrency(name: 'Bitcoin', price: 60000.0, symbol: 'BTC'),
new CryptoCurrency(name: 'Ethereum', price: 40000.0, symbol: 'ETH'),
new CryptoCurrency(name: 'Binance Coin', price: 1500.0, symbol: 'BNB')
];
$collection = Collection::createFrom(elements: $elements);

/** @When slicing with length 0 */
$actual = $collection->slice(index: 1, length: 0);

/** @Then the result should be an empty array */
self::assertEmpty($actual->toArray());
}

public function testSliceWithLengthOne(): void
{
/** @Given a collection of CryptoCurrency objects */
$elements = [
new CryptoCurrency(name: 'Bitcoin', price: 60000.0, symbol: 'BTC'),
new CryptoCurrency(name: 'Ethereum', price: 40000.0, symbol: 'ETH'),
new CryptoCurrency(name: 'Binance Coin', price: 1500.0, symbol: 'BNB')
];
$collection = Collection::createFrom(elements: $elements);

/** @When slicing with length 1 */
$actual = $collection->slice(index: 1, length: 1);

/** @Then the result should contain only one element */
self::assertSame([1 => $elements[1]->toArray()], $actual->toArray());
}

public function testSliceWithNegativeTwoLength(): void
{
/** @Given a collection of CryptoCurrency objects */
$elements = [
new CryptoCurrency(name: 'Bitcoin', price: 60000.0, symbol: 'BTC'),
new CryptoCurrency(name: 'Ethereum', price: 40000.0, symbol: 'ETH'),
new CryptoCurrency(name: 'Binance Coin', price: 1500.0, symbol: 'BNB'),
new CryptoCurrency(name: 'Cardano', price: 2.0, symbol: 'ADA')
];
$collection = Collection::createFrom(elements: $elements);

/** @When slicing with length -2 */
$actual = $collection->slice(index: 1, length: -2);

/** @Then the result should contain only the first element after the index */
self::assertSame([1 => $elements[1]->toArray()], $actual->toArray());
}

public function testSliceWithoutPassingLength(): void
{
/** @Given a collection of CryptoCurrency objects */
$elements = [
new CryptoCurrency(name: 'Bitcoin', price: 60000.0, symbol: 'BTC'),
new CryptoCurrency(name: 'Ethereum', price: 40000.0, symbol: 'ETH'),
new CryptoCurrency(name: 'Binance Coin', price: 1500.0, symbol: 'BNB'),
new CryptoCurrency(name: 'Cardano', price: 2.0, symbol: 'ADA')
];
$collection = Collection::createFrom(elements: $elements);

/** @When slicing without a passing length (defaults to -1) */
$actual = $collection->slice(index: 1);

/** @Then the result should contain all elements starting from the index */
self::assertSame([
1 => $elements[1]->toArray(),
2 => $elements[2]->toArray(),
3 => $elements[3]->toArray()
], $actual->toArray());
}
}
Loading