diff --git a/CHANGELOG.md b/CHANGELOG.md index cef8713..40c46cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## 1.0.2 under development - New #91: Add method `ArrayHelper::group()` that groups the array according to a specified key (sagittaracc) +- New #5: Add method `ArrayHelper::indexAndRemoveKey()` that indexes and/or groups the array according + to a specified key and remove this key (vjik) ## 1.0.1 February 10, 2021 diff --git a/README.md b/README.md index 88e9a06..619c864 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ Overall the helper has the following method groups. ### Transformation - index +- indexAndRemoveKey - group - filter - map diff --git a/src/ArrayHelper.php b/src/ArrayHelper.php index b995e56..d29d91d 100644 --- a/src/ArrayHelper.php +++ b/src/ArrayHelper.php @@ -659,6 +659,8 @@ public static function removeValue(array &$array, $value): array * ] * ``` * + * @see self::indexAndRemoveKey + * * @param array $array The array that needs to be indexed or grouped. * @param Closure|string|null $key The column name or anonymous function which result will be used * to index the array. @@ -672,17 +674,116 @@ public static function removeValue(array &$array, $value): array * @return array The indexed and/or grouped array. */ public static function index(array $array, $key, $groups = []): array + { + return self::indexArray($array, $key, $groups, false); + } + + /** + * Indexes and/or groups the array according to a specified key and remove this key. + * The input should be either multidimensional array. + * + * `$groups` is an array of keys, that will be used to group the input array into one or more sub-arrays based + * on keys specified. + * + * If a value of an element corresponding to the key is `null` in addition to `$groups` not specified then + * the element is discarded. + * + * For example: + * + * ```php + * $array = [ + * ['id' => '123', 'data' => 'abc', 'device' => 'laptop'], + * ['id' => '345', 'data' => 'def', 'device' => 'tablet'], + * ['id' => '345', 'data' => 'hgi', 'device' => 'smartphone'], + * ]; + * $result = ArrayHelper::indexAndRemoveKey($array, 'id'); + * ``` + * + * The result will be an associative array, where the key is the value of `id` attribute and this attribute + * will be removed: + * + * ```php + * [ + * '123' => ['data' => 'abc', 'device' => 'laptop'], + * '345' => ['data' => 'hgi', 'device' => 'smartphone'] + * // The second element of an original array is overwritten by the last element because of the same id + * ] + * ``` + * + * The anonymous function can be used in the array of grouping keys as well: + * + * ```php + * $result = ArrayHelper::index($array, 'data', [function ($element) { + * return $element['id']; + * }, 'device']); + * ``` + * + * The result will be a multidimensional array grouped by `id` on the first level, by the `device` on + * the second one, indexed by the `data` on the third level and attribute `data` will be removed: + * + * ```php + * [ + * '123' => [ + * 'laptop' => [ + * 'abc' => ['id' => '123', 'device' => 'laptop'] + * ] + * ], + * '345' => [ + * 'tablet' => [ + * 'def' => ['id' => '345', 'device' => 'tablet'] + * ], + * 'smartphone' => [ + * 'hgi' => ['id' => '345', 'device' => 'smartphone'] + * ] + * ] + * ] + * ``` + * + * @see self::index + * + * @param array $array The array that needs to be indexed or grouped. + * @param string $key The column name will be used to index the array. + * @param Closure[]|string|string[]|null $groups The array of keys, that will be used to group the input array + * by one or more keys. If value for the particular element is null and `$groups` is not defined, the array element + * will be discarded. Otherwise, if `$groups` is specified, array element will be added to the result array without + * any key. + * + * @psalm-param array $array + * + * @return array The indexed and/or grouped array. + */ + public static function indexAndRemoveKey(array $array, string $key, $groups = []): array + { + return self::indexArray($array, $key, $groups, true); + } + + /** + * @see self::index + * @see self::indexAndRemoveKey + * + * @param Closure|string|null $key + * @param Closure[]|string|string[]|null $groups + */ + private static function indexArray(array $array, $key, $groups, bool $removeKey): array { $result = []; $groups = (array)$groups; /** @var mixed $element */ foreach ($array as $element) { - if (!is_array($element) && !is_object($element)) { - throw new InvalidArgumentException( - 'index() can not get value from ' . gettype($element) . - '. The $array should be either multidimensional array or an array of objects.' - ); + if (!is_array($element)) { + if ($removeKey) { + throw new InvalidArgumentException( + 'indexAndRemoveKey() can not get value from ' . self::getVariableType($element) . + '. The $array should be either multidimensional array.' + ); + } + if (!is_object($element)) { + throw new InvalidArgumentException( + 'index() can not get value from ' . gettype($element) . + '. The $array should be either multidimensional array or an array of objects.' + ); + } } $lastArray = &$result; @@ -705,6 +806,10 @@ public static function index(array $array, $key, $groups = []): array /** @var mixed */ $value = static::getValue($element, $key); if ($value !== null) { + if ($removeKey) { + /** @psalm-suppress PossiblyInvalidArrayAccess */ + unset($element[$key]); + } $lastArray[static::normalizeArrayKey($value)] = $element; } } @@ -1288,4 +1393,12 @@ private static function normalizeArrayKey($key): string { return is_float($key) ? NumericHelper::normalize($key) : (string)$key; } + + /** + * @param mixed $variable + */ + private static function getVariableType($variable): string + { + return is_object($variable) ? get_class($variable) : gettype($variable); + } } diff --git a/tests/ArrayHelper/IndexAndRemoveKeyTest.php b/tests/ArrayHelper/IndexAndRemoveKeyTest.php new file mode 100644 index 0000000..5705418 --- /dev/null +++ b/tests/ArrayHelper/IndexAndRemoveKeyTest.php @@ -0,0 +1,128 @@ + '123', 'data' => 'abc'], + ['id' => '345', 'data' => 'def'], + ['id' => '345', 'data' => 'ghi'], + ]; + + $result = ArrayHelper::indexAndRemoveKey($array, 'id'); + + $this->assertSame( + [ + '123' => ['data' => 'abc'], + '345' => ['data' => 'ghi'], + ], + $result + ); + } + + public function testWithElementsWithoutKey(): void + { + $array = [ + ['id' => '123', 'data' => 'abc'], + ['id' => '345', 'data' => 'def'], + ['data' => 'ghi'], + ]; + + $expected = [ + 123 => ['data' => 'abc'], + 345 => ['data' => 'def'], + ]; + + $result = ArrayHelper::indexAndRemoveKey($array, 'id'); + + $this->assertSame($expected, $result); + } + + public function testSimpleGroupBy(): void + { + $array = [ + ['id' => '123', 'data' => 'abc'], + ['id' => '345', 'data' => 'def'], + ['id' => '345', 'data' => 'ghi'], + ]; + + $expected = [ + '123' => [ + 'abc' => ['id' => '123'], + ], + '345' => [ + 'def' => ['id' => '345'], + 'ghi' => ['id' => '345'], + ], + ]; + + $this->assertSame($expected, ArrayHelper::indexAndRemoveKey($array, 'data', ['id'])); + $this->assertSame($expected, ArrayHelper::indexAndRemoveKey($array, 'data', 'id')); + } + + public function testStringElement(): void + { + $array = [ + ['id' => '123', 'data' => 'abc'], + ['id' => '345', 'data' => 'def'], + 'data', + ]; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('indexAndRemoveKey() can not get value from string. The $array should be either multidimensional array.'); + ArrayHelper::indexAndRemoveKey($array, 'id'); + } + + public function testObjectElement(): void + { + $array = [ + ['id' => '123', 'data' => 'abc'], + ['id' => '345', 'data' => 'def'], + new stdClass(), + ]; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('indexAndRemoveKey() can not get value from stdClass. The $array should be either multidimensional array.'); + ArrayHelper::indexAndRemoveKey($array, 'id'); + } + + public function testGroupByWithKey(): void + { + $this->expectException(InvalidArgumentException::class); + ArrayHelper::indexAndRemoveKey(['id' => '1'], 'id', ['id']); + } + + /** + * @see https://github.com/yiisoft/yii2/issues/11739 + */ + public function te1stIndexFloat(): void + { + $array = [ + ['id' => 1e6, 'data' => 'a'], + ['id' => 1e32, 'data' => 'b'], + ['id' => 1e64, 'data' => 'c'], + ['id' => 1465540807.522109, 'data' => 'd'], + ]; + + $expected = [ + '1000000' => ['data' => 'a'], + '1.0E+32' => ['data' => 'b'], + '1.0E+64' => ['data' => 'c'], + '1465540807.5221' => ['data' => 'd'], + ]; + + $result = ArrayHelper::index($array, 'id'); + + $this->assertEquals($expected, $result); + } +}