From f4fbbc5a60cdb695ce1055b7154a818f8669a780 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Fri, 14 Apr 2023 23:27:27 +0200 Subject: [PATCH 01/12] array_group_by and unit tests --- system/Helpers/array_helper.php | 46 ++ tests/system/Helpers/ArrayHelperTest.php | 794 +++++++++++++++++++++++ 2 files changed, 840 insertions(+) diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 9b88cc2a0e0f..7c5c6d2c5de2 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -218,3 +218,49 @@ function array_flatten_with_dots(iterable $array, string $id = ''): array return $flattened; } } + +if (! function_exists('array_group_by')) { + /** + * Groups all rows by their index values. Result's depth equals number of indexes + * + * @param array $array Data array (i.e. from query result) + * @param array $indexes Indexes to group by. Dot syntax used. Returns $array if empty + * @param bool $includeEmpty If true, null and '' are also added as valid keys to group + * + * @return array Result array where rows are grouped together by indexes values. + */ + function array_group_by(array $array, array $indexes, bool $includeEmpty = false): array + { + if (empty($indexes)) { + return $array; + } + + $result = []; + + foreach ($array as $row) { + $currentLevel = &$result; + $valid = true; + + foreach ($indexes as $index) { + $value = dot_array_search($index, $row); + + if (! $includeEmpty && empty($value)) { + $valid = false; + break; + } + + if (! array_key_exists($value, $currentLevel)) { + $currentLevel[$value] = []; + } + + $currentLevel = &$currentLevel[$value]; + } + + if ($valid) { + $currentLevel[] = $row; + } + } + + return $result; + } +} diff --git a/tests/system/Helpers/ArrayHelperTest.php b/tests/system/Helpers/ArrayHelperTest.php index a2db5b06e7d7..e19c410a32cd 100644 --- a/tests/system/Helpers/ArrayHelperTest.php +++ b/tests/system/Helpers/ArrayHelperTest.php @@ -498,4 +498,798 @@ public function arrayFlattenProvider(): iterable ], ]; } + + /** + * @dataProvider arrayGroupByIncludeEmptyProvider + */ + public function testArrayGroupByIncludeEmpty(array $indexes, array $data, array $expected): void + { + $actual = array_group_by($data, $indexes, true); + + $this->assertSame($expected, $actual, 'array including empty not the same'); + } + + /** + * @dataProvider arrayGroupByExcludeEmptyProvider + */ + public function testArrayGroupByExcludeEmpty(array $indexes, array $data, array $expected): void + { + $actual = array_group_by($data, $indexes, false); + + $this->assertSame($expected, $actual, 'array excluding empty not the same'); + } + + public function arrayGroupByIncludeEmptyProvider(): iterable + { + yield 'simple group-by test' => [ + ['color'], + [ + [ + 'id' => 1, + 'item' => 'ball', + 'color' => 'blue', + ], + [ + 'id' => 2, + 'item' => 'book', + 'color' => 'red', + ], + [ + 'id' => 3, + 'item' => 'bird', + 'age' => 5, + ], + [ + 'id' => 4, + 'item' => 'jeans', + 'color' => 'blue', + ], + ], + [ + 'blue' => [ + [ + 'id' => 1, + 'item' => 'ball', + 'color' => 'blue', + ], + [ + 'id' => 4, + 'item' => 'jeans', + 'color' => 'blue', + ], + ], + 'red' => [ + [ + 'id' => 2, + 'item' => 'book', + 'color' => 'red', + ], + ], + '' => [ + [ + 'id' => 3, + 'item' => 'bird', + 'age' => 5, + ], + ], + ], + ]; + + yield '2 index data' => [ + ['gender', 'country'], + [ + [ + 'id' => 1, + 'first_name' => 'Scarface', + 'gender' => 'Male', + 'country' => 'Germany', + ], + [ + 'id' => 2, + 'first_name' => 'Fletch', + 'gender' => 'Male', + 'country' => 'France', + ], + [ + 'id' => 3, + 'first_name' => 'Wrennie', + 'gender' => 'Female', + 'country' => 'France', + ], + [ + 'id' => 4, + 'first_name' => 'Virgilio', + 'gender' => 'Male', + 'country' => 'France', + ], + [ + 'id' => 5, + 'first_name' => 'Cathlene', + 'gender' => 'Polygender', + 'country' => 'France', + ], + [ + 'id' => 6, + 'first_name' => 'Far', + 'gender' => 'Male', + 'country' => 'Canada', + ], + [ + 'id' => 7, + 'first_name' => 'Dolores', + 'gender' => 'Female', + 'country' => 'Canada', + ], + [ + 'id' => 8, + 'first_name' => 'Sissy', + 'gender' => 'Female', + 'country' => null, + ], + [ + 'id' => 9, + 'first_name' => 'Chlo', + 'gender' => 'Female', + 'country' => 'France', + ], + [ + 'id' => 10, + 'first_name' => 'Gabbie', + 'gender' => 'Male', + 'country' => 'Canada', + ], + ], + [ + 'Male' => [ + 'Germany' => [ + [ + 'id' => 1, + 'first_name' => 'Scarface', + 'gender' => 'Male', + 'country' => 'Germany', + ], + ], + 'France' => [ + [ + 'id' => 2, + 'first_name' => 'Fletch', + 'gender' => 'Male', + 'country' => 'France', + ], + [ + 'id' => 4, + 'first_name' => 'Virgilio', + 'gender' => 'Male', + 'country' => 'France', + ], + ], + 'Canada' => [ + [ + 'id' => 6, + 'first_name' => 'Far', + 'gender' => 'Male', + 'country' => 'Canada', + ], + [ + 'id' => 10, + 'first_name' => 'Gabbie', + 'gender' => 'Male', + 'country' => 'Canada', + ], + ], + ], + 'Female' => [ + 'France' => [ + [ + 'id' => 3, + 'first_name' => 'Wrennie', + 'gender' => 'Female', + 'country' => 'France', + ], + [ + 'id' => 9, + 'first_name' => 'Chlo', + 'gender' => 'Female', + 'country' => 'France', + ], + ], + 'Canada' => [ + [ + 'id' => 7, + 'first_name' => 'Dolores', + 'gender' => 'Female', + 'country' => 'Canada', + ], + ], + '' => [ + [ + 'id' => 8, + 'first_name' => 'Sissy', + 'gender' => 'Female', + 'country' => null, + ], + ], + ], + 'Polygender' => [ + 'France' => [ + [ + 'id' => 5, + 'first_name' => 'Cathlene', + 'gender' => 'Polygender', + 'country' => 'France', + ], + ], + ], + ], + ]; + + yield 'nested data with dot syntax' => [ + ['gender', 'hr.department'], + [ + [ + 'id' => 1, + 'first_name' => 'Urbano', + 'gender' => null, + 'hr' => [ + 'country' => 'Canada', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 2, + 'first_name' => 'Case', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Marketing', + ], + ], + [ + 'id' => 3, + 'first_name' => 'Emera', + 'gender' => 'Female', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 4, + 'first_name' => 'Richy', + 'gender' => null, + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + [ + 'id' => 5, + 'first_name' => 'Mandy', + 'gender' => null, + 'hr' => [ + 'country' => 'France', + 'department' => 'Sales', + ], + ], + [ + 'id' => 6, + 'first_name' => 'Risa', + 'gender' => 'Female', + 'hr' => [ + 'country' => null, + 'department' => 'Engineering', + ], + ], + [ + 'id' => 7, + 'first_name' => 'Alfred', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 8, + 'first_name' => 'Tabby', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Marketing', + ], + ], + [ + 'id' => 9, + 'first_name' => 'Ario', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + [ + 'id' => 10, + 'first_name' => 'Somerset', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'Germany', + 'department' => 'Marketing', + ], + ], + ], + [ + '' => [ + 'Engineering' => [ + [ + 'id' => 1, + 'first_name' => 'Urbano', + 'gender' => null, + 'hr' => [ + 'country' => 'Canada', + 'department' => 'Engineering', + ], + ], + ], + 'Sales' => [ + [ + 'id' => 4, + 'first_name' => 'Richy', + 'gender' => null, + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + [ + 'id' => 5, + 'first_name' => 'Mandy', + 'gender' => null, + 'hr' => [ + 'country' => 'France', + 'department' => 'Sales', + ], + ], + ], + ], + 'Male' => [ + 'Marketing' => [ + [ + 'id' => 2, + 'first_name' => 'Case', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Marketing', + ], + ], + [ + 'id' => 8, + 'first_name' => 'Tabby', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Marketing', + ], + ], + [ + 'id' => 10, + 'first_name' => 'Somerset', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'Germany', + 'department' => 'Marketing', + ], + ], + ], + 'Engineering' => [ + [ + 'id' => 7, + 'first_name' => 'Alfred', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + ], + 'Sales' => [ + [ + 'id' => 9, + 'first_name' => 'Ario', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + ], + ], + 'Female' => [ + 'Engineering' => [ + [ + 'id' => 3, + 'first_name' => 'Emera', + 'gender' => 'Female', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 6, + 'first_name' => 'Risa', + 'gender' => 'Female', + 'hr' => [ + 'country' => null, + 'department' => 'Engineering', + ], + ], + ], + ], + ], + ]; + } + + public function arrayGroupByExcludeEmptyProvider(): iterable + { + yield 'simple group-by test' => [ + ['color'], + [ + [ + 'id' => 1, + 'item' => 'ball', + 'color' => 'blue', + ], + [ + 'id' => 2, + 'item' => 'book', + 'color' => 'red', + ], + [ + 'id' => 3, + 'item' => 'bird', + 'age' => 5, + ], + [ + 'id' => 4, + 'item' => 'jeans', + 'color' => 'blue', + ], + ], + [ + 'blue' => [ + [ + 'id' => 1, + 'item' => 'ball', + 'color' => 'blue', + ], + [ + 'id' => 4, + 'item' => 'jeans', + 'color' => 'blue', + ], + ], + 'red' => [ + [ + 'id' => 2, + 'item' => 'book', + 'color' => 'red', + ], + ], + ], + ]; + + yield '2 index data' => [ + ['gender', 'country'], + [ + [ + 'id' => 1, + 'first_name' => 'Scarface', + 'gender' => 'Male', + 'country' => 'Germany', + ], + [ + 'id' => 2, + 'first_name' => 'Fletch', + 'gender' => 'Male', + 'country' => 'France', + ], + [ + 'id' => 3, + 'first_name' => 'Wrennie', + 'gender' => 'Female', + 'country' => 'France', + ], + [ + 'id' => 4, + 'first_name' => 'Virgilio', + 'gender' => 'Male', + 'country' => 'France', + ], + [ + 'id' => 5, + 'first_name' => 'Cathlene', + 'gender' => 'Polygender', + 'country' => 'France', + ], + [ + 'id' => 6, + 'first_name' => 'Far', + 'gender' => 'Male', + 'country' => 'Canada', + ], + [ + 'id' => 7, + 'first_name' => 'Dolores', + 'gender' => 'Female', + 'country' => 'Canada', + ], + [ + 'id' => 8, + 'first_name' => 'Sissy', + 'gender' => 'Female', + 'country' => null, + ], + [ + 'id' => 9, + 'first_name' => 'Chlo', + 'gender' => 'Female', + 'country' => 'France', + ], + [ + 'id' => 10, + 'first_name' => 'Gabbie', + 'gender' => 'Male', + 'country' => 'Canada', + ], + ], + [ + 'Male' => [ + 'Germany' => [ + [ + 'id' => 1, + 'first_name' => 'Scarface', + 'gender' => 'Male', + 'country' => 'Germany', + ], + ], + 'France' => [ + [ + 'id' => 2, + 'first_name' => 'Fletch', + 'gender' => 'Male', + 'country' => 'France', + ], + [ + 'id' => 4, + 'first_name' => 'Virgilio', + 'gender' => 'Male', + 'country' => 'France', + ], + ], + 'Canada' => [ + [ + 'id' => 6, + 'first_name' => 'Far', + 'gender' => 'Male', + 'country' => 'Canada', + ], + [ + 'id' => 10, + 'first_name' => 'Gabbie', + 'gender' => 'Male', + 'country' => 'Canada', + ], + ], + ], + 'Female' => [ + 'France' => [ + [ + 'id' => 3, + 'first_name' => 'Wrennie', + 'gender' => 'Female', + 'country' => 'France', + ], + [ + 'id' => 9, + 'first_name' => 'Chlo', + 'gender' => 'Female', + 'country' => 'France', + ], + ], + 'Canada' => [ + [ + 'id' => 7, + 'first_name' => 'Dolores', + 'gender' => 'Female', + 'country' => 'Canada', + ], + ], + ], + 'Polygender' => [ + 'France' => [ + [ + 'id' => 5, + 'first_name' => 'Cathlene', + 'gender' => 'Polygender', + 'country' => 'France', + ], + ], + ], + ], + ]; + + yield 'nested data with dot syntax' => [ + ['gender', 'hr.department'], + [ + [ + 'id' => 1, + 'first_name' => 'Urbano', + 'gender' => null, + 'hr' => [ + 'country' => 'Canada', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 2, + 'first_name' => 'Case', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Marketing', + ], + ], + [ + 'id' => 3, + 'first_name' => 'Emera', + 'gender' => 'Female', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 4, + 'first_name' => 'Richy', + 'gender' => null, + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + [ + 'id' => 5, + 'first_name' => 'Mandy', + 'gender' => null, + 'hr' => [ + 'country' => 'France', + 'department' => 'Sales', + ], + ], + [ + 'id' => 6, + 'first_name' => 'Risa', + 'gender' => 'Female', + 'hr' => [ + 'country' => null, + 'department' => 'Engineering', + ], + ], + [ + 'id' => 7, + 'first_name' => 'Alfred', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 8, + 'first_name' => 'Tabby', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Marketing', + ], + ], + [ + 'id' => 9, + 'first_name' => 'Ario', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + [ + 'id' => 10, + 'first_name' => 'Somerset', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'Germany', + 'department' => 'Marketing', + ], + ], + ], + [ + 'Male' => [ + 'Marketing' => [ + [ + 'id' => 2, + 'first_name' => 'Case', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Marketing', + ], + ], + [ + 'id' => 8, + 'first_name' => 'Tabby', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Marketing', + ], + ], + [ + 'id' => 10, + 'first_name' => 'Somerset', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'Germany', + 'department' => 'Marketing', + ], + ], + ], + 'Engineering' => [ + [ + 'id' => 7, + 'first_name' => 'Alfred', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + ], + 'Sales' => [ + [ + 'id' => 9, + 'first_name' => 'Ario', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + ], + ], + 'Female' => [ + 'Engineering' => [ + [ + 'id' => 3, + 'first_name' => 'Emera', + 'gender' => 'Female', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 6, + 'first_name' => 'Risa', + 'gender' => 'Female', + 'hr' => [ + 'country' => null, + 'department' => 'Engineering', + ], + ], + ], + ], + ], + ]; + } } From 76993a5fb02dadf044d48a633d0b369aacaf8dd8 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Sat, 15 Apr 2023 00:11:38 +0200 Subject: [PATCH 02/12] Documentation --- .../source/helpers/array_helper.rst | 24 ++++ .../source/helpers/array_helper/012.php | 94 +++++++++++++++ .../source/helpers/array_helper/013.php | 81 +++++++++++++ .../source/helpers/array_helper/014.php | 114 ++++++++++++++++++ 4 files changed, 313 insertions(+) create mode 100644 user_guide_src/source/helpers/array_helper/012.php create mode 100644 user_guide_src/source/helpers/array_helper/013.php create mode 100644 user_guide_src/source/helpers/array_helper/014.php diff --git a/user_guide_src/source/helpers/array_helper.rst b/user_guide_src/source/helpers/array_helper.rst index cf9517925234..730dae957222 100644 --- a/user_guide_src/source/helpers/array_helper.rst +++ b/user_guide_src/source/helpers/array_helper.rst @@ -111,3 +111,27 @@ The following functions are available: will be supplying an initial ``$id``, it will be prepended to all keys. .. literalinclude:: array_helper/011.php + +.. php:function:: array_group_by(array $array, array $indexes[, bool $includeEmpty = false]): array + + :param array $array: Data rows (most likely from query results) + :param array $indexes: Indexes to group values. Follows dot syntax + :param bool $includeEmpty: If true, ``null`` and ``''`` values are not filtered out + :rtype: array + :returns: An array grouped by indexes values + + This function allows you to group data rows together by index values. + The depth of returned array equals the number of indexes passed as parameter. + + The example shows some data (i.e. loaded from an API) with nested arrays. + + .. literalinclude:: array_helper/012.php + + We want to group them first by "gender", then by "hr.department" (max depth = 2). + First the result when excluding empty values: + + .. literalinclude:: array_helper/013.php + + And here the same code, but this time we want to include empty values: + + .. literalinclude:: array_helper/014.php diff --git a/user_guide_src/source/helpers/array_helper/012.php b/user_guide_src/source/helpers/array_helper/012.php new file mode 100644 index 000000000000..8aeb6d705c7d --- /dev/null +++ b/user_guide_src/source/helpers/array_helper/012.php @@ -0,0 +1,94 @@ + 1, + 'first_name' => 'Urbano', + 'gender' => null, + 'hr' => [ + 'country' => 'Canada', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 2, + 'first_name' => 'Case', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Marketing', + ], + ], + [ + 'id' => 3, + 'first_name' => 'Emera', + 'gender' => 'Female', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 4, + 'first_name' => 'Richy', + 'gender' => null, + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + [ + 'id' => 5, + 'first_name' => 'Mandy', + 'gender' => null, + 'hr' => [ + 'country' => 'France', + 'department' => 'Sales', + ], + ], + [ + 'id' => 6, + 'first_name' => 'Risa', + 'gender' => 'Female', + 'hr' => [ + 'country' => null, + 'department' => 'Engineering', + ], + ], + [ + 'id' => 7, + 'first_name' => 'Alfred', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 8, + 'first_name' => 'Tabby', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Marketing', + ], + ], + [ + 'id' => 9, + 'first_name' => 'Ario', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + [ + 'id' => 10, + 'first_name' => 'Somerset', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'Germany', + 'department' => 'Marketing', + ], + ], +]; diff --git a/user_guide_src/source/helpers/array_helper/013.php b/user_guide_src/source/helpers/array_helper/013.php new file mode 100644 index 000000000000..5b7e722fce75 --- /dev/null +++ b/user_guide_src/source/helpers/array_helper/013.php @@ -0,0 +1,81 @@ + [ + 'Marketing' => [ + [ + 'id' => 2, + 'first_name' => 'Case', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Marketing', + ], + ], + [ + 'id' => 8, + 'first_name' => 'Tabby', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Marketing', + ], + ], + [ + 'id' => 10, + 'first_name' => 'Somerset', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'Germany', + 'department' => 'Marketing', + ], + ], + ], + 'Engineering' => [ + [ + 'id' => 7, + 'first_name' => 'Alfred', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + ], + 'Sales' => [ + [ + 'id' => 9, + 'first_name' => 'Ario', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + ], + ], + 'Female' => [ + 'Engineering' => [ + [ + 'id' => 3, + 'first_name' => 'Emera', + 'gender' => 'Female', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 6, + 'first_name' => 'Risa', + 'gender' => 'Female', + 'hr' => [ + 'country' => null, + 'department' => 'Engineering', + ], + ], + ], + ], +]; diff --git a/user_guide_src/source/helpers/array_helper/014.php b/user_guide_src/source/helpers/array_helper/014.php new file mode 100644 index 000000000000..99089d236598 --- /dev/null +++ b/user_guide_src/source/helpers/array_helper/014.php @@ -0,0 +1,114 @@ + [ + 'Engineering' => [ + [ + 'id' => 1, + 'first_name' => 'Urbano', + 'gender' => null, + 'hr' => [ + 'country' => 'Canada', + 'department' => 'Engineering', + ], + ], + ], + 'Sales' => [ + [ + 'id' => 4, + 'first_name' => 'Richy', + 'gender' => null, + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + [ + 'id' => 5, + 'first_name' => 'Mandy', + 'gender' => null, + 'hr' => [ + 'country' => 'France', + 'department' => 'Sales', + ], + ], + ], + ], + 'Male' => [ + 'Marketing' => [ + [ + 'id' => 2, + 'first_name' => 'Case', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Marketing', + ], + ], + [ + 'id' => 8, + 'first_name' => 'Tabby', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Marketing', + ], + ], + [ + 'id' => 10, + 'first_name' => 'Somerset', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'Germany', + 'department' => 'Marketing', + ], + ], + ], + 'Engineering' => [ + [ + 'id' => 7, + 'first_name' => 'Alfred', + 'gender' => 'Male', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + ], + 'Sales' => [ + [ + 'id' => 9, + 'first_name' => 'Ario', + 'gender' => 'Male', + 'hr' => [ + 'country' => null, + 'department' => 'Sales', + ], + ], + ], + ], + 'Female' => [ + 'Engineering' => [ + [ + 'id' => 3, + 'first_name' => 'Emera', + 'gender' => 'Female', + 'hr' => [ + 'country' => 'France', + 'department' => 'Engineering', + ], + ], + [ + 'id' => 6, + 'first_name' => 'Risa', + 'gender' => 'Female', + 'hr' => [ + 'country' => null, + 'department' => 'Engineering', + ], + ], + ], + ], +]; From a3bc77ed088807e7d49d6733d4fd68d1fbbdb713 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Sat, 15 Apr 2023 10:26:12 +0200 Subject: [PATCH 03/12] strict comparison, phpstan fixes --- system/Helpers/array_helper.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 7c5c6d2c5de2..6de04726eaaa 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -231,7 +231,7 @@ function array_flatten_with_dots(iterable $array, string $id = ''): array */ function array_group_by(array $array, array $indexes, bool $includeEmpty = false): array { - if (empty($indexes)) { + if ($indexes === []) { return $array; } @@ -244,12 +244,16 @@ function array_group_by(array $array, array $indexes, bool $includeEmpty = false foreach ($indexes as $index) { $value = dot_array_search($index, $row); - if (! $includeEmpty && empty($value)) { + if (is_array($value) || is_object($value) || (! $includeEmpty && ($value === null || $value === ''))) { $valid = false; break; } - if (! array_key_exists($value, $currentLevel)) { + if ($value === null) { + $value = ''; + } + + if (! isset($currentLevel[$value])) { $currentLevel[$value] = []; } From 0b3a04e8211e78c2d2046bd6c723af58a77745bb Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Sat, 15 Apr 2023 12:33:12 +0200 Subject: [PATCH 04/12] logic improvements, phpstan fixes --- system/Helpers/array_helper.php | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 6de04726eaaa..02c0afbcea82 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -244,19 +244,23 @@ function array_group_by(array $array, array $indexes, bool $includeEmpty = false foreach ($indexes as $index) { $value = dot_array_search($index, $row); - if (is_array($value) || is_object($value) || (! $includeEmpty && ($value === null || $value === ''))) { - $valid = false; - break; - } - - if ($value === null) { + if (is_array($value) || is_object($value) || $value === null) { $value = ''; } - if (! isset($currentLevel[$value])) { - $currentLevel[$value] = []; + if (is_bool($value)) { + $value = intval($value); + } + + if (! $includeEmpty && $value === '') { + $valid = false; + break; } + if (! array_key_exists($value, $currentLevel)) { + $currentLevel[$value] = []; + } + $currentLevel = &$currentLevel[$value]; } From c2a7402a81d8b6350fe4b8e348d1ee4216ded3a2 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Sat, 15 Apr 2023 13:06:39 +0200 Subject: [PATCH 05/12] cs fix, always use string value --- system/Helpers/array_helper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 02c0afbcea82..aa392e98bc03 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -258,8 +258,8 @@ function array_group_by(array $array, array $indexes, bool $includeEmpty = false } if (! array_key_exists($value, $currentLevel)) { - $currentLevel[$value] = []; - } + $currentLevel[$value] = []; + } $currentLevel = &$currentLevel[$value]; } From 443fd77302e7314c68f235d14f989d87ed1af338 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Sat, 15 Apr 2023 13:18:07 +0200 Subject: [PATCH 06/12] additional CS fixes --- system/Helpers/array_helper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index aa392e98bc03..e89b99cd99a4 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -249,8 +249,8 @@ function array_group_by(array $array, array $indexes, bool $includeEmpty = false } if (is_bool($value)) { - $value = intval($value); - } + $value = (int) $value; + } if (! $includeEmpty && $value === '') { $valid = false; From 8bff019e69d97f416f7aae147316d8b5d5859cb0 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Sun, 16 Apr 2023 19:29:42 +0200 Subject: [PATCH 07/12] phpstan ignore false-positive error --- system/Helpers/array_helper.php | 1 + 1 file changed, 1 insertion(+) diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index e89b99cd99a4..2245606d1b25 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -257,6 +257,7 @@ function array_group_by(array $array, array $indexes, bool $includeEmpty = false break; } + // @phpstan-ignore-next-line if (! array_key_exists($value, $currentLevel)) { $currentLevel[$value] = []; } From 752fbbebc28f747e79a1acf2b96e8702434a8c9c Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Sun, 16 Apr 2023 20:56:56 +0200 Subject: [PATCH 08/12] Recursive approach with internal function --- system/Helpers/array_helper.php | 54 +++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 2245606d1b25..74d62b081f59 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -238,38 +238,46 @@ function array_group_by(array $array, array $indexes, bool $includeEmpty = false $result = []; foreach ($array as $row) { - $currentLevel = &$result; - $valid = true; + $result = _array_attach_indexed_value($result, $row, $indexes, $includeEmpty); + } - foreach ($indexes as $index) { - $value = dot_array_search($index, $row); + return $result; + } +} - if (is_array($value) || is_object($value) || $value === null) { - $value = ''; - } +if (!function_exists('_array_attach_indexed_value')) { + /** + * Used by `array_group_by` to recursively attach $row to the $indexes path of values found by + * `dot_array_search` + * + * @internal This should not be used on its own + */ + function _array_attach_indexed_value(array $result, array $row, array $indexes, bool $includeEmpty): array + { + if (($index = array_shift($indexes)) === null) { + $result[] = $row; + return $result; + } - if (is_bool($value)) { - $value = (int) $value; - } + $value = dot_array_search($index, $row); - if (! $includeEmpty && $value === '') { - $valid = false; - break; - } + if (is_array($value) || is_object($value) || $value === null) { + $value = ''; + } - // @phpstan-ignore-next-line - if (! array_key_exists($value, $currentLevel)) { - $currentLevel[$value] = []; - } + if (is_bool($value)) { + $value = (int) $value; + } - $currentLevel = &$currentLevel[$value]; - } + if (! $includeEmpty && $value === '') { + return $result; + } - if ($valid) { - $currentLevel[] = $row; - } + if (! array_key_exists($value, $result)) { + $result[$value] = []; } + $result[$value] = _array_attach_indexed_value($result[$value], $row, $indexes, $includeEmpty); return $result; } } From 2dd683d0c1a9a74fd921a8582f7a6f41ffa9cae7 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Sun, 16 Apr 2023 21:17:30 +0200 Subject: [PATCH 09/12] CS fixes --- system/Helpers/array_helper.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 74d62b081f59..2ed71cf7b29d 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -245,7 +245,7 @@ function array_group_by(array $array, array $indexes, bool $includeEmpty = false } } -if (!function_exists('_array_attach_indexed_value')) { +if (! function_exists('_array_attach_indexed_value')) { /** * Used by `array_group_by` to recursively attach $row to the $indexes path of values found by * `dot_array_search` @@ -256,6 +256,7 @@ function _array_attach_indexed_value(array $result, array $row, array $indexes, { if (($index = array_shift($indexes)) === null) { $result[] = $row; + return $result; } @@ -278,6 +279,7 @@ function _array_attach_indexed_value(array $result, array $row, array $indexes, } $result[$value] = _array_attach_indexed_value($result[$value], $row, $indexes, $includeEmpty); + return $result; } } From 4e69051d0cb0d43bcf3e3a06469e6d2b24eea604 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Wed, 26 Apr 2023 19:56:16 +0200 Subject: [PATCH 10/12] Added entry in changelogs --- user_guide_src/source/changelogs/v4.4.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index df5b2c6da321..1385d24fb104 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -103,6 +103,7 @@ Others - **Request:** Added ``IncomingRequest::setValidLocales()`` method to set valid locales. - **Table:** Added ``Table::setSyncRowsWithHeading()`` method to synchronize row columns with headings. See :ref:`table-sync-rows-with-headings` for details. - **Error Handling:** Now you can use :ref:`custom-exception-handlers`. +- **Array:** Added ``array_group_by()`` helper function to group data values together. Supports dot-notation syntax. Message Changes *************** From 84a89adc1c9e7e83a0eafcf41ecb3a4cf735a265 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Thu, 27 Apr 2023 20:09:35 +0200 Subject: [PATCH 11/12] Fixed changelog --- user_guide_src/source/changelogs/v4.4.0.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.4.0.rst b/user_guide_src/source/changelogs/v4.4.0.rst index 1385d24fb104..388690daecfe 100644 --- a/user_guide_src/source/changelogs/v4.4.0.rst +++ b/user_guide_src/source/changelogs/v4.4.0.rst @@ -87,6 +87,9 @@ Libraries Helpers and Functions ===================== +- **Array:** Added :php:func:`array_group_by()` helper function to group data + values together. Supports dot-notation syntax. + Others ====== @@ -103,7 +106,6 @@ Others - **Request:** Added ``IncomingRequest::setValidLocales()`` method to set valid locales. - **Table:** Added ``Table::setSyncRowsWithHeading()`` method to synchronize row columns with headings. See :ref:`table-sync-rows-with-headings` for details. - **Error Handling:** Now you can use :ref:`custom-exception-handlers`. -- **Array:** Added ``array_group_by()`` helper function to group data values together. Supports dot-notation syntax. Message Changes *************** From 5a977fcbab02d0424d4552ad4614ffa6303c60e3 Mon Sep 17 00:00:00 2001 From: Christian Rumpf Date: Mon, 1 May 2023 19:04:36 +0200 Subject: [PATCH 12/12] Using is_scalar --- system/Helpers/array_helper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 2ed71cf7b29d..632e8a3a6c7c 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -262,7 +262,7 @@ function _array_attach_indexed_value(array $result, array $row, array $indexes, $value = dot_array_search($index, $row); - if (is_array($value) || is_object($value) || $value === null) { + if (! is_scalar($value)) { $value = ''; }