add collection queries
This commit is contained in:
parent
1cddbe796a
commit
483375240b
@ -27,7 +27,8 @@
|
||||
"./src/Collections/Internal/functions.php",
|
||||
"./src/Collections/Internal/containers.php",
|
||||
"./src/Collections/transforms.php",
|
||||
"./src/Collections/collectors.php"
|
||||
"./src/Collections/collectors.php",
|
||||
"./src/Collections/queries.php"
|
||||
]
|
||||
},
|
||||
"suggest": {
|
||||
|
||||
425
src/Collections/queries.php
Normal file
425
src/Collections/queries.php
Normal file
@ -0,0 +1,425 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright 2026 Kacper Donat
|
||||
*
|
||||
* @author Kacper "Kadet" Donat <kacper@kadet.net>
|
||||
*
|
||||
* Full license available in separate LICENSE file
|
||||
*/
|
||||
|
||||
/**
|
||||
* Collection queries.
|
||||
*
|
||||
* Every function returns a closure that takes any iterable (generators included) and gives a single result, so they
|
||||
* end a pipeline, eager or lazy. They stop reading as soon as the answer is known.
|
||||
*
|
||||
* Callbacks receive `($value, $key)`, also for `Ds\Map`. Inside this namespace `count`, `min`, `max` and `join`
|
||||
* shadow the PHP builtins, so library code uses `\count()` and friends.
|
||||
*/
|
||||
|
||||
namespace Kadet\Functional\Collections;
|
||||
|
||||
use Kadet\Functional\Exception\EmptyCollectionException;
|
||||
|
||||
use function Kadet\Functional\Collections\Internal\callback;
|
||||
use function Kadet\Functional\compile;
|
||||
|
||||
/**
|
||||
* Yields [key, value] pairs of $data from the end, without copying it where the container allows.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @param iterable<mixed, mixed> $data
|
||||
*
|
||||
* @return \Generator<int, array{mixed, mixed}>
|
||||
*/
|
||||
function reversedEntries(iterable $data): \Generator
|
||||
{
|
||||
if (is_array($data)) {
|
||||
for (end($data); ($key = key($data)) !== null; prev($data)) {
|
||||
yield [$key, current($data)];
|
||||
}
|
||||
} elseif ($data instanceof \Ds\Seq || $data instanceof \Ds\Set) {
|
||||
for ($index = \count($data) - 1; $index >= 0; $index--) {
|
||||
yield [$index, $data->get($index)];
|
||||
}
|
||||
} elseif ($data instanceof \Ds\Map) {
|
||||
foreach ($data->pairs()->reversed() as $pair) {
|
||||
yield [$pair->key, $pair->value];
|
||||
}
|
||||
} else {
|
||||
$entries = [];
|
||||
foreach ($data as $key => $value) {
|
||||
$entries[] = [$key, $value];
|
||||
}
|
||||
|
||||
yield from array_reverse($entries);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first value for which `$predicate($value, $key)` is truthy, or the first value without a predicate.
|
||||
*
|
||||
* Returns null when nothing matches; with `$required` it throws instead, so a found `null` stays distinguishable.
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
*
|
||||
* @param (callable(TValue, TKey): mixed)|null $predicate
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): ?TValue
|
||||
*
|
||||
* @throws EmptyCollectionException when `$required` is set and nothing matched
|
||||
*/
|
||||
function first(?callable $predicate = null, bool $required = false): \Closure
|
||||
{
|
||||
return firstOf('first', $predicate, $required);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last value for which `$predicate($value, $key)` is truthy, or the last value without a predicate.
|
||||
*
|
||||
* Arrays and php-ds containers are read from the end, other iterables are read to the end. Returns null when
|
||||
* nothing matches; with `$required` it throws instead.
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
*
|
||||
* @param (callable(TValue, TKey): mixed)|null $predicate
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): ?TValue
|
||||
*
|
||||
* @throws EmptyCollectionException when `$required` is set and nothing matched
|
||||
*/
|
||||
function last(?callable $predicate = null, bool $required = false): \Closure
|
||||
{
|
||||
$predicate = $predicate === null ? null : callback($predicate);
|
||||
|
||||
return static function (iterable $data) use ($predicate, $required) {
|
||||
$reversible = is_array($data) || $data instanceof \Ds\Seq || $data instanceof \Ds\Set
|
||||
|| $data instanceof \Ds\Map;
|
||||
|
||||
if ($reversible) {
|
||||
foreach (reversedEntries($data) as [$key, $value]) {
|
||||
if ($predicate === null || $predicate($value, $key)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$found = false;
|
||||
$result = null;
|
||||
foreach ($data as $key => $value) {
|
||||
if ($predicate === null || $predicate($value, $key)) {
|
||||
$found = true;
|
||||
$result = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($found) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
if ($required) {
|
||||
throw EmptyCollectionException::noMatch('last', $predicate !== null);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias of {@see first()} with a mandatory predicate: returns the first value matching `$predicate($value, $key)`.
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
*
|
||||
* @param callable(TValue, TKey): mixed $predicate
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): ?TValue
|
||||
*
|
||||
* @throws EmptyCollectionException when `$required` is set and nothing matched
|
||||
*/
|
||||
function find(callable $predicate, bool $required = false): \Closure
|
||||
{
|
||||
return firstOf('find', $predicate, $required);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared implementation of {@see first()} and {@see find()}, so that the exception names the called function.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @return \Closure(iterable<mixed, mixed>): mixed
|
||||
*/
|
||||
function firstOf(string $name, ?callable $predicate, bool $required): \Closure
|
||||
{
|
||||
$predicate = $predicate === null ? null : callback($predicate);
|
||||
|
||||
return static function (iterable $data) use ($name, $predicate, $required) {
|
||||
foreach ($data as $key => $value) {
|
||||
if ($predicate === null || $predicate($value, $key)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($required) {
|
||||
throw EmptyCollectionException::noMatch($name, $predicate !== null);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the collection holds `$value`, compared strictly (`Ds\Key::equals` when `$value` implements `Ds\Key`).
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): bool
|
||||
*/
|
||||
function contains(mixed $value): \Closure
|
||||
{
|
||||
$isKey = interface_exists(\Ds\Key::class) && $value instanceof \Ds\Key;
|
||||
|
||||
return static function (iterable $data) use ($value, $isKey): bool {
|
||||
foreach ($data as $item) {
|
||||
if ($isKey ? $item instanceof \Ds\Key && $value->equals($item) : $item === $value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether any value satisfies `$predicate($value, $key)`. False for an empty collection.
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
*
|
||||
* @param callable(TValue, TKey): mixed $predicate
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): bool
|
||||
*/
|
||||
function any(callable $predicate): \Closure
|
||||
{
|
||||
$predicate = callback($predicate);
|
||||
|
||||
return static function (iterable $data) use ($predicate): bool {
|
||||
foreach ($data as $key => $value) {
|
||||
if ($predicate($value, $key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias of {@see any()}.
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
*
|
||||
* @param callable(TValue, TKey): mixed $predicate
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): bool
|
||||
*/
|
||||
function some(callable $predicate): \Closure
|
||||
{
|
||||
return any($predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether every value satisfies `$predicate($value, $key)`. True for an empty collection.
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
*
|
||||
* @param callable(TValue, TKey): mixed $predicate
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): bool
|
||||
*/
|
||||
function every(callable $predicate): \Closure
|
||||
{
|
||||
$predicate = callback($predicate);
|
||||
|
||||
return static function (iterable $data) use ($predicate): bool {
|
||||
foreach ($data as $key => $value) {
|
||||
if (!$predicate($value, $key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the values. Containers that know their size are not iterated.
|
||||
*
|
||||
* @return \Closure(iterable<mixed, mixed>): int
|
||||
*/
|
||||
function count(): \Closure
|
||||
{
|
||||
return static function (iterable $data): int {
|
||||
if (is_array($data) || $data instanceof \Countable) {
|
||||
return \count($data);
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
foreach ($data as $_) {
|
||||
$count++;
|
||||
}
|
||||
|
||||
return $count;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the collection has no values. Reads at most one value.
|
||||
*
|
||||
* @return \Closure(iterable<mixed, mixed>): bool
|
||||
*/
|
||||
function isEmpty(): \Closure
|
||||
{
|
||||
return static function (iterable $data): bool {
|
||||
if (is_array($data) || $data instanceof \Countable) {
|
||||
return \count($data) === 0;
|
||||
}
|
||||
|
||||
foreach ($data as $_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The smallest value (compared with `<=>`), or the smallest of `$selector($value, $key)` when given. Null when empty.
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
*
|
||||
* @param (callable(TValue, TKey): mixed)|null $selector
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): mixed
|
||||
*/
|
||||
function min(?callable $selector = null): \Closure
|
||||
{
|
||||
return extremum(-1, $selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* The largest value (compared with `<=>`), or the largest of `$selector($value, $key)` when given. Null when empty.
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
*
|
||||
* @param (callable(TValue, TKey): mixed)|null $selector
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): mixed
|
||||
*/
|
||||
function max(?callable $selector = null): \Closure
|
||||
{
|
||||
return extremum(1, $selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared implementation of {@see min()} (`$direction` -1) and {@see max()} (1).
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @return \Closure(iterable<mixed, mixed>): mixed
|
||||
*/
|
||||
function extremum(int $direction, ?callable $selector): \Closure
|
||||
{
|
||||
$selector = $selector === null ? static fn ($value) => $value : callback($selector);
|
||||
|
||||
return static function (iterable $data) use ($direction, $selector) {
|
||||
$best = null;
|
||||
$first = true;
|
||||
foreach ($data as $key => $value) {
|
||||
$value = $selector($value, $key);
|
||||
if ($first || ($value <=> $best) === $direction) {
|
||||
$best = $value;
|
||||
$first = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $best;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Folds the collection into a single value with `$fn($carry, $value, $key)`, starting from `$initial`.
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
* @template TCarry
|
||||
*
|
||||
* @param callable(TCarry, TValue, TKey): TCarry $fn
|
||||
* @param TCarry $initial
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): TCarry
|
||||
*/
|
||||
function reduce(callable $fn, mixed $initial = null): \Closure
|
||||
{
|
||||
// The reducer takes the carry first, so it is compiled without the ($value, $key) adaptation.
|
||||
$fn = compile($fn);
|
||||
|
||||
return static function (iterable $data) use ($fn, $initial) {
|
||||
$carry = $initial;
|
||||
foreach ($data as $key => $value) {
|
||||
$carry = $fn($carry, $value, $key);
|
||||
}
|
||||
|
||||
return $carry;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sums the values, or `$selector($value, $key)` when given. An empty collection sums to 0.
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
*
|
||||
* @param (callable(TValue, TKey): int|float)|null $selector
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): (int|float)
|
||||
*/
|
||||
function sum(?callable $selector = null): \Closure
|
||||
{
|
||||
$selector = $selector === null ? static fn ($value) => $value : callback($selector);
|
||||
|
||||
return static function (iterable $data) use ($selector): int|float {
|
||||
$sum = 0;
|
||||
foreach ($data as $key => $value) {
|
||||
$sum += $selector($value, $key);
|
||||
}
|
||||
|
||||
return $sum;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins the values into a string with `$glue`.
|
||||
*
|
||||
* @return \Closure(iterable<mixed, string|\Stringable|int|float>): string
|
||||
*/
|
||||
function join(string $glue = ''): \Closure
|
||||
{
|
||||
return static function (iterable $data) use ($glue): string {
|
||||
$parts = [];
|
||||
foreach ($data as $value) {
|
||||
$parts[] = (string) $value;
|
||||
}
|
||||
|
||||
return implode($glue, $parts);
|
||||
};
|
||||
}
|
||||
18
src/Exception/EmptyCollectionException.php
Normal file
18
src/Exception/EmptyCollectionException.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright 2026 Kacper Donat
|
||||
*
|
||||
* @author Kacper "Kadet" Donat <kacper@kadet.net>
|
||||
*
|
||||
* Full license available in separate LICENSE file
|
||||
*/
|
||||
|
||||
namespace Kadet\Functional\Exception;
|
||||
|
||||
class EmptyCollectionException extends FunctionalException
|
||||
{
|
||||
public static function noMatch(string $function, bool $filtered): self
|
||||
{
|
||||
return new self(sprintf('%s(): %s', $function, $filtered ? 'no element matched' : 'collection is empty'));
|
||||
}
|
||||
}
|
||||
225
tests/Collections/QueriesTest.php
Normal file
225
tests/Collections/QueriesTest.php
Normal file
@ -0,0 +1,225 @@
|
||||
<?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 Kadet\Functional\Collections as c;
|
||||
use Kadet\Functional\Exception\EmptyCollectionException;
|
||||
|
||||
class QueriesTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testFirstReturnsNullOnEmpty()
|
||||
{
|
||||
$this->assertNull(c\first()([]));
|
||||
$this->assertNull(c\first(fn ($v) => $v > 5)([1, 2]));
|
||||
}
|
||||
|
||||
public function testFirstRequiredThrowsWithMessage()
|
||||
{
|
||||
$this->expectException(EmptyCollectionException::class);
|
||||
$this->expectExceptionMessage('first(): no element matched');
|
||||
c\first(fn ($v) => $v > 5, required: true)([1, 2]);
|
||||
}
|
||||
|
||||
public function testFirstRequiredWithoutPredicateSaysEmpty()
|
||||
{
|
||||
$this->expectException(EmptyCollectionException::class);
|
||||
$this->expectExceptionMessage('first(): collection is empty');
|
||||
c\first(required: true)([]);
|
||||
}
|
||||
|
||||
public function testFirstRequiredAcceptsNullMatch()
|
||||
{
|
||||
$this->assertNull(c\first(fn ($v) => $v === null, required: true)([1, null]));
|
||||
}
|
||||
|
||||
public function testFirstFalsyValueFound()
|
||||
{
|
||||
$this->assertSame(0, c\first()([0]));
|
||||
}
|
||||
|
||||
public function testFirstStopsEarly()
|
||||
{
|
||||
$gen = (static function () {
|
||||
yield 1;
|
||||
throw new \LogicException('consumed too far');
|
||||
})();
|
||||
|
||||
$this->assertSame(1, c\first()($gen));
|
||||
}
|
||||
|
||||
public function testFirstPassesKeyToPredicate()
|
||||
{
|
||||
$this->assertSame(2, c\first(fn ($v, $k) => $k === 'b')(['a' => 1, 'b' => 2]));
|
||||
$this->assertSame(2, c\first(fn ($v, $k) => $k === 'b')(new Map(['a' => 1, 'b' => 2])));
|
||||
}
|
||||
|
||||
public function testFindIsFirstWithPredicate()
|
||||
{
|
||||
$this->assertSame(2, c\find(fn ($v) => $v > 1)([1, 2, 3]));
|
||||
|
||||
$this->expectException(EmptyCollectionException::class);
|
||||
$this->expectExceptionMessage('find(): no element matched');
|
||||
c\find(fn ($v) => $v > 5, required: true)([1, 2, 3]);
|
||||
}
|
||||
|
||||
public function testLastOnArrayAndPredicate()
|
||||
{
|
||||
$this->assertSame(3, c\last()([1, 2, 3]));
|
||||
$this->assertSame(2, c\last(fn ($v) => $v < 3)([1, 2, 3]));
|
||||
$this->assertNull(c\last()([]));
|
||||
}
|
||||
|
||||
public function testLastOnGenerator()
|
||||
{
|
||||
$gen = (static function () {
|
||||
yield 'a' => 1;
|
||||
yield 'b' => 2;
|
||||
})();
|
||||
|
||||
$this->assertSame(2, c\last()($gen));
|
||||
}
|
||||
|
||||
public function testLastOnSeqAndSet()
|
||||
{
|
||||
$this->assertSame(3, c\last()(new Seq([1, 2, 3])));
|
||||
$this->assertSame(2, c\last(fn ($v, $k) => $k < 2)(new Seq([1, 2, 3])));
|
||||
$this->assertSame(3, c\last()(new \Ds\Set([1, 2, 3])));
|
||||
$this->assertNull(c\last()(new Seq()));
|
||||
}
|
||||
|
||||
public function testLastOnMapReturnsValue()
|
||||
{
|
||||
$this->assertSame('y', c\last()(new Map(['a' => 'x', 'b' => 'y'])));
|
||||
$this->assertSame('x', c\last(fn ($v, $k) => $k === 'a')(new Map(['a' => 'x', 'b' => 'y'])));
|
||||
}
|
||||
|
||||
public function testLastRequired()
|
||||
{
|
||||
$this->assertNull(c\last(fn ($v) => $v === null, required: true)([null, 1]));
|
||||
|
||||
$this->expectException(EmptyCollectionException::class);
|
||||
$this->expectExceptionMessage('last(): collection is empty');
|
||||
c\last(required: true)([]);
|
||||
}
|
||||
|
||||
public function testLastRequiredNoMatchMessage()
|
||||
{
|
||||
$this->expectException(EmptyCollectionException::class);
|
||||
$this->expectExceptionMessage('last(): no element matched');
|
||||
c\last(fn ($v) => false, required: true)([1]);
|
||||
}
|
||||
|
||||
public function testContainsIsStrict()
|
||||
{
|
||||
$this->assertFalse(c\contains('1')([1]));
|
||||
$this->assertTrue(c\contains(1)([3, 1]));
|
||||
$this->assertTrue(c\contains(null)([1, null]));
|
||||
}
|
||||
|
||||
public function testContainsUsesDsKey()
|
||||
{
|
||||
$key = new class (1) implements \Ds\Key {
|
||||
public function __construct(public int $id) {}
|
||||
|
||||
public function equals(mixed $obj): bool
|
||||
{
|
||||
return $obj->id === $this->id;
|
||||
}
|
||||
|
||||
public function hash(): mixed
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
};
|
||||
|
||||
$this->assertTrue(c\contains($key)([new $key(1)]));
|
||||
$this->assertFalse(c\contains($key)([new $key(2)]));
|
||||
}
|
||||
|
||||
public function testAnySomeEvery()
|
||||
{
|
||||
$this->assertTrue(c\any(fn ($v) => $v > 2)([1, 3]));
|
||||
$this->assertFalse(c\any(fn ($v) => $v > 5)([1, 3]));
|
||||
$this->assertFalse(c\any(fn ($v) => true)([]));
|
||||
$this->assertTrue(c\some(fn ($v) => $v > 2)([1, 3]));
|
||||
$this->assertTrue(c\every(fn ($v) => $v > 0)([1, 3]));
|
||||
$this->assertFalse(c\every(fn ($v) => $v > 1)([1, 3]));
|
||||
}
|
||||
|
||||
public function testEveryOnEmptyIsTrue()
|
||||
{
|
||||
$this->assertTrue(c\every(fn ($v) => false)([]));
|
||||
}
|
||||
|
||||
public function testAnyStopsEarly()
|
||||
{
|
||||
$gen = (static function () {
|
||||
yield 1;
|
||||
throw new \LogicException('consumed too far');
|
||||
})();
|
||||
|
||||
$this->assertTrue(c\any(fn ($v) => $v === 1)($gen));
|
||||
}
|
||||
|
||||
public function testCountAndIsEmpty()
|
||||
{
|
||||
$gen = (static function () {
|
||||
yield 1;
|
||||
yield 2;
|
||||
})();
|
||||
|
||||
$this->assertSame(2, c\count()($gen));
|
||||
$this->assertSame(3, c\count()(new Seq([1, 2, 3])));
|
||||
$this->assertSame(0, c\count()([]));
|
||||
$this->assertTrue(c\isEmpty()([]));
|
||||
$this->assertFalse(c\isEmpty()(new Seq([0])));
|
||||
$this->assertTrue(c\isEmpty()(new Map()));
|
||||
}
|
||||
|
||||
public function testMinMax()
|
||||
{
|
||||
$this->assertSame(1, c\min()([3, 1, 2]));
|
||||
$this->assertSame(3, c\max()([3, 1, 2]));
|
||||
$this->assertNull(c\min()([]));
|
||||
$this->assertNull(c\max()([]));
|
||||
}
|
||||
|
||||
public function testMinWithSelector()
|
||||
{
|
||||
$this->assertSame(1, c\min(fn ($v) => strlen($v))(['ccc', 'a', 'bb']));
|
||||
$this->assertSame(3, c\max(fn ($v) => strlen($v))(['ccc', 'a', 'bb']));
|
||||
}
|
||||
|
||||
public function testReduce()
|
||||
{
|
||||
$this->assertSame(6, c\reduce(fn ($carry, $v) => $carry + $v, 0)([1, 2, 3]));
|
||||
$this->assertSame('a:1,b:2,', c\reduce(fn ($carry, $v, $k) => "$carry$k:$v,", '')(['a' => 1, 'b' => 2]));
|
||||
$this->assertNull(c\reduce(fn ($carry, $v) => $v)([]));
|
||||
}
|
||||
|
||||
public function testSum()
|
||||
{
|
||||
$this->assertSame(6, c\sum()([1, 2, 3]));
|
||||
$this->assertSame(3.5, c\sum()([1, 2.5]));
|
||||
$this->assertSame(4, c\sum(fn ($v) => strlen($v))(['ab', 'cd']));
|
||||
}
|
||||
|
||||
public function testSumEmptyIsZero()
|
||||
{
|
||||
$this->assertSame(0, c\sum()([]));
|
||||
}
|
||||
|
||||
public function testJoin()
|
||||
{
|
||||
$this->assertSame('a, b', c\join(', ')(['a', 'b']));
|
||||
$this->assertSame('12', c\join()(new Seq([1, 2])));
|
||||
$this->assertSame('', c\join(',')([]));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user