From adb234d1741121c7a72c644f92900207d8c34654 Mon Sep 17 00:00:00 2001 From: Gustavo Freze Date: Tue, 3 Mar 2026 01:53:53 -0300 Subject: [PATCH] feat: Add merge method to Collectible for lazy concatenation of collections. --- README.md | 9 ++ src/Collectible.php | 15 +++ src/Collection.php | 6 + src/Internal/Operations/Write/Merge.php | 31 +++++ .../Write/CollectionMergeOperationTest.php | 116 ++++++++++++++++++ 5 files changed, 177 insertions(+) create mode 100644 src/Internal/Operations/Write/Merge.php create mode 100644 tests/Internal/Operations/Write/CollectionMergeOperationTest.php diff --git a/README.md b/README.md index f8f817b..8804001 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,15 @@ These methods enable adding, removing, and modifying elements in the Collection. $collection->add('X', 'Y', 'Z'); ``` +#### Merging collections + +- `merge`: Merges the elements of another Collectible into the current Collection lazily, without materializing either + collection. + + ```php + $collectionA->merge(other: $collectionB); + ``` + #### Removing elements - `remove`: Removes a specific element from the Collection. diff --git a/src/Collectible.php b/src/Collectible.php index 7833d92..7258192 100644 --- a/src/Collectible.php +++ b/src/Collectible.php @@ -213,6 +213,21 @@ public function last(mixed $defaultValueIfNotFound = null): mixed; */ public function map(Closure ...$transformations): Collectible; + /** + * Merges the elements of another Collectible into the current Collection. + * + * Unlike {@see add()}, which accepts individual elements via variadic parameters, + * this method accepts an entire Collectible and concatenates its elements lazily + * without materializing either collection. + * + * Complexity (when consumed): O(n + m) time and O(1) additional space, + * where `m` is the number of elements in the other Collectible. + * + * @param Collectible $other The Collectible whose elements will be appended. + * @return Collectible A new Collection containing elements from both collections. + */ + public function merge(Collectible $other): Collectible; + /** * Removes a specific element from the Collection. * diff --git a/src/Collection.php b/src/Collection.php index 687edd7..6feb921 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -23,6 +23,7 @@ use TinyBlocks\Collection\Internal\Operations\Transform\Map; use TinyBlocks\Collection\Internal\Operations\Write\Add; use TinyBlocks\Collection\Internal\Operations\Write\Create; +use TinyBlocks\Collection\Internal\Operations\Write\Merge; use TinyBlocks\Collection\Internal\Operations\Write\Remove; use TinyBlocks\Collection\Internal\Operations\Write\RemoveAll; use TinyBlocks\Mapper\IterableMappability; @@ -139,6 +140,11 @@ public function map(Closure ...$transformations): static return new static(iterator: $this->iterator->add(operation: Map::from(...$transformations))); } + public function merge(Collectible $other): static + { + return new static(iterator: $this->iterator->add(operation: Merge::from(otherElements: $other))); + } + public function remove(mixed $element): static { return new static(iterator: $this->iterator->add(operation: Remove::from(element: $element))); diff --git a/src/Internal/Operations/Write/Merge.php b/src/Internal/Operations/Write/Merge.php new file mode 100644 index 0000000..08d9572 --- /dev/null +++ b/src/Internal/Operations/Write/Merge.php @@ -0,0 +1,31 @@ +otherElements as $element) { + yield $element; + } + } +} diff --git a/tests/Internal/Operations/Write/CollectionMergeOperationTest.php b/tests/Internal/Operations/Write/CollectionMergeOperationTest.php new file mode 100644 index 0000000..d0eebd5 --- /dev/null +++ b/tests/Internal/Operations/Write/CollectionMergeOperationTest.php @@ -0,0 +1,116 @@ +merge(other: $collectionB); + + /** @Then the result should be an empty collection */ + self::assertEmpty($actual->toArray()); + } + + public function testMergeIntoEmptyCollection(): void + { + /** @Given an empty collection and a non-empty collection */ + $collectionA = Collection::createFromEmpty(); + $collectionB = Collection::createFrom(elements: [4, 5, 6]); + + /** @When merging the non-empty collection into the empty one */ + $actual = $collectionA->merge(other: $collectionB); + + /** @Then the result should contain only the elements from the non-empty collection */ + self::assertSame([4, 5, 6], $actual->toArray()); + } + + public function testMergeWithEmptyCollection(): void + { + /** @Given a non-empty collection and an empty collection */ + $collectionA = Collection::createFrom(elements: [1, 2, 3]); + $collectionB = Collection::createFromEmpty(); + + /** @When merging the empty collection into the non-empty one */ + $actual = $collectionA->merge(other: $collectionB); + + /** @Then the result should contain only the original elements */ + self::assertSame([1, 2, 3], $actual->toArray()); + } + + public function testMergeTwoCollections(): void + { + /** @Given two collections with distinct elements */ + $collectionA = Collection::createFrom(elements: [1, 2, 3]); + $collectionB = Collection::createFrom(elements: [4, 5, 6]); + + /** @When merging collection B into collection A */ + $actual = $collectionA->merge(other: $collectionB); + + /** @Then the result should contain all elements in order */ + self::assertSame([1, 2, 3, 4, 5, 6], $actual->toArray()); + } + + public function testMergePreservesLazyEvaluation(): void + { + /** @Given two collections created from generators */ + $collectionA = Collection::createFrom( + elements: (static function () { + yield 1; + yield 2; + })() + ); + + $collectionB = Collection::createFrom( + elements: (static function () { + yield 3; + yield 4; + })() + ); + + /** @When merging and retrieving only the first element */ + $actual = $collectionA->merge(other: $collectionB)->first(); + + /** @Then the first element should be from collection A without materializing all elements */ + self::assertSame(1, $actual); + } + + public function testMergeMultipleCollections(): void + { + /** @Given three collections */ + $collectionA = Collection::createFrom(elements: [1, 2]); + $collectionB = Collection::createFrom(elements: [3, 4]); + $collectionC = Collection::createFrom(elements: [5, 6]); + + /** @When chaining multiple merge operations */ + $actual = $collectionA + ->merge(other: $collectionB) + ->merge(other: $collectionC); + + /** @Then the result should contain all elements in order */ + self::assertSame([1, 2, 3, 4, 5, 6], $actual->toArray()); + } + + public function testMergeWithDuplicateElements(): void + { + /** @Given two collections with overlapping elements */ + $collectionA = Collection::createFrom(elements: [1, 2, 3]); + $collectionB = Collection::createFrom(elements: [3, 4, 5]); + + /** @When merging the collections */ + $actual = $collectionA->merge(other: $collectionB); + + /** @Then the result should contain all elements including duplicates */ + self::assertSame([1, 2, 3, 3, 4, 5], $actual->toArray()); + } +}