From 47c9036147b1fd730f0ebe4da672b227fea39b74 Mon Sep 17 00:00:00 2001 From: Roberto Butti Date: Tue, 15 Oct 2024 19:42:25 +0200 Subject: [PATCH] groupByFunction --- README.md | 79 ++++++++++++++++-- src/Traits/FormattableBlock.php | 1 - src/Traits/QueryableBlock.php | 21 ++++- tests/Unit/Traits/QueryableBlockTest.php | 100 ++++++++++++++++------- 4 files changed, 160 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index d473a1f..a37fc53 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,14 @@ PHP Package for Managing Nested Data

+This PHP package provides classes and methods for easily managing, querying, filtering, and setting nested data structures. +The PHP Data Block package offers a streamlined approach to handling nested data, whether you're working with complex JSON data, hierarchical configurations, or deeply nested arrays. -This PHP package provides classes and methods for managing, querying, filtering, and setting nested data structures with ease. -Whether you're working with complex JSON data, hierarchical configurations, or deeply nested arrays, this package offers a streamlined approach to handling nested data easly. +One "core" element of the PHP Data Block package is the Block PHP class. ## The Block class -The **Block** class offers a comprehensive set of methods to create, manage, and access nested data structures. +The **Block** class offers comprehensive methods to create, manage, and access nested data structures. The **Block** class provides various methods, including: @@ -45,15 +46,15 @@ The **Block** class provides various methods, including: ## Installing and using the Block class -For adding to your projects, the Block class with its method and helper you can run the `composer require` command: +For adding to your projects, the Block class with its methods and helpers, you can run the `composer require` command: ```php composer require hi-folks/data-block ``` -> For supporting the development you can star the repository: https://github.com/Hi-Folks/data-block +> To support the development, you can "star" ⭐ the repository: https://github.com/Hi-Folks/data-block -Then in your PHP files, you can import the `HiFolks\DataType\Block` Namespace: +Then, in your PHP files, you can import the `HiFolks\DataType\Block` Namespace: ```php use HiFolks\DataType\Block; @@ -124,7 +125,7 @@ $data->get('avocado.color'); // returns the string "green" For example, with the `$fruitsArray` sample data, the `$data->get("avocado")` is: - an array; -- has 5 elements; +- has five elements; For example, the `$data->get("avocado.color")` is: - a string; @@ -186,7 +187,7 @@ The `getFormattedByte()` method retrieves and formats a byte value from a specif Parameters: - `$path` (string): The path to the field containing the byte value (e.g., "assets.0.total_bytes"). -- `$precision` (int): (Optional) Number of decimal places to include in the formatted result. Default is 2. +- `$precision` (int): (Optional) Number of decimal places to include in the formatted result. The default is 2. Example usage: @@ -199,12 +200,13 @@ $data1->getFormattedByte("assets.1.total_bytes", 0); // Returns "2 GB" Key Features: - Automatic unit conversion: converts bytes into appropriate units (e.g., KB, MB, GB) based on the size. -- customizable precision: you can specify the number of decimal places for the output, making it flexible for various use cases. +- Customizable precision: you can specify the number of decimal places for the output, making it flexible for various use cases. ### The `getString()` method The `getString()` method retrieves the value of a specified field as a string from a data block. If the field does not exist or is null, it returns a default value, which can be customized. Parameters: + - `$path` (string): The path to the field (e.g., "0.commit.author.date"). - `$default` (string): (Optional) The default value to return if the field doesn't exist. Defaults to an empty string (""). @@ -653,6 +655,65 @@ $grouped->dumpJson(); */ ``` +### The `groupByFunction()` method + +The `groupByFunction()` method allows you to group items from an Block based on a grouping logic provided by a callback function (closure). The function returns an associative array where the keys represent groupings defined by the callback, and the values are arrays of elements that belong to each group. + +```php +$fruits = [ + ['name' => 'Apple', 'type' => 'Citrus', 'quantity' => 15], + ['name' => 'Banana', 'type' => 'Tropical', 'quantity' => 10], + ['name' => 'Orange', 'type' => 'Citrus', 'quantity' => 8], + ['name' => 'Mango', 'type' => 'Tropical', 'quantity' => 5], + ['name' => 'Lemon', 'type' => 'Citrus', 'quantity' => 12] +]; +$fruitsBlock = Block::make($fruits); +$groupedByQuantityRange = $fruitsBlock->groupByFunction( + fn($fruit): string => + match (true) { + $fruit['quantity'] < 10 => 'Low', + $fruit['quantity'] < 15 => 'Medium', + default => 'High', + }, +); +// It returns: +/* +{ + "High": [ + { + "name": "Apple", + "type": "Citrus", + "quantity": 15 + } + ], + "Medium": [ + { + "name": "Banana", + "type": "Tropical", + "quantity": 10 + }, + { + "name": "Lemon", + "type": "Citrus", + "quantity": 12 + } + ], + "Low": [ + { + "name": "Orange", + "type": "Citrus", + "quantity": 8 + }, + { + "name": "Mango", + "type": "Tropical", + "quantity": 5 + } + ] +} +*/ +``` + ### The `exists()` method You can use the `exists()` method to check if an element that meets a certain condition exists. This method is a convenient way to determine if any records match your query without needing to count them explicitly. diff --git a/src/Traits/FormattableBlock.php b/src/Traits/FormattableBlock.php index 2ae4f15..c1a0a51 100644 --- a/src/Traits/FormattableBlock.php +++ b/src/Traits/FormattableBlock.php @@ -75,7 +75,6 @@ public function getFormattedByte( * @param mixed $key the filed key , can be nested for example "commits.0.name" * @param string|null $defaultValue the default value returned if no value is found * @param non-empty-string $charNestedKey for nested field the . character is the default - * @return string */ public function getString( mixed $key, diff --git a/src/Traits/QueryableBlock.php b/src/Traits/QueryableBlock.php index 9271ad6..5812e83 100644 --- a/src/Traits/QueryableBlock.php +++ b/src/Traits/QueryableBlock.php @@ -59,7 +59,7 @@ public function where( Operator::STRICT_NOT_EQUAL => $elementToCheck->get($field) !== $value, Operator::IN => in_array($elementToCheck->get($field), $value), Operator::HAS => in_array($value, $elementToCheck->get($field)), - Operator::LIKE => str_contains($elementToCheck->get($field), $value), + Operator::LIKE => str_contains($elementToCheck->get($field), (string) $value), default => $elementToCheck->get($field) === $value, }; if ($found) { @@ -152,6 +152,25 @@ public function groupBy(string|int $field): self return self::make($result); } + public function groupByFunction(callable $groupFunction): self + { + $result = []; + + foreach ($this->data as $item) { + // Call the closure to determine the group key + $groupKey = $groupFunction($item); + + // Group items under the same key + if (!array_key_exists($groupKey, $result)) { + $result[$groupKey] = []; + } + + $result[$groupKey][] = $item; + } + return self::make($result); + } + + private static function castVariableForStrval(mixed $property): bool|float|int|string|null { return match (gettype($property)) { diff --git a/tests/Unit/Traits/QueryableBlockTest.php b/tests/Unit/Traits/QueryableBlockTest.php index ba4d3b7..5e1cba3 100644 --- a/tests/Unit/Traits/QueryableBlockTest.php +++ b/tests/Unit/Traits/QueryableBlockTest.php @@ -4,8 +4,7 @@ use HiFolks\DataType\Enums\Operator; $fruitsArray = [ - "avocado" => - [ + 'avocado' => [ 'name' => 'Avocado', 'fruit' => '🥑', 'wikipedia' => 'https://en.wikipedia.org/wiki/Avocado', @@ -13,8 +12,7 @@ 'rating' => 8, 'tags' => ['healthy', 'creamy', 'green'], ], - "apple" => - [ + 'apple' => [ 'name' => 'Apple', 'fruit' => '🍎', 'wikipedia' => 'https://en.wikipedia.org/wiki/Apple', @@ -22,8 +20,7 @@ 'rating' => 7, 'tags' => ['classic', 'crunchy', 'juicy', 'red', 'sweet'], ], - "banana" => - [ + 'banana' => [ 'name' => 'Banana', 'fruit' => '🍌', 'wikipedia' => 'https://en.wikipedia.org/wiki/Banana', @@ -31,8 +28,7 @@ 'rating' => 8.5, 'tags' => ['sweet', 'soft', 'yellow'], ], - "cherry" => - [ + 'cherry' => [ 'name' => 'Cherry', 'fruit' => '🍒', 'wikipedia' => 'https://en.wikipedia.org/wiki/Cherry', @@ -46,12 +42,11 @@ 'Test query greater than x', function () use ($fruitsArray): void { $data = Block::make($fruitsArray); - $highRated = $data->where("rating", Operator::GREATER_THAN, 8); + $highRated = $data->where('rating', Operator::GREATER_THAN, 8); expect($highRated)->tohaveCount(2); - $sorted = $data->where("rating", Operator::GREATER_THAN, 8)->orderBy("rating", "desc"); + $sorted = $data->where('rating', Operator::GREATER_THAN, 8)->orderBy('rating', 'desc'); expect($sorted)->tohaveCount(2); - }, ); @@ -59,11 +54,11 @@ function () use ($fruitsArray): void { 'group by', function () use ($fruitsArray): void { $table = Block::make($fruitsArray); - $grouped = $table->groupBy("color"); - expect($grouped->getBlock("red"))->tohaveCount(2); - expect($grouped->getBlock("yellow"))->tohaveCount(1); - expect($grouped->getBlock("NotExists"))->tohaveCount(0); - + $grouped = $table->groupBy('color'); + expect($grouped->getBlock('red')) + ->tohaveCount(2) + ->and($grouped->getBlock('yellow'))->tohaveCount(1) + ->and($grouped->getBlock('NotExists'))->tohaveCount(0); }, ); @@ -76,23 +71,69 @@ function (): void { ['type' => 'vegetable', 'name' => 'carrot'], ]); $grouped = $data->groupBy('type'); - expect($grouped->getBlock("fruit"))->tohaveCount(2); - expect($grouped->getBlock("vegetable"))->tohaveCount(1); - expect($grouped->getBlock("NotExists"))->tohaveCount(0); + expect($grouped->getBlock('fruit'))->tohaveCount(2); + expect($grouped->getBlock('vegetable'))->tohaveCount(1); + expect($grouped->getBlock('NotExists'))->tohaveCount(0); }, ); +test( + 'group by function', + function (): void { + $fruits = [ + ['name' => 'Apple', 'type' => 'Citrus', 'quantity' => 15], + ['name' => 'Banana', 'type' => 'Tropical', 'quantity' => 10], + ['name' => 'Orange', 'type' => 'Citrus', 'quantity' => 8], + ['name' => 'Mango', 'type' => 'Tropical', 'quantity' => 5], + ['name' => 'Lemon', 'type' => 'Citrus', 'quantity' => 12], + ]; + $fruitsBlock = Block::make($fruits); + $groupedByQuantityRange = $fruitsBlock->groupByFunction( + fn($fruit): string => + match (true) { + $fruit['quantity'] < 10 => 'Low', + $fruit['quantity'] < 15 => 'Medium', + default => 'High', + }, + ); + $groupedByQuantityRange->dumpJson(); + expect($groupedByQuantityRange) + ->tohaveCount(3) + ->and($groupedByQuantityRange->getBlock('Low')) + ->tohaveCount(2) + ->and($groupedByQuantityRange->getBlock('Medium')) + ->tohaveCount(2) + ->and($groupedByQuantityRange->getBlock('High')) + ->tohaveCount(1); + + $groupedByNameLenght = $fruitsBlock->groupByFunction( + fn($fruit): int => strlen((string) $fruit['name']), + ); + + expect($groupedByNameLenght) + ->tohaveCount(2) + ->and($groupedByNameLenght->hasKey(5)) + ->toBeTrue() + ->and($groupedByNameLenght->hasKey("6")) + ->toBeTrue() + ->and($groupedByNameLenght->get("5")) + ->tohaveCount(3) + ->and($groupedByNameLenght->get("6")) + ->tohaveCount(2); + }, +); + test( 'where method, in operator', function () use ($fruitsArray): void { $data = Block::make($fruitsArray); - $greenOrBlack = $data->where("color", Operator::IN, ["green", "black"]); + $greenOrBlack = $data->where('color', Operator::IN, ['green', 'black']); expect($greenOrBlack)->tohaveCount(1); - $noResult = $data->where("color", Operator::IN, []); + $noResult = $data->where('color', Operator::IN, []); expect($noResult)->tohaveCount(0); - $greenOrRed = $data->where("color", Operator::IN, ["green", "red"]); + $greenOrRed = $data->where('color', Operator::IN, ['green', 'red']); expect($greenOrRed)->tohaveCount(3); }, ); @@ -101,26 +142,25 @@ function () use ($fruitsArray): void { 'where method, has operator', function () use ($fruitsArray): void { $data = Block::make($fruitsArray); - $sweet = $data->where("tags", Operator::HAS, "sweet"); + $sweet = $data->where('tags', Operator::HAS, 'sweet'); expect($sweet)->tohaveCount(2); - $noResult = $data->where("tags", Operator::HAS, "not-existent"); + $noResult = $data->where('tags', Operator::HAS, 'not-existent'); expect($noResult)->tohaveCount(0); - $softFruit = $data->where("tags", Operator::HAS, "soft"); + $softFruit = $data->where('tags', Operator::HAS, 'soft'); expect($softFruit)->tohaveCount(1); }, ); - test( 'query with operators', function (): void { - $data1 = Block::fromJsonFile(__DIR__ . "/../../data/commits-json/commits-10-p1.json"); - $data2 = Block::fromJsonFile(__DIR__ . "/../../data/commits-json/commits-10-p2.json"); - $data3 = Block::fromJsonFile(__DIR__ . "/../../data/commits-json/commits-10-p3.json"); + $data1 = Block::fromJsonFile(__DIR__ . '/../../data/commits-json/commits-10-p1.json'); + $data2 = Block::fromJsonFile(__DIR__ . '/../../data/commits-json/commits-10-p2.json'); + $data3 = Block::fromJsonFile(__DIR__ . '/../../data/commits-json/commits-10-p3.json'); $data1->append($data2)->append($data3); expect($data1)->toHaveCount(30); expect($data2)->toHaveCount(10); - $block = $data1->where("commit.author.name", Operator::LIKE, "Roberto"); + $block = $data1->where('commit.author.name', Operator::LIKE, 'Roberto'); expect($block->count())->toEqual(29); },