Comma separated lists in django templates

PythonDjangoListDjango Templates

Python Problem Overview


If fruits is the list ['apples', 'oranges', 'pears'],

is there a quick way using django template tags to produce "apples, oranges, and pears"?

I know it's not difficult to do this using a loop and {% if counter.last %} statements, but because I'm going to use this repeatedly I think I'm going to have to learn how to write custom tags filters, and I don't want to reinvent the wheel if it's already been done.

As an extension, my attempts to drop the Oxford Comma (ie return "apples, oranges and pears") are even messier.

Python Solutions


Solution 1 - Python

First choice: use the existing join template tag.

http://docs.djangoproject.com/en/dev/ref/templates/builtins/#join

Here's their example

{{ value|join:" // " }}

Second choice: do it in the view.

fruits_text = ", ".join( fruits )

Provide fruits_text to the template for rendering.

Solution 2 - Python

Here's a super simple solution. Put this code into comma.html:

{% if not forloop.last %}{% ifequal forloop.revcounter 2 %} and {% else %}, {% endifequal %}{% else %}{% endif %}

And now wherever you'd put the comma, include "comma.html" instead:

{% for cat in cats %}
Kitty {{cat.name}}{% include "comma.html" %}
{% endfor %}

Update: @user3748764 gives us a slightly more compact version, without the deprecated ifequal syntax:

{% if not forloop.first %}{% if forloop.last %} and {% else %}, {% endif %}{% endif %}

Note that it should be used before the element, not after.

Solution 3 - Python

On the Django template this all you need to do for establishing a comma after each fruit. The comma will stop once its reached the last fruit.

{% if not forloop.last %}, {% endif %}

Solution 4 - Python

I would suggest a custom django templating filter rather than a custom tag -- filter is handier and simpler (where appropriate, like here). {{ fruits | joinby:", " }} looks like what I'd want to have for the purpose... with a custom joinby filter:

def joinby(value, arg):
    return arg.join(value)

which as you see is simplicity itself!

Solution 5 - Python

Here's the filter I wrote to solve my problem (it doesn't include the Oxford comma)

def join_with_commas(obj_list):
    """Takes a list of objects and returns their string representations,
    separated by commas and with 'and' between the penultimate and final items
    For example, for a list of fruit objects:
    [<Fruit: apples>, <Fruit: oranges>, <Fruit: pears>] -> 'apples, oranges and pears'
    """
    if not obj_list:
        return ""
    l=len(obj_list)
    if l==1:
        return u"%s" % obj_list[0]
    else:    
        return ", ".join(str(obj) for obj in obj_list[:l-1]) \
                + " and " + str(obj_list[l-1])

To use it in the template: {{ fruits|join_with_commas }}

Solution 6 - Python

If you want a '.' on the end of Michael Matthew Toomim's answer, then use:

{% if not forloop.last %}{% ifequal forloop.revcounter 2 %} and {% else %}, {% endifequal %}{% else %}{% endif %}{% if forloop.last %}.{% endif %}

Solution 7 - Python

All of the answers here fail one or more of the following:

  • They rewrite something (poorly!) that's in the standard template library (ack, top answer!)
  • They don't use and for the last item.
  • They lack a serial (oxford) comma.
  • They use negative indexing, which won't work for django querysets.
  • They don't usually handle string sanitation properly.

Here's my entry into this canon. First, the tests:

class TestTextFilters(TestCase):

    def test_oxford_zero_items(self):
        self.assertEqual(oxford_comma([]), '')

    def test_oxford_one_item(self):
        self.assertEqual(oxford_comma(['a']), 'a')

    def test_oxford_two_items(self):
        self.assertEqual(oxford_comma(['a', 'b']), 'a and b')

    def test_oxford_three_items(self):
        self.assertEqual(oxford_comma(['a', 'b', 'c']), 'a, b, and c')

And now the code. Yes, it gets a bit messy, but you'll see that it doesn't use negative indexing:

from django.utils.encoding import force_text
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe

@register.filter(is_safe=True, needs_autoescape=True)
def oxford_comma(l, autoescape=True):
    """Join together items in a list, separating them with commas or ', and'"""
    l = map(force_text, l)
    if autoescape:
        l = map(conditional_escape, l)

    num_items = len(l)
    if num_items == 0:
        s = ''
    elif num_items == 1:
        s = l[0]
    elif num_items == 2:
        s = l[0] + ' and ' + l[1]
    elif num_items > 2:
        for i, item in enumerate(l):
            if i == 0:
                # First item
                s = item
            elif i == (num_items - 1):
                # Last item.
                s += ', and ' + item
            else:
                # Items in the middle
                s += ', ' + item

    return mark_safe(s)

You can use this in a django template with:

{% load my_filters %}
{{ items|oxford_comma }}

Solution 8 - Python

I would simply use ', '.join(['apples', 'oranges', 'pears']) before sending it to the template as a context data.

UPDATE:

data = ['apples', 'oranges', 'pears']
print(', '.join(data[0:-1]) + ' and ' + data[-1])

You will get apples, oranges and pears output.

Solution 9 - Python

Django doesn't have support for this out-of-the-box. You can define a custom filter for this:

from django import template


register = template.Library()


@register.filter
def join_and(value):
    """Given a list of strings, format them with commas and spaces, but
    with 'and' at the end.

    >>> join_and(['apples', 'oranges', 'pears'])
    "apples, oranges, and pears"

    """
    # convert numbers to strings
    value = [str(item) for item in value]

    if len(value) == 1:
        return value[0]

    # join all but the last element
    all_but_last = ", ".join(value[:-1])
    return "%s, and %s" % (all_but_last, value[-1])

However, if you want to deal with something more complex than just lists of strings, you'll have to use an explicit {% for x in y %} loop in your template.

Solution 10 - Python

If you like one-liners:

@register.filter
def lineup(ls): return ', '.join(ls[:-1])+' and '+ls[-1] if len(ls)>1 else ls[0]

and then in the template:

{{ fruits|lineup }}

Solution 11 - Python

I think the simplest solution might be:

@register.filter
def comma_list(p_values: Iterable[str]) -> List[str]:
    values = list(p_values)
    if len(values) > 1:
        values[-1] = u'and %s' % values[-1]
    if len(values) > 2:
        return u', '.join(values)
    return u' '.join(values)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAlasdairView Question on Stackoverflow
Solution 1 - PythonS.LottView Answer on Stackoverflow
Solution 2 - PythonMichael Matthew ToomimView Answer on Stackoverflow
Solution 3 - PythonTommygunView Answer on Stackoverflow
Solution 4 - PythonAlex MartelliView Answer on Stackoverflow
Solution 5 - PythonAlasdairView Answer on Stackoverflow
Solution 6 - PythonTodd DaviesView Answer on Stackoverflow
Solution 7 - PythonmlissnerView Answer on Stackoverflow
Solution 8 - PythonyigidixView Answer on Stackoverflow
Solution 9 - PythonWilfred HughesView Answer on Stackoverflow
Solution 10 - PythonfmalinaView Answer on Stackoverflow
Solution 11 - PythonmannyszView Answer on Stackoverflow