diff --git a/src/Utils/Iterables.php b/src/Utils/Iterables.php new file mode 100644 index 000000000..a2a0b53ec --- /dev/null +++ b/src/Utils/Iterables.php @@ -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); + } + } +} diff --git a/tests/Utils/Iterables.contains().phpt b/tests/Utils/Iterables.contains().phpt new file mode 100644 index 000000000..a357d36a3 --- /dev/null +++ b/tests/Utils/Iterables.contains().phpt @@ -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')); diff --git a/tests/Utils/Iterables.containsKey().phpt b/tests/Utils/Iterables.containsKey().phpt new file mode 100644 index 000000000..f6b1d4336 --- /dev/null +++ b/tests/Utils/Iterables.containsKey().phpt @@ -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')); diff --git a/tests/Utils/Iterables.every().phpt b/tests/Utils/Iterables.every().phpt new file mode 100644 index 000000000..3401bdf38 --- /dev/null +++ b/tests/Utils/Iterables.every().phpt @@ -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); +}); diff --git a/tests/Utils/Iterables.filter().phpt b/tests/Utils/Iterables.filter().phpt new file mode 100644 index 000000000..e157f476f --- /dev/null +++ b/tests/Utils/Iterables.filter().phpt @@ -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, + )), +); diff --git a/tests/Utils/Iterables.first().phpt b/tests/Utils/Iterables.first().phpt new file mode 100644 index 000000000..caa096be6 --- /dev/null +++ b/tests/Utils/Iterables.first().phpt @@ -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)); +}); diff --git a/tests/Utils/Iterables.firstKey().phpt b/tests/Utils/Iterables.firstKey().phpt new file mode 100644 index 000000000..1f3d63991 --- /dev/null +++ b/tests/Utils/Iterables.firstKey().phpt @@ -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::firstKey(new ArrayIterator([]))); + Assert::same(0, Iterables::firstKey(new ArrayIterator([null]))); + Assert::same(0, Iterables::firstKey(new ArrayIterator([1, 2, 3]))); + Assert::same(5, Iterables::firstKey(new ArrayIterator([5 => 1, 2, 3]))); +}); + +test('internal array pointer is not affected', function () { + $arr = [1, 2, 3]; + end($arr); + Assert::same(0, Iterables::firstKey($arr)); + Assert::same(3, current($arr)); +}); + +test('with predicate', function () { + Assert::null(Iterables::firstKey([], fn() => true)); + Assert::null(Iterables::firstKey([], fn() => false)); + Assert::null(Iterables::firstKey(['' => 'x'], fn() => false)); + Assert::same(0, Iterables::firstKey([null], fn() => true)); + Assert::null(Iterables::firstKey([null], fn() => false)); + Assert::same(0, Iterables::firstKey([1, 2, 3], fn() => true)); + Assert::null(Iterables::firstKey([1, 2, 3], fn() => false)); + Assert::same(2, Iterables::firstKey([1, 2, 3], fn($v) => $v > 2)); + Assert::same(0, Iterables::firstKey([1, 2, 3], fn($v) => $v < 2)); +}); + +test('predicate arguments', function () { + Iterables::firstKey([2 => 'x'], fn() => Assert::same(['x', 2, [2 => 'x']], func_get_args())); +}); + +test('else', function () { + Assert::same(123, Iterables::firstKey(new ArrayIterator([]), else: fn() => 123)); +}); diff --git a/tests/Utils/Iterables.map().phpt b/tests/Utils/Iterables.map().phpt new file mode 100644 index 000000000..fd8c89a81 --- /dev/null +++ b/tests/Utils/Iterables.map().phpt @@ -0,0 +1,55 @@ +<?php + +/** + * Test: Nette\Utils\Iterables::map() + */ + +declare(strict_types=1); + +use Nette\Utils\Iterables; +use Tester\Assert; + +require __DIR__ . '/../bootstrap.php'; + + +test('', function () { + $arr = new ArrayIterator([]); + $log = []; + $res = Iterables::map( + $arr, + function ($v, $k, $arr) use (&$log) { + $log[] = func_get_args(); + return true; + }, + ); + Assert::same([], iterator_to_array($res)); + Assert::same([], $log); +}); + +test('', function () { + $arr = new ArrayIterator(['a', 'b']); + $log = []; + $res = Iterables::map( + $arr, + function ($v, $k, $arr) use (&$log) { + $log[] = func_get_args(); + return $v . $v; + }, + ); + Assert::same(['aa', 'bb'], iterator_to_array($res)); + Assert::same([['a', 0, $arr], ['b', 1, $arr]], $log); +}); + +test('', function () { + $arr = new ArrayIterator(['x' => 'a', 'y' => 'b']); + $log = []; + $res = Iterables::map( + $arr, + function ($v, $k, $arr) use (&$log) { + $log[] = func_get_args(); + return $v . $v; + }, + ); + Assert::same(['x' => 'aa', 'y' => 'bb'], iterator_to_array($res)); + Assert::same([['a', 'x', $arr], ['b', 'y', $arr]], $log); +}); diff --git a/tests/Utils/Iterables.some().phpt b/tests/Utils/Iterables.some().phpt new file mode 100644 index 000000000..d9ff3090b --- /dev/null +++ b/tests/Utils/Iterables.some().phpt @@ -0,0 +1,97 @@ +<?php + +/** + * Test: Nette\Utils\Iterables::some() + */ + +declare(strict_types=1); + +use Nette\Utils\Iterables; +use Tester\Assert; + +require __DIR__ . '/../bootstrap.php'; + + +test('', function () { + $arr = new ArrayIterator([]); + $log = []; + $res = Iterables::some( + $arr, + function ($v, $k, $arr) use (&$log) { + $log[] = func_get_args(); + return false; + }, + ); + Assert::false($res); + Assert::same([], $log); +}); + +test('', function () { + $arr = new ArrayIterator([]); + $log = []; + $res = Iterables::some( + $arr, + function ($v, $k, $arr) use (&$log) { + $log[] = func_get_args(); + return true; + }, + ); + Assert::false($res); + Assert::same([], $log); +}); + +test('', function () { + $arr = new ArrayIterator(['a', 'b']); + $log = []; + $res = Iterables::some( + $arr, + function ($v, $k, $arr) use (&$log) { + $log[] = func_get_args(); + return false; + }, + ); + Assert::false($res); + Assert::same([['a', 0, $arr], ['b', 1, $arr]], $log); +}); + +test('', function () { + $arr = new ArrayIterator(['a', 'b']); + $log = []; + $res = Iterables::some( + $arr, + function ($v, $k, $arr) use (&$log) { + $log[] = func_get_args(); + return true; + }, + ); + Assert::true($res); + Assert::same([['a', 0, $arr]], $log); +}); + +test('', function () { + $arr = new ArrayIterator(['a', 'b']); + $log = []; + $res = Iterables::some( + $arr, + function ($v, $k, $arr) use (&$log) { + $log[] = func_get_args(); + return $v === 'a'; + }, + ); + Assert::true($res); + Assert::same([['a', 0, $arr]], $log); +}); + +test('', function () { + $arr = new ArrayIterator(['x' => 'a', 'y' => 'b']); + $log = []; + $res = Iterables::some( + $arr, + function ($v, $k, $arr) use (&$log) { + $log[] = func_get_args(); + return $v === 'a'; + }, + ); + Assert::true($res); + Assert::same([['a', 'x', $arr]], $log); +});