43

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
  • 105,000
  • 18
  • 159
  • 188
daisyl
  • 431
  • 1
  • 4
  • 5
  • Note that the rules about joining lists differ from language to language. For example, in English with Oxford comma: "Motorcycle, Bus, and Car", in German: "Motorcycle, Bus oder Car". JavaScript has the [`Intl.ListFormat` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat) for this, it's too bad Python doesn't seem to have the equivalent. – Flimm Feb 03 '23 at 08:31

10 Answers10

45
"&".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
  • 110,522
  • 12
  • 160
  • 179
  • 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. – Niko Föhr Nov 21 '20 at 20:32
16

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
  • 349,597
  • 67
  • 533
  • 578
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,122
  • 13
  • 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,713
  • 5
  • 37
  • 63
  • 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
  • 105,000
  • 18
  • 159
  • 188
3

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
  • 31
  • 2
3

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
  • 16
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,615
  • 2
  • 31
  • 52
1

My Idea is as following I dug a bit deeper and tried to make it more flexible as requested.

Concept: Use dictionary to create a person with an attached list of friends.

Advantage: Can contain/store multiple persons with regarding friends. :)

How it works:

  1. The "for" loop iterates through the number of keys stored inside the dictionary.
  2. The key (here the entered person) is then put in the first string of the loop with the argument end="" which tells the print function to operate in the same line.
  3. After that a second variable is implemented which is given the length of the key-value by requesting the stored variable of the dictionary behind the key. We need that value later to determine the last entry of the list. ;) (Note: with every for loop it gets a new value)
  4. In the next for loop we will iterate through all friends on the list by pointing to the key-value (here the list) through the key from the outer for loop iteration.
  5. Inside this for loop are three mechanism intertwined:

5.a) The value n_friend decrements by one with each cycle.

5.b) As long as n_friend does not reach zero every friend will be printed with the argument end="" as explained in point 2.

5.c) After n_friend reaches zero the if/else conditional statment will switch to else and will print the last iteration of the for loop with the requested "and...[enter friend here].". This is without a end="" so the next print will switch into a new line.

  1. Continue the cycle by 1 again.

.break for code.

friends_list={
    "person_1":['Jack', 'John','Jane'],
    "person_2":['Luke', 'Darth','Han','Wednesday']
}

for person in friends_list:
    print(f"{person} likes following persons: ",end="")
    n_friend = len(friends_list[person])
    for friend in friends_list[person]:
        n_friend -= 1
        if n_friend > 0:
            print(friend, end=" ")
        else:
            print(f"and {friend}.")
404rorre
  • 89
  • 7
  • 1
    Thank you for contributing to our community :) Please feel free also to add some comments to explain your code. – Seymour Dec 21 '22 at 21:30
  • 1
    Suggesting to add some textual explanations in addition to only code. – Seymour Dec 21 '22 at 21:31
  • My bad. Here with a bit more context. I left the for loop principle outside as this will be covered in other posts. I hope. Twas my first answer. ;) – 404rorre Dec 22 '22 at 01:42
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