Skip to content

Feat: add iterable_merge function #45

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

Merged
merged 3 commits into from
Oct 15, 2022
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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This package provides functions to work with [iterables](https://wiki.php.net/rf
- [iterable_to_array()](#iterable_to_array)
- [iterable_to_traversable()](#iterable_to_traversable)
- [iterable_map()](#iterable_map)
- [iterable_merge()](#iterable_merge)
- [iterable_reduce()](#iterable_reduce)
- [iterable_filter()](#iterable_filter)
- [iterable_values()](#iterable_values)
Expand Down Expand Up @@ -69,6 +70,27 @@ foreach (iterable_map($generator(), 'strtoupper') as $item) {
}
```

iterable_merge()
--------------

Works like an `array_merge` with an `array` or a `Traversable`.

```php
use function BenTools\IterableFunctions\iterable_merge;

$generator1 = function () {
yield 'foo';
};

$generator2 = function () {
yield 'bar';
};

foreach (iterable_merge($generator1(), $generator2()) as $item) {
var_dump($item); // foo, bar
}
```

iterable_reduce()
--------------

Expand Down
7 changes: 6 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
"vimeo/psalm": "^4.4"
},
"config": {
"sort-packages": true
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": false,
"dealerdirect/phpcodesniffer-composer-installer": true,
"phpstan/extension-installer": true
}
}
}
40 changes: 38 additions & 2 deletions src/IterableObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

namespace BenTools\IterableFunctions;

use AppendIterator;
use ArrayIterator;
use CallbackFilterIterator;
use Iterator;
use IteratorAggregate;
use IteratorIterator;
use Traversable;
Expand All @@ -26,10 +29,14 @@ final class IterableObject implements IteratorAggregate
/** @var iterable<TKey, TValue> */
private $iterable;

/** @var bool */
private $preserveKeys;

/** @param iterable<TKey, TValue> $iterable */
public function __construct(iterable $iterable)
public function __construct(iterable $iterable, bool $preserveKeys = true)
{
$this->iterable = $iterable;
$this->preserveKeys = $preserveKeys;
}

/**
Expand Down Expand Up @@ -70,6 +77,35 @@ public function map(callable $mapper): self
return new self(array_map($mapper, $this->iterable));
}

/**
* @param iterable<TKey, TValue> ...$args
*
* @return self<TKey, TValue>
*/
public function merge(iterable ...$args): self
{
if ($args === []) {
return $this;
}

$toIterator = static function (iterable $iterable): Iterator {
if ($iterable instanceof Traversable) {
return new IteratorIterator($iterable);
}

return new ArrayIterator($iterable);
};

$iterator = new AppendIterator();
$iterator->append($toIterator($this->iterable));

foreach ($args as $iterable) {
$iterator->append($toIterator($iterable));
}

return new self($iterator, false);
}

/**
* @return self<int, TValue>
*/
Expand All @@ -87,6 +123,6 @@ public function getIterator(): Traversable
/** @return array<array-key, TValue> */
public function asArray(): array
{
return $this->iterable instanceof Traversable ? iterator_to_array($this->iterable) : $this->iterable;
return $this->iterable instanceof Traversable ? iterator_to_array($this->iterable, $this->preserveKeys) : $this->iterable;
}
}
16 changes: 16 additions & 0 deletions src/iterable-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use EmptyIterator;
use Traversable;

use function array_slice;
use function array_values;
use function iterator_to_array;

Expand All @@ -28,6 +29,21 @@ function iterable_map(iterable $iterable, callable $mapper): iterable
return iterable($iterable)->map($mapper);
}

/**
* Merge iterables
*
* @param iterable<TKey, TValue> ...$args
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW I think this template will not work for merging iterables with different value types.

Also it seems, there's no support for typing it properly phpstan/phpstan#3814 (comment)

*
* @return IterableObject<TKey, TValue>
*
* @template TKey
* @template TValue
*/
function iterable_merge(iterable ...$args): iterable
{
return iterable($args[0] ?? null)->merge(...array_slice($args, 1));
}

/**
* Copy the iterable into an array.
*
Expand Down
73 changes: 73 additions & 0 deletions tests/IterableMergeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

declare(strict_types=1);

namespace BenTools\IterableFunctions\Tests;

use Generator;
use SplFixedArray;

use function array_merge;
use function BenTools\IterableFunctions\iterable;
use function BenTools\IterableFunctions\iterable_merge;
use function BenTools\IterableFunctions\iterable_to_array;
use function it;
use function PHPUnit\Framework\assertEquals;
use function PHPUnit\Framework\assertSame;

it('can be called without parameters', function (): void {
/** @phpstan-ignore-next-line */
assertEquals(iterable(null), iterable_merge());
});

it('merges an array', function (): void {
$iterable = [0 => 'zero', 1 => 'one'];
$array = [2 => 'two'];
assertSame([0 => 'zero', 1 => 'one', 2 => 'two'], iterable_to_array(iterable_merge($iterable, $array)));
});

it('merges multiples values', function (): void {
$iterable = [0 => 'zero', 1 => 'one'];
$array1 = [2 => 'two'];
$array2 = [3 => 'three', 4 => 'four'];
assertSame(
[0 => 'zero', 1 => 'one', 2 => 'two', 3 => 'three', 4 => 'four'],
iterable_to_array(iterable_merge($iterable, $array1, $array2)),
);
});

it('merges a Traversable object', function (): void {
$iterable = SplFixedArray::fromArray([0 => 'zero', 1 => 'one']);
$array = [2 => 'two'];
$merged = iterable_merge($iterable, $array);
assertSame([0 => 'zero', 1 => 'one', 2 => 'two'], iterable_to_array($merged));
});

it('iterable_merge should be equals to array_merge result', function (): void {
$array1 = ['bar', 'baz'];
$array2 = ['bat', 'baz'];
/** @var callable(): Generator<int, string> $generator1 */
$generator1 = fn () => yield from $array1;
/** @var callable(): Generator<int, string> $generator2 */
$generator2 = fn () => yield from $array2;

assertSame(
array_merge($array1, $array2),
iterable_merge($generator1(), $generator2())->asArray(),
);
})->with(function (): Generator {
yield 'simple array' => [
['bar', 'baz'],
['bat', 'baz'],
];

yield 'associative array' => [
['key1' => 'bar', 'key2' => 'baz'],
['key3' => 'bat', 'key4' => 'baz'],
];

yield 'associative array with duplicate keys' => [
['bar' => 'bar', 'baz' => 'baz'],
['bat' => 'bat', 'baz' => 'baz'],
];
});
23 changes: 23 additions & 0 deletions tests/IterableObjectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,26 @@ static function (string $value) {
assertInstanceOf(IterableObject::class, $iterableObject);
assertEquals(['zero', 'one', 'two'], $iterableObject->asArray());
});

it('merge iterators', function (iterable $input, array $mergeInputs, array $expected): void {
$iterableObject = iterable($input)->merge(...$mergeInputs);
assertInstanceOf(IterableObject::class, $iterableObject);
assertEquals($expected, $iterableObject->asArray());
})->with(function (): Generator {
yield 'no merge arg return initial iterable' => [['zero', 'one', 'two'], [], ['zero', 'one', 'two']];
yield 'all values merged' => [
['zero', 'one', 'two'],
[
['three', 'four', 'five'],
],
['zero', 'one', 'two', 'three', 'four', 'five'],
];
yield 'merge all args into a new iterable' => [
[0 => 'zero', 1 => 'one', 2 => 'two'],
[
[3 => 'three', 4 => 'four'],
[5 => 'five', 6 => 'six', 7 => 'seven'],
],
[0 => 'zero', 1 => 'one', 2 => 'two', 3 => 'three', 4 => 'four', 5 => 'five', 6 => 'six', 7 => 'seven'],
];
});