Convert list of dictionaries into list of nested dictionaries

Viewed 229

I tried and saw multiple solutions for the above question but could not find a solution that suits my case. I have following list of dictionaries.

input_list = [
                {'k0':'v0','level':0,'row':0},
                {'k1':'v1','level':1,'row':1},
                {'k2':'v2','level':2,'row':2},
                {'k3':'v3','level':2,'row':3},
                {'k4':'v4','level':1,'row':4},
                {'k5':'v5','level':2,'row':5},
                {'k6':'v6','level':1,'row':6},
                {'k7':'v7','level':0,'row':7},
                {'k8':'v8','level':1,'row':8},
                {'k9':'v9','level':2,'row':9},
            ]

and I need to convert this list to list of nested dictionaries in the following way

[
  {'k0':'v0',
   'level':0,
   'child':[
          {'k1':'v1','level':1,'child':[{'k2':'v2','level':2}, 
                                        {'k3':'v3','level':2}]},
          {'k4':'v4','level':1,'child':[{'k5':'v5','level':2}]},
          {'k6':'v6','level':1}
         ]},

  {'k7':'v7',
   'level':0,
   'child':[{'k8':'v8','level':1,'child':[{'k9':'v9','level':2}]}]}
]

Solutions which I tried are as follow

levels = dict()
for n in input_list:
    levels.setdefault(n['level'], []).append(n)

this gives me dictionaries with levels separated but I couldn't move past this code for my desired solution

Any help would be appreciated. Thanks!

2 Answers

You can use recursion with itertools.groupby:

from itertools import groupby as gb
data = [{'k0': 'v0', 'level': 0, 'row': 0}, {'k1': 'v1', 'level': 1, 'row': 1}, {'k2': 'v2', 'level': 2, 'row': 2}, {'k3': 'v3', 'level': 2, 'row': 3}, {'k4': 'v4', 'level': 1, 'row': 4}, {'k5': 'v5', 'level': 2, 'row': 5}, {'k6': 'v6', 'level': 1, 'row': 6}, {'k7': 'v7', 'level': 0, 'row': 7}, {'k8': 'v8', 'level': 1, 'row': 8}, {'k9': 'v9', 'level': 2, 'row': 9}]
_d = [{a:b for a, b in i.items() if a not in {'row'}} for i in data]

def get_results(d, l = 0):
  r, p = [(a, list(b)) for a, b in gb(d, key=lambda x:x['level'] == l)], []
  for a, b in r:
     if a:
        p.extend(b)
     else:      
        p[-1]['child'] = get_results(b, l+1)
  return p

import json
print(json.dumps(get_results(_d), indent=4))

Output:

[
  {
    "k0": "v0",
    "level": 0,
    "child": [
        {
            "k1": "v1",
            "level": 1,
            "child": [
                {
                    "k2": "v2",
                    "level": 2
                },
                {
                    "k3": "v3",
                    "level": 2
                }
            ]
        },
        {
            "k4": "v4",
            "level": 1,
            "child": [
                {
                    "k5": "v5",
                    "level": 2
                }
            ]
        },
        {
            "k6": "v6",
            "level": 1
        }
    ]
 },
 {
    "k7": "v7",
    "level": 0,
    "child": [
        {
            "k8": "v8",
            "level": 1,
            "child": [
                {
                    "k9": "v9",
                    "level": 2
                }
            ]
         }
      ]
    }
]
# This stores last dicts seen by their levels
dict_by_levels = [None] * len(input_list)

# To be returned; begins with the first "root"
output_list = [input_list[0]]

for prev_dict, cur_dict in zip(input_list, input_list[1:]):
    # Get previous and current level, and store them in levels dict
    prev_level, cur_level = prev_dict["level"], cur_dict["level"]
    dict_by_levels[prev_level] = prev_dict
    dict_by_levels[cur_level] = cur_dict

    # If we are currently in a deeper level, append to previous dict's child
    if cur_level > prev_level:
        prev_dict.setdefault("child", []).append(cur_dict) 

    # If we are currently in a shallower or equal level *and* the current level is not 0,
    # then get the last one-level-before dict seen so far, and append to it
    elif cur_level <= prev_level and cur_level != 0:
        dict_by_levels[cur_level-1].setdefault("child", []).append(cur_dict)

    # This else is equivalent to "if cur_level == 0" i.e. a "root" dict
    else:
        output_list.append(cur_dict)

I keep track of two level for each turn of the loop i.e. previous and current dicts. I also make dict_by_levels which is a list of dicts by level; index 0, 1, 2.. will have the last dictionary seen by that level, if any. This helps in the case where we go a shallower or equal level (e.g. from 2 to 2).

If the current level is higher than previous level (e.g. 1 > 0), then we simply append to children of current dict.

If the current level is less than equal to the previous level, this means that we should be appending to the children of last seen dict with level current level - 1 (dict_by_levels helps here). Of course, if current level is 0, we don't do this - we have a new root.

And that root case is handled in else clause via appending to the output list to be returned.

Related