Skip to content

Commit 7136af2

Browse files
authored
Feat: Add iterable_merge function (#45)
1 parent 95571ed commit 7136af2

6 files changed

+178
-3
lines changed

README.md

+22
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This package provides functions to work with [iterables](https://wiki.php.net/rf
1212
- [iterable_to_array()](#iterable_to_array)
1313
- [iterable_to_traversable()](#iterable_to_traversable)
1414
- [iterable_map()](#iterable_map)
15+
- [iterable_merge()](#iterable_merge)
1516
- [iterable_reduce()](#iterable_reduce)
1617
- [iterable_filter()](#iterable_filter)
1718
- [iterable_values()](#iterable_values)
@@ -69,6 +70,27 @@ foreach (iterable_map($generator(), 'strtoupper') as $item) {
6970
}
7071
```
7172

73+
iterable_merge()
74+
--------------
75+
76+
Works like an `array_merge` with an `array` or a `Traversable`.
77+
78+
```php
79+
use function BenTools\IterableFunctions\iterable_merge;
80+
81+
$generator1 = function () {
82+
yield 'foo';
83+
};
84+
85+
$generator2 = function () {
86+
yield 'bar';
87+
};
88+
89+
foreach (iterable_merge($generator1(), $generator2()) as $item) {
90+
var_dump($item); // foo, bar
91+
}
92+
```
93+
7294
iterable_reduce()
7395
--------------
7496

composer.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
"vimeo/psalm": "^4.4"
3232
},
3333
"config": {
34-
"sort-packages": true
34+
"sort-packages": true,
35+
"allow-plugins": {
36+
"pestphp/pest-plugin": false,
37+
"dealerdirect/phpcodesniffer-composer-installer": true,
38+
"phpstan/extension-installer": true
39+
}
3540
}
3641
}

src/IterableObject.php

+38-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
namespace BenTools\IterableFunctions;
66

7+
use AppendIterator;
8+
use ArrayIterator;
79
use CallbackFilterIterator;
10+
use Iterator;
811
use IteratorAggregate;
912
use IteratorIterator;
1013
use Traversable;
@@ -26,10 +29,14 @@ final class IterableObject implements IteratorAggregate
2629
/** @var iterable<TKey, TValue> */
2730
private $iterable;
2831

32+
/** @var bool */
33+
private $preserveKeys;
34+
2935
/** @param iterable<TKey, TValue> $iterable */
30-
public function __construct(iterable $iterable)
36+
public function __construct(iterable $iterable, bool $preserveKeys = true)
3137
{
3238
$this->iterable = $iterable;
39+
$this->preserveKeys = $preserveKeys;
3340
}
3441

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

80+
/**
81+
* @param iterable<TKey, TValue> ...$args
82+
*
83+
* @return self<TKey, TValue>
84+
*/
85+
public function merge(iterable ...$args): self
86+
{
87+
if ($args === []) {
88+
return $this;
89+
}
90+
91+
$toIterator = static function (iterable $iterable): Iterator {
92+
if ($iterable instanceof Traversable) {
93+
return new IteratorIterator($iterable);
94+
}
95+
96+
return new ArrayIterator($iterable);
97+
};
98+
99+
$iterator = new AppendIterator();
100+
$iterator->append($toIterator($this->iterable));
101+
102+
foreach ($args as $iterable) {
103+
$iterator->append($toIterator($iterable));
104+
}
105+
106+
return new self($iterator, false);
107+
}
108+
73109
/**
74110
* @return self<int, TValue>
75111
*/
@@ -87,6 +123,6 @@ public function getIterator(): Traversable
87123
/** @return array<array-key, TValue> */
88124
public function asArray(): array
89125
{
90-
return $this->iterable instanceof Traversable ? iterator_to_array($this->iterable) : $this->iterable;
126+
return $this->iterable instanceof Traversable ? iterator_to_array($this->iterable, $this->preserveKeys) : $this->iterable;
91127
}
92128
}

src/iterable-functions.php

+16
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use EmptyIterator;
99
use Traversable;
1010

11+
use function array_slice;
1112
use function array_values;
1213
use function iterator_to_array;
1314

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

32+
/**
33+
* Merge iterables
34+
*
35+
* @param iterable<TKey, TValue> ...$args
36+
*
37+
* @return IterableObject<TKey, TValue>
38+
*
39+
* @template TKey
40+
* @template TValue
41+
*/
42+
function iterable_merge(iterable ...$args): iterable
43+
{
44+
return iterable($args[0] ?? null)->merge(...array_slice($args, 1));
45+
}
46+
3147
/**
3248
* Copy the iterable into an array.
3349
*

tests/IterableMergeTest.php

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace BenTools\IterableFunctions\Tests;
6+
7+
use Generator;
8+
use SplFixedArray;
9+
10+
use function array_merge;
11+
use function BenTools\IterableFunctions\iterable;
12+
use function BenTools\IterableFunctions\iterable_merge;
13+
use function BenTools\IterableFunctions\iterable_to_array;
14+
use function it;
15+
use function PHPUnit\Framework\assertEquals;
16+
use function PHPUnit\Framework\assertSame;
17+
18+
it('can be called without parameters', function (): void {
19+
/** @phpstan-ignore-next-line */
20+
assertEquals(iterable(null), iterable_merge());
21+
});
22+
23+
it('merges an array', function (): void {
24+
$iterable = [0 => 'zero', 1 => 'one'];
25+
$array = [2 => 'two'];
26+
assertSame([0 => 'zero', 1 => 'one', 2 => 'two'], iterable_to_array(iterable_merge($iterable, $array)));
27+
});
28+
29+
it('merges multiples values', function (): void {
30+
$iterable = [0 => 'zero', 1 => 'one'];
31+
$array1 = [2 => 'two'];
32+
$array2 = [3 => 'three', 4 => 'four'];
33+
assertSame(
34+
[0 => 'zero', 1 => 'one', 2 => 'two', 3 => 'three', 4 => 'four'],
35+
iterable_to_array(iterable_merge($iterable, $array1, $array2)),
36+
);
37+
});
38+
39+
it('merges a Traversable object', function (): void {
40+
$iterable = SplFixedArray::fromArray([0 => 'zero', 1 => 'one']);
41+
$array = [2 => 'two'];
42+
$merged = iterable_merge($iterable, $array);
43+
assertSame([0 => 'zero', 1 => 'one', 2 => 'two'], iterable_to_array($merged));
44+
});
45+
46+
it('iterable_merge should be equals to array_merge result', function (): void {
47+
$array1 = ['bar', 'baz'];
48+
$array2 = ['bat', 'baz'];
49+
/** @var callable(): Generator<int, string> $generator1 */
50+
$generator1 = fn () => yield from $array1;
51+
/** @var callable(): Generator<int, string> $generator2 */
52+
$generator2 = fn () => yield from $array2;
53+
54+
assertSame(
55+
array_merge($array1, $array2),
56+
iterable_merge($generator1(), $generator2())->asArray(),
57+
);
58+
})->with(function (): Generator {
59+
yield 'simple array' => [
60+
['bar', 'baz'],
61+
['bat', 'baz'],
62+
];
63+
64+
yield 'associative array' => [
65+
['key1' => 'bar', 'key2' => 'baz'],
66+
['key3' => 'bat', 'key4' => 'baz'],
67+
];
68+
69+
yield 'associative array with duplicate keys' => [
70+
['bar' => 'bar', 'baz' => 'baz'],
71+
['bat' => 'bat', 'baz' => 'baz'],
72+
];
73+
});

tests/IterableObjectTest.php

+23
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,26 @@ static function (string $value) {
124124
assertInstanceOf(IterableObject::class, $iterableObject);
125125
assertEquals(['zero', 'one', 'two'], $iterableObject->asArray());
126126
});
127+
128+
it('merge iterators', function (iterable $input, array $mergeInputs, array $expected): void {
129+
$iterableObject = iterable($input)->merge(...$mergeInputs);
130+
assertInstanceOf(IterableObject::class, $iterableObject);
131+
assertEquals($expected, $iterableObject->asArray());
132+
})->with(function (): Generator {
133+
yield 'no merge arg return initial iterable' => [['zero', 'one', 'two'], [], ['zero', 'one', 'two']];
134+
yield 'all values merged' => [
135+
['zero', 'one', 'two'],
136+
[
137+
['three', 'four', 'five'],
138+
],
139+
['zero', 'one', 'two', 'three', 'four', 'five'],
140+
];
141+
yield 'merge all args into a new iterable' => [
142+
[0 => 'zero', 1 => 'one', 2 => 'two'],
143+
[
144+
[3 => 'three', 4 => 'four'],
145+
[5 => 'five', 6 => 'six', 7 => 'seven'],
146+
],
147+
[0 => 'zero', 1 => 'one', 2 => 'two', 3 => 'three', 4 => 'four', 5 => 'five', 6 => 'six', 7 => 'seven'],
148+
];
149+
});

0 commit comments

Comments
 (0)