Reputation: 81
I'm trying to merge list items with previous items if they don't contain a certain prefix, and adding a \n
between said list items when doing so.
prefix = '!'
cmds = ['!test','hello','world','!echo','!embed','oh god']
output = ['!test\nhello\nworld','!echo','!embed\noh god']
I tried something like
for i in list(range(0,len(cmds))):
if not cmds[i+1].startswith(prefix):
cmds[i] += cmds.pop(i+1)
but always get the list index out of range
error.
My apologies if this is badly worded, or seems like an obvious fix, I'm fairly new to python/programming.
Edit:
I had managed to get it to work with
prefix = '!'
cmds = ['!test','hello','world','!echo','!embed','oh god']
print(list(range(0,len(cmds))))
for i in reversed(range(len(cmds))):
if not cmds[i].startswith(prefix):
cmds[i-1] += '\n'+cmds.pop(i)
print(cmds)
but your answers seem so much neater and efficient. Much thanks everyone
Upvotes: 7
Views: 979
Reputation: 114310
Here's a one liner solution using itertools.groupby
and itertools.accumulate
:
from itertools import accumulate, groupby
from operator import itemgetter
x = ['!test','hello','world','!echo','!embed','oh god']
cumsum = accumulate(map(lambda s: s.startswith('!'), x))
result = ['\n'.join(map(itemgetter(0), g)) for _, g in groupby(zip(x, cumsum), itemgetter(1))]
This looks like a two liner because I wanted to make it semi-legible, but that's not always necessary:
result = ['\n'.join(map(itemgetter(0), g)) for _, g in groupby(zip(x, accumulate(map(lambda s: s.startswith('!'), x))), itemgetter(1))]
cumsum
provides the number of elements starting with !
found so far. This makes for a nice key to groupby
. It works by accumulating the booleans returned by str.startswith
into an integer.
The final result uses cumsum
as the key, but joins the grouped elements of x
with newlines.
Here's an IDEOne Link to play with.
Upvotes: 3
Reputation: 24232
A solution that is a bit longer, but that can easily be generalized to other situations, using itertools.groupby:
from itertools import groupby
class StartGroupOnPrefix:
def __init__(self, prefix):
self.output = False
self.prefix = prefix
def __call__(self, item):
if item.startswith(self.prefix):
self.output = not self.output
return self.output
prefix = '!'
cmds = ['!test','hello','world','!echo','!embed','oh god']
condition = StartGroupOnPrefix(prefix)
out = ['\n'.join(group) for f, group in groupby(cmds, condition)]
print(out)
# ['!test\nhello\nworld','!echo','!embed\noh god']
As we have an iterator, we also don't have to create the whole output list at once, we can generate each output on-the-fly:
for grouped_item in ('\n'.join(group) for f, group in groupby(cmds, condition)):
print('-----------\n', grouped_item)
# -----------
# !test
# hello
# world
# -----------
# !echo
# -----------
# !embed
# oh god
A bit of explanation :groupby(iterable)
starts a new group each time it gets a different item from iterable
. groupby(iterable, key)
starts a new group everytime the return value of the key
function changes. Our condition
function alternates its output between True
and False
each time the item starts with the prefix.
Upvotes: 1
Reputation: 9335
You can do it with list comprehension "also".
In [1]: cmds = ['!test','hello','world','!echo','!embed','oh god']
In [2]: prefix = '!'
In [3]: inds = [i for i, x in enumerate(cmds) if prefix in x]
In [4]: inds.append(len(cmds))
In [5]: lens = list(zip(inds, inds[1:]))
# [(0, 3), (3, 4), (4, 6)]
In [6]: ["\n".join(cmds[a:b]) for a, b in lens]
Out[6]: ['!test\nhello\nworld', '!echo', '!embed\noh god']
Upvotes: 1
Reputation: 27283
I propose to create a new list, as you showed in your problem specification:
prefix = '!'
cmds = ['!test','hello','world','!echo','!embed','oh god']
output = []
for cmd in cmds:
if cmd.startswith(prefix) or not output:
output.append(cmd)
else:
output[-1] += "\n" + cmd # change the string in the last element of output
The result is:
>>> output
['!test\nhello\nworld', '!echo', '!embed\noh god']
Upvotes: 10