From a9a852199e8773dec6ea9add2814a9579f6a2129 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 5 Dec 2023 15:37:42 +0100 Subject: [PATCH] added filter |group --- src/Latte/Essential/CoreExtension.php | 1 + src/Latte/Essential/Filters.php | 32 ++++++++++ tests/filters/group.phpt | 88 +++++++++++++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 tests/filters/group.phpt diff --git a/src/Latte/Essential/CoreExtension.php b/src/Latte/Essential/CoreExtension.php index 0760d87c4..1a4980f7b 100644 --- a/src/Latte/Essential/CoreExtension.php +++ b/src/Latte/Essential/CoreExtension.php @@ -133,6 +133,7 @@ public function getFilters(): array ? [$this->filters, 'firstUpper'] : fn() => throw new RuntimeException('Filter |firstUpper requires mbstring extension.'), 'floor' => [$this->filters, 'floor'], + 'group' => [$this->filters, 'group'], 'implode' => [$this->filters, 'implode'], 'indent' => [$this->filters, 'indent'], 'join' => [$this->filters, 'implode'], diff --git a/src/Latte/Essential/Filters.php b/src/Latte/Essential/Filters.php index 8b1fcacbc..0705bcdf5 100644 --- a/src/Latte/Essential/Filters.php +++ b/src/Latte/Essential/Filters.php @@ -473,6 +473,38 @@ public static function sort(iterable $iterable, ?\Closure $comparison = null): i } + /** + * Groups elements by the element indices and preserves the key association and order. + */ + public static function group(iterable $iterable, string|int|\Closure $by): \Generator + { + $fn = $by instanceof \Closure ? $by : fn($a) => is_array($a) ? $a[$by] : $a->$by; + $keys = $groups = $prevKey = []; + + foreach ($iterable as $k => $v) { + $groupKey = $fn($v, $k); + if (!$groups || $prevKey !== $groupKey) { + $index = array_search($groupKey, $keys, true); + if ($index === false) { + $index = count($keys); + $keys[$index] = $groupKey; + } + $prevKey = $groupKey; + } + $groups[$index][0][] = $k; + $groups[$index][1][] = $v; + } + + foreach ($groups as $index => $pair) { + yield $keys[$index] => (static function () use ($pair): \Generator { + foreach ($pair[1] as $i => $value) { + yield $pair[0][$i] => $value; + } + })(); + } + } + + /** * Returns value clamped to the inclusive range of min and max. */ diff --git a/tests/filters/group.phpt b/tests/filters/group.phpt new file mode 100644 index 000000000..ac86d3685 --- /dev/null +++ b/tests/filters/group.phpt @@ -0,0 +1,88 @@ + 55] => ['k' => 22, 'k2']; + yield ['a' => 66] => (object) ['k' => 22, 'k2']; + yield ['a' => 77] => ['k' => 11]; + yield ['a' => 88] => ['k' => 33]; +} + + +function exportIterator(Traversable $iterator): array +{ + $res = []; + foreach ($iterator as $key => $value) { + $res[] = [$key, $value instanceof Traversable ? exportIterator($value) : $value]; + } + return $res; +} + + +test('array', function () { + Assert::equal( + [ + [22, [ + [0, ['k' => 22, 'k2']], + [1, (object) ['k' => 22, 'k2']], + ]], + [11, [[2, ['k' => 11]]]], + [33, [[3, ['k' => 33]]]], + ], + exportIterator(Filters::group( + [['k' => 22, 'k2'], (object) ['k' => 22, 'k2'], ['k' => 11], ['k' => 33]], + 'k', + )), + ); + Assert::same([], exportIterator(Filters::group([], 'k'))); +}); + + +test('iterator', function () { + Assert::equal( + [ + [22, [ + [['a' => 55], ['k' => 22, 'k2']], + [['a' => 66], (object) ['k' => 22, 'k2']], + ]], + [11, [[['a' => 77], ['k' => 11]]]], + [33, [[['a' => 88], ['k' => 33]]]], + ], + exportIterator(Filters::group(iterator(), 'k')), + ); +}); + + +test('array + callback', function () { + Assert::same( + [[220, [[0, 22]]], [110, [[1, 11]]], [330, [[2, 33]]]], + exportIterator(Filters::group([22, 11, 33], fn($a) => $a * 10)), + ); +}); + + +test('iterator + callback', function () { + Assert::equal( + [ + [-22, [ + [['a' => 55], ['k' => 22, 'k2']], + [['a' => 66], (object) ['k' => 22, 'k2']], + ]], + [-11, [[['a' => 77], ['k' => 11]]]], + [-33, [[['a' => 88], ['k' => 33]]]], + ], + exportIterator(Filters::group(iterator(), fn($a) => -((array) $a)['k'])), + ); +});