Skip to content

Commit 0571023

Browse files
authored
Release/1.6.0 (#15)
1 parent 606adf2 commit 0571023

File tree

9 files changed

+235
-22
lines changed

9 files changed

+235
-22
lines changed

README.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,16 @@ elements, or finding elements that match a specific condition.
188188
$collection->count();
189189
```
190190

191+
#### Retrieve by condition
192+
193+
- `findBy`: Finds the first element that matches one or more predicates.
194+
195+
```php
196+
$collection->findBy(predicates: fn(CryptoCurrency $crypto): bool => $crypto->symbol === 'ETH');
197+
```
198+
199+
<div id='comparing'></div>
200+
191201
#### Retrieve single elements
192202

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

211-
#### Retrieve by condition
221+
#### Retrieve collection elements
212222

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

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

219-
<div id='comparing'></div>
220-
221232
### Comparing
222233

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

273284
- `groupBy`: Groups the elements in the Collection based on the provided grouping criterion.

src/Collectible.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,20 @@ public function reduce(Closure $aggregator, mixed $initial): mixed;
193193
*/
194194
public function sort(Order $order = Order::ASCENDING_KEY, ?Closure $predicate = null): Collectible;
195195

196+
/**
197+
* Returns a subset of the collection starting at the specified index and containing the specified number of
198+
* elements.
199+
*
200+
* If the `length` is negative, it will exclude that many elements from the end of the collection.
201+
* If the `length` is not provided or set to `-1`, it returns all elements starting from the index until the end.
202+
*
203+
* @param int $index The zero-based index at which to start the slice.
204+
* @param int $length The number of elements to include in the slice. If negative, removes that many from the end.
205+
* Default is `-1`, meaning all elements from the index onward will be included.
206+
* @return Collectible<Element> A new collection containing the sliced elements.
207+
*/
208+
public function slice(int $index, int $length = -1): Collectible;
209+
196210
/**
197211
* Converts the collection to an array.
198212
*

src/Collection.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace TinyBlocks\Collection;
66

77
use Closure;
8-
use TinyBlocks\Collection\Internal\Iterators\InternalIterator;
8+
use TinyBlocks\Collection\Internal\Iterators\LazyIterator;
99
use TinyBlocks\Collection\Internal\Operations\Aggregate\Reduce;
1010
use TinyBlocks\Collection\Internal\Operations\Compare\Contains;
1111
use TinyBlocks\Collection\Internal\Operations\Compare\Equals;
@@ -16,6 +16,7 @@
1616
use TinyBlocks\Collection\Internal\Operations\Retrieve\First;
1717
use TinyBlocks\Collection\Internal\Operations\Retrieve\Get;
1818
use TinyBlocks\Collection\Internal\Operations\Retrieve\Last;
19+
use TinyBlocks\Collection\Internal\Operations\Retrieve\Slice;
1920
use TinyBlocks\Collection\Internal\Operations\Transform\Each;
2021
use TinyBlocks\Collection\Internal\Operations\Transform\GroupBy;
2122
use TinyBlocks\Collection\Internal\Operations\Transform\Map;
@@ -35,16 +36,16 @@
3536
*/
3637
class Collection implements Collectible
3738
{
38-
private InternalIterator $iterator;
39+
private LazyIterator $iterator;
3940

40-
private function __construct(InternalIterator $iterator)
41+
private function __construct(LazyIterator $iterator)
4142
{
4243
$this->iterator = $iterator;
4344
}
4445

4546
public static function createFrom(iterable $elements): static
4647
{
47-
return new static(iterator: InternalIterator::from(elements: $elements, operation: Create::fromEmpty()));
48+
return new static(iterator: LazyIterator::from(elements: $elements, operation: Create::fromEmpty()));
4849
}
4950

5051
public static function createFromEmpty(): static
@@ -150,6 +151,15 @@ public function sort(Order $order = Order::ASCENDING_KEY, ?Closure $predicate =
150151
);
151152
}
152153

154+
public function slice(int $index, int $length = -1): static
155+
{
156+
return new static(
157+
iterator: $this->iterator->add(
158+
operation: Slice::from(index: $index, length: $length)
159+
)
160+
);
161+
}
162+
153163
public function toArray(PreserveKeys $preserveKeys = PreserveKeys::PRESERVE): array
154164
{
155165
return MapToArray::from(elements: $this->iterator->getIterator(), preserveKeys: $preserveKeys)->toArray();

src/Internal/Iterators/InternalIterator.php renamed to src/Internal/Iterators/LazyIterator.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
* @template Value of mixed
1717
* @implements IteratorAggregate<Key, Value>
1818
*/
19-
final class InternalIterator implements IteratorAggregate
19+
final class LazyIterator implements IteratorAggregate
2020
{
2121
/**
2222
* @var LazyOperation[]
@@ -35,18 +35,18 @@ private function __construct(private readonly iterable $elements, LazyOperation
3535
/**
3636
* @param iterable $elements
3737
* @param LazyOperation $operation
38-
* @return InternalIterator
38+
* @return LazyIterator
3939
*/
40-
public static function from(iterable $elements, LazyOperation $operation): InternalIterator
40+
public static function from(iterable $elements, LazyOperation $operation): LazyIterator
4141
{
42-
return new InternalIterator(elements: $elements, operation: $operation);
42+
return new LazyIterator(elements: $elements, operation: $operation);
4343
}
4444

4545
/**
4646
* @param LazyOperation $operation
47-
* @return InternalIterator
47+
* @return LazyIterator
4848
*/
49-
public function add(LazyOperation $operation): InternalIterator
49+
public function add(LazyOperation $operation): LazyIterator
5050
{
5151
$this->operations[] = $operation;
5252
return $this;

src/Internal/Iterators/IterableIteratorAggregate.php renamed to src/Internal/Iterators/LazyIteratorAggregate.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,22 @@
1515
* @template Value
1616
* @implements IteratorAggregate<Key, Value>
1717
*/
18-
final readonly class IterableIteratorAggregate implements IteratorAggregate
18+
final readonly class LazyIteratorAggregate implements IteratorAggregate
1919
{
20-
public function __construct(private iterable $elements)
20+
/**
21+
* @param iterable<Key, Value> $elements
22+
*/
23+
private function __construct(private iterable $elements)
24+
{
25+
}
26+
27+
/**
28+
* @param iterable $elements
29+
* @return LazyIteratorAggregate
30+
*/
31+
public static function from(iterable $elements): LazyIteratorAggregate
2132
{
33+
return new LazyIteratorAggregate(elements: $elements);
2234
}
2335

2436
/**

src/Internal/Operations/Compare/Equals.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace TinyBlocks\Collection\Internal\Operations\Compare;
66

77
use TinyBlocks\Collection\Collectible;
8-
use TinyBlocks\Collection\Internal\Iterators\IterableIteratorAggregate;
8+
use TinyBlocks\Collection\Internal\Iterators\LazyIteratorAggregate;
99
use TinyBlocks\Collection\Internal\Operations\ImmediateOperation;
1010

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

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

3232
while ($currentIterator->valid() || $targetIterator->valid()) {
3333
if (!$currentIterator->valid() || !$targetIterator->valid()) {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TinyBlocks\Collection\Internal\Operations\Retrieve;
6+
7+
use Generator;
8+
use TinyBlocks\Collection\Internal\Operations\LazyOperation;
9+
10+
final readonly class Slice implements LazyOperation
11+
{
12+
private function __construct(private int $index, private int $length)
13+
{
14+
}
15+
16+
public static function from(int $index, int $length): Slice
17+
{
18+
return new Slice(index: $index, length: $length);
19+
}
20+
21+
public function apply(iterable $elements): Generator
22+
{
23+
$collected = [];
24+
$currentIndex = 0;
25+
26+
foreach ($elements as $key => $value) {
27+
if ($currentIndex++ < $this->index) {
28+
continue;
29+
}
30+
31+
$collected[] = [$key, $value];
32+
}
33+
34+
if ($this->length !== -1) {
35+
$collected = array_slice($collected, 0, $this->length);
36+
}
37+
38+
foreach ($collected as [$key, $value]) {
39+
yield $key => $value;
40+
}
41+
}
42+
}

src/Internal/Operations/Transform/Each.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public static function from(Closure ...$actions): Each
2323

2424
public function execute(iterable $elements): void
2525
{
26-
$runActions = static function ($actions) use ($elements): void {
26+
$runActions = static function (iterable $actions) use ($elements): void {
2727
foreach ($elements as $key => $value) {
2828
foreach ($actions as $action) {
2929
$action($value, $key);
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TinyBlocks\Collection\Operations\Retrieve;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use TinyBlocks\Collection\Collection;
9+
use TinyBlocks\Collection\Models\CryptoCurrency;
10+
11+
final class CollectionSliceOperationTest extends TestCase
12+
{
13+
public function testSliceReturnsSubsetOfElements(): void
14+
{
15+
/** @Given a collection of CryptoCurrency objects */
16+
$elements = [
17+
new CryptoCurrency(name: 'Bitcoin', price: 60000.0, symbol: 'BTC'),
18+
new CryptoCurrency(name: 'Ethereum', price: 40000.0, symbol: 'ETH'),
19+
new CryptoCurrency(name: 'Binance Coin', price: 1500.0, symbol: 'BNB'),
20+
new CryptoCurrency(name: 'Cardano', price: 2.0, symbol: 'ADA')
21+
];
22+
$collection = Collection::createFrom(elements: $elements);
23+
24+
/** @When slicing the collection */
25+
$actual = $collection->slice(index: 1, length: 2);
26+
27+
/** @Then the result should contain the sliced elements */
28+
self::assertSame([
29+
1 => $elements[1]->toArray(),
30+
2 => $elements[2]->toArray()
31+
], $actual->toArray());
32+
}
33+
34+
public function testSliceReturnsEmptyWhenIndexExceedsCollectionSize(): void
35+
{
36+
/** @Given a collection of CryptoCurrency objects */
37+
$elements = [
38+
new CryptoCurrency(name: 'Bitcoin', price: 60000.0, symbol: 'BTC'),
39+
new CryptoCurrency(name: 'Ethereum', price: 40000.0, symbol: 'ETH'),
40+
new CryptoCurrency(name: 'Binance Coin', price: 1500.0, symbol: 'BNB')
41+
];
42+
$collection = Collection::createFrom(elements: $elements);
43+
44+
/** @When slicing the collection with an index that exceeds the collection size */
45+
$actual = $collection->slice(index: 5, length: 2);
46+
47+
/** @Then the result should be an empty array */
48+
self::assertEmpty($actual->toArray());
49+
}
50+
51+
public function testSliceWithZeroLengthReturnsEmpty(): void
52+
{
53+
/** @Given a collection of CryptoCurrency objects */
54+
$elements = [
55+
new CryptoCurrency(name: 'Bitcoin', price: 60000.0, symbol: 'BTC'),
56+
new CryptoCurrency(name: 'Ethereum', price: 40000.0, symbol: 'ETH'),
57+
new CryptoCurrency(name: 'Binance Coin', price: 1500.0, symbol: 'BNB')
58+
];
59+
$collection = Collection::createFrom(elements: $elements);
60+
61+
/** @When slicing with length 0 */
62+
$actual = $collection->slice(index: 1, length: 0);
63+
64+
/** @Then the result should be an empty array */
65+
self::assertEmpty($actual->toArray());
66+
}
67+
68+
public function testSliceWithLengthOne(): void
69+
{
70+
/** @Given a collection of CryptoCurrency objects */
71+
$elements = [
72+
new CryptoCurrency(name: 'Bitcoin', price: 60000.0, symbol: 'BTC'),
73+
new CryptoCurrency(name: 'Ethereum', price: 40000.0, symbol: 'ETH'),
74+
new CryptoCurrency(name: 'Binance Coin', price: 1500.0, symbol: 'BNB')
75+
];
76+
$collection = Collection::createFrom(elements: $elements);
77+
78+
/** @When slicing with length 1 */
79+
$actual = $collection->slice(index: 1, length: 1);
80+
81+
/** @Then the result should contain only one element */
82+
self::assertSame([1 => $elements[1]->toArray()], $actual->toArray());
83+
}
84+
85+
public function testSliceWithNegativeTwoLength(): void
86+
{
87+
/** @Given a collection of CryptoCurrency objects */
88+
$elements = [
89+
new CryptoCurrency(name: 'Bitcoin', price: 60000.0, symbol: 'BTC'),
90+
new CryptoCurrency(name: 'Ethereum', price: 40000.0, symbol: 'ETH'),
91+
new CryptoCurrency(name: 'Binance Coin', price: 1500.0, symbol: 'BNB'),
92+
new CryptoCurrency(name: 'Cardano', price: 2.0, symbol: 'ADA')
93+
];
94+
$collection = Collection::createFrom(elements: $elements);
95+
96+
/** @When slicing with length -2 */
97+
$actual = $collection->slice(index: 1, length: -2);
98+
99+
/** @Then the result should contain only the first element after the index */
100+
self::assertSame([1 => $elements[1]->toArray()], $actual->toArray());
101+
}
102+
103+
public function testSliceWithoutPassingLength(): void
104+
{
105+
/** @Given a collection of CryptoCurrency objects */
106+
$elements = [
107+
new CryptoCurrency(name: 'Bitcoin', price: 60000.0, symbol: 'BTC'),
108+
new CryptoCurrency(name: 'Ethereum', price: 40000.0, symbol: 'ETH'),
109+
new CryptoCurrency(name: 'Binance Coin', price: 1500.0, symbol: 'BNB'),
110+
new CryptoCurrency(name: 'Cardano', price: 2.0, symbol: 'ADA')
111+
];
112+
$collection = Collection::createFrom(elements: $elements);
113+
114+
/** @When slicing without a passing length (defaults to -1) */
115+
$actual = $collection->slice(index: 1);
116+
117+
/** @Then the result should contain all elements starting from the index */
118+
self::assertSame([
119+
1 => $elements[1]->toArray(),
120+
2 => $elements[2]->toArray(),
121+
3 => $elements[3]->toArray()
122+
], $actual->toArray());
123+
}
124+
}

0 commit comments

Comments
 (0)