# Django template filter - sort by multiple keys

I have prepared an implementation of sort_by_keys filter:

{% for donation in donations|sort_by_keys:'date,-value' %}
<img src="{{ donation.stamp_img_url }}" />
{% endfor %}


Example data:

donations = [
{
date: '29-07-2014',
value: 150,
},
{
date: '07-10-2014',
value: 450,
},
{
date: '29-07-2014',
value: 500,
},
]


Expected result:

donations|sort_by_keys:'date,-value'


Should return:

[
{
date: '29-07-2014',
value: 500,
},
{
date: '29-07-2014',
value: 150,
},
{
date: '07-10-2014',
value: 450,
},
]


My implementation:

@register.filter
def sort_by_keys(value, keys):
_list = list(value)

def compare(item1, item2):
for key in item1.__keys_for_sort.split(','):

if key.startswith('-'):
key = key.replace('-', '')

if getattr(item1, key) > getattr(item2, key):
return -1
elif getattr(item1, key) < getattr(item2, key):
return 1
else:
if getattr(item1, key) < getattr(item2, key):
return -1
elif getattr(item1, key) > getattr(item2, key):
return 1

return 0

for item in _list:
setattr(item, '__keys_for_sort', keys)

_list = sorted(_list, cmp=compare)

for item in _list:
delattr(item, '__keys_for_sort')

return _list


I really don't like the idea of injection special attributes like __keys_for_sort, to have possibility of using this value in comparator, but I don't know how to write this better.

keys is already visible in compare, so you can even do the splitting outside of compare and still use it inside of it. And the duplication of code for the else branch should be eliminated, by only doing the comparison once (cmp should work I guess) and then negating the result if necessary. This means processing all the keys before running the sorting into a useful data-structure. You want to compare in the order they are specified, so a list of tuples will do fine.

Then, by using cmp instead of normal comparisons three lines per branch are eliminated. Also, which is maybe more important, you don't have to worry about the comparisons and what -1/1, which personally takes me way to long all the time while reading comparison functions like this.

The two branches are also unnecessary, since you can just negate the result depending on the sort key (factor in the following code).

So I arrive at this, untested of course.

@register.filter
def sort_by_keys(value, keys):
transformed = []
for key in keys.split(','):
factor = 1
if key.startswith('-'):
key = key.replace('-', '')
factor = -1
transformed.append((key, factor))

def compare(item1, item2):
for (key, factor) in transformed:
result = factor * cmp(getattr(item1, key), getattr(item2, key))
if result != 0:
return result
return 0

return sorted(list(value), cmp=compare)


It looks like you can't use sort directly on the input value? Otherwise I'd suggest to do that as well.

Apart from completely compiling the sort function beforehand (using lambdas), this seems the most compact I think.