Skip to content

Commit ce6ed88

Browse files
authored
Release/1.14.0 (#36)
1 parent ccf5e8b commit ce6ed88

23 files changed

+161
-36
lines changed

src/Collectible.php

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
/**
1414
* Represents a collection that can be manipulated, iterated, and counted.
1515
*
16+
* Complexity notes (Big O):
17+
* - Unless stated otherwise, complexities refer to consuming the collection **once**.
18+
* - `n`: number of elements produced when consuming the collection once.
19+
* - Callback cost is not included (assumed O(1) per callback invocation).
20+
*
1621
* @template Key of int|string
1722
* @template Value of mixed
1823
* @template Element of mixed
@@ -23,6 +28,9 @@ interface Collectible extends Countable, IteratorAggregate
2328
/**
2429
* Creates a new Collectible instance from the given elements.
2530
*
31+
* Complexity: O(1) time and O(1) space to create the collection.
32+
* Consuming the collection is O(n) time and O(1) additional space.
33+
*
2634
* @param iterable<Element> $elements The elements to initialize the Collection with.
2735
* @return Collectible<Element> A new Collectible instance.
2836
*/
@@ -31,13 +39,18 @@ public static function createFrom(iterable $elements): Collectible;
3139
/**
3240
* Creates an empty Collectible instance.
3341
*
42+
* Complexity: O(1) time and O(1) space.
43+
*
3444
* @return Collectible<Element> An empty Collectible instance.
3545
*/
3646
public static function createFromEmpty(): Collectible;
3747

3848
/**
3949
* Adds one or more elements to the Collection.
4050
*
51+
* Complexity (when consumed): O(n + k) time and O(1) additional space,
52+
* where `k` is the number of elements passed to this method.
53+
*
4154
* @param Element ...$elements The elements to be added to the Collection.
4255
* @return Collectible<Element> The updated Collection.
4356
*/
@@ -46,6 +59,8 @@ public function add(mixed ...$elements): Collectible;
4659
/**
4760
* Checks if the Collection contains a specific element.
4861
*
62+
* Complexity: best-case O(1), worst-case O(n) time (early termination), O(1) space.
63+
*
4964
* @param Element $element The element to check for.
5065
* @return bool True if the element is found, false otherwise.
5166
*/
@@ -54,13 +69,18 @@ public function contains(mixed $element): bool;
5469
/**
5570
* Returns the total number of elements in the Collection.
5671
*
72+
* Complexity: O(n) time and O(1) additional space.
73+
*
5774
* @return int The number of elements in the Collection.
5875
*/
5976
public function count(): int;
6077

6178
/**
6279
* Executes actions on each element in the Collection without modifying it.
6380
*
81+
* Complexity: O(n · a) time and O(1) additional space,
82+
* where `a` is the number of actions passed to this method.
83+
*
6484
* @param Closure(Element): void ...$actions The actions to perform on each element.
6585
* @return Collectible<Element> The original Collection for chaining.
6686
*/
@@ -69,6 +89,9 @@ public function each(Closure ...$actions): Collectible;
6989
/**
7090
* Compares the Collection with another Collection for equality.
7191
*
92+
* Complexity: best-case O(1), worst-case O(min(n, m)) time (early termination), O(1) space,
93+
* where `m` is the size of the other collection.
94+
*
7295
* @param Collectible<Element> $other The Collection to compare with.
7396
* @return bool True if the collections are equal, false otherwise.
7497
*/
@@ -78,37 +101,50 @@ public function equals(Collectible $other): bool;
78101
* Filters elements in the Collection based on the provided predicates.
79102
* If no predicates are provided, all empty or falsy values (e.g., null, false, empty arrays) will be removed.
80103
*
104+
* Complexity (when consumed): O(n · p) time and O(1) additional space,
105+
* where `p` is the number of predicates.
106+
*
81107
* @param Closure(Element): bool|null ...$predicates
82108
* @return Collectible<Element> The updated Collection.
83109
*/
84110
public function filter(?Closure ...$predicates): Collectible;
85111

86112
/**
87-
* Finds the first element matching the provided predicates.
113+
* Finds the first element that matches any of the provided predicates.
88114
*
89-
* @param Closure(Element): bool ...$predicates The predicates to match.
115+
* Complexity: best-case O(1), worst-case O(n · q) time (early termination), O(1) space,
116+
* where `q` is the number of predicates.
117+
*
118+
* @param Closure(Element): bool ...$predicates The predicates to match (evaluated as a logical OR).
90119
* @return Element|null The first matching element, or null if none is found.
91120
*/
92121
public function findBy(Closure ...$predicates): mixed;
93122

94123
/**
95124
* Retrieves the first element in the Collection or a default value if not found.
96125
*
126+
* Complexity: best-case O(1), worst-case O(n) time (early termination), O(1) space.
127+
*
97128
* @param Element|null $defaultValueIfNotFound The default value returns if no element is found.
98129
* @return Element|null The first element or the default value.
99130
*/
100131
public function first(mixed $defaultValueIfNotFound = null): mixed;
101132

102133
/**
103-
* Flattens a Collection by removing any nested collections and returning a single Collection with all elements.
134+
* Flattens the collection by expanding iterable elements by one level (shallow flatten).
104135
*
105-
* @return Collectible<Element> A new Collectible instance with all elements flattened into a single Collection.
136+
* Complexity (when consumed): O(n + s) time and O(1) additional space, where `s` is the total number of elements
137+
* inside nested iterables that are expanded.
138+
*
139+
* @return Collectible<Element> A new Collectible instance with elements flattened by one level.
106140
*/
107141
public function flatten(): Collectible;
108142

109143
/**
110144
* Retrieves an element by its index or a default value if not found.
111145
*
146+
* Complexity: O(n) time and O(1) additional space.
147+
*
112148
* @param int $index The index of the element to retrieve.
113149
* @param Element|null $defaultValueIfNotFound The default value returns if no element is found.
114150
* @return Element|null The element at the specified index or the default value.
@@ -118,29 +154,38 @@ public function getBy(int $index, mixed $defaultValueIfNotFound = null): mixed;
118154
/**
119155
* Returns an iterator for traversing the Collection.
120156
*
157+
* Complexity: O(1) time and O(1) space to obtain the iterator.
158+
*
121159
* @return Traversable<Key, Value> An iterator for the Collection.
122160
*/
123161
public function getIterator(): Traversable;
124162

125163
/**
126164
* Groups the elements in the Collection based on the provided criteria.
127165
*
166+
* Complexity (when consumed): O(n) time and O(n) additional space (materializes all groups).
167+
*
128168
* @param Closure(Element): Key $grouping The function to define the group key for each element.
129-
* @return Collectible<Key, Collectible<Key, Element, Element>, Element> A Collection of collections,
130-
* grouped by the key returned by the closure.
169+
* @return Collectible<Key, list<Element>, Element> A Collection where each value is a list of elements,
170+
* grouped by the key returned by the closure.
131171
*/
132172
public function groupBy(Closure $grouping): Collectible;
133173

134174
/**
135175
* Determines if the Collection is empty.
136176
*
177+
* Complexity: best-case O(1), worst-case O(n) time (may need to advance until the first element is produced),
178+
* O(1) space.
179+
*
137180
* @return bool True if the Collection is empty, false otherwise.
138181
*/
139182
public function isEmpty(): bool;
140183

141184
/**
142185
* Joins the elements of the Collection into a string, separated by a given separator.
143186
*
187+
* Complexity: O(n + L) time and O(L) space, where `L` is the length of the resulting string.
188+
*
144189
* @param string $separator The string used to separate the elements.
145190
* @return string The concatenated string of all elements in the Collection.
146191
*/
@@ -149,6 +194,8 @@ public function joinToString(string $separator): string;
149194
/**
150195
* Retrieves the last element in the Collection or a default value if not found.
151196
*
197+
* Complexity: O(n) time and O(1) space.
198+
*
152199
* @param Element|null $defaultValueIfNotFound The default value returns if no element is found.
153200
* @return Element|null The last element or the default value.
154201
*/
@@ -158,6 +205,9 @@ public function last(mixed $defaultValueIfNotFound = null): mixed;
158205
* Applies transformations to each element in the Collection and returns a new Collection with the transformed
159206
* elements.
160207
*
208+
* Complexity (when consumed): O(n · t) time and O(1) additional space,
209+
* where `t` is the number of transformations.
210+
*
161211
* @param Closure(Element): Element ...$transformations The transformations to apply.
162212
* @return Collectible<Element> A new Collection with the applied transformations.
163213
*/
@@ -166,6 +216,8 @@ public function map(Closure ...$transformations): Collectible;
166216
/**
167217
* Removes a specific element from the Collection.
168218
*
219+
* Complexity (when consumed): O(n) time and O(1) additional space.
220+
*
169221
* @param Element $element The element to remove.
170222
* @return Collectible<Element> The updated Collection.
171223
*/
@@ -175,6 +227,8 @@ public function remove(mixed $element): Collectible;
175227
* Removes elements from the Collection based on the provided filter.
176228
* If no filter is passed, all elements in the Collection will be removed.
177229
*
230+
* Complexity (when consumed): O(n) time and O(1) additional space.
231+
*
178232
* @param Closure(Element): bool|null $filter The filter to determine which elements to remove.
179233
* @return Collectible<Element> The updated Collection.
180234
*/
@@ -183,6 +237,8 @@ public function removeAll(?Closure $filter = null): Collectible;
183237
/**
184238
* Reduces the elements in the Collection to a single value by applying an aggregator function.
185239
*
240+
* Complexity: O(n) time and O(1) additional space.
241+
*
186242
* @param Closure(mixed, Element): mixed $aggregator The function that aggregates the elements.
187243
* It receives the current accumulated value and the current element.
188244
* @param mixed $initial The initial value to start the aggregation.
@@ -201,6 +257,8 @@ public function reduce(Closure $aggregator, mixed $initial): mixed;
201257
*
202258
* By default, `Order::ASCENDING_KEY` is used.
203259
*
260+
* Complexity (when consumed): O(n log n) time and O(n) additional space (materializes elements to sort).
261+
*
204262
* @param Order $order The order in which to sort the Collection.
205263
* @param Closure(Element, Element): int|null $predicate The predicate to use for sorting.
206264
* @return Collectible<Element> The updated Collection.
@@ -217,18 +275,27 @@ public function sort(Order $order = Order::ASCENDING_KEY, ?Closure $predicate =
217275
* @param int $index The zero-based index at which to start the slice.
218276
* @param int $length The number of elements to include in the slice. If negative, remove that many from the end.
219277
* Default is `-1`, meaning all elements from the index onward will be included.
278+
*
279+
* Complexity (when consumed):
280+
* - If `length === 0`: O(1) time and O(1) additional space.
281+
* - If `length === -1`: O(n) time and O(1) additional space.
282+
* - If `length >= 0`: O(min(n, index + length)) time and O(1) additional space (may stop early).
283+
* - If `length < -1`: O(n) time and O(|length|) additional space (uses a buffer).
284+
*
220285
* @return Collectible<Element> A new Collection containing the sliced elements.
221286
*/
222287
public function slice(int $index, int $length = -1): Collectible;
223288

224289
/**
225290
* Converts the Collection to an array.
226291
*
227-
* The key preservation behavior should be provided from the `PreserveKeys` enum:
292+
* The key preservation behavior should be provided from the `KeyPreservation` enum:
228293
* - {@see KeyPreservation::PRESERVE}: Preserves the array keys.
229294
* - {@see KeyPreservation::DISCARD}: Discards the array keys.
230295
*
231-
* By default, `PreserveKeys::PRESERVE` is used.
296+
* By default, `KeyPreservation::PRESERVE` is used.
297+
*
298+
* Complexity: O(n) time and O(n) space.
232299
*
233300
* @param KeyPreservation $keyPreservation The option to preserve or discard array keys.
234301
* @return array<Key, Value> The resulting array.
@@ -238,11 +305,13 @@ public function toArray(KeyPreservation $keyPreservation = KeyPreservation::PRES
238305
/**
239306
* Converts the Collection to a JSON string.
240307
*
241-
* The key preservation behavior should be provided from the `PreserveKeys` enum:
308+
* The key preservation behavior should be provided from the `KeyPreservation` enum:
242309
* - {@see KeyPreservation::PRESERVE}: Preserves the array keys.
243310
* - {@see KeyPreservation::DISCARD}: Discards the array keys.
244311
*
245-
* By default, `PreserveKeys::PRESERVE` is used.
312+
* By default, `KeyPreservation::PRESERVE` is used.
313+
*
314+
* Complexity: O(n + L) time and O(n + L) space, where `L` is the length of the resulting JSON.
246315
*
247316
* @param KeyPreservation $keyPreservation The option to preserve or discard array keys.
248317
* @return string The resulting JSON string.

src/Internal/Operations/Retrieve/Slice.php

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

77
use Generator;
8+
use SplQueue;
89
use TinyBlocks\Collection\Internal\Operations\LazyOperation;
910

1011
final readonly class Slice implements LazyOperation
@@ -48,21 +49,27 @@ public function apply(iterable $elements): Generator
4849

4950
private function applyWithBufferedSlice(iterable $elements): Generator
5051
{
51-
$collected = [];
52+
$buffer = new SplQueue();
53+
$skipFromEnd = abs($this->length);
5254
$currentIndex = 0;
5355

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

59-
$collected[] = [$key, $value];
60-
}
61+
$buffer->enqueue([$key, $value]);
62+
63+
if ($buffer->count() <= $skipFromEnd) {
64+
continue;
65+
}
6166

62-
$collected = array_slice($collected, 0, $this->length);
67+
$dequeued = $buffer->dequeue();
6368

64-
foreach ($collected as [$key, $value]) {
65-
yield $key => $value;
69+
if (is_array($dequeued)) {
70+
[$yieldKey, $yieldValue] = $dequeued;
71+
yield $yieldKey => $yieldValue;
72+
}
6673
}
6774
}
6875
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Test\TinyBlocks\Collection\Internal\Iterators;
6+
7+
use Generator;
8+
use PHPUnit\Framework\TestCase;
9+
use TinyBlocks\Collection\Internal\Iterators\LazyIterator;
10+
use TinyBlocks\Collection\Internal\Operations\LazyOperation;
11+
12+
final class LazyIteratorTest extends TestCase
13+
{
14+
public function testFromAppliesInitialOperation(): void
15+
{
16+
/** @Given elements and an operation that changes values */
17+
$elements = [1, 2, 3];
18+
19+
$operation = new class implements LazyOperation {
20+
public function apply(iterable $elements): Generator
21+
{
22+
foreach ($elements as $key => $value) {
23+
yield $key => $value * 2;
24+
}
25+
}
26+
};
27+
28+
/** @When creating a LazyIterator from the elements and operation */
29+
$iterator = LazyIterator::from(elements: $elements, operation: $operation);
30+
31+
/** @Then the yielded elements should include the operation effect */
32+
self::assertSame([2, 4, 6], iterator_to_array($iterator));
33+
}
34+
}

tests/Operations/Aggregate/CollectionReduceOperationTest.php renamed to tests/Internal/Operations/Aggregate/CollectionReduceOperationTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace Test\TinyBlocks\Collection\Operations\Aggregate;
5+
namespace Test\TinyBlocks\Collection\Internal\Operations\Aggregate;
66

77
use PHPUnit\Framework\TestCase;
88
use Test\TinyBlocks\Collection\Models\InvoiceSummaries;

tests/Operations/Compare/CollectionContainsOperationTest.php renamed to tests/Internal/Operations/Compare/CollectionContainsOperationTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace Test\TinyBlocks\Collection\Operations\Compare;
5+
namespace Test\TinyBlocks\Collection\Internal\Operations\Compare;
66

77
use PHPUnit\Framework\Attributes\DataProvider;
88
use PHPUnit\Framework\TestCase;

0 commit comments

Comments
 (0)