Skip to content

Commit

Permalink
added Iterables
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Jan 17, 2024
1 parent 4b3c27e commit d3ad0aa
Show file tree
Hide file tree
Showing 9 changed files with 569 additions and 0 deletions.
159 changes: 159 additions & 0 deletions src/Utils/Iterables.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<?php

/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/

declare(strict_types=1);

namespace Nette\Utils;

use Nette;


/**
* Utilities for iterables.
*/
final class Iterables
{
use Nette\StaticClass;

/**
* Tests for the presence of value.
*/
public static function contains(iterable $iterable, mixed $value): bool
{
foreach ($iterable as $v) {
if ($v === $value) {
return true;
}
}
return false;
}


/**
* Tests for the presence of key.
*/
public static function containsKey(iterable $iterable, mixed $key): bool
{
foreach ($iterable as $k => $v) {
if ($k === $key) {
return true;
}
}
return false;
}


/**
* Returns the first item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
* The $predicate has the signature `function (mixed $value, mixed $key, iterable $iterable): bool`.
* @template T
* @param iterable<T> $iterable
* @return ?T
*/
public static function first(iterable $iterable, ?callable $predicate = null, ?callable $else = null): mixed
{
foreach ($iterable as $k => $v) {
if (!$predicate || $predicate($v, $k, $iterable)) {
return $v;
}
}
return $else ? $else() : null;
}


/**
* Returns the key of first item (matching the specified predicate if given). If there is no such item, it returns result of invoking $else or null.
* The $predicate has the signature `function (mixed $value, mixed $key, iterable $iterable): bool`.
* @template T
* @param iterable<T, mixed> $iterable
* @return ?T
*/
public static function firstKey(iterable $iterable, ?callable $predicate = null, ?callable $else = null): mixed
{
foreach ($iterable as $k => $v) {
if (!$predicate || $predicate($v, $k, $iterable)) {
return $k;
}
}
return $else ? $else() : null;
}


/**
* Tests whether at least one element in the iterator passes the test implemented by the
* provided callback with signature `function (mixed $value, mixed $key, iterable $iterable): bool`.
* @template K
* @template V
* @param iterable<K, V> $iterable
* @param callable(V, K, iterable<K, V>): bool $predicate
*/
public static function some(iterable $iterable, callable $predicate): bool
{
foreach ($iterable as $k => $v) {
if ($predicate($v, $k, $iterable)) {
return true;
}
}
return false;
}


/**
* Tests whether all elements in the iterator pass the test implemented by the provided function,
* which has the signature `function (mixed $value, mixed $key, iterable $iterable): bool`.
* @template K
* @template V
* @param iterable<K, V> $iterable
* @param callable(V, K, iterable<K, V>): bool $predicate
*/
public static function every(iterable $iterable, callable $predicate): bool
{
foreach ($iterable as $k => $v) {
if (!$predicate($v, $k, $iterable)) {
return false;
}
}
return true;
}


/**
* Iterator that filters elements according to a given $predicate. Maintains original keys.
* The callback has the signature `function (mixed $value, mixed $key, iterable $iterable): bool`.
* @template K
* @template V
* @param iterable<K, V> $iterable
* @param callable(V, K, iterable<K, V>): bool $predicate
* @return \Generator<K, V>
*/
public static function filter(iterable $iterable, callable $predicate): \Generator
{
foreach ($iterable as $k => $v) {
if ($predicate($v, $k, $iterable)) {
yield $k => $v;
}
}
}


/**
* Iterator that transforms values by calling $transformer. Maintains original keys.
* The callback has the signature `function (mixed $value, mixed $key, iterable $iterable): bool`.
* @template K
* @template V
* @template R
* @param iterable<K, V> $iterable
* @param callable(V, K, iterable<K, V>): R $transformer
* @return \Generator<K, R>
*/
public static function map(iterable $iterable, callable $transformer): \Generator
{
foreach ($iterable as $k => $v) {
yield $k => $transformer($v, $k, $iterable);
}
}
}
15 changes: 15 additions & 0 deletions tests/Utils/Iterables.contains().phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

use Nette\Utils\Iterables;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


Assert::false(Iterables::contains(new ArrayIterator([]), 'a'));
Assert::true(Iterables::contains(new ArrayIterator(['a']), 'a'));
Assert::true(Iterables::contains(new ArrayIterator([1, 2, 'a']), 'a'));
Assert::false(Iterables::contains(new ArrayIterator([1, 2, 3]), 'a'));
Assert::false(Iterables::contains(new ArrayIterator([1, 2, 3]), '1'));
15 changes: 15 additions & 0 deletions tests/Utils/Iterables.containsKey().phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

use Nette\Utils\Iterables;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


Assert::false(Iterables::containsKey(new ArrayIterator([]), 'a'));
Assert::true(Iterables::containsKey(new ArrayIterator(['a']), 0));
Assert::true(Iterables::containsKey(new ArrayIterator(['x' => 1, 'y' => 2, 'z' => 3]), 'y'));
Assert::false(Iterables::containsKey(new ArrayIterator(['x' => 1, 'y' => 2, 'z' => 3]), ''));
Assert::false(Iterables::containsKey(new ArrayIterator([1, 2, 3]), '1'));
97 changes: 97 additions & 0 deletions tests/Utils/Iterables.every().phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

/**
* Test: Nette\Utils\Iterables::every()
*/

declare(strict_types=1);

use Nette\Utils\Iterables;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


test('', function () {
$arr = new ArrayIterator([]);
$log = [];
$res = Iterables::every(
$arr,
function ($v, $k, $arr) use (&$log) {
$log[] = func_get_args();
return false;
},
);
Assert::true($res);
Assert::same([], $log);
});

test('', function () {
$arr = new ArrayIterator([]);
$log = [];
$res = Iterables::every(
$arr,
function ($v, $k, $arr) use (&$log) {
$log[] = func_get_args();
return true;
},
);
Assert::true($res);
Assert::same([], $log);
});

test('', function () {
$arr = new ArrayIterator(['a', 'b']);
$log = [];
$res = Iterables::every(
$arr,
function ($v, $k, $arr) use (&$log) {
$log[] = func_get_args();
return false;
},
);
Assert::false($res);
Assert::same([['a', 0, $arr]], $log);
});

test('', function () {
$arr = new ArrayIterator(['a', 'b']);
$log = [];
$res = Iterables::every(
$arr,
function ($v, $k, $arr) use (&$log) {
$log[] = func_get_args();
return true;
},
);
Assert::true($res);
Assert::same([['a', 0, $arr], ['b', 1, $arr]], $log);
});

test('', function () {
$arr = new ArrayIterator(['a', 'b']);
$log = [];
$res = Iterables::every(
$arr,
function ($v, $k, $arr) use (&$log) {
$log[] = func_get_args();
return $v === 'a';
},
);
Assert::false($res);
Assert::same([['a', 0, $arr], ['b', 1, $arr]], $log);
});

test('', function () {
$arr = new ArrayIterator(['x' => 'a', 'y' => 'b']);
$log = [];
$res = Iterables::every(
$arr,
function ($v, $k, $arr) use (&$log) {
$log[] = func_get_args();
return true;
},
);
Assert::true($res);
Assert::same([['a', 'x', $arr], ['b', 'y', $arr]], $log);
});
45 changes: 45 additions & 0 deletions tests/Utils/Iterables.filter().phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

/**
* Test: Nette\Utils\Iterables::filter()
*/

declare(strict_types=1);

use Nette\Utils\Iterables;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


Assert::same(
['a' => 1, 'b' => 2],
iterator_to_array(Iterables::filter(
new ArrayIterator(['a' => 1, 'b' => 2, 'c' => 3]),
fn($v) => $v < 3,
)),
);

Assert::same(
['c' => 3],
iterator_to_array(Iterables::filter(
new ArrayIterator(['a' => 1, 'b' => 2, 'c' => 3]),
fn($v, $k) => $k === 'c',
)),
);

Assert::same(
['a' => 1, 'b' => 2, 'c' => 3],
iterator_to_array(Iterables::filter(
$it = new ArrayIterator(['a' => 1, 'b' => 2, 'c' => 3]),
fn($v, $k, $a) => $a === $it,
)),
);

Assert::same(
[],
iterator_to_array(Iterables::filter(
new ArrayIterator([]),
fn() => true,
)),
);
43 changes: 43 additions & 0 deletions tests/Utils/Iterables.first().phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

use Nette\Utils\Iterables;
use Tester\Assert;

require __DIR__ . '/../bootstrap.php';


test('no predicate', function () {
Assert::null(Iterables::first(new ArrayIterator([])));
Assert::null(Iterables::first(new ArrayIterator([null])));
Assert::false(Iterables::first(new ArrayIterator([false])));
Assert::same(1, Iterables::first(new ArrayIterator([1, 2, 3])));
});

test('internal array pointer is not affected', function () {
$arr = [1, 2, 3];
end($arr);
Assert::same(1, Iterables::first($arr));
Assert::same(3, current($arr));
});

test('with predicate', function () {
Assert::null(Iterables::first([], fn() => true));
Assert::null(Iterables::first([], fn() => false));
Assert::null(Iterables::first(['' => 'x'], fn() => false));
Assert::null(Iterables::first([null], fn() => true));
Assert::null(Iterables::first([null], fn() => false));
Assert::same(1, Iterables::first([1, 2, 3], fn() => true));
Assert::null(Iterables::first([1, 2, 3], fn() => false));
Assert::same(3, Iterables::first([1, 2, 3], fn($v) => $v > 2));
Assert::same(1, Iterables::first([1, 2, 3], fn($v) => $v < 2));
});

test('predicate arguments', function () {
Iterables::first([2 => 'x'], fn() => Assert::same(['x', 2, [2 => 'x']], func_get_args()));
});

test('else', function () {
Assert::same(123, Iterables::first(new ArrayIterator([]), else: fn() => 123));
});
Loading

0 comments on commit d3ad0aa

Please sign in to comment.