zhuyxn
zhuyxn

Reputation: 7091

Python: Linking Lists Together

Suppose I have a list where each index is either a name, or a list of rooms the preceding name index reserved.

[["Bob"],["125A, "154B", "643A"],["142C", "192B"], ["653G"], 
["Carol"], ["95H", 123C"], ["David"], ["120G"]]

So in this case, Bob has the rooms: 125A, 154B, 643A, 152C, 192B, and 653G reserved, etc.

How do I construct a function which would make the above into the following format:

[["Bob", "125A, "154B", "643A", "142C", "192B", "653G"], ["Carol"... 

Essentially concatenating [name] with all the [list of room reservations], until the next instance of [name]. I have a function which takes a list, and returns True if a list is a name, and False if it is a list of room reservations, so effectively I have:

[True, False, False, False, True, False, True False] for the above list, but not sure how that would help me, if at all. Assume that if a list contains names, it only has one name.

Upvotes: 1

Views: 191

Answers (5)

Benedict
Benedict

Reputation: 2821

reduce is your answer. Your data is this:

l=[['Bob'], ['125A', '154B', '643A'], ['142C', '192B'], ['653G'], ['Carol'], ['95H', '123C'], ['David'], ['120G']]

You say you've already got a function that determines if an element is a name. Here is my one:

import re
def is_name(s):
  return re.match("[A-z]+$",s) and True or False

Then, using reduce, it is a one liner:

reduce(lambda c, n: is_name(n[0]) and c+[n] or c[:-1]+[c[-1]+n], l, [])

Result is:

[['Bob', '125A', '154B', '643A', '142C', '192B', '653G'], ['Carol', '95H', '123C'], ['David', '120G']]

Upvotes: 0

Burhan Khalid
Burhan Khalid

Reputation: 174662

Really, you should be using a dict for this. This assumes that the order of lists doesn't change (the name is always first).

As others suggested you should re-evaluate your data structure.

>>> from itertools import chain
>>> li_combo = list(chain.from_iterable(lst))
>>> d = {}
>>> for i in li_combo:
...    if is_name(i):
...       k = i
...    if k not in d:
...       d[k] = []
...    else:
...       d[k].append(i)
... 
>>> final_list = [[k]+d[k] for k in d]
>>> final_list
[['Bob', '125A', '154B', '643A', '142C', '192B', '653G'], ['Carol', '95H', '123C'], ['David', '120G']]

Upvotes: 0

sloth
sloth

Reputation: 101132

Given the following method

def is_name(x):
  return # if x is a name or not

a simply and short solution is to use a defaultdict


Example:

from collections import defaultdict

def do_it(source):
  dd = defaultdict(lambda: [])
  for item in sum(source, []): # just use your favourite flattening method here
    if is_name(item):
      name = item
    else:
      dd[name].append(item)
  return [[k]+v for k,v in dd.items()]

for s in do_it(l):
  print s

Output:

['Bob', '125A', '154B', '643A', '142C', '192B', '653G']
['Carol', '95H', '123C']
['David', '120G']


Bonus:

This one uses a generator for laziness

import itertools 

def do_it(source):
  name, items = None, []
  for item in itertools.chain.from_iterable(source):
    if is_name(item):
      if name: 
        yield [name] + items
        name, items = None, []
      name = item
    else:
      items.append(item)
  yield [name] + items

Upvotes: 1

Herbie
Herbie

Reputation: 346

Assume that the function which takes a list and returns True or False based on whether list contains name or rooms is called containsName() ...

def process(items):
  results = []
  name_and_rooms = []
  for item in items:
    if containsName(item):
      if name_and_rooms:
        results.append(name_and_rooms[:])
        name_and_rooms = []
      name_and_rooms.append(item[0])
    else:
      name_and_rooms.extend(item)
  if name_and_rooms:
    results.append(name_and_rooms[:])
  return results

This will print out name even if there are no list of rooms to follow, e.g. [['bob'],['susan']].

Also, this will not merge repeated names, e.g. [['bob'],['123'],['bob'],['456']]. If that is desired, then you'll need to shove names into a temporary dict instead, with each room list as values to it. And then spit out the key-values of the dict at the end. But that on its own will not preserve the order of the names. If you care to preserve the order of the names, you can have another list that contains the order of the names and use that when spitting out the values in the dict.

Upvotes: 0

RocketDonkey
RocketDonkey

Reputation: 37269

I'll preface this by saying that I strongly agree with @uʍopǝpısdn's suggestion. However if your setup precludes changing it for some reason, this seems to work (although it isn't pretty):

# Original list
l = [["Bob"],["125A", "154B", "643A"],["142C", "192B"], ["653G"], ["Carol"], ["95H", "123C"], ["David"], ["120G"]]
# This is the result of your checking function
mapper = [True, False, False, False, True, False, True, False]

# Final list
combined = []

# Generic counters
# Position in arrays
i = 0
# Position in combined list
k = 0

# Loop through the main list until the end.
# We don't use a for loop here because we want to be able to control the
# position of i.
while i < len(l):
  # If the corresponding value is True, start building the list
  if mapper[i]:
    # This is an example of how the code gets messy quickly
    combined.append([l[i][0]])
    i += 1
    # Now that we've hit a name, loop until we hit another, adding the
    # non-name information to the original list
    while i < len(mapper) and not mapper[i]:
      combined[k].append(l[i][0])
      i += 1

    # increment the position in our combined list
    k += 1


print combined

Upvotes: 0

Related Questions