add eager collection transforms
This commit is contained in:
parent
159e9a56ab
commit
b48f28e9dc
@ -24,7 +24,9 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"./src/functions.php",
|
"./src/functions.php",
|
||||||
"./src/Predicates/index.php",
|
"./src/Predicates/index.php",
|
||||||
"./src/Collections/Internal/functions.php"
|
"./src/Collections/Internal/functions.php",
|
||||||
|
"./src/Collections/Internal/containers.php",
|
||||||
|
"./src/Collections/transforms.php"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
|
|||||||
184
src/Collections/Internal/containers.php
Normal file
184
src/Collections/Internal/containers.php
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Copyright 2026 Kacper Donat
|
||||||
|
*
|
||||||
|
* @author Kacper "Kadet" Donat <kacper@kadet.net>
|
||||||
|
*
|
||||||
|
* Full license available in separate LICENSE file
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Kadet\Functional\Collections\Internal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether php-ds (the extension or the polyfill) is available.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
function dsAvailable(): bool
|
||||||
|
{
|
||||||
|
return class_exists(\Ds\Seq::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a keyed result in the same kind of container as $input.
|
||||||
|
*
|
||||||
|
* Arrays and `Ds\Map` keep the keys of $entries, `Ds\Seq` and `Ds\Set` take only the values. Any other iterable
|
||||||
|
* gives a `Ds\Seq` of the values, or a list array when php-ds is unavailable.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @param iterable<mixed, mixed> $input
|
||||||
|
* @param iterable<mixed, mixed> $entries
|
||||||
|
*
|
||||||
|
* @return iterable<mixed, mixed>
|
||||||
|
*/
|
||||||
|
function keyedLike(iterable $input, iterable $entries): iterable
|
||||||
|
{
|
||||||
|
if (is_array($input)) {
|
||||||
|
$result = [];
|
||||||
|
foreach ($entries as $key => $value) {
|
||||||
|
$result[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($input instanceof \Ds\Map) {
|
||||||
|
$result = new \Ds\Map();
|
||||||
|
foreach ($entries as $key => $value) {
|
||||||
|
$result->put($key, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return listLike($input, $entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a list result (sequential keys) for $input from the values of $values.
|
||||||
|
*
|
||||||
|
* Arrays give a list array, `Ds\Set` gives a `Ds\Set`, and anything else gives a `Ds\Seq`, or a list array when
|
||||||
|
* php-ds is unavailable.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @param iterable<mixed, mixed> $input
|
||||||
|
* @param iterable<mixed, mixed> $values
|
||||||
|
*
|
||||||
|
* @return iterable<int, mixed>
|
||||||
|
*/
|
||||||
|
function listLike(iterable $input, iterable $values): iterable
|
||||||
|
{
|
||||||
|
$list = [];
|
||||||
|
foreach ($values as $value) {
|
||||||
|
$list[] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($input)) {
|
||||||
|
return $list;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($input instanceof \Ds\Set) {
|
||||||
|
return new \Ds\Set($list);
|
||||||
|
}
|
||||||
|
|
||||||
|
return seqOf($list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a list in a `Ds\Seq`, or returns it as it is when php-ds is unavailable.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @template TValue
|
||||||
|
*
|
||||||
|
* @param list<TValue> $list
|
||||||
|
*
|
||||||
|
* @return iterable<int, TValue>
|
||||||
|
*/
|
||||||
|
function seqOf(array $list): iterable
|
||||||
|
{
|
||||||
|
return dsAvailable() ? new \Ds\Seq($list) : $list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects the entries of an iterable as [key, value] pairs, so that keys of any type survive.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @param iterable<mixed, mixed> $iterable
|
||||||
|
*
|
||||||
|
* @return list<array{mixed, mixed}>
|
||||||
|
*/
|
||||||
|
function entries(iterable $iterable): array
|
||||||
|
{
|
||||||
|
$entries = [];
|
||||||
|
foreach ($iterable as $key => $value) {
|
||||||
|
$entries[] = [$key, $value];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yields [key, value] pairs back as key => value.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @param iterable<array{mixed, mixed}> $entries
|
||||||
|
*
|
||||||
|
* @return \Generator<mixed, mixed>
|
||||||
|
*/
|
||||||
|
function unpairs(iterable $entries): \Generator
|
||||||
|
{
|
||||||
|
foreach ($entries as [$key, $value]) {
|
||||||
|
yield $key => $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks values already seen, compared strictly (objects by identity).
|
||||||
|
*
|
||||||
|
* The returned closure answers whether the given value was seen before and remembers it. Objects are kept
|
||||||
|
* referenced, so a temporary object's id can't be reused by a later one.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @return \Closure(mixed): bool
|
||||||
|
*/
|
||||||
|
function seen(): \Closure
|
||||||
|
{
|
||||||
|
$hashed = [];
|
||||||
|
$objects = [];
|
||||||
|
$other = [];
|
||||||
|
|
||||||
|
return static function (mixed $value) use (&$hashed, &$objects, &$other): bool {
|
||||||
|
if (is_object($value)) {
|
||||||
|
$id = spl_object_id($value);
|
||||||
|
if (isset($objects[$id])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$objects[$id] = $value;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_int($value) || is_string($value) || is_bool($value) || $value === null) {
|
||||||
|
$hash = get_debug_type($value) . ':' . $value;
|
||||||
|
if (isset($hashed[$hash])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$hashed[$hash] = true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($value, $other, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
$other[] = $value;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
437
src/Collections/transforms.php
Normal file
437
src/Collections/transforms.php
Normal file
@ -0,0 +1,437 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Copyright 2026 Kacper Donat
|
||||||
|
*
|
||||||
|
* @author Kacper "Kadet" Donat <kacper@kadet.net>
|
||||||
|
*
|
||||||
|
* Full license available in separate LICENSE file
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eager collection transforms.
|
||||||
|
*
|
||||||
|
* Every function returns a closure that takes the data, so they compose with the pipe operator. The result is the
|
||||||
|
* same kind of container as the input: an array gives an array (keys preserved, unless stated otherwise), `Ds\Seq`
|
||||||
|
* gives `Ds\Seq`, `Ds\Map` gives `Ds\Map` and `Ds\Set` gives `Ds\Set`. Any other iterable (e.g. a generator) is
|
||||||
|
* consumed and gives a `Ds\Seq` of the results, or a list array when php-ds is unavailable.
|
||||||
|
*
|
||||||
|
* Callbacks receive `($value, $key)`, also for `Ds\Map`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Kadet\Functional\Collections;
|
||||||
|
|
||||||
|
use function Kadet\Functional\Collections\Internal\callback;
|
||||||
|
use function Kadet\Functional\Collections\Internal\entries;
|
||||||
|
use function Kadet\Functional\Collections\Internal\keyedLike;
|
||||||
|
use function Kadet\Functional\Collections\Internal\listLike;
|
||||||
|
use function Kadet\Functional\Collections\Internal\seen;
|
||||||
|
use function Kadet\Functional\Collections\Internal\seqOf;
|
||||||
|
use function Kadet\Functional\Collections\Internal\unpairs;
|
||||||
|
use function Kadet\Functional\compile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms each value with `$fn($value, $key)`, keeping the keys.
|
||||||
|
*
|
||||||
|
* For `Ds\Set` this follows `Ds\Set::map`, so equal results collapse into one.
|
||||||
|
*
|
||||||
|
* @template TKey
|
||||||
|
* @template TValue
|
||||||
|
* @template TResult
|
||||||
|
*
|
||||||
|
* @param callable(TValue, TKey): TResult $fn
|
||||||
|
*
|
||||||
|
* @return \Closure(iterable<TKey, TValue>): iterable<TKey, TResult>
|
||||||
|
*/
|
||||||
|
function map(callable $fn): \Closure
|
||||||
|
{
|
||||||
|
return static function (iterable $input) use ($fn): iterable {
|
||||||
|
$fn = callback($fn);
|
||||||
|
|
||||||
|
return keyedLike($input, (static function () use ($input, $fn) {
|
||||||
|
foreach ($input as $key => $value) {
|
||||||
|
yield $key => $fn($value, $key);
|
||||||
|
}
|
||||||
|
})());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps the values for which `$fn($value, $key)` is truthy, or the truthy values when `$fn` is null.
|
||||||
|
*
|
||||||
|
* Keys are preserved for arrays and `Ds\Map`; `Ds\Seq` is reindexed.
|
||||||
|
*
|
||||||
|
* @template TKey
|
||||||
|
* @template TValue
|
||||||
|
*
|
||||||
|
* @param (callable(TValue, TKey): mixed)|null $fn
|
||||||
|
*
|
||||||
|
* @return \Closure(iterable<TKey, TValue>): iterable<TKey, TValue>
|
||||||
|
*/
|
||||||
|
function filter(?callable $fn = null): \Closure
|
||||||
|
{
|
||||||
|
return static function (iterable $input) use ($fn): iterable {
|
||||||
|
$fn = $fn === null ? static fn ($value) => $value : callback($fn);
|
||||||
|
|
||||||
|
return keyedLike($input, (static function () use ($input, $fn) {
|
||||||
|
foreach ($input as $key => $value) {
|
||||||
|
if ($fn($value, $key)) {
|
||||||
|
yield $key => $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drops the values for which `$fn($value, $key)` is truthy. The inverse of {@see filter()}.
|
||||||
|
*
|
||||||
|
* @template TKey
|
||||||
|
* @template TValue
|
||||||
|
*
|
||||||
|
* @param callable(TValue, TKey): mixed $fn
|
||||||
|
*
|
||||||
|
* @return \Closure(iterable<TKey, TValue>): iterable<TKey, TValue>
|
||||||
|
*/
|
||||||
|
function reject(callable $fn): \Closure
|
||||||
|
{
|
||||||
|
return static function (iterable $input) use ($fn): iterable {
|
||||||
|
$fn = callback($fn);
|
||||||
|
|
||||||
|
return filter(static fn ($value, $key) => !$fn($value, $key))($input);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps each value to an iterable with `$fn($value, $key)` and flattens the results one level into a list.
|
||||||
|
*
|
||||||
|
* Arrays give a list array, `Ds\Set` gives `Ds\Set`, anything else gives `Ds\Seq`.
|
||||||
|
*
|
||||||
|
* @template TKey
|
||||||
|
* @template TValue
|
||||||
|
* @template TResult
|
||||||
|
*
|
||||||
|
* @param callable(TValue, TKey): iterable<mixed, TResult> $fn
|
||||||
|
*
|
||||||
|
* @return \Closure(iterable<TKey, TValue>): iterable<int, TResult>
|
||||||
|
*/
|
||||||
|
function flatMap(callable $fn): \Closure
|
||||||
|
{
|
||||||
|
return static function (iterable $input) use ($fn): iterable {
|
||||||
|
$fn = callback($fn);
|
||||||
|
|
||||||
|
return listLike($input, (static function () use ($input, $fn) {
|
||||||
|
foreach ($input as $key => $value) {
|
||||||
|
$result = $fn($value, $key);
|
||||||
|
|
||||||
|
if (!is_iterable($result)) {
|
||||||
|
throw new \UnexpectedValueException(sprintf(
|
||||||
|
'flatMap(): the callback must return an iterable, got %s.',
|
||||||
|
get_debug_type($result),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($result as $item) {
|
||||||
|
yield $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drops the keys: an array gives a list array, `Ds\Map` gives `Ds\Seq`.
|
||||||
|
*
|
||||||
|
* @return \Closure<TValue>(iterable<mixed, TValue>): iterable<int, TValue>
|
||||||
|
*/
|
||||||
|
function values(): \Closure
|
||||||
|
{
|
||||||
|
return static fn (iterable $input): iterable => listLike($input, $input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gives the keys as a list: a list array for an array, a `Ds\Seq` for anything else.
|
||||||
|
*
|
||||||
|
* @return \Closure<TKey>(iterable<TKey, mixed>): iterable<int, TKey>
|
||||||
|
*/
|
||||||
|
function keys(): \Closure
|
||||||
|
{
|
||||||
|
return static function (iterable $input): iterable {
|
||||||
|
if (is_array($input)) {
|
||||||
|
return array_keys($input);
|
||||||
|
}
|
||||||
|
|
||||||
|
$keys = [];
|
||||||
|
foreach ($input as $key => $_) {
|
||||||
|
$keys[] = $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return seqOf($keys);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes duplicate values, keeping the first occurrence. Values are compared strictly, objects by identity.
|
||||||
|
*
|
||||||
|
* @return \Closure<TKey, TValue>(iterable<TKey, TValue>): iterable<TKey, TValue>
|
||||||
|
*/
|
||||||
|
function unique(): \Closure
|
||||||
|
{
|
||||||
|
return uniqueBy(static fn ($value) => $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes values whose `$selector($value, $key)` result was already seen, keeping the first occurrence.
|
||||||
|
* Selected values are compared strictly, objects by identity.
|
||||||
|
*
|
||||||
|
* @template TKey
|
||||||
|
* @template TValue
|
||||||
|
*
|
||||||
|
* @param callable(TValue, TKey): mixed $selector
|
||||||
|
*
|
||||||
|
* @return \Closure(iterable<TKey, TValue>): iterable<TKey, TValue>
|
||||||
|
*/
|
||||||
|
function uniqueBy(callable $selector): \Closure
|
||||||
|
{
|
||||||
|
return static function (iterable $input) use ($selector): iterable {
|
||||||
|
$selector = callback($selector);
|
||||||
|
$seen = seen();
|
||||||
|
|
||||||
|
return keyedLike($input, (static function () use ($input, $selector, $seen) {
|
||||||
|
foreach ($input as $key => $value) {
|
||||||
|
if (!$seen($selector($value, $key))) {
|
||||||
|
yield $key => $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @template TValue
|
||||||
|
*
|
||||||
|
* @param (callable(TValue, TValue): int)|null $comparator
|
||||||
|
*
|
||||||
|
* @return \Closure<TKey>(iterable<TKey, TValue>): iterable<TKey, TValue>
|
||||||
|
*/
|
||||||
|
function sort(?callable $comparator = null): \Closure
|
||||||
|
{
|
||||||
|
return static function (iterable $input) use ($comparator): iterable {
|
||||||
|
// Comparators always take two values, so they are compiled without the ($value, $key) adaptation.
|
||||||
|
$comparator = $comparator === null ? static fn ($a, $b) => $a <=> $b : compile($comparator);
|
||||||
|
|
||||||
|
$entries = entries($input);
|
||||||
|
usort($entries, static fn ($a, $b) => $comparator($a[1], $b[1]));
|
||||||
|
|
||||||
|
return keyedLike($input, unpairs($entries));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* `Ds\Map`. The selector is called once per value. The input is never modified.
|
||||||
|
*
|
||||||
|
* @template TKey
|
||||||
|
* @template TValue
|
||||||
|
*
|
||||||
|
* @param callable(TValue, TKey): mixed $selector
|
||||||
|
*
|
||||||
|
* @return \Closure(iterable<TKey, TValue>): iterable<TKey, TValue>
|
||||||
|
*/
|
||||||
|
function sortBy(callable $selector, bool $descending = false): \Closure
|
||||||
|
{
|
||||||
|
return static function (iterable $input) use ($selector, $descending): iterable {
|
||||||
|
$selector = callback($selector);
|
||||||
|
|
||||||
|
$entries = [];
|
||||||
|
foreach ($input as $key => $value) {
|
||||||
|
$entries[] = [$key, $value, $selector($value, $key)];
|
||||||
|
}
|
||||||
|
|
||||||
|
$direction = $descending ? -1 : 1;
|
||||||
|
usort($entries, static fn ($a, $b) => $direction * ($a[2] <=> $b[2]));
|
||||||
|
|
||||||
|
return keyedLike($input, unpairs($entries));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes `$length` values (all remaining when null) starting at position `$offset`, like `array_slice()`.
|
||||||
|
*
|
||||||
|
* Negative `$offset` counts from the end, negative `$length` stops that many values before the end. Keys of arrays
|
||||||
|
* and `Ds\Map` are preserved. With non-negative arguments a generator is consumed only as far as needed.
|
||||||
|
*
|
||||||
|
* @return \Closure<TKey, TValue>(iterable<TKey, TValue>): iterable<TKey, TValue>
|
||||||
|
*/
|
||||||
|
function slice(int $offset, ?int $length = null): \Closure
|
||||||
|
{
|
||||||
|
return static function (iterable $input) use ($offset, $length): iterable {
|
||||||
|
if ($offset < 0 || ($length !== null && $length < 0)) {
|
||||||
|
return keyedLike($input, unpairs(array_slice(entries($input), $offset, $length)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyedLike($input, (static function () use ($input, $offset, $length) {
|
||||||
|
if ($length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$position = 0;
|
||||||
|
$taken = 0;
|
||||||
|
foreach ($input as $key => $value) {
|
||||||
|
if ($position++ < $offset) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield $key => $value;
|
||||||
|
|
||||||
|
if (++$taken === $length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes the first `$n` values. See {@see slice()}.
|
||||||
|
*
|
||||||
|
* @return \Closure<TKey, TValue>(iterable<TKey, TValue>): iterable<TKey, TValue>
|
||||||
|
*/
|
||||||
|
function take(int $n): \Closure
|
||||||
|
{
|
||||||
|
return slice(0, $n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skips the first `$n` values. See {@see slice()}.
|
||||||
|
*
|
||||||
|
* @return \Closure<TKey, TValue>(iterable<TKey, TValue>): iterable<TKey, TValue>
|
||||||
|
*/
|
||||||
|
function skip(int $n): \Closure
|
||||||
|
{
|
||||||
|
return slice($n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Puts `$values` before the values of the input, as a list (keys are dropped).
|
||||||
|
*
|
||||||
|
* @template TValue
|
||||||
|
*
|
||||||
|
* @param TValue ...$values
|
||||||
|
*
|
||||||
|
* @return \Closure(iterable<mixed, TValue>): iterable<int, TValue>
|
||||||
|
*/
|
||||||
|
function prepend(mixed ...$values): \Closure
|
||||||
|
{
|
||||||
|
return static fn (iterable $input): iterable => listLike($input, (static function () use ($input, $values) {
|
||||||
|
yield from $values;
|
||||||
|
yield from $input;
|
||||||
|
})());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Puts `$values` after the values of the input, as a list (keys are dropped).
|
||||||
|
*
|
||||||
|
* @template TValue
|
||||||
|
*
|
||||||
|
* @param TValue ...$values
|
||||||
|
*
|
||||||
|
* @return \Closure(iterable<mixed, TValue>): iterable<int, TValue>
|
||||||
|
*/
|
||||||
|
function append(mixed ...$values): \Closure
|
||||||
|
{
|
||||||
|
return concat($values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Puts the values of `$other` after the values of the input, as a list (keys are dropped).
|
||||||
|
*
|
||||||
|
* @template TValue
|
||||||
|
*
|
||||||
|
* @param iterable<mixed, TValue> $other
|
||||||
|
*
|
||||||
|
* @return \Closure(iterable<mixed, TValue>): iterable<int, TValue>
|
||||||
|
*/
|
||||||
|
function concat(iterable $other): \Closure
|
||||||
|
{
|
||||||
|
return static fn (iterable $input): iterable => listLike($input, (static function () use ($input, $other) {
|
||||||
|
yield from $input;
|
||||||
|
yield from $other;
|
||||||
|
})());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges `$other` into the input by key, later keys win. Integer keys of arrays are kept, unlike `array_merge()`.
|
||||||
|
*
|
||||||
|
* Arrays and `Ds\Map` are merged by key; for `Ds\Seq`, `Ds\Set` and other iterables the values are appended.
|
||||||
|
*
|
||||||
|
* @template TKey
|
||||||
|
* @template TValue
|
||||||
|
*
|
||||||
|
* @param iterable<TKey, TValue> $other
|
||||||
|
*
|
||||||
|
* @return \Closure(iterable<TKey, TValue>): iterable<TKey, TValue>
|
||||||
|
*/
|
||||||
|
function merge(iterable $other): \Closure
|
||||||
|
{
|
||||||
|
return static fn (iterable $input): iterable => keyedLike($input, (static function () use ($input, $other) {
|
||||||
|
yield from $input;
|
||||||
|
yield from $other;
|
||||||
|
})());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls `$fn($value, $key)` for each value, for its side effects, and returns the input unchanged.
|
||||||
|
*
|
||||||
|
* The return value of `$fn` is ignored. Arrays and php-ds collections are returned as they are; any other
|
||||||
|
* iterable is consumed and its values are returned as a `Ds\Seq` (or a list array without php-ds).
|
||||||
|
*
|
||||||
|
* @template TKey
|
||||||
|
* @template TValue
|
||||||
|
*
|
||||||
|
* @param callable(TValue, TKey): mixed $fn
|
||||||
|
*
|
||||||
|
* @return \Closure(iterable<TKey, TValue>): iterable<TKey, TValue>
|
||||||
|
*/
|
||||||
|
function each(callable $fn): \Closure
|
||||||
|
{
|
||||||
|
return static function (iterable $input) use ($fn): iterable {
|
||||||
|
$fn = callback($fn);
|
||||||
|
|
||||||
|
$reusable = is_array($input) || $input instanceof \Ds\Seq || $input instanceof \Ds\Map || $input instanceof \Ds\Set;
|
||||||
|
if ($reusable) {
|
||||||
|
foreach ($input as $key => $value) {
|
||||||
|
$fn($value, $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $input;
|
||||||
|
}
|
||||||
|
|
||||||
|
return listLike($input, (static function () use ($input, $fn) {
|
||||||
|
foreach ($input as $key => $value) {
|
||||||
|
$fn($value, $key);
|
||||||
|
yield $value;
|
||||||
|
}
|
||||||
|
})());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias of {@see each()}.
|
||||||
|
*
|
||||||
|
* @template TKey
|
||||||
|
* @template TValue
|
||||||
|
*
|
||||||
|
* @param callable(TValue, TKey): mixed $fn
|
||||||
|
*
|
||||||
|
* @return \Closure(iterable<TKey, TValue>): iterable<TKey, TValue>
|
||||||
|
*/
|
||||||
|
function tap(callable $fn): \Closure
|
||||||
|
{
|
||||||
|
return each($fn);
|
||||||
|
}
|
||||||
404
tests/Collections/TransformsTest.php
Normal file
404
tests/Collections/TransformsTest.php
Normal file
@ -0,0 +1,404 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Copyright 2026 Kacper Donat
|
||||||
|
*
|
||||||
|
* @author Kacper "Kadet" Donat <kacper@kadet.net>
|
||||||
|
*
|
||||||
|
* Full license available in separate LICENSE file
|
||||||
|
*/
|
||||||
|
|
||||||
|
use Ds\Map;
|
||||||
|
use Ds\Seq;
|
||||||
|
use Ds\Set;
|
||||||
|
use Kadet\Functional\Collections as c;
|
||||||
|
use function Kadet\Functional\partial;
|
||||||
|
use const Kadet\Functional\_;
|
||||||
|
|
||||||
|
class TransformsTest extends \PHPUnit\Framework\TestCase
|
||||||
|
{
|
||||||
|
private static function gen(iterable $entries): \Generator
|
||||||
|
{
|
||||||
|
foreach ($entries as $key => $value) {
|
||||||
|
yield $key => $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assertSeq(array $expected, $actual)
|
||||||
|
{
|
||||||
|
$this->assertInstanceOf(Seq::class, $actual);
|
||||||
|
$this->assertSame($expected, $actual->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assertMap(array $expected, $actual)
|
||||||
|
{
|
||||||
|
$this->assertInstanceOf(Map::class, $actual);
|
||||||
|
$this->assertSame($expected, $actual->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
// map
|
||||||
|
|
||||||
|
public function testMapOnArrayPreservesKeys()
|
||||||
|
{
|
||||||
|
$this->assertSame(['a' => 2, 'b' => 4], c\map(fn ($v) => $v * 2)(['a' => 1, 'b' => 2]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMapOnSeq()
|
||||||
|
{
|
||||||
|
$this->assertSeq([2, 4], c\map(fn ($v) => $v * 2)(new Seq([1, 2])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMapOnMapPassesValueThenKey()
|
||||||
|
{
|
||||||
|
$this->assertEquals(new Map(['a' => 'a:1']), c\map(fn ($v, $k) => "$k:$v")(new Map(['a' => 1])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMapOnGeneratorGivesSeq()
|
||||||
|
{
|
||||||
|
$this->assertSeq([2, 4], c\map(fn ($v) => $v * 2)(self::gen([1, 2])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMapOnGeneratorPassesGeneratorKeys()
|
||||||
|
{
|
||||||
|
$this->assertSeq(['x:1', 'y:2'], c\map(fn ($v, $k) => "$k:$v")(self::gen(['x' => 1, 'y' => 2])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMapOnSetFollowsSetMap()
|
||||||
|
{
|
||||||
|
$result = c\map(fn ($v) => $v % 2)(new Set([1, 2, 3]));
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Set::class, $result);
|
||||||
|
$this->assertSame([1, 0], $result->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMapWithInternalFunctionGetsValueOnly()
|
||||||
|
{
|
||||||
|
$this->assertSame(['a' => 'x'], c\map(trim(...))(['a' => ' x ']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTransformsWorkWithDecoratedCallback()
|
||||||
|
{
|
||||||
|
$this->assertSame(['aa'], c\map(partial(str_repeat(...), _, 2))(['a']));
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter / reject
|
||||||
|
|
||||||
|
public function testFilterOnSeqReindexes()
|
||||||
|
{
|
||||||
|
$this->assertSame([1, 2], c\filter()(new Seq([0, 1, '', 2]))->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFilterOnArrayPreservesKeys()
|
||||||
|
{
|
||||||
|
$this->assertSame([1 => 1, 3 => 2], c\filter()([0, 1, '', 2]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFilterOnMapPassesValueThenKey()
|
||||||
|
{
|
||||||
|
$this->assertMap(['b' => 2], c\filter(fn ($v, $k) => $k === 'b')(new Map(['a' => 1, 'b' => 2])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFilterOnGeneratorGivesSeq()
|
||||||
|
{
|
||||||
|
$this->assertSeq([2], c\filter(fn ($v) => $v % 2 === 0)(self::gen([1, 2, 3])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFilterWithInternalPredicate()
|
||||||
|
{
|
||||||
|
$this->assertSame([1 => 'a'], c\filter(is_string(...))([1, 'a']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testReject()
|
||||||
|
{
|
||||||
|
$odd = fn ($v) => $v % 2 === 1;
|
||||||
|
|
||||||
|
$this->assertSame([1 => 2], c\reject($odd)([1, 2, 3]));
|
||||||
|
$this->assertSeq([2], c\reject($odd)(new Seq([1, 2, 3])));
|
||||||
|
$this->assertMap(['b' => 2], c\reject($odd)(new Map(['a' => 1, 'b' => 2])));
|
||||||
|
$this->assertSeq([2], c\reject($odd)(self::gen([1, 2, 3])));
|
||||||
|
}
|
||||||
|
|
||||||
|
// flatMap
|
||||||
|
|
||||||
|
public function testFlatMap()
|
||||||
|
{
|
||||||
|
$pair = fn ($v) => [$v, $v * 10];
|
||||||
|
|
||||||
|
$this->assertSame([1, 10, 2, 20], c\flatMap($pair)(['a' => 1, 'b' => 2]));
|
||||||
|
$this->assertSeq([1, 10, 2, 20], c\flatMap($pair)(new Seq([1, 2])));
|
||||||
|
$this->assertSeq([1, 10, 2, 20], c\flatMap($pair)(new Map(['a' => 1, 'b' => 2])));
|
||||||
|
$this->assertSeq([1, 10, 2, 20], c\flatMap($pair)(self::gen([1, 2])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFlatMapFlattensOneLevelOnly()
|
||||||
|
{
|
||||||
|
$this->assertSame([[1], 2], c\flatMap(fn ($v) => $v)([[[1]], [2]]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFlatMapAcceptsIterableResults()
|
||||||
|
{
|
||||||
|
$this->assertSame(['a', 'b', 'c'], c\flatMap(fn ($v) => new Seq(str_split($v)))(['ab', 'c']));
|
||||||
|
}
|
||||||
|
|
||||||
|
// values / keys
|
||||||
|
|
||||||
|
public function testValues()
|
||||||
|
{
|
||||||
|
$this->assertSame([1, 2], c\values()(['a' => 1, 'b' => 2]));
|
||||||
|
$this->assertSeq([1, 2], c\values()(new Seq([1, 2])));
|
||||||
|
$this->assertSeq([1, 2], c\values()(new Map(['a' => 1, 'b' => 2])));
|
||||||
|
$this->assertSeq([1, 2], c\values()(self::gen(['a' => 1, 'b' => 2])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testKeys()
|
||||||
|
{
|
||||||
|
$this->assertSame(['a', 'b'], c\keys()(['a' => 1, 'b' => 2]));
|
||||||
|
$this->assertSeq([0, 1], c\keys()(new Seq([5, 6])));
|
||||||
|
$this->assertSeq(['a', 'b'], c\keys()(new Map(['a' => 1, 'b' => 2])));
|
||||||
|
$this->assertSeq(['a', 'b'], c\keys()(self::gen(['a' => 1, 'b' => 2])));
|
||||||
|
}
|
||||||
|
|
||||||
|
// unique / uniqueBy
|
||||||
|
|
||||||
|
public function testUnique()
|
||||||
|
{
|
||||||
|
$this->assertSame([0 => 1, 1 => '1', 3 => 2], c\unique()([1, '1', 1, 2]));
|
||||||
|
$this->assertSeq([1, '1', 2], c\unique()(new Seq([1, '1', 1, 2])));
|
||||||
|
$this->assertMap(['a' => 1, 'c' => 2], c\unique()(new Map(['a' => 1, 'b' => 1, 'c' => 2])));
|
||||||
|
$this->assertSeq([1, 2], c\unique()(self::gen([1, 2, 2, 1])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUniqueComparesObjectsByIdentity()
|
||||||
|
{
|
||||||
|
$a = new \stdClass();
|
||||||
|
$b = new \stdClass();
|
||||||
|
|
||||||
|
$this->assertSame([$a, $b], array_values(c\unique()([$a, $b, $a])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUniqueComparesFloatsAndArraysStrictly()
|
||||||
|
{
|
||||||
|
$this->assertSame([0 => 1.0, 1 => 1, 2 => [1], 4 => ['1']], c\unique()([1.0, 1, [1], 1.0, ['1'], [1]]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUniqueByKeepsFirst()
|
||||||
|
{
|
||||||
|
$items = [['id' => 1, 'n' => 'a'], ['id' => 1, 'n' => 'b'], ['id' => 2, 'n' => 'c']];
|
||||||
|
|
||||||
|
$this->assertSame(['a', 'c'], array_column(c\uniqueBy(fn ($o) => $o['id'])($items), 'n'));
|
||||||
|
$this->assertSame([0, 2], array_keys(c\uniqueBy(fn ($o) => $o['id'])($items)));
|
||||||
|
$this->assertSame(
|
||||||
|
['a', 'c'],
|
||||||
|
c\uniqueBy(fn ($o) => $o['id'])(new Seq($items))->map(fn ($o) => $o['n'])->toArray(),
|
||||||
|
);
|
||||||
|
$this->assertSame(['x', 'z'], c\uniqueBy(fn ($o) => $o['id'])(new Map(['x' => $items[0], 'y' => $items[1], 'z' => $items[2]]))->keys()->toArray());
|
||||||
|
$this->assertCount(2, c\uniqueBy(fn ($o) => $o['id'])(self::gen($items)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUniqueByKeepsTemporaryObjectsDistinct()
|
||||||
|
{
|
||||||
|
$this->assertSame([1, 2, 3], c\uniqueBy(fn ($v) => new \stdClass())([1, 2, 3]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort / sortBy
|
||||||
|
|
||||||
|
public function testSort()
|
||||||
|
{
|
||||||
|
$this->assertSame([1 => 1, 2 => 2, 0 => 3], c\sort()([3, 1, 2]));
|
||||||
|
$this->assertSeq([1, 2, 3], c\sort()(new Seq([3, 1, 2])));
|
||||||
|
$this->assertMap(['b' => 1, 'a' => 3], c\sort()(new Map(['a' => 3, 'b' => 1])));
|
||||||
|
$this->assertSeq([1, 2, 3], c\sort()(self::gen([3, 1, 2])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSortWithComparator()
|
||||||
|
{
|
||||||
|
$this->assertSame([3, 2, 1], array_values(c\sort(fn ($a, $b) => $b <=> $a)([1, 3, 2])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSortWithInternalComparatorGetsBothArguments()
|
||||||
|
{
|
||||||
|
$this->assertSame(['a', 'b', 'c'], array_values(c\sort(strcmp(...))(['c', 'a', 'b'])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSortIsStable()
|
||||||
|
{
|
||||||
|
$items = [['k' => 1, 'n' => 'a'], ['k' => 0, 'n' => 'b'], ['k' => 1, 'n' => 'c'], ['k' => 0, 'n' => 'd']];
|
||||||
|
|
||||||
|
$this->assertSame(['b', 'd', 'a', 'c'], array_column(c\sort(fn ($a, $b) => $a['k'] <=> $b['k'])($items), 'n'));
|
||||||
|
$this->assertSame(['a', 'c', 'b', 'd'], array_column(c\sortBy(fn ($o) => $o['k'], descending: true)($items), 'n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSortBy()
|
||||||
|
{
|
||||||
|
$len = fn ($v) => strlen($v);
|
||||||
|
|
||||||
|
$this->assertSame([1 => 'a', 2 => 'bb', 0 => 'ccc'], c\sortBy($len)(['ccc', 'a', 'bb']));
|
||||||
|
$this->assertSame([0 => 'ccc', 2 => 'bb', 1 => 'a'], c\sortBy($len, true)(['ccc', 'a', 'bb']));
|
||||||
|
$this->assertSeq(['a', 'bb', 'ccc'], c\sortBy($len)(new Seq(['ccc', 'a', 'bb'])));
|
||||||
|
$this->assertMap(['y' => 'a', 'x' => 'ccc'], c\sortBy($len)(new Map(['x' => 'ccc', 'y' => 'a'])));
|
||||||
|
$this->assertSeq(['a', 'bb', 'ccc'], c\sortBy($len)(self::gen(['ccc', 'a', 'bb'])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSortByPassesKey()
|
||||||
|
{
|
||||||
|
$this->assertSame(['b' => 1, 'a' => 2], c\sortBy(fn ($v, $k) => $k, true)(['a' => 2, 'b' => 1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSortDoesNotMutateInput()
|
||||||
|
{
|
||||||
|
$seq = new Seq([3, 1, 2]);
|
||||||
|
$map = new Map(['a' => 3, 'b' => 1]);
|
||||||
|
|
||||||
|
c\sortBy(fn ($v) => $v)($seq);
|
||||||
|
c\sort()($seq);
|
||||||
|
c\sort()($map);
|
||||||
|
|
||||||
|
$this->assertSame([3, 1, 2], $seq->toArray());
|
||||||
|
$this->assertSame(['a' => 3, 'b' => 1], $map->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
// slice / take / skip
|
||||||
|
|
||||||
|
public function testSlice()
|
||||||
|
{
|
||||||
|
$this->assertSame(['b' => 2, 'c' => 3], c\slice(1, 2)(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]));
|
||||||
|
$this->assertSeq([2, 3], c\slice(1, 2)(new Seq([1, 2, 3, 4])));
|
||||||
|
$this->assertMap(['b' => 2, 'c' => 3], c\slice(1, 2)(new Map(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4])));
|
||||||
|
$this->assertSeq([2, 3], c\slice(1, 2)(self::gen([1, 2, 3, 4])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSliceWithNegativeArguments()
|
||||||
|
{
|
||||||
|
$this->assertSame([2 => 3, 3 => 4], c\slice(-2)([1, 2, 3, 4]));
|
||||||
|
$this->assertSame([1 => 2], c\slice(1, -2)([1, 2, 3, 4]));
|
||||||
|
$this->assertSeq([3], c\slice(-2, -1)(self::gen([1, 2, 3, 4])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTake()
|
||||||
|
{
|
||||||
|
$this->assertSame(['a' => 1], c\take(1)(['a' => 1, 'b' => 2]));
|
||||||
|
$this->assertSeq([1, 2], c\take(2)(new Seq([1, 2, 3])));
|
||||||
|
$this->assertMap(['a' => 1], c\take(1)(new Map(['a' => 1, 'b' => 2])));
|
||||||
|
$this->assertSeq([], c\take(0)(new Seq([1, 2, 3])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTakeStopsConsumingGenerator()
|
||||||
|
{
|
||||||
|
$infinite = (function () {
|
||||||
|
for ($i = 1; ; $i++) {
|
||||||
|
yield $i;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
$this->assertSeq([1, 2, 3], c\take(3)($infinite));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSkip()
|
||||||
|
{
|
||||||
|
$this->assertSame(['b' => 2], c\skip(1)(['a' => 1, 'b' => 2]));
|
||||||
|
$this->assertSeq([3], c\skip(2)(new Seq([1, 2, 3])));
|
||||||
|
$this->assertMap(['b' => 2], c\skip(1)(new Map(['a' => 1, 'b' => 2])));
|
||||||
|
$this->assertSeq([2, 3], c\skip(1)(self::gen([1, 2, 3])));
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepend / append / concat / merge
|
||||||
|
|
||||||
|
public function testPrepend()
|
||||||
|
{
|
||||||
|
$this->assertSame([0, 1, 1, 2], c\prepend(0, 1)(['a' => 1, 'b' => 2]));
|
||||||
|
$this->assertSeq([0, 1, 2], c\prepend(0)(new Seq([1, 2])));
|
||||||
|
$this->assertSeq([0, 1, 2], c\prepend(0)(new Map(['a' => 1, 'b' => 2])));
|
||||||
|
$this->assertSeq([0, 1, 2], c\prepend(0)(self::gen([1, 2])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAppend()
|
||||||
|
{
|
||||||
|
$this->assertSame([1, 2, 3, 4], c\append(3, 4)(['a' => 1, 'b' => 2]));
|
||||||
|
$this->assertSeq([1, 2, 3], c\append(3)(new Seq([1, 2])));
|
||||||
|
$this->assertSeq([1, 2, 3], c\append(3)(new Map(['a' => 1, 'b' => 2])));
|
||||||
|
$this->assertSeq([1, 2, 3], c\append(3)(self::gen([1, 2])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConcat()
|
||||||
|
{
|
||||||
|
$this->assertSame([1, 2, 3, 4], c\concat(['x' => 3, 'a' => 4])(['a' => 1, 'b' => 2]));
|
||||||
|
$this->assertSeq([1, 2, 3], c\concat(self::gen([3]))(new Seq([1, 2])));
|
||||||
|
$this->assertSeq([1, 2, 3], c\concat([3])(new Map(['a' => 1, 'b' => 2])));
|
||||||
|
$this->assertSeq([1, 2, 3], c\concat(new Seq([3]))(self::gen([1, 2])));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testListOperationsOnSetGiveSet()
|
||||||
|
{
|
||||||
|
$result = c\append(2, 3)(new Set([1, 2]));
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Set::class, $result);
|
||||||
|
$this->assertSame([1, 2, 3], $result->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMerge()
|
||||||
|
{
|
||||||
|
$this->assertSame(['a' => 1, 'b' => 3, 'c' => 4], c\merge(['b' => 3, 'c' => 4])(['a' => 1, 'b' => 2]));
|
||||||
|
$this->assertSame([0 => 'x', 1 => 'b', 5 => 'c'], c\merge([0 => 'x', 5 => 'c'])(['a', 'b']));
|
||||||
|
$this->assertSeq([1, 2, 3], c\merge([3])(new Seq([1, 2])));
|
||||||
|
$this->assertMap(['a' => 1, 'b' => 3, 'c' => 4], c\merge(new Map(['b' => 3, 'c' => 4]))(new Map(['a' => 1, 'b' => 2])));
|
||||||
|
$this->assertSeq([1, 2, 3], c\merge([3])(self::gen([1, 2])));
|
||||||
|
}
|
||||||
|
|
||||||
|
// each / tap
|
||||||
|
|
||||||
|
public function testEachDoesNotReplaceValues()
|
||||||
|
{
|
||||||
|
$calls = 0;
|
||||||
|
$seq = new Seq([1, 2, 3]);
|
||||||
|
|
||||||
|
$result = c\each(function ($v) use (&$calls) { $calls++; return 'x'; })($seq);
|
||||||
|
|
||||||
|
$this->assertSame([1, 2, 3], $result->toArray());
|
||||||
|
$this->assertSame([1, 2, 3], $seq->toArray());
|
||||||
|
$this->assertSame(3, $calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEachPassesValueThenKey()
|
||||||
|
{
|
||||||
|
$seen = [];
|
||||||
|
$fn = function ($v, $k) use (&$seen) { $seen[] = "$k:$v"; };
|
||||||
|
|
||||||
|
$this->assertSame(['a' => 1], c\each($fn)(['a' => 1]));
|
||||||
|
$map = new Map(['b' => 2]);
|
||||||
|
$this->assertSame($map, c\each($fn)($map));
|
||||||
|
$this->assertSeq([3], c\each($fn)(self::gen(['c' => 3])));
|
||||||
|
|
||||||
|
$this->assertSame(['a:1', 'b:2', 'c:3'], $seen);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTapIsEach()
|
||||||
|
{
|
||||||
|
$seen = [];
|
||||||
|
|
||||||
|
$this->assertSame([1, 2], c\tap(function ($v) use (&$seen) { $seen[] = $v; })([1, 2]));
|
||||||
|
$this->assertSame([1, 2], $seen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// object keys (Review Focus 3)
|
||||||
|
|
||||||
|
public function testMapWithObjectKeysKeepsKeys()
|
||||||
|
{
|
||||||
|
$k1 = new \stdClass();
|
||||||
|
$k2 = new \stdClass();
|
||||||
|
$map = new Map();
|
||||||
|
$map->put($k1, 2);
|
||||||
|
$map->put($k2, 1);
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
c\map(fn ($v) => $v * 10),
|
||||||
|
c\filter(),
|
||||||
|
c\sortBy(fn ($v) => $v),
|
||||||
|
c\uniqueBy(fn ($v, $k) => $k),
|
||||||
|
] as $transform) {
|
||||||
|
$result = $transform($map);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Map::class, $result);
|
||||||
|
$this->assertTrue($result->hasKey($k1));
|
||||||
|
$this->assertTrue($result->hasKey($k2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user