Skip to content
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions src/Collectible.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<Element> $other The Collectible whose elements will be appended.
* @return Collectible<Element> A new Collection containing elements from both collections.
*/
public function merge(Collectible $other): Collectible;

/**
* Removes a specific element from the Collection.
*
Expand Down
6 changes: 6 additions & 0 deletions src/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)));
Expand Down
31 changes: 31 additions & 0 deletions src/Internal/Operations/Write/Merge.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\Collection\Internal\Operations\Write;

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

final readonly class Merge implements LazyOperation
{
private function __construct(private iterable $otherElements)
{
}

public static function from(iterable $otherElements): Merge
{
return new Merge(otherElements: $otherElements);
}

public function apply(iterable $elements): Generator
{
foreach ($elements as $element) {
yield $element;
}

foreach ($this->otherElements as $element) {
yield $element;
}
}
}
116 changes: 116 additions & 0 deletions tests/Internal/Operations/Write/CollectionMergeOperationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

declare(strict_types=1);

namespace Test\TinyBlocks\Collection\Internal\Operations\Write;

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

final class CollectionMergeOperationTest extends TestCase
{
public function testMergeEmptyCollections(): void
{
/** @Given two empty collections */
$collectionA = Collection::createFromEmpty();
$collectionB = Collection::createFromEmpty();

/** @When merging the two collections */
$actual = $collectionA->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());
}
}