From d3ad0aa3b9f934602cb3e3902ebccf10be34d218 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 26 Nov 2023 23:49:50 +0100 Subject: [PATCH] added Iterables --- src/Utils/Iterables.php | 159 +++++++++++++++++++++++ tests/Utils/Iterables.contains().phpt | 15 +++ tests/Utils/Iterables.containsKey().phpt | 15 +++ tests/Utils/Iterables.every().phpt | 97 ++++++++++++++ tests/Utils/Iterables.filter().phpt | 45 +++++++ tests/Utils/Iterables.first().phpt | 43 ++++++ tests/Utils/Iterables.firstKey().phpt | 43 ++++++ tests/Utils/Iterables.map().phpt | 55 ++++++++ tests/Utils/Iterables.some().phpt | 97 ++++++++++++++ 9 files changed, 569 insertions(+) create mode 100644 src/Utils/Iterables.php create mode 100644 tests/Utils/Iterables.contains().phpt create mode 100644 tests/Utils/Iterables.containsKey().phpt create mode 100644 tests/Utils/Iterables.every().phpt create mode 100644 tests/Utils/Iterables.filter().phpt create mode 100644 tests/Utils/Iterables.first().phpt create mode 100644 tests/Utils/Iterables.firstKey().phpt create mode 100644 tests/Utils/Iterables.map().phpt create mode 100644 tests/Utils/Iterables.some().phpt 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 @@ + $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 $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 $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 $iterable + * @param callable(V, K, iterable): 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 $iterable + * @param callable(V, K, iterable): 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 $iterable + * @param callable(V, K, iterable): bool $predicate + * @return \Generator + */ + 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 $iterable + * @param callable(V, K, iterable): R $transformer + * @return \Generator + */ + 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 @@ + 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 @@ + '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 @@ + 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 @@ + 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 @@ + 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 @@ + '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 @@ + '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); +});