Compare commits

..

2 Commits

Author SHA1 Message Date
Kacper Donat
6cb6a22f18 release 1.1.0 2026-10-28 21:33:44 +01:00
Kacper Donat
a61db75c7e add chunk to split values into arrays of a size 2026-10-28 21:32:47 +01:00
7 changed files with 129 additions and 3 deletions

View File

@ -1,5 +1,11 @@
# Changelog
## 1.1.0
### Added
- `chunk($size, $preserveKeys = false)` splits the values into arrays of `$size`, like `array_chunk()`. The lazy one yields each chunk as soon as it is full, so a streamed source is never held whole; the eager one gives a list array for an array and a `Ds\Seq` for anything else. A size below 1 throws `InvalidArgumentException`.
## 1.0.1
### Changed

View File

@ -253,7 +253,7 @@ array. The details:
¹ `map`, `filter`, `reject`, `unique`, `uniqueBy`, `sort`, `sortBy`, `slice`, `take`, `skip`, `merge`, `each`,
`tap`. ² `flatMap`, `values`, `keys`, `prepend`, `append`, `concat`: these produce a list, and a `Ds\Map` can't hold
a list, so it becomes a `Ds\Seq`.
a list, so it becomes a `Ds\Seq`. `chunk` also produces a list, of arrays, and gives a `Ds\Seq` for a `Ds\Set` too.
A few consequences worth knowing:
@ -311,7 +311,8 @@ exactly like its target:
`map`, `filter` (keeps truthy values without a callback), `reject`, `flatMap`, `values`, `keys`, `unique`,
`uniqueBy`, `sort` (comparator or `<=>`), `sortBy` (selector, optionally `descending: true`), `slice`, `take`,
`skip`, `prepend`, `append`, `concat`, `merge`, `each`, `tap`.
`skip`, `chunk` (size, optionally `preserveKeys: true`, like `array_chunk()`), `prepend`, `append`, `concat`, `merge`,
`each`, `tap`.
```php
$delays = $departures

View File

@ -12,7 +12,8 @@
*
* Every function returns a closure that takes any iterable and returns a generator, so nothing is done until the
* result is consumed and a chain walks its source once. Keys are preserved, except for {@see values()},
* {@see keys()}, {@see flatMap()}, {@see prepend()}, {@see append()} and {@see concat()}, which yield sequential keys.
* {@see keys()}, {@see flatMap()}, {@see prepend()}, {@see append()}, {@see concat()} and {@see chunk()}, which yield
* sequential keys.
* End a chain with a collector or query from `Kadet\Functional\Collections`, e.g. `toArray()`.
*
* The eager transforms in `Kadet\Functional\Collections` are built on these, so both flavours behave the same.
@ -257,6 +258,42 @@ function skip(int $n): \Closure
return slice($n);
}
/**
* Splits the values into arrays of `$size` (the last one may be shorter), like `array_chunk()`, yielding each as soon
* as it is full, so the source is never held whole.
*
* Chunks are lists unless `$preserveKeys` is true, which keeps the keys in them. Keys must then be valid array keys,
* and values under a key repeated within one chunk replace each other, as in any array.
*
* @return \Closure<TKey, TValue>(iterable<TKey, TValue>): \Generator<int, array<TKey, TValue>>
*/
function chunk(int $size, bool $preserveKeys = false): \Closure
{
if ($size < 1) {
throw new \InvalidArgumentException(sprintf('chunk(): size must be >= 1, got %d', $size));
}
return static function (iterable $input) use ($size, $preserveKeys): \Generator {
$chunk = [];
foreach ($input as $key => $value) {
if ($preserveKeys) {
$chunk[$key] = $value;
} else {
$chunk[] = $value;
}
if (count($chunk) === $size) {
yield $chunk;
$chunk = [];
}
}
if ($chunk !== []) {
yield $chunk;
}
};
}
/**
* Puts `$values` before the values of the input, with sequential keys.
*

View File

@ -490,6 +490,25 @@ function skip(int $n): \Closure
return slice($n);
}
/**
* Splits the values into arrays of `$size` (the last one may be shorter), like `array_chunk()`. See
* {@see Lazy\chunk()}.
*
* The chunks come in a list array for an array, and in a `Ds\Seq` (a list array without php-ds) for anything else.
*
* @return \Closure<TKey, TValue>(iterable<TKey, TValue>): iterable<int, array<TKey, TValue>>
*/
function chunk(int $size, bool $preserveKeys = false): \Closure
{
$lazy = lazy\chunk($size, $preserveKeys);
return static function (iterable $input) use ($lazy): iterable {
$chunks = iterator_to_array($lazy(iterated($input)), false);
return is_array($input) ? $chunks : seqOf($chunks);
};
}
/**
* Puts `$values` before the values of the input, as a list (keys are dropped).
*

View File

@ -167,4 +167,48 @@ class LazyTest extends \PHPUnit\Framework\TestCase
$this->assertSame([2, 3], iterator_to_array($step([1, 2])));
$this->assertSame([4], iterator_to_array($step([3])));
}
public function testChunkSplitsIntoListsOfTheSize()
{
$this->assertSame(
[[1, 2], [3, 4], [5]],
iterator_to_array(lazy\chunk(2)(self::gen(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5]))),
);
}
public function testChunkPreservesKeysWhenAsked()
{
$this->assertSame(
[['a' => 1, 'b' => 2], ['c' => 3]],
iterator_to_array(lazy\chunk(2, preserveKeys: true)(self::gen(['a' => 1, 'b' => 2, 'c' => 3]))),
);
}
public function testChunkKeepsValuesUnderRepeatedKeys()
{
$repeated = (function () {
yield 'a' => 1;
yield 'a' => 2;
})();
$this->assertSame([[1, 2]], iterator_to_array(lazy\chunk(2)($repeated)));
}
public function testChunkYieldsEachChunkOnceFull()
{
$this->assertSame([1, 2, 3], lazy\chunk(3)(self::naturals())->current());
}
public function testChunkOfNothingYieldsNothing()
{
$this->assertSame([], iterator_to_array(lazy\chunk(2)([])));
}
public function testChunkRejectsSizeBelowOneWhenBuilt()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('chunk(): size must be >= 1, got 0');
lazy\chunk(0);
}
}

View File

@ -426,6 +426,23 @@ class TransformsTest extends \PHPUnit\Framework\TestCase
$this->assertSeq([1, 2, 3], c\take(3)($infinite));
}
public function testChunk()
{
$this->assertSame([[1, 2], [3]], c\chunk(2)(['a' => 1, 'b' => 2, 'c' => 3]));
$this->assertSame([['a' => 1, 'b' => 2], ['c' => 3]], c\chunk(2, preserveKeys: true)(['a' => 1, 'b' => 2, 'c' => 3]));
$this->assertSeq([[1, 2], [3]], c\chunk(2)(new Seq([1, 2, 3])));
$this->assertSeq([['a' => 1], ['b' => 2]], c\chunk(1, preserveKeys: true)(new Map(['a' => 1, 'b' => 2])));
$this->assertSeq([[1, 2], [3]], c\chunk(2)(self::gen([1, 2, 3])));
}
public function testChunkRejectsSizeBelowOne()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('chunk(): size must be >= 1, got 0');
c\chunk(0);
}
public function testSkip()
{
$this->assertSame(['b' => 2], c\skip(1)(['a' => 1, 'b' => 2]));

View File

@ -67,10 +67,12 @@ function transforms(
// Transforms without a callback keep the element types, but not the container.
assertType('iterable<int, Foo>', c\values()($foos));
assertType('iterable<int<0, max>, Foo>', c\take(2)($list));
assertType('iterable<int, array<int<0, max>, Foo>>', c\chunk(2)($list));
// Lazy transforms give generators.
assertType('Generator<mixed, string, mixed, mixed>', lazy\map(fn (Foo $foo): string => $foo->name)($list));
assertType('Generator<mixed, Foo, mixed, mixed>', lazy\filter(fn (Foo $foo): bool => $foo->id > 0)($list));
assertType('Generator<int, array<int<0, max>, Foo>, mixed, mixed>', lazy\chunk(2)($list));
}
/**