From c8a31db07c149646263df2f72e26631f6102146d Mon Sep 17 00:00:00 2001 From: Gturpin <34162504+gturpin-dev@users.noreply.github.com> Date: Wed, 13 Nov 2024 11:58:32 +0100 Subject: [PATCH] feat(support): add methods `reduce`, `chunk` and `findKey` to `ArrayHelper` (#720) Co-authored-by: Karel Faille Co-authored-by: Enzo Innocenzi --- src/Tempest/Support/src/ArrayHelper.php | 69 +++++++ src/Tempest/Support/tests/ArrayHelperTest.php | 181 ++++++++++++++++++ 2 files changed, 250 insertions(+) diff --git a/src/Tempest/Support/src/ArrayHelper.php b/src/Tempest/Support/src/ArrayHelper.php index dedc0ae11..f1465d030 100644 --- a/src/Tempest/Support/src/ArrayHelper.php +++ b/src/Tempest/Support/src/ArrayHelper.php @@ -44,6 +44,75 @@ public function __construct( } } + /** + * Finds a value in the array and return the corresponding key if successful. + * + * @param (Closure(TValue, TKey): bool)|mixed $value The value to search for, a Closure will find the first item that returns true. + * @param bool $strict Whether to use strict comparison. + * + * @return array-key|null The key for `$value` if found, `null` otherwise. + */ + public function findKey(mixed $value, bool $strict = false): int|string|null + { + if (! $value instanceof Closure) { + $search = array_search($value, $this->array, $strict); + + return $search === false ? null : $search; // Keep empty values but convert false to null + } + + foreach ($this->array as $key => $item) { + if ($value($item, $key) === true) { + return $key; + } + } + + return null; + } + + /** + * Chunks the array into chunks of the given size. + * + * @param int $size The size of each chunk. + * @param bool $preserveKeys Whether to preserve the keys of the original array. + * + * @return self + */ + public function chunk(int $size, bool $preserveKeys = true): self + { + if ($size <= 0) { + return new self(); + } + + $chunks = []; + foreach (array_chunk($this->array, $size, $preserveKeys) as $chunk) { + $chunks[] = new self($chunk); + } + + return new self($chunks); + } + + /** + * Reduces the array to a single value using a callback. + * + * @template TReduceInitial + * @template TReduceReturnType + * + * @param callable(TReduceInitial|TReduceReturnType, TValue, TKey): TReduceReturnType $callback + * @param TReduceInitial $initial + * + * @return TReduceReturnType + */ + public function reduce(callable $callback, mixed $initial = null): mixed + { + $result = $initial; + + foreach ($this->array as $key => $value) { + $result = $callback($result, $value, $key); + } + + return $result; + } + /** * Gets a value from the array and remove it. * diff --git a/src/Tempest/Support/tests/ArrayHelperTest.php b/src/Tempest/Support/tests/ArrayHelperTest.php index 666a0155c..91f3246b2 100644 --- a/src/Tempest/Support/tests/ArrayHelperTest.php +++ b/src/Tempest/Support/tests/ArrayHelperTest.php @@ -1381,4 +1381,185 @@ public function test_sort_keys_by_callback(): void actual: $array->sortKeysByCallback(fn ($a, $b) => $a <=> $b)->toArray(), ); } + + public function test_basic_reduce(): void + { + $collection = arr([ + 'first_name' => 'John', + 'last_name' => 'Doe', + 'age' => 42, + ]); + + $this->assertSame( + actual: $collection->reduce(fn ($carry, $value) => $carry . ' ' . $value, 'Hello'), + expected: 'Hello John Doe 42', + ); + } + + public function test_reduce_with_existing_function(): void + { + $collection = arr([ + [1, 2, 2, 3], + [2, 3, 3, 4], + [3, 1, 3, 1], + ]); + + $this->assertSame( + actual: $collection->reduce('max'), + expected: [3, 1, 3, 1], + ); + } + + public function test_empty_array_reduce(): void + { + $this->assertSame( + actual: arr()->reduce(fn ($carry, $value) => $carry . ' ' . $value, 'default'), + expected: 'default', + ); + } + + public function test_chunk(): void + { + $collection = arr([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + + $this->assertSame( + actual: $collection + ->chunk(2, preserveKeys: false) + ->map(fn ($chunk) => $chunk->toArray()) + ->toArray(), + expected: [ + [1, 2], + [3, 4], + [5, 6], + [7, 8], + [9, 10], + ], + ); + + $this->assertSame( + actual: $collection + ->chunk(3, preserveKeys: false) + ->map(fn ($chunk) => $chunk->toArray()) + ->toArray(), + expected: [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [10], + ], + ); + } + + public function test_chunk_preserve_keys(): void + { + $collection = arr([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + + $this->assertSame( + actual: $collection + ->chunk(2) + ->map(fn ($chunk) => $chunk->toArray()) + ->toArray(), + expected: [ + [0 => 1, 1 => 2], + [2 => 3, 3 => 4], + [4 => 5, 5 => 6], + [6 => 7, 7 => 8], + [8 => 9, 9 => 10], + ], + ); + + $this->assertSame( + actual: $collection + ->chunk(3) + ->map(fn ($chunk) => $chunk->toArray()) + ->toArray(), + expected: [ + [0 => 1, 1 => 2, 2 => 3], + [3 => 4, 4 => 5, 5 => 6], + [6 => 7, 7 => 8, 8 => 9], + [9 => 10], + ], + ); + } + + public function test_find_key_with_simple_value(): void + { + $collection = arr(['apple', 'banana', 'orange']); + + $this->assertSame(1, $collection->findKey('banana')); + $this->assertSame(0, $collection->findKey('apple')); + $this->assertNull($collection->findKey('grape')); + } + + public function test_find_key_with_strict_comparison(): void + { + $collection = arr([1, '1', 2, '2']); + + $this->assertSame(0, $collection->findKey(1, strict: false)); + $this->assertSame(0, $collection->findKey('1', strict: false)); + + $this->assertSame(0, $collection->findKey(1, strict: true)); + $this->assertSame(1, $collection->findKey('1', strict: true)); + } + + public function test_find_key_with_closure(): void + { + $collection = arr([ + ['id' => 1, 'name' => 'John'], + ['id' => 2, 'name' => 'Jane'], + ['id' => 3, 'name' => 'Bob'], + ]); + + $result = $collection->findKey(fn ($item) => $item['name'] === 'Jane'); + $this->assertSame(1, $result); + + $result = $collection->findKey(fn ($item, $key) => $key === 2); + $this->assertSame(2, $result); + + $result = $collection->findKey(fn ($item) => $item['name'] === 'Alice'); + $this->assertNull($result); + } + + public function test_find_key_with_string_keys(): void + { + $collection = arr([ + 'first' => 'value1', + 'second' => 'value2', + 'third' => 'value3', + ]); + + $this->assertSame('second', $collection->findKey('value2')); + $this->assertNull($collection->findKey('value4')); + } + + public function test_find_key_with_null_values(): void + { + $collection = arr(['a', null, 'b', '']); + + $this->assertSame(1, $collection->findKey(null)); + $this->assertSame(1, $collection->findKey('')); + } + + public function test_find_key_with_complex_closure(): void + { + $collection = arr([ + ['age' => 25, 'active' => true], + ['age' => 30, 'active' => false], + ['age' => 35, 'active' => true], + ]); + + $result = $collection->findKey(function ($item) { + return $item['age'] > 28 && $item['active'] === true; + }); + + $this->assertSame(2, $result); + } + + public function test_find_key_with_empty_array(): void + { + $collection = arr([]); + + $this->assertNull($collection->findKey('anything')); + $this->assertNull($collection->findKey(fn () => true)); + } }