Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

min and max filters #737

Merged
merged 2 commits into from Jul 6, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES
Expand Up @@ -25,8 +25,10 @@ Version 2.10
- The ``random`` filter is no longer incorrectly constant folded and will
produce a new random choice each time the template is rendered. (`#478`_)
- Add a ``unique`` filter. (`#469`_)
- Add ``min`` and ``max`` filters. (`#475`_)

.. _#469: https://github.com/pallets/jinja/pull/469
.. _#475: https://github.com/pallets/jinja/pull/475
.. _#478: https://github.com/pallets/jinja/pull/478

Version 2.9.6
Expand Down
71 changes: 52 additions & 19 deletions jinja2/filters.py
Expand Up @@ -12,7 +12,7 @@
import math
import random

from itertools import groupby
from itertools import groupby, chain
from collections import namedtuple
from jinja2.utils import Markup, escape, pformat, urlize, soft_unicode, \
unicode_urlencode, htmlsafe_json_dumps
Expand Down Expand Up @@ -279,25 +279,11 @@ def do_unique(environment, value, case_sensitive=False, attribute=None):
{{ ['foo', 'bar', 'foobar', 'FooBar']|unique }}
-> ['foo', 'bar', 'foobar']

This filter complements the `groupby` filter, which sorts and groups an
iterable by a certain attribute. The `unique` filter groups the items
from the iterable by themselves instead and always returns a flat list of
unique items. That can be useful for example when you need to concatenate
that items:
The unique items are yielded in the same order as their first occurrence in
the iterable passed to the filter.

.. sourcecode:: jinja

{{ ['foo', 'bar', 'foobar', 'FooBar']|unique|join(',') }}
-> foo,bar,foobar

Also note that the resulting list contains the items in the same order
as their first occurrence in the iterable passed to the filter. If sorting
is needed you can still chain the `unique` and `sort` filter:

.. sourcecode:: jinja

{{ ['foo', 'bar', 'foobar', 'FooBar']|unique|sort }}
-> ['bar', 'foo', 'foobar']
:param case_sensitive: Treat upper and lower case strings as distinct.
:param attribute: Filter objects with unique values for this attribute.
"""
getter = make_attrgetter(
environment, attribute,
Expand All @@ -313,6 +299,51 @@ def do_unique(environment, value, case_sensitive=False, attribute=None):
yield item


def _min_or_max(environment, value, func, case_sensitive, attribute):
it = iter(value)

try:
first = next(it)
except StopIteration:
return environment.undefined('No aggregated item, sequence was empty.')

key_func = make_attrgetter(
environment, attribute,
ignore_case if not case_sensitive else None
)
return func(chain([first], it), key=key_func)


@environmentfilter
def do_min(environment, value, case_sensitive=False, attribute=None):
"""Return the smallest item from the sequence.

.. sourcecode:: jinja

{{ [1, 2, 3]|min }}
-> 1

:param case_sensitive: Treat upper and lower case strings as distinct.
:param attribute: Get the object with the max value of this attribute.
"""
return _min_or_max(environment, value, min, case_sensitive, attribute)


@environmentfilter
def do_max(environment, value, case_sensitive=False, attribute=None):
"""Return the smallest item from the sequence.

.. sourcecode:: jinja

{{ [1, 2, 3]|max }}
-> 3

:param case_sensitive: Treat upper and lower case strings as distinct.
:param attribute: Get the object with the max value of this attribute.
"""
return _min_or_max(environment, value, max, case_sensitive, attribute)


def do_default(value, default_value=u'', boolean=False):
"""If the value is undefined it will return the passed default value,
otherwise the value of the variable:
Expand Down Expand Up @@ -1097,6 +1128,8 @@ def select_or_reject(args, kwargs, modfunc, lookup_attr):
'list': do_list,
'lower': do_lower,
'map': do_map,
'min': do_min,
'max': do_max,
'pprint': do_pprint,
'random': do_random,
'reject': do_reject,
Expand Down
20 changes: 20 additions & 0 deletions tests/test_filters.py
Expand Up @@ -403,6 +403,26 @@ def test_unique_attribute(self, env):
t = env.from_string("{{ items|unique(attribute='value')|join }}")
assert t.render(items=map(Magic, [3, 2, 4, 1, 2])) == '3241'

@pytest.mark.parametrize('source,expect', (
('{{ ["a", "B"]|min }}', 'a'),
('{{ ["a", "B"]|min(case_sensitive=true) }}', 'B'),
('{{ []|min }}', ''),
('{{ ["a", "B"]|max }}', 'B'),
('{{ ["a", "B"]|max(case_sensitive=true) }}', 'a'),
('{{ []|max }}', ''),
))
def test_min_max(self, env, source, expect):
t = env.from_string(source)
assert t.render() == expect

@pytest.mark.parametrize('name,expect', (
('min', '1'),
('max', '9'),
))
def test_min_max_attribute(self, env, name, expect):
t = env.from_string('{{ items|' + name + '(attribute="value") }}')
assert t.render(items=map(Magic, [5, 1, 9])) == expect

def test_groupby(self, env):
tmpl = env.from_string('''
{%- for grouper, list in [{'foo': 1, 'bar': 2},
Expand Down