How do I merge a dictionary based on a condition?

Viewed 702

Say I have the following list of dictionaries:

x = [{
  '218': {
    'text': 'profit',
    'start': 0,
    'end': 21
  }
}, {
  '312': {
    'text': 'for',
    'start': 30,
    'end': 60
  }
}, {
  '350': {
    'text': 'year',
    'start': 70,
    'end': 85
  }
}, {
  '370': {
    'text': 'next column',
    'start': 120,
    'end': 130
  }
}, {
  '385': {
    'text': 'next_column',
    'start': 160,
    'end': 169
  }
}]

I want to merge some of the dictionaries, condition is whenever the end of first dict and the start of next dict have a difference less than 20 than I need to merge all the dict, and concatenate all the text.

The output should look like this:

x_new = [{
  '218,312,350': {
    'text': 'profit for year',
    'start': 0,
    'end': 85
  }
}, {
  '370': {
    'text': 'next column',
    'start': 120,
    'end': 130
  }
}, {
  '385': {
    'text': 'next_column',
    'start': 160,
    'end': 169
  }
}]

I have already solved it with the basic approach, but it does not look good, is there any solution using itertools or something like that?

What i have tried

x_updated=sorted(x, key=lambda x: x.values()[0])
final_merge=[]
merge=[]
for first, second in zip(x_updated, x_updated[1:]):
    if abs(second.values()[0]['start']-first.values()[0]['end'])<25:
        print "its belong to the same column"
        merge=merge+[first.keys()[0]]
    else:
        merge=merge+[first.keys()[0]]
        final_merge=final_merge+[merge]
        merge=[]
merge=merge+[second.keys()[0]]      
final_merge=final_merge+[merge]

And once i have final_merge, which tells me which value to merge its easy to add the values. but for the above code is there any simple way.Also, in the end after the loop i manually added the last dict because in my situation the last one would always be a different column, but what if it belongs to the same?

3 Answers

Try this:

x=[{'218':{'text':'profit','start':0,'end':21}},
   {'312':{'text':'for','start':30,'end':60}},
   {'350':{'text':'year','start':70,'end':85}},
   {'370':{'text':'next column','start':120,'end':130}},
   {'385':{'text':'next_column','start':160,'end':169}}]

x_new = []
d_keys = []
first_start_value = 0

def merge_dict(d_keys,x,i,first_start_value,current_index_dict_key):
    # remove duplicate list of string
    d_keys = list(set(d_keys))

    # sort list by number
    d_keys.sort(key=int)
    new_key = ','.join(d_keys)

    # update start value
    x[i][current_index_dict_key]['start'] = first_start_value
    dict1 = {new_key: x[i][current_index_dict_key]}
    return  dict1

for i in range(0,len(x)):
    current_index_dict_key = list(x[i].keys())[0]

    #check next index of list is valid
    if i+1 > len(x)-1:
        if len(d_keys) > 0:
            # merge dictionary
            dict1 = merge_dict(d_keys, x, i, first_start_value, current_index_dict_key)
            x_new.append(dict1)
            break

        dict1 = {current_index_dict_key: x[i][current_index_dict_key]}
        x_new.append(dict1)
        break

    next_index_dict_key = list(x[i+1].keys())[0]
    start = x[i+1][next_index_dict_key]['start']
    end = x[i][current_index_dict_key]['end']
    diff = start - end

    #compare current and next list of dicstionary end and start value
    if diff < 20:
        if len(d_keys) <= 0 and i == 1:
            first_start_value = x[i][current_index_dict_key]['start']

        d_keys.append(current_index_dict_key)
        d_keys.append(next_index_dict_key)
    else:

        if len(d_keys) > 0:
            # merge dictionary
            dict1 = merge_dict(d_keys,x,i,first_start_value,current_index_dict_key)
            d_keys = []
            first_start_value = x[i][current_index_dict_key]['start']
        else:
            dict1 = {current_index_dict_key: x[i][current_index_dict_key]}

        x_new.append(dict1)

print(x_new)

O/P:

[
  {
    '218,312,350': {
      'text': 'year',
      'start': 0,
      'end': 85
    }
  },
  {
    '370': {
      'text': 'next column',
      'start': 120,
      'end': 130
    }
  },
  {
    '385': {
      'text': 'next_column',
      'start': 160,
      'end': 169
    }
  }
]

I would create a class for these objects you use:

class my_dict:
    __init__(self, id, text, start, end):
        self.id = id
        self.text = text
        self.start = start
        self.end = end

    merge(self, other):
        self.id = "{},{}".format(self.id, other.id)
        self.text = "{} {}".format(self.text, other.text)
        self.end = other.end

And then the main code loop will be:

x_new = [x[0]]
for obj in x[1:]:
    last = x_new[-1]
    if obj.start - last.end > 20:
        x_new.append(obj)
    else:
        last.merge(obj)

This is what I would do:

first I would make some helper functions:

def merge(d1, d2):
    return {",".join([list(d1)[0], list(d2)[0]]): {'text': " ".join([list(d1.values())[0]['text'], list(d2.values())[0]['text']]), 'start': list(d1.values())[0]['start'], 'end': list(d2.values())[0]['end']}}

def should_merge(d1, d2):
    if (d1 is None) or (d2 is None):
        return False
    return abs(list(d1.values())[0]['end'] - list(d2.values())[0]['start']) < 20

The first function merges two dictionaries

The second returns True if two dictionaries should merge.

All that's left is the actual merge function:

from itertools import zip_longest
def merged_dicts(x):
    actual_merge = []
    last_merged = False
    for d1, d2 in zip_longest(x, x[1:], fillvalue=None):
        if should_merge(d1, d2) and last_merged:
            actual_merge.append(merge(actual_merge.pop(), d2))
        elif should_merge(d1, d2):
            actual_merge.append(merge(d1, d2))
            last_merged = True
        elif last_merged:
            last_merged = False
        else:
            actual_merge.append(d1)
            last_merged = False
    print(actual_merge)

That is a little more readable though it doesn't use any "fancy" itertool functions.

I would also consider changing the id of the dict to be inside the inner dict:

d= {'id': '385',
    'text': 'next_column',
    'start': 160,
    'end': 169
  }

That is a little less complicated and cleaner.

Related