How to generate "a, b, c, and d"?

R

Roy Smith

I've got a list, ['a', 'b', 'c', 'd']. I want to generate the string, "a, b, c, and d" (I'll settle for no comma after 'c'). Is there some standard way to do this, handling all the special cases?

[] ==> ''
['a'] ==> 'a'
['a', 'b'] ==> 'a and b'
['a', 'b', 'c', 'd'] ==> 'a, b, and c'

It seems like the kind of thing django.contrib.humanize would handle, but alas, it doesn't.
 
M

MRAB

I've got a list, ['a', 'b', 'c', 'd']. I want to generate the string, "a, b, c, and d" (I'll settle for no comma after 'c'). Is there some standard way to do this, handling all the special cases?

[] ==> ''
['a'] ==> 'a'
['a', 'b'] ==> 'a and b'
['a', 'b', 'c', 'd'] ==> 'a, b, and c'

It seems like the kind of thing django.contrib.humanize would handle, but alas, it doesn't.

How about this:

def and_list(items):
if len(items) <= 2:
return " and ".join(items)

return ", ".join(items[ : -1]) + ", and " + items[-1]

print(and_list([]))
print(and_list(['a']))
print(and_list(['a', 'b']))
print(and_list(['a', 'b', 'c']))
print(and_list(['a', 'b', 'c', 'd']))
 
T

Tim Chase

I've got a list, ['a', 'b', 'c', 'd']. I want to generate the string, "a, b, c, and d" (I'll settle for no comma after 'c'). Is there some standard way to do this, handling all the special cases?

[] ==> ''
['a'] ==> 'a'
['a', 'b'] ==> 'a and b'
['a', 'b', 'c', 'd'] ==> 'a, b, and c'

It seems like the kind of thing django.contrib.humanize would handle, but alas, it doesn't.

If you have a list, it's pretty easy as MRAB suggests. For
arbitrary iterators, it's a bit more complex. Especially with
the odd edge-case of 2 items where there's no comma before the
conjunction (where >2 has the comma before the conjunction). If
you were willing to forgo the Oxford comma, it would tidy up the
code a bit. Sample code below

-tkc

def gen_list(i, conjunction="and"):
i = iter(i)
first = i.next()
try:
prev = i.next()
except StopIteration:
yield first
else:
more_than_two = False
for item in i:
if not more_than_two: yield first
yield prev
prev = item
more_than_two = True
if more_than_two:
yield "%s %s" % (conjunction, prev)
else:
yield "%s %s %s" % (first, conjunction, prev)

def listify(lst, conjunction="and"):
return ', '.join(gen_list(lst, conjunction))

for test, expected in (
([], ''),
(['a'], 'a'),
(['a', 'b'], 'a and b'),
(['a', 'b', 'c'], 'a, b, and c'),
(['a', 'b', 'c', 'd'], 'a, b, c, and d'),
):
result = listify(test)
print "%r -> %r (got %r) %s" % (
test, expected, result,
result == expected and "PASSED" or "FAILED"
)
 
E

Ethan Furman

Tim said:
I've got a list, ['a', 'b', 'c', 'd']. I want to generate the string,
"a, b, c, and d" (I'll settle for no comma after 'c'). Is there some
standard way to do this, handling all the special cases?

[] ==> ''
['a'] ==> 'a'
['a', 'b'] ==> 'a and b'
['a', 'b', 'c', 'd'] ==> 'a, b, and c'

It seems like the kind of thing django.contrib.humanize would handle,
but alas, it doesn't.

If you have a list, it's pretty easy as MRAB suggests. For arbitrary
iterators, it's a bit more complex. Especially with the odd edge-case
of 2 items where there's no comma before the conjunction (where >2 has
the comma before the conjunction). If you were willing to forgo the
Oxford comma, it would tidy up the code a bit. Sample code below

<snip>

Why go through all that instead of just converting the iterator into a
list at the beginning of MRAB's solution and then running with it?

~Ethan~
 
T

Tim Chase

Tim said:
I've got a list, ['a', 'b', 'c', 'd']. I want to generate the string,
"a, b, c, and d" (I'll settle for no comma after 'c'). Is there some
standard way to do this, handling all the special cases?

If you have a list, it's pretty easy as MRAB suggests. For arbitrary
iterators, it's a bit more complex. Especially with the odd edge-case
of 2 items where there's no comma before the conjunction (where>2 has
the comma before the conjunction). If you were willing to forgo the
Oxford comma, it would tidy up the code a bit.

Why go through all that instead of just converting the iterator into a
list at the beginning of MRAB's solution and then running with it?

For the fun/challenge? Because you have a REALLY big data source
that you don't want to keep in memory (in addition the resulting
string)?

Yeah, for most non-pathological cases, it would make more sense
to just make it a list and then deal with the 4 cases (no
elements, one element, 2 elements, and >2 elements) individually.

-tkc
 
R

Roy Smith

FWIW, I ended up with:

n = len(names)
if n == 0:
return ''
if n == 1:
return names[0]
pre = ', '.join(names[:-1])
post = names[-1]
return '%s, and %s' (pre, post)

the slice-and-join() takes care of both the 2 and >2 element cases at the same time :)

It would be nice if there were some standard way to do this. I'm sure I'veseen something that was essentially a join() that took two delimiters; onefor most elements, the other a special-case for the last one. I can't remember where I saw it. I'm guessing in some web framework.
 
R

Roy Smith

FWIW, I ended up with:

n = len(names)
if n == 0:
return ''
if n == 1:
return names[0]
pre = ', '.join(names[:-1])
post = names[-1]
return '%s, and %s' (pre, post)

the slice-and-join() takes care of both the 2 and >2 element cases at the same time :)

It would be nice if there were some standard way to do this. I'm sure I'veseen something that was essentially a join() that took two delimiters; onefor most elements, the other a special-case for the last one. I can't remember where I saw it. I'm guessing in some web framework.
 
M

MRAB

Tim said:
On 12/15/11 10:48, Roy Smith wrote:
I've got a list, ['a', 'b', 'c', 'd']. I want to generate the
string, "a, b, c, and d" (I'll settle for no comma after 'c').
Is there some standard way to do this, handling all the special
cases?

If you have a list, it's pretty easy as MRAB suggests. For
arbitrary iterators, it's a bit more complex. Especially with
the odd edge-case of 2 items where there's no comma before the
conjunction (where>2 has the comma before the conjunction). If
you were willing to forgo the Oxford comma, it would tidy up the
code a bit.

Why go through all that instead of just converting the iterator
into a list at the beginning of MRAB's solution and then running
with it?

For the fun/challenge? Because you have a REALLY big data source
that you don't want to keep in memory (in addition the resulting
string)?

Yeah, for most non-pathological cases, it would make more sense to
just make it a list and then deal with the 4 cases (no elements, one
element, 2 elements, and>2 elements) individually.
I was going to question it too, but then I wondered what would happen if
there were a very large number of items and the string would be too big
for memory, for example, writing a list of all the numbers from one to a
billion to a file.
 
I

Ian Kelly

For the fun/challenge?  Because you have a REALLY big data source that you
don't want to keep in memory (in addition the resulting string)?

If you have that much data, then I question why you would want to
build such a large human-readable list in the first place. Nobody is
going to want to read that no matter how you format it.
 
T

Terry Reedy

I've got a list, ['a', 'b', 'c', 'd']. I want to generate the string,
"a, b, c, and d" (I'll settle for no comma after 'c'). Is there some
standard way to do this, handling all the special cases?

[] ==> ''
['a'] ==> 'a'
['a', 'b'] ==> 'a and b'
['a', 'b', 'c', 'd'] ==> 'a, b, and c'

It seems like the kind of thing django.contrib.humanize would handle,
but alas, it doesn't.

How about this:

def and_list(items):
if len(items) <= 2:
return " and ".join(items)

return ", ".join(items[ : -1]) + ", and " + items[-1]

To avoid making a slice copy,

last = items.pop()
return ", ".join(items) + (", and " + last)

I parenthesized the last two small items to avoid copying the long
string twice with two appends. Even better is

items[-1] = "and " + items[-1]
return ", ".join(items)

so the entire output is created in one operation with no copy.

But I would only mutate the list if I started with
items = list(iterable)
where iterable was the input, so I was mutating a private copy.
 
C

Chris Angelico

 items[-1] = "and " + items[-1]
 return ", ".join(items)

This works only if you're sure there are at least two items, and if
you don't mind two items coming out as "a, and b".

ChrisA
 
T

Terry Reedy

items[-1] = "and " + items[-1]
return ", ".join(items)

This works only if you're sure there are at least two items, and if
you don't mind two items coming out as "a, and b".

Please read the context that you removed.
The original second return line and my replacements come after

if len(items) <= 2:
return " and ".join(items)
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,772
Messages
2,569,593
Members
45,112
Latest member
VinayKumar Nevatia
Top