слияние многомерных массивов php с тем же ключом и значением

#php #arrays

#php #массивы

Вопрос:

Я потратил часы, пытаясь найти ответ на этот вопрос, но я борюсь. Я достаточно хорошо знаком с PHP и различными встроенными функциями и могу создать для этого сложный цикл foreach(), но я подумал, что попрошу посмотреть, есть ли у кого-нибудь более разумное решение моей проблемы.

У меня есть следующий упрощенный пример массива с тремя «строками» (реальный массив обычно намного больше и сложнее, но проблема та же).

 $rows[] = [
    "widget_id" => "widget1",
    "size" => "large",
    "item" => [
        "item_id" => "item1",
        "shape" => "circle",
        "paint" => [
            "paint_id" => "paint1",
            "colour" => "red",
        ]
    ]
];

# Exactly the same as above, except the "paint" child array is different
$rows[] = [
    "widget_id" => "widget1",
    "size" => "large",
    "item" => [
        "item_id" => "item1",
        "shape" => "circle",
        "paint" => [
            "paint_id" => "paint2",
            "colour" => "green",
        ]
    ]
];

# Same children ("item" and "paint") as the first row, but different parents ("widget_id" is different)
$rows[] = [
    "widget_id" => "widget2",
    "size" => "medium",
    "item" => [
        "item_id" => "item1",
        "shape" => "circle",
        "paint" => [
            "paint_id" => "paint1",
            "colour" => "red",
        ]
    ]
];
 

Я пытаюсь получить следующий результат:

 [[
    "widget_id" => "widget1",
    "size" => "large",
    "item" => [
        "item_id" => "item1",
        "shape" => "circle",
        "paint" => [[
            "paint_id" => "paint1",
            "colour" => "red",
        ],[
            "paint_id" => "paint2",
            "colour" => "green",
        ]]
    ]
],[
    "widget_id" => "widget2",
    "size" => "medium",
    "item" => [
        "item_id" => "item1",
        "shape" => "circle",
        "paint" => [
            "paint_id" => "paint1",
            "colour" => "red",
        ]
    ]
]]
 

В принципе, когда две строки имеют один и тот же ключ и значения, объедините их. Если ключ тот же, но значение другое, сохраните оба значения и поместите их в числовой массив под ключом (вроде как, как array_merge_recursive это делается).

Проблема в том, что значения сами по себе могут быть массивами, и существует неизвестное количество уровней. Есть ли разумный и эффективный способ сделать это, или мне нужно прибегнуть к foreach циклу с высокой нагрузкой?

Спасибо за просмотр, надеюсь, что есть люди более умные, чем я, читающие это!

Комментарии:

1. Что вы пробовали до сих пор?

2. Если у вас несколько уровней, вам может понадобиться рекурсивная функция

3. @FelippeDuarte Я пишу рекурсивную foreach() функцию, потому что подозреваю, что нет никаких «умных» решений (пожалуйста, докажите, что я ошибаюсь!). Я опубликую это, как только закончу с этим. Это будет некрасиво!

Ответ №1:

Я добился получения ожидаемой структуры массива с помощью следующей функции, я надеюсь, что комментарии четко указывают на то, что внутри:

 function complex_merge(array $arr): array
{
    // Grouped items
    $result = [];
    $iterationKey = 0;

    // Loop through every item
    while (($element = array_shift($arr)) !== null) {
        // Save scalar values as is
        $scalarValues = array_filter($element, 'is_scalar');

        // Save array values in an array
        $arrayValues = array_map(fn(array $arrVal) => [$arrVal], array_filter($element, 'is_array'));
        $arrayValuesKeys = array_keys($arrayValues);

        $result[$iterationKey] = array_merge($scalarValues, $arrayValues);

        // Compare with remaining items
        for ($i = 0; $i < count($arr); $i  ) {
            $comparisonScalarValues = array_filter($arr[$i], 'is_scalar');

            // Scalar values are same, add the array values to the containing arrays
            if ($scalarValues === $comparisonScalarValues) {
                $comparisonArrayValues = array_filter($arr[$i], 'is_array');
                foreach ($arrayValuesKeys as $arrayKey) {
                    $result[$iterationKey][$arrayKey][] = $comparisonArrayValues[$arrayKey];
                }

                // Remove matching item
                array_splice($arr, $i, 1);
                $i--;
            }
        }

        // Merge array values
        foreach ($arrayValuesKeys as $arrayKey) {
            $result[$iterationKey][$arrayKey] = complex_merge($result[$iterationKey][$arrayKey]);

            // array key contains a single item, extract it
            if (count($result[$iterationKey][$arrayKey]) === 1) {
                $result[$iterationKey][$arrayKey] = $result[$iterationKey][$arrayKey][0];
            }
        }

        // Increment result key
        $iterationKey  ;
    }
    return $result;
}
 

Просто перейдите $rows к функции, быстрая проверка значений:

 echo '<pre>' . print_r(complex_merge($rows), true) . '</pre>';

/*
Displays:
Array
(
    [0] => Array
        (
            [widget_id] => widget1
            [size] => large
            [item] => Array
                (
                    [item_id] => item1
                    [shape] => circle
                    [paint] => Array
                        (
                            [0] => Array
                                (
                                    [paint_id] => paint1
                                    [colour] => red
                                )

                            [1] => Array
                                (
                                    [paint_id] => paint2
                                    [colour] => green
                                )

                        )

                )

        )

    [1] => Array
        (
            [widget_id] => widget2
            [size] => medium
            [item] => Array
                (
                    [item_id] => item1
                    [shape] => circle
                    [paint] => Array
                        (
                            [paint_id] => paint1
                            [colour] => red
                        )

                )

        )

)
*/
 

Комментарии:

1. Он мог бы иметь лучшие имена переменных и, вероятно, мог бы быть оптимизирован, но он работает, и мне было весело это делать 🙂

2. Спасибо за этот отличный пример. Я также опубликовал свою собственную попытку, но я думаю, что ваша лучше.

3. Я провел быстрый тест, мой, похоже, работает быстрее примерно на 0,01 миллисекунды. Но объектно-ориентированный подход может быть проще в использовании 🙂

4. Спасибо. На самом деле я многому научился у вашего подхода, вы использовали некоторые встроенные методы, с которыми я не был знаком (или удобен!). Может быть, я поиграю с гибридной версией, когда у меня будет время, а пока я реализовал вашу версию, поскольку она короче! Еще раз спасибо за вашу помощь.

Ответ №2:

Вот моя собственная попытка. Я думаю, что предпочитаю версию AymDev, хотя она намного более лаконична. Интересно, что быстрее.

 class ComplexMerge{
    /**
     * Checks to see whether an array has sequential numerical keys (only),
     * starting from 0 to n, where n is the array count minus one.
     *
     * @link https://codereview.stackexchange.com/questions/201/is-numeric-array-is-missing/204
     *
     * @param $arr
     *
     * @return bool
     */
    private static function isNumericArray($arr)
    {
        if(!is_array($arr)){
            return false;
        }
        return array_keys($arr) === range(0, (count($arr) - 1));
    }

    /**
     * Given an array, separate out
     * array values that themselves are arrays
     * and those that are not.
     *
     * @param array $array
     *
     * @return array[]
     */
    private static function separateOutArrayValues(array $array): array
    {
        $valuesThatAreArrays = [];
        $valuesThatAreNotArrays = [];

        foreach($array as $key => $val){
            if(is_array($val)){
                $valuesThatAreArrays[$key] = $val;
            } else {
                $valuesThatAreNotArrays[$key] = $val;
            }
        }

        return [$valuesThatAreArrays, $valuesThatAreNotArrays];
    }

    /**
     * Groups row keys together that have the same non-array values.
     * If every row is already unique, returns NULL.
     *
     * @param $array
     *
     * @return array|null
     */
    private static function groupRowKeysWithSameNonArrayValues($array): ?array
    {
        foreach($array as $key => $row){
            # Separate out the values that are arrays and those that are not
            [$a, $v] = self::separateOutArrayValues($row);

            # Serialise the values that are not arrays and create a unique ID from them
            $uniqueRowId = md5(serialize($v));

            # Store all the original array keys under the unique ID
            $deduplicatedArray[$uniqueRowId][] = $key;
        }

        # If every row is unique, there are no more rows to combine, and our work is done
        if(!$a amp;amp; count($array) == count($deduplicatedArray)){
            return NULL;
        }

        return $deduplicatedArray;
    }

    private static function mergeRows(array $array): array
    {
        # Get the grouped row keys
        if(!$groupedRowKeys = self::groupRowKeysWithSameNonArrayValues($array)){
            //If there are no more rows to merge
            return $array;
        }

        foreach($groupedRowKeys as $uniqueRowId => $keys){

            foreach($keys as $id => $key){
                # Separate out the values that are arrays and those that are not
                [$valuesThatAreArrays, $valuesThatAreNotArrays] = self::separateOutArrayValues($array[$key]);
                //We're using the key from the grouped row keys array, but using it on the original array

                # If this is the first row from the group, throw in the non-array values
                if(!$id){
                    $unique[$uniqueRowId] = $valuesThatAreNotArrays;
                }

                # For each of the values that are arrays include them back in
                foreach($valuesThatAreArrays as $k => $childArray){
                    $unique[$uniqueRowId][$k][] = $childArray;
                    //Wrap them in a numerical key array so that only children and siblings are have the same parent-child relationship
                }
            }
        }

        # Go deeper
        foreach($unique as $key => $val){
            foreach($val as $k => $valuesThatAreNotArrays){
                if(self::isNumericArray($valuesThatAreNotArrays)){
                    $unique[$key][$k] = self::mergeRows($unique[$key][$k]);
                }
            }
        }

        # No need to include the unique row IDs
        return array_values($unique);
    }

    public static function normalise($array): ?array
    {
        $array = self::mergeRows($array);
        return $array;
    }
}
 

Использование:

 $array = ComplexMerge::normalise($array);
 

ДЕМОНСТРАЦИЯ