38

I'm trying to turn a list into separated strings joined with an ampersand if there are only two items, or commas and an ampersand between the last two e.g.

Jones & Ben
Jim, Jack & James

I currently have this:

pa = ' & '.join(listauthors[search])

and don't know how to make sort out the comma/ampersand issue. Beginner so a full explanation would be appreciated.

Mazdak
  • 100,514
  • 17
  • 155
  • 179
daisyl
  • 381
  • 1
  • 3
  • 5

9 Answers9

40
"&".join([",".join(my_list[:-1]),my_list[-1]])

I would think would work

or maybe just

",".join(my_list[:-1]) +"&"+my_list[-1]

to handle edge cases where only 2 items you could

"&".join([",".join(my_list[:-1]),my_list[-1]] if len(my_list) > 2 else my_list)
Joran Beasley
  • 103,130
  • 11
  • 146
  • 174
  • A million times I have needed this. I wish there was a builtin string method for this, or that the `join()` would be updated to accept an optional second argument for the last join. – np8 Nov 21 '20 at 20:32
15

You could break this up into two joins. Join all but the last item with ", ". Then join this string and the last item with " & ".

all_but_last = ', '.join(authors[:-1])
last = authors[-1]

' & '.join([all_but_last, last])

Note: This doesn't deal with edge cases, such as when authors is empty or has only one element.

John Kugelman
  • 330,190
  • 66
  • 504
  • 555
14

One liner. Just concatenate all but the last element with , as the delimiter. Then just append & and then the last element finally to the end.

print ', '.join(lst[:-1]) + ' & ' + lst[-1]

If you wish to handle empty lists or such:

if len(lst) > 1:
    print ', '.join(lst[:-1]) + ' & ' + lst[-1]
elif len(lst) == 1:
    print lst[0]
Saksham Varma
  • 2,052
  • 11
  • 15
4
('{}, '*(len(authors)-2) + '{} & '*(len(authors)>1) + '{}').format(*authors)

This solution can handle a list of authors of length > 0, though it can be modified to handle 0-length lists as well. The idea is to first create a format string that we can format by unpacking list. This solution avoids slicing the list so it should be fairly efficient for large lists of authors.

First we concatenate '{}, ' for every additional author beyond two authors. Then we concatenate '{} & ' if there are two or more authors. Finally we append '{}' for the last author, but this subexpression can be '{}'*(len(authors)>0) instead if we wish to be able to handle an empty list of authors. Finally, we format our completed string by unpacking the elements of the list using the * unpacking syntax.

If you don't need a one-liner, here is the code in an efficient function form.

def format_authors(authors):
    n = len(authors)
    if n > 1:
        return ('{}, '*(n-2) + '{} & {}').format(*authors)
    elif n > 0:
        return authors[0]
    else:
        return ''

This can handle a list of authors of any length.

Shashank
  • 13,217
  • 5
  • 34
  • 61
  • I like it! I didn't think of attempting it with just a formatting string. Not sure I would've known where to start. – Deacon May 06 '15 at 18:14
  • 2
    @DougR. Well the idea is to just sit back and think of the math formulas. We only want the ampersand thing if the length of authors is greater than 1. We only want 1 comma separator for each author greater than 2. It's graceful because `'anystring'*0` is just the empty string. – Shashank May 06 '15 at 18:17
  • I do have another question. Can this method be adapted to insert a comma prior to the ampersand for a list of length *n*, where *n > 2*? I.e., "Jim, **Jack,** & John." This isn't being asked as a "gotcha," but as a genuine question, as most American style manuals mandate this. Frankly, I don't see a way to do this without turning `if n > 1: ...` into `if n > 2: ... / elif n > 1: ...`, and that just seems wrong somehow to do to such an elegant solution. Thanks! – Deacon May 06 '15 at 20:08
  • 1
    @DougR. Tbh, there's nothing wrong with the `if n > 2: ... elif n > 1` solution. And it *is* quite elegant. In fact that is exactly how I would do it. It would be as simple as `if n > 2: return ('{}, '*(n-2) + '{}, & {}').format(*authors) elif: n > 1: return '{} & {}'.format(*authors) elif etc....` But if you really wanted to avoid adding another if-block for whatever reason, you could use `'{}' + ','*(n>2) + ' & {}'` for the last part. That's actually *less* elegant in my opinion, but it works since booleans get implicitly converted to 1s and 0s. :) – Shashank May 06 '15 at 20:18
  • To use an Oxford comma (a comma after the last word before the & you can just expand on the logic for the format string. `('{}, '*(len(authors)-2) + '{} and '*(len(authors)==2) + '{}, and '*(len(authors)>2) + '{}').format(*authors)` The challenge with the Oxford common is that if the list only has length two you do not want the comma before the "And". The function solution is trivial as it is simply another condition so I left it out. – statuser Jul 17 '20 at 20:07
3

You can simply use Indexng and F-strings (Python-3.6+):

In [1]: l=['Jim','Dave','James','Laura','Kasra']                                                                                                                                                            

In [3]: ', '.join(l[:-1]) + f' & {l[-1]}'                                                                                                                                                                      

Out[3]: 'Jim, Dave, James, Laura & Kasra'
Mazdak
  • 100,514
  • 17
  • 155
  • 179
2

It looks like while I was working on my answer, someone may have beaten me to the punch with a similar one. Here's mine for comparison. Note that this also handles cases of 0, 1, or 2 members in the list.

# Python 3.x, should also work with Python 2.x.
def my_join(my_list):
    x = len(my_list)
    if x > 2:
        s = ', & '.join([', '.join(my_list[:-1]), my_list[-1]])
    elif x == 2:
        s = ' & '.join(my_list)
    elif x == 1:
        s = my_list[0]
    else:
        s = ''
    return s

assert my_join(['Jim', 'Jack', 'John']) == 'Jim, Jack, & John'
assert my_join(['Jim', 'Jack']) == 'Jim & Jack'
assert my_join(['Jim',]) == 'Jim'
assert my_join([]) == ''
Deacon
  • 3,397
  • 24
  • 52
2

Just a more grammatically correct example :)

def list_of_items_to_grammatical_text(items):
    if len(items) <= 1:
        return ''.join(items)
    if len(items) == 2:
        return ' and '.join(items)
    return '{}, and {}'.format(', '.join(items[:-1]), items[-1])

Output example:

l1 = []
l2 = ["one"]
l3 = ["one", "two"]
l4 = ["one", "two", "three"]

list_of_items_to_grammatical_text(l1)
Out: ''

list_of_items_to_grammatical_text(l2)
Out: 'one'

list_of_items_to_grammatical_text(l3)
Out: 'one and two'

list_of_items_to_grammatical_text(l4)
Out: 'one, two, and three'
maxandron
  • 1,650
  • 2
  • 12
  • 15
1

Here is a one line example that handles all the edge cases (empty list, one entry, two entries):

' & '.join(filter(None, [', '.join(my_list[:-1])] + my_list[-1:]))

The filter() function is used to filter out the empty entries that happens when my_list is empty or only has one entry.

Saur
  • 11
  • 2
0

Here's a simple one that also works for empty or 1 element lists:

' and '.join([', '.join(mylist[:-1])]+mylist[-1:])

The reason it works is that for empty lists both [:-1] and [-1:] give us an empty list again