Compare commits
No commits in common. "master" and "v1.0.0" have entirely different histories.
18
CHANGELOG.md
18
CHANGELOG.md
@ -1,23 +1,5 @@
|
|||||||
# Changelog
|
# 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
|
|
||||||
|
|
||||||
- `sort()` without a comparator and `sortBy()` sort natively, comparing like `<=>`. Enums used directly as the sort key (or as the values for `sort()`), which `<=>` can't order, are now grouped by case, in an unspecified order, and placed after the other values (before them in descending order). Enums nested inside arrays are unaffected.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- `sortBy()` and `sort()` are several times faster: they use PHP's native sort instead of `usort()` with a userland comparator.
|
|
||||||
- `compile()` of a closure of a curried function (`$curried(...)`), alone or in a pipeline, keeps currying instead of throwing `ArgumentCountError`.
|
|
||||||
- `filter()` on arrays with a callback taking its value by reference (`fn (&$value) => ...`) no longer emits "must be passed by reference" warnings.
|
|
||||||
|
|
||||||
## 1.0.0
|
## 1.0.0
|
||||||
|
|
||||||
### Breaking
|
### Breaking
|
||||||
|
|||||||
@ -253,7 +253,7 @@ array. The details:
|
|||||||
|
|
||||||
¹ `map`, `filter`, `reject`, `unique`, `uniqueBy`, `sort`, `sortBy`, `slice`, `take`, `skip`, `merge`, `each`,
|
¹ `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
|
`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`. `chunk` also produces a list, of arrays, and gives a `Ds\Seq` for a `Ds\Set` too.
|
a list, so it becomes a `Ds\Seq`.
|
||||||
|
|
||||||
A few consequences worth knowing:
|
A few consequences worth knowing:
|
||||||
|
|
||||||
@ -268,10 +268,6 @@ A few consequences worth knowing:
|
|||||||
- `merge()` merges arrays and `Ds\Map` by key (integer keys of arrays are kept, unlike `array_merge()`); for other
|
- `merge()` merges arrays and `Ds\Map` by key (integer keys of arrays are kept, unlike `array_merge()`); for other
|
||||||
inputs it appends.
|
inputs it appends.
|
||||||
- `sort()`/`sortBy()` are stable and never modify their input, unlike `Ds\Seq::sort()`.
|
- `sort()`/`sortBy()` are stable and never modify their input, unlike `Ds\Seq::sort()`.
|
||||||
- `sort()` without a comparator and `sortBy()` sort natively, comparing like `<=>`. The one difference is enums
|
|
||||||
used directly as the sort key (or as the values for `sort()`), which `<=>` can't order: they are grouped by case,
|
|
||||||
in an unspecified order (PHP compares object addresses, not declaration order), and placed after the other values
|
|
||||||
(before them in descending order). Enums nested inside arrays compare as `<=>` does.
|
|
||||||
|
|
||||||
### Finding things: `first`, `find` and `required`
|
### Finding things: `first`, `find` and `required`
|
||||||
|
|
||||||
@ -311,8 +307,7 @@ exactly like its target:
|
|||||||
|
|
||||||
`map`, `filter` (keeps truthy values without a callback), `reject`, `flatMap`, `values`, `keys`, `unique`,
|
`map`, `filter` (keeps truthy values without a callback), `reject`, `flatMap`, `values`, `keys`, `unique`,
|
||||||
`uniqueBy`, `sort` (comparator or `<=>`), `sortBy` (selector, optionally `descending: true`), `slice`, `take`,
|
`uniqueBy`, `sort` (comparator or `<=>`), `sortBy` (selector, optionally `descending: true`), `slice`, `take`,
|
||||||
`skip`, `chunk` (size, optionally `preserveKeys: true`, like `array_chunk()`), `prepend`, `append`, `concat`, `merge`,
|
`skip`, `prepend`, `append`, `concat`, `merge`, `each`, `tap`.
|
||||||
`each`, `tap`.
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$delays = $departures
|
$delays = $departures
|
||||||
|
|||||||
@ -147,28 +147,6 @@ function entries(iterable $iterable): array
|
|||||||
return $entries;
|
return $entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a `Ds\Map` from entries given by position, in the order of the keys of $order (positions).
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @param list<mixed> $keys
|
|
||||||
* @param array<int, mixed> $values
|
|
||||||
* @param array<int, mixed> $order
|
|
||||||
*
|
|
||||||
* @return \Ds\Map<mixed, mixed>
|
|
||||||
*/
|
|
||||||
function mapInOrder(array $keys, array $values, array $order): \Ds\Map
|
|
||||||
{
|
|
||||||
$result = new \Ds\Map();
|
|
||||||
$result->allocate(count($order));
|
|
||||||
foreach ($order as $position => $_) {
|
|
||||||
$result->put($keys[$position], $values[$position]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Yields [key, value] pairs back as key => value.
|
* Yields [key, value] pairs back as key => value.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -12,8 +12,7 @@
|
|||||||
*
|
*
|
||||||
* Every function returns a closure that takes any iterable and returns a generator, so nothing is done until the
|
* 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()},
|
* 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()}, {@see concat()} and {@see chunk()}, which yield
|
* {@see keys()}, {@see flatMap()}, {@see prepend()}, {@see append()} and {@see concat()}, which yield sequential keys.
|
||||||
* sequential keys.
|
|
||||||
* End a chain with a collector or query from `Kadet\Functional\Collections`, e.g. `toArray()`.
|
* 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.
|
* The eager transforms in `Kadet\Functional\Collections` are built on these, so both flavours behave the same.
|
||||||
@ -258,42 +257,6 @@ function skip(int $n): \Closure
|
|||||||
return slice($n);
|
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.
|
* Puts `$values` before the values of the input, with sequential keys.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -22,13 +22,11 @@ namespace Kadet\Functional\Collections;
|
|||||||
|
|
||||||
use Kadet\Functional\Collections\Lazy as lazy;
|
use Kadet\Functional\Collections\Lazy as lazy;
|
||||||
use function Kadet\Functional\Internal\callback;
|
use function Kadet\Functional\Internal\callback;
|
||||||
use function Kadet\Functional\Internal\takesByReference;
|
|
||||||
use function Kadet\Functional\Collections\Internal\entries;
|
use function Kadet\Functional\Collections\Internal\entries;
|
||||||
use function Kadet\Functional\Collections\Internal\iterated;
|
use function Kadet\Functional\Collections\Internal\iterated;
|
||||||
use function Kadet\Functional\Collections\Internal\keyedLike;
|
use function Kadet\Functional\Collections\Internal\keyedLike;
|
||||||
use function Kadet\Functional\Collections\Internal\seen;
|
use function Kadet\Functional\Collections\Internal\seen;
|
||||||
use function Kadet\Functional\Collections\Internal\listLike;
|
use function Kadet\Functional\Collections\Internal\listLike;
|
||||||
use function Kadet\Functional\Collections\Internal\mapInOrder;
|
|
||||||
use function Kadet\Functional\Collections\Internal\seqOf;
|
use function Kadet\Functional\Collections\Internal\seqOf;
|
||||||
use function Kadet\Functional\Collections\Internal\unpairs;
|
use function Kadet\Functional\Collections\Internal\unpairs;
|
||||||
use function Kadet\Functional\Collections\Internal\valuesOf;
|
use function Kadet\Functional\Collections\Internal\valuesOf;
|
||||||
@ -102,18 +100,14 @@ function filter(?callable $fn = null): \Closure
|
|||||||
{
|
{
|
||||||
$fn = $fn === null ? static fn ($value) => $value : callback($fn);
|
$fn = $fn === null ? static fn ($value) => $value : callback($fn);
|
||||||
|
|
||||||
// array_filter() passes values, not variables, so a callback taking a parameter by reference would warn on each
|
return static function (iterable $input) use ($fn): iterable {
|
||||||
// call. Through this wrapper it gets variables; as before, its changes don't reach the result.
|
|
||||||
$native = takesByReference($fn) ? static fn ($value, $key) => $fn($value, $key) : $fn;
|
|
||||||
|
|
||||||
return static function (iterable $input) use ($fn, $native): iterable {
|
|
||||||
if ($input instanceof \Ds\Map) {
|
if ($input instanceof \Ds\Map) {
|
||||||
return $input->filter(static fn ($key, $value) => $fn($value, $key));
|
return $input->filter(static fn ($key, $value) => $fn($value, $key));
|
||||||
}
|
}
|
||||||
|
|
||||||
$values = iterated($input);
|
$values = iterated($input);
|
||||||
if (is_array($values)) {
|
if (is_array($values)) {
|
||||||
$values = array_filter($values, $native, ARRAY_FILTER_USE_BOTH);
|
$values = array_filter($values, $fn, ARRAY_FILTER_USE_BOTH);
|
||||||
|
|
||||||
return is_array($input) ? $values : listLike($input, $values);
|
return is_array($input) ? $values : listLike($input, $values);
|
||||||
}
|
}
|
||||||
@ -310,8 +304,7 @@ function uniqueBy(callable $selector): \Closure
|
|||||||
/**
|
/**
|
||||||
* Sorts the values with `$comparator($a, $b)` (an int like `<=>`), or with `<=>` when it is null.
|
* Sorts the values with `$comparator($a, $b)` (an int like `<=>`), or with `<=>` when it is null.
|
||||||
*
|
*
|
||||||
* The sort is stable and keeps the keys of arrays and `Ds\Map`. The input is never modified. Without a comparator,
|
* The sort is stable and keeps the keys of arrays and `Ds\Map`. The input is never modified.
|
||||||
* enums are ordered differently from `<=>` (see "What you get back" in the README).
|
|
||||||
*
|
*
|
||||||
* @template TValue = mixed
|
* @template TValue = mixed
|
||||||
*
|
*
|
||||||
@ -321,51 +314,14 @@ function uniqueBy(callable $selector): \Closure
|
|||||||
*/
|
*/
|
||||||
function sort(?callable $comparator = null): \Closure
|
function sort(?callable $comparator = null): \Closure
|
||||||
{
|
{
|
||||||
if ($comparator === null) {
|
|
||||||
// The native SORT_REGULAR sorts compare like `<=>` and are stable.
|
|
||||||
return static function (iterable $input): iterable {
|
|
||||||
if (is_array($input)) {
|
|
||||||
asort($input);
|
|
||||||
|
|
||||||
return $input;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($input instanceof \Ds\Map) {
|
|
||||||
$keys = $input->keys()->toArray();
|
|
||||||
$values = $input->values()->toArray();
|
|
||||||
asort($values);
|
|
||||||
|
|
||||||
return mapInOrder($keys, $values, $values);
|
|
||||||
}
|
|
||||||
|
|
||||||
$values = valuesOf($input);
|
|
||||||
\sort($values);
|
|
||||||
|
|
||||||
return listLike($input, $values);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comparators always take two values, so they are compiled without the ($value, $key) adaptation.
|
// Comparators always take two values, so they are compiled without the ($value, $key) adaptation.
|
||||||
$comparator = compile($comparator);
|
$comparator = $comparator === null ? static fn ($a, $b) => $a <=> $b : compile($comparator);
|
||||||
|
|
||||||
return static function (iterable $input) use ($comparator): iterable {
|
return static function (iterable $input) use ($comparator): iterable {
|
||||||
if (is_array($input)) {
|
|
||||||
uasort($input, $comparator);
|
|
||||||
|
|
||||||
return $input;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($input instanceof \Ds\Map) {
|
|
||||||
$entries = entries($input);
|
$entries = entries($input);
|
||||||
usort($entries, static fn ($a, $b) => $comparator($a[1], $b[1]));
|
usort($entries, static fn ($a, $b) => $comparator($a[1], $b[1]));
|
||||||
|
|
||||||
return keyedLike($input, unpairs($entries));
|
return keyedLike($input, unpairs($entries));
|
||||||
}
|
|
||||||
|
|
||||||
$values = valuesOf($input);
|
|
||||||
usort($values, $comparator);
|
|
||||||
|
|
||||||
return listLike($input, $values);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -373,8 +329,7 @@ function sort(?callable $comparator = null): \Closure
|
|||||||
* Sorts the values by `$selector($value, $key)`, compared with `<=>`, in ascending or descending order.
|
* Sorts the values by `$selector($value, $key)`, compared with `<=>`, in ascending or descending order.
|
||||||
*
|
*
|
||||||
* The sort is stable (equal values keep their order in both directions) and keeps the keys of arrays and
|
* The sort is stable (equal values keep their order in both directions) and keeps the keys of arrays and
|
||||||
* `Ds\Map`. The selector is called once per value. The input is never modified. Enum keys are ordered differently
|
* `Ds\Map`. The selector is called once per value. The input is never modified.
|
||||||
* from `<=>` (see "What you get back" in the README).
|
|
||||||
*
|
*
|
||||||
* @template TKey = mixed
|
* @template TKey = mixed
|
||||||
* @template TValue = mixed
|
* @template TValue = mixed
|
||||||
@ -393,47 +348,16 @@ function sortBy(callable $selector, bool $descending = false): \Closure
|
|||||||
{
|
{
|
||||||
$selector = callback($selector);
|
$selector = callback($selector);
|
||||||
|
|
||||||
// The selected values are sorted natively: SORT_REGULAR compares like `<=>`, and PHP's sorts are stable, also
|
|
||||||
// arsort() for ties.
|
|
||||||
return static function (iterable $input) use ($selector, $descending): iterable {
|
return static function (iterable $input) use ($selector, $descending): iterable {
|
||||||
if (is_array($input)) {
|
$entries = [];
|
||||||
$order = [];
|
|
||||||
foreach ($input as $key => $value) {
|
foreach ($input as $key => $value) {
|
||||||
$order[$key] = $selector($value, $key);
|
$entries[] = [$key, $value, $selector($value, $key)];
|
||||||
}
|
}
|
||||||
|
|
||||||
$descending ? arsort($order) : asort($order);
|
$direction = $descending ? -1 : 1;
|
||||||
|
usort($entries, static fn ($a, $b) => $direction * ($a[2] <=> $b[2]));
|
||||||
|
|
||||||
$result = [];
|
return keyedLike($input, unpairs($entries));
|
||||||
foreach ($order as $key => $_) {
|
|
||||||
$result[$key] = $input[$key];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys of `Ds\Map` can be objects, so the entries are sorted by position.
|
|
||||||
$keys = [];
|
|
||||||
$values = [];
|
|
||||||
$order = [];
|
|
||||||
foreach (iterated($input) as $key => $value) {
|
|
||||||
$keys[] = $key;
|
|
||||||
$values[] = $value;
|
|
||||||
$order[] = $selector($value, $key);
|
|
||||||
}
|
|
||||||
|
|
||||||
$descending ? arsort($order) : asort($order);
|
|
||||||
|
|
||||||
if ($input instanceof \Ds\Map) {
|
|
||||||
return mapInOrder($keys, $values, $order);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = [];
|
|
||||||
foreach ($order as $position => $_) {
|
|
||||||
$result[] = $values[$position];
|
|
||||||
}
|
|
||||||
|
|
||||||
return listLike($input, $result);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,25 +414,6 @@ function skip(int $n): \Closure
|
|||||||
return slice($n);
|
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).
|
* Puts `$values` before the values of the input, as a list (keys are dropped).
|
||||||
*
|
*
|
||||||
|
|||||||
@ -79,22 +79,6 @@ function callback(callable $callable, int $arity = 1): \Closure
|
|||||||
: static fn ($carry, $value) => $compiled($carry, $value);
|
: static fn ($carry, $value) => $compiled($carry, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether a function takes any of its parameters by reference.
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
function takesByReference(\Closure $closure): bool
|
|
||||||
{
|
|
||||||
foreach ((new \ReflectionFunction($closure))->getParameters() as $parameter) {
|
|
||||||
if ($parameter->isPassedByReference()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the function a callable ultimately calls: decorators are unwrapped, also when they were turned into
|
* Finds the function a callable ultimately calls: decorators are unwrapped, also when they were turned into
|
||||||
* closures, and a pipeline gives its first function, which gets all of its arguments.
|
* closures, and a pipeline gives its first function, which gets all of its arguments.
|
||||||
|
|||||||
@ -71,9 +71,6 @@ function memoize(callable $callable)
|
|||||||
* Flattens a callable into a plain closure. Compilable objects (decorators, pipelines, predicates) are compiled, so
|
* Flattens a callable into a plain closure. Compilable objects (decorators, pipelines, predicates) are compiled, so
|
||||||
* calling the result does not go through them, also when given as a closure of their `__invoke` (e.g. `$pipe(...)`);
|
* calling the result does not go through them, also when given as a closure of their `__invoke` (e.g. `$pipe(...)`);
|
||||||
* any other callable is converted with closure().
|
* any other callable is converted with closure().
|
||||||
*
|
|
||||||
* A closure of a curried function's `__invoke` (`$curried(...)`) is kept as it is, so it keeps currying; the compiled
|
|
||||||
* curried function itself calls its target and does not curry further.
|
|
||||||
*/
|
*/
|
||||||
function compile(callable $callable): \Closure
|
function compile(callable $callable): \Closure
|
||||||
{
|
{
|
||||||
@ -81,9 +78,7 @@ function compile(callable $callable): \Closure
|
|||||||
$reflection = new \ReflectionFunction($callable);
|
$reflection = new \ReflectionFunction($callable);
|
||||||
$bound = $reflection->getClosureThis();
|
$bound = $reflection->getClosureThis();
|
||||||
|
|
||||||
return $bound instanceof Compilable && !$bound instanceof CurriedFunction && $reflection->getName() === '__invoke'
|
return $bound instanceof Compilable && $reflection->getName() === '__invoke' ? $bound->compile() : $callable;
|
||||||
? $bound->compile()
|
|
||||||
: $callable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $callable instanceof Compilable ? $callable->compile() : closure($callable);
|
return $callable instanceof Compilable ? $callable->compile() : closure($callable);
|
||||||
|
|||||||
@ -167,48 +167,4 @@ class LazyTest extends \PHPUnit\Framework\TestCase
|
|||||||
$this->assertSame([2, 3], iterator_to_array($step([1, 2])));
|
$this->assertSame([2, 3], iterator_to_array($step([1, 2])));
|
||||||
$this->assertSame([4], iterator_to_array($step([3])));
|
$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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -107,45 +107,6 @@ class TransformsTest extends \PHPUnit\Framework\TestCase
|
|||||||
$this->assertSeq([2], c\filter(fn ($v) => $v % 2 === 0)(self::gen([1, 2, 3])));
|
$this->assertSeq([2], c\filter(fn ($v) => $v % 2 === 0)(self::gen([1, 2, 3])));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testFilterWithByReferenceCallbackDoesNotWarn()
|
|
||||||
{
|
|
||||||
$warnings = [];
|
|
||||||
set_error_handler(function (int $level, string $message) use (&$warnings) {
|
|
||||||
$warnings[] = $message;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
$array = c\filter(fn (&$v) => $v > 1)([1, 2, 3]);
|
|
||||||
$seq = c\filter(fn (&$v) => $v > 1)(new Seq([1, 2, 3]));
|
|
||||||
$both = c\filter(fn (&$v, $k) => $k > 0)([1, 2, 3]);
|
|
||||||
} finally {
|
|
||||||
restore_error_handler();
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->assertSame([], $warnings);
|
|
||||||
$this->assertSame([1 => 2, 2 => 3], $array);
|
|
||||||
$this->assertSeq([2, 3], $seq);
|
|
||||||
$this->assertSame([1 => 2, 2 => 3], $both);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testByReferenceCallbacksWriteBackOutsideNativeFilter()
|
|
||||||
{
|
|
||||||
// Where the callback is called with a variable, its changes to the value are kept, as in 1.0.0.
|
|
||||||
$trim = function (&$v) {
|
|
||||||
$v = trim($v);
|
|
||||||
|
|
||||||
return $v !== '';
|
|
||||||
};
|
|
||||||
$gen = fn () => self::gen([' a ', ' ', 'b ']);
|
|
||||||
|
|
||||||
$this->assertSame(['a', 'b'], iterator_to_array(\Kadet\Functional\Collections\Lazy\filter($trim)($gen()), false));
|
|
||||||
$this->assertSame([1 => ''], c\reject($trim)([' a ', ' ', 'b ']));
|
|
||||||
$this->assertSame('x', c\find($trim)(new Seq([' x '])));
|
|
||||||
$this->assertSame('a', c\first($trim)($gen()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testFilterWithInternalPredicate()
|
public function testFilterWithInternalPredicate()
|
||||||
{
|
{
|
||||||
$this->assertSame([1 => 'a'], c\filter(is_string(...))([1, 'a']));
|
$this->assertSame([1 => 'a'], c\filter(is_string(...))([1, 'a']));
|
||||||
@ -287,96 +248,6 @@ class TransformsTest extends \PHPUnit\Framework\TestCase
|
|||||||
$this->assertSame(['b' => 1, 'a' => 2], c\sortBy(fn ($v, $k) => $k, true)(['a' => 2, 'b' => 1]));
|
$this->assertSame(['b' => 1, 'a' => 2], c\sortBy(fn ($v, $k) => $k, true)(['a' => 2, 'b' => 1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSortByIsStableForTies()
|
|
||||||
{
|
|
||||||
$items = [['k' => 1, 'n' => 'a'], ['k' => 0, 'n' => 'b'], ['k' => 1, 'n' => 'c'], ['k' => 0, 'n' => 'd'], ['k' => 1, 'n' => 'e']];
|
|
||||||
|
|
||||||
$this->assertSame(['b', 'd', 'a', 'c', 'e'], array_column(c\sortBy(fn ($o) => $o['k'])($items), 'n'));
|
|
||||||
$this->assertSame(['b', 'd', 'a', 'c', 'e'], array_column(c\sortBy(fn ($o) => $o['k'])(new Seq($items))->toArray(), 'n'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSortByDescendingIsStableForTies()
|
|
||||||
{
|
|
||||||
$items = [['k' => 0, 'n' => 'a'], ['k' => 1, 'n' => 'b'], ['k' => 0, 'n' => 'c'], ['k' => 1, 'n' => 'd'], ['k' => 0, 'n' => 'e']];
|
|
||||||
|
|
||||||
$this->assertSame(['b', 'd', 'a', 'c', 'e'], array_column(c\sortBy(fn ($o) => $o['k'], true)($items), 'n'));
|
|
||||||
$this->assertSame(['b', 'd', 'a', 'c', 'e'], array_column(c\sortBy(fn ($o) => $o['k'], true)(new Seq($items))->toArray(), 'n'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSortByWithDateTimeKeys()
|
|
||||||
{
|
|
||||||
$at = fn (string $time) => new \DateTimeImmutable("2026-01-01 $time");
|
|
||||||
$items = ['c' => $at('10:00'), 'a' => $at('08:00'), 'd' => $at('10:00:01'), 'b' => $at('09:30')];
|
|
||||||
|
|
||||||
$this->assertSame(['a', 'b', 'c', 'd'], array_keys(c\sortBy(fn ($v) => $v)($items)));
|
|
||||||
$this->assertSame(['d', 'c', 'b', 'a'], array_keys(c\sortBy(fn ($v) => $v, true)($items)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSortByMixedNumericKeys()
|
|
||||||
{
|
|
||||||
$values = [3, 1.5, -2, 2.0, 0, 1, -0.5];
|
|
||||||
|
|
||||||
$this->assertSame([-2, -0.5, 0, 1, 1.5, 2.0, 3], c\sortBy(fn ($v) => $v)(new Seq($values))->toArray());
|
|
||||||
$this->assertSame([3, 2.0, 1.5, 1, 0, -0.5, -2], array_values(c\sortBy(fn ($v) => $v, true)($values)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSortAndSortByCompareLikeSpaceship()
|
|
||||||
{
|
|
||||||
$keys = [10, '9', 'abc', null, 2.5, '10', true, 'b', '', 0, [1, 2], [1], '1e1', ' 9', false];
|
|
||||||
|
|
||||||
$ascending = $keys;
|
|
||||||
usort($ascending, fn ($a, $b) => $a <=> $b);
|
|
||||||
$descending = $keys;
|
|
||||||
usort($descending, fn ($a, $b) => $b <=> $a);
|
|
||||||
|
|
||||||
$this->assertSame($ascending, array_values(c\sortBy(fn ($v) => $v)($keys)));
|
|
||||||
$this->assertSame($descending, array_values(c\sortBy(fn ($v) => $v, true)($keys)));
|
|
||||||
$this->assertSame($ascending, array_values(c\sort()($keys)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSortByKeepsObjectKeysOfMap()
|
|
||||||
{
|
|
||||||
$a = new \stdClass();
|
|
||||||
$b = new \stdClass();
|
|
||||||
$map = new Map();
|
|
||||||
$map->put($a, 2);
|
|
||||||
$map->put($b, 1);
|
|
||||||
|
|
||||||
$sorted = c\sortBy(fn ($v) => $v)($map);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(Map::class, $sorted);
|
|
||||||
$this->assertSame([$b, $a], $sorted->keys()->toArray());
|
|
||||||
$this->assertSame([1, 2], $sorted->values()->toArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSortWithComparatorKeepsObjectKeysOfMap()
|
|
||||||
{
|
|
||||||
$a = new \stdClass();
|
|
||||||
$b = new \stdClass();
|
|
||||||
$map = new Map();
|
|
||||||
$map->put($a, 1);
|
|
||||||
$map->put($b, 2);
|
|
||||||
|
|
||||||
$sorted = c\sort(fn ($x, $y) => $y <=> $x)($map);
|
|
||||||
|
|
||||||
$this->assertInstanceOf(Map::class, $sorted);
|
|
||||||
$this->assertSame([$b, $a], $sorted->keys()->toArray());
|
|
||||||
$this->assertSame([2, 1], $sorted->values()->toArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSortByDoesNotMutateInput()
|
|
||||||
{
|
|
||||||
$array = ['x' => 3, 'y' => 1, 'z' => 2];
|
|
||||||
$seq = new Seq([3, 1, 2]);
|
|
||||||
|
|
||||||
c\sortBy(fn ($v) => $v)($array);
|
|
||||||
c\sortBy(fn ($v) => $v, true)($seq);
|
|
||||||
c\sort()($array);
|
|
||||||
|
|
||||||
$this->assertSame(['x' => 3, 'y' => 1, 'z' => 2], $array);
|
|
||||||
$this->assertSame([3, 1, 2], $seq->toArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSortDoesNotMutateInput()
|
public function testSortDoesNotMutateInput()
|
||||||
{
|
{
|
||||||
$seq = new Seq([3, 1, 2]);
|
$seq = new Seq([3, 1, 2]);
|
||||||
@ -426,23 +297,6 @@ class TransformsTest extends \PHPUnit\Framework\TestCase
|
|||||||
$this->assertSeq([1, 2, 3], c\take(3)($infinite));
|
$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()
|
public function testSkip()
|
||||||
{
|
{
|
||||||
$this->assertSame(['b' => 2], c\skip(1)(['a' => 1, 'b' => 2]));
|
$this->assertSame(['b' => 2], c\skip(1)(['a' => 1, 'b' => 2]));
|
||||||
|
|||||||
@ -243,23 +243,6 @@ class CompileTest extends \PHPUnit\Framework\TestCase
|
|||||||
$this->assertSame($pipeline('a')('b'), f\compile($pipeline)('a')('b'));
|
$this->assertSame($pipeline('a')('b'), f\compile($pipeline)('a')('b'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCompiledClosureOfCurriedKeepsCurrying()
|
|
||||||
{
|
|
||||||
$curried = f\curry(fn ($a, $b) => "$a$b");
|
|
||||||
|
|
||||||
$this->assertSame('ab', f\compile($curried(...))('a')('b'));
|
|
||||||
$this->assertSame('ab', f\compile($curried(...))('a', 'b'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testCompiledPipelineOfCurriedClosureKeepsCurrying()
|
|
||||||
{
|
|
||||||
$curried = f\curry(fn ($a, $b) => "$a$b");
|
|
||||||
$pipeline = f\pipe(strtoupper(...), $curried(...));
|
|
||||||
|
|
||||||
$this->assertSame($pipeline('a')('b'), f\compile($pipeline)('a')('b'));
|
|
||||||
$this->assertSame('ab', f\compile(f\pipe($curried(...), fn ($rest) => $rest('b')))('a'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testCompileUnwrapsClosureOfCompilable()
|
public function testCompileUnwrapsClosureOfCompilable()
|
||||||
{
|
{
|
||||||
$spy = new class(strtoupper(...)) extends FunctionPipeline {
|
$spy = new class(strtoupper(...)) extends FunctionPipeline {
|
||||||
|
|||||||
@ -67,12 +67,10 @@ function transforms(
|
|||||||
// Transforms without a callback keep the element types, but not the container.
|
// Transforms without a callback keep the element types, but not the container.
|
||||||
assertType('iterable<int, Foo>', c\values()($foos));
|
assertType('iterable<int, Foo>', c\values()($foos));
|
||||||
assertType('iterable<int<0, max>, Foo>', c\take(2)($list));
|
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.
|
// Lazy transforms give generators.
|
||||||
assertType('Generator<mixed, string, mixed, mixed>', lazy\map(fn (Foo $foo): string => $foo->name)($list));
|
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<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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user