publik: add mathematic filters + split (#66993)

split, decimal, add, substract, multiply, divide, ceil, floor, abs, sum
This commit is contained in:
Lauréline Guérin 2022-07-06 10:35:55 +02:00
parent c71350052e
commit a08f263abd
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
2 changed files with 311 additions and 0 deletions

View File

@ -14,8 +14,14 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import math
from decimal import Decimal
from decimal import DivisionByZero as DecimalDivisionByZero
from decimal import InvalidOperation as DecimalInvalidOperation
from django import template from django import template
from django.template import defaultfilters from django.template import defaultfilters
from django.utils.encoding import force_text
register = template.Library() register = template.Library()
@ -48,6 +54,11 @@ def as_list(obj):
return list(obj) return list(obj)
@register.filter
def split(string, separator=' '):
return (force_text(string) or '').split(separator)
@register.filter @register.filter
def first(value): def first(value):
try: try:
@ -62,3 +73,89 @@ def last(value):
return defaultfilters.last(value) return defaultfilters.last(value)
except TypeError: except TypeError:
return '' return ''
def parse_decimal(value, default=Decimal(0)):
if isinstance(value, str):
# replace , by . for French users comfort
value = value.replace(',', '.')
try:
return Decimal(value).quantize(Decimal('1.0000')).normalize()
except (ArithmeticError, TypeError):
return default
@register.filter(is_safe=False)
def decimal(value, arg=None):
if not isinstance(value, Decimal):
value = parse_decimal(value)
if arg is None:
return value
return defaultfilters.floatformat(value, arg=arg)
@register.filter
def add(term1, term2):
'''replace the "add" native django filter'''
if term1 is None:
term1 = ''
if term2 is None:
term2 = ''
term1_decimal = parse_decimal(term1, default=None)
term2_decimal = parse_decimal(term2, default=None)
if term1_decimal is not None and term2_decimal is not None:
return term1_decimal + term2_decimal
if term1 == '' and term2_decimal is not None:
return term2_decimal
if term2 == '' and term1_decimal is not None:
return term1_decimal
return defaultfilters.add(term1, term2)
@register.filter
def subtract(term1, term2):
return parse_decimal(term1) - parse_decimal(term2)
@register.filter
def multiply(term1, term2):
return parse_decimal(term1) * parse_decimal(term2)
@register.filter
def divide(term1, term2):
try:
return parse_decimal(term1) / parse_decimal(term2)
except DecimalInvalidOperation:
return ''
except DecimalDivisionByZero:
return ''
@register.filter
def ceil(value):
'''the smallest integer value greater than or equal to value'''
return decimal(math.ceil(parse_decimal(value)))
@register.filter
def floor(value):
return decimal(math.floor(parse_decimal(value)))
@register.filter(name='abs')
def abs_(value):
return decimal(abs(parse_decimal(value)))
@register.filter(name='sum')
def sum_(list_):
if isinstance(list_, str):
# do not consider string as iterable, to avoid misusage
return ''
try:
return sum(parse_decimal(term) for term in list_)
except TypeError: # list_ is not iterable
return ''

View File

@ -54,6 +54,15 @@ def test_getlist():
assert t.render(context) == 'None,baz,' assert t.render(context) == 'None,baz,'
def test_split():
t = Template('{% for x in plop|split %}{{x}}<br>{% endfor %}')
assert t.render(Context({'plop': 'ab cd ef'})) == 'ab<br>cd<br>ef<br>'
t = Template('{% for x in plop|split:"|" %}{{x}} {% endfor %}')
assert t.render(Context({'plop': 'ab|cd|ef'})) == 'ab cd ef '
t = Template('{% for x in plop|split:"|" %}{{x}} {% endfor %}')
assert t.render(Context({'plop': 42})) == '42 '
def test_first(): def test_first():
t = Template('{{ foo|first }}') t = Template('{{ foo|first }}')
@ -84,3 +93,208 @@ def test_last():
context = Context({'foo': None}) context = Context({'foo': None})
assert t.render(context) == '' assert t.render(context) == ''
def test_decimal():
tmpl = Template('{{ plop|decimal }}')
assert tmpl.render(Context({'plop': 'toto'})) == '0'
assert tmpl.render(Context({'plop': '3.14'})) == '3.14'
assert tmpl.render(Context({'plop': '3,14'})) == '3.14'
assert tmpl.render(Context({'plop': 3.14})) == '3.14'
assert tmpl.render(Context({'plop': 12345.678})) == '12345.678'
assert tmpl.render(Context({'plop': None})) == '0'
assert tmpl.render(Context({'plop': 0})) == '0'
tmpl = Template('{{ plop|decimal:3 }}')
assert tmpl.render(Context({'plop': '3.14'})) == '3.140'
assert tmpl.render(Context({'plop': None})) == '0.000'
tmpl = Template('{{ plop|decimal:"3" }}')
assert tmpl.render(Context({'plop': '3.14'})) == '3.140'
assert tmpl.render(Context({'plop': None})) == '0.000'
tmpl = Template('{% if plop|decimal > 2 %}hello{% endif %}')
assert tmpl.render(Context({'plop': 3})) == 'hello'
assert tmpl.render(Context({'plop': '3'})) == 'hello'
assert tmpl.render(Context({'plop': 2.001})) == 'hello'
assert tmpl.render(Context({'plop': '2.001'})) == 'hello'
assert tmpl.render(Context({'plop': 1})) == ''
assert tmpl.render(Context({'plop': 1.99})) == ''
assert tmpl.render(Context({'plop': '1.99'})) == ''
assert tmpl.render(Context({'plop': 'x'})) == ''
assert tmpl.render(Context({'plop': None})) == ''
assert tmpl.render(Context({'plop': 0})) == ''
tmpl = Template('{% if "3"|decimal == 3 %}hello{% endif %}')
assert tmpl.render(Context()) == 'hello'
tmpl = Template('{% if "3"|decimal == 3.0 %}hello{% endif %}')
assert tmpl.render(Context()) == 'hello'
tmpl = Template('{% if 3|decimal == 3 %}hello{% endif %}')
assert tmpl.render(Context()) == 'hello'
tmpl = Template('{% if 3.0|decimal == 3 %}hello{% endif %}')
assert tmpl.render(Context()) == 'hello'
tmpl = Template('{% if 3|decimal|decimal == 3 %}hello{% endif %}')
assert tmpl.render(Context()) == 'hello'
def test_add():
tmpl = Template('{{ term1|add:term2 }}')
# using strings
assert tmpl.render(Context({'term1': '1.1', 'term2': 0})) == '1.1'
assert tmpl.render(Context({'term1': 'not a number', 'term2': 1.2})) == ''
assert tmpl.render(Context({'term1': 0.3, 'term2': "1"})) == '1.3'
assert tmpl.render(Context({'term1': 1.4, 'term2': "not a number"})) == ''
# add
assert tmpl.render(Context({'term1': 4, 'term2': -0.9})) == '3.1'
assert tmpl.render(Context({'term1': '4', 'term2': -0.8})) == '3.2'
assert tmpl.render(Context({'term1': 4, 'term2': '-0.7'})) == '3.3'
assert tmpl.render(Context({'term1': '4', 'term2': '-0.6'})) == '3.4'
assert tmpl.render(Context({'term1': '', 'term2': 3.5})) == '3.5'
assert tmpl.render(Context({'term1': None, 'term2': 3.5})) == '3.5'
assert tmpl.render(Context({'term1': 3.6, 'term2': ''})) == '3.6'
assert tmpl.render(Context({'term1': '', 'term2': ''})) == ''
assert tmpl.render(Context({'term1': 3.6, 'term2': None})) == '3.6'
assert tmpl.render(Context({'term1': 0, 'term2': ''})) == '0'
assert tmpl.render(Context({'term1': '', 'term2': 0})) == '0'
assert tmpl.render(Context({'term1': 0, 'term2': 0})) == '0'
# if term is '' or None and other term is decimal
assert tmpl.render(Context({'term1': '', 'term2': 2.2})) == '2.2'
assert tmpl.render(Context({'term1': None, 'term2': 2.2})) == '2.2'
assert tmpl.render(Context({'term1': 2.2, 'term2': ''})) == '2.2'
assert tmpl.render(Context({'term1': 2.2, 'term2': None})) == '2.2'
# add using ',' instead of '.' decimal separator
assert tmpl.render(Context({'term1': '1,1', 'term2': '2,2'})) == '3.3'
assert tmpl.render(Context({'term1': '1,1', 'term2': '2.2'})) == '3.3'
assert tmpl.render(Context({'term1': '1,1', 'term2': 2.2})) == '3.3'
assert tmpl.render(Context({'term1': '1,1', 'term2': 0})) == '1.1'
assert tmpl.render(Context({'term1': '1,1', 'term2': ''})) == '1.1'
assert tmpl.render(Context({'term1': '1,1', 'term2': None})) == '1.1'
assert tmpl.render(Context({'term1': '1.1', 'term2': '2,2'})) == '3.3'
assert tmpl.render(Context({'term1': 1.1, 'term2': '2,2'})) == '3.3'
assert tmpl.render(Context({'term1': 0, 'term2': '2,2'})) == '2.2'
assert tmpl.render(Context({'term1': '', 'term2': '2,2'})) == '2.2'
assert tmpl.render(Context({'term1': None, 'term2': '2,2'})) == '2.2'
# fallback to Django native add filter
assert tmpl.render(Context({'term1': 'foo', 'term2': 'bar'})) == 'foobar'
assert tmpl.render(Context({'term1': 'foo', 'term2': ''})) == 'foo'
assert tmpl.render(Context({'term1': 'foo', 'term2': None})) == 'foo'
assert tmpl.render(Context({'term1': 'foo', 'term2': 0})) == ''
assert tmpl.render(Context({'term1': '', 'term2': 'bar'})) == 'bar'
assert tmpl.render(Context({'term1': '', 'term2': ''})) == ''
assert tmpl.render(Context({'term1': '', 'term2': None})) == ''
assert tmpl.render(Context({'term1': None, 'term2': 'bar'})) == 'bar'
assert tmpl.render(Context({'term1': None, 'term2': ''})) == ''
assert tmpl.render(Context({'term1': None, 'term2': None})) == ''
assert tmpl.render(Context({'term1': 0, 'term2': 'bar'})) == ''
def test_substract():
tmpl = Template('{{ term1|subtract:term2 }}')
assert tmpl.render(Context({'term1': 5.1, 'term2': 1})) == '4.1'
assert tmpl.render(Context({'term1': '5.2', 'term2': 1})) == '4.2'
assert tmpl.render(Context({'term1': 5.3, 'term2': '1'})) == '4.3'
assert tmpl.render(Context({'term1': '5.4', 'term2': '1'})) == '4.4'
assert tmpl.render(Context({'term1': '', 'term2': -4.5})) == '4.5'
assert tmpl.render(Context({'term1': 4.6, 'term2': ''})) == '4.6'
assert tmpl.render(Context({'term1': '', 'term2': ''})) == '0'
assert tmpl.render(Context({'term1': 0, 'term2': ''})) == '0'
assert tmpl.render(Context({'term1': '', 'term2': 0})) == '0'
assert tmpl.render(Context({'term1': 0, 'term2': 0})) == '0'
def test_multiply():
tmpl = Template('{{ term1|multiply:term2 }}')
assert tmpl.render(Context({'term1': '3', 'term2': '2'})) == '6'
assert tmpl.render(Context({'term1': 2.5, 'term2': 2})) == '5.0'
assert tmpl.render(Context({'term1': '2.5', 'term2': 2})) == '5.0'
assert tmpl.render(Context({'term1': 2.5, 'term2': '2'})) == '5.0'
assert tmpl.render(Context({'term1': '2.5', 'term2': '2'})) == '5.0'
assert tmpl.render(Context({'term1': '', 'term2': '2'})) == '0'
assert tmpl.render(Context({'term1': 2.5, 'term2': ''})) == '0.0'
assert tmpl.render(Context({'term1': '', 'term2': ''})) == '0'
assert tmpl.render(Context({'term1': 0, 'term2': ''})) == '0'
assert tmpl.render(Context({'term1': '', 'term2': 0})) == '0'
assert tmpl.render(Context({'term1': 0, 'term2': 0})) == '0'
def test_divide():
tmpl = Template('{{ term1|divide:term2 }}')
assert tmpl.render(Context({'term1': 16, 'term2': 2})) == '8'
assert tmpl.render(Context({'term1': 6, 'term2': 0.75})) == '8'
assert tmpl.render(Context({'term1': '6', 'term2': 0.75})) == '8'
assert tmpl.render(Context({'term1': 6, 'term2': '0.75'})) == '8'
assert tmpl.render(Context({'term1': '6', 'term2': '0.75'})) == '8'
assert tmpl.render(Context({'term1': '', 'term2': '2'})) == '0'
assert tmpl.render(Context({'term1': 6, 'term2': ''})) == ''
assert tmpl.render(Context({'term1': '', 'term2': ''})) == ''
assert tmpl.render(Context({'term1': 0, 'term2': ''})) == ''
assert tmpl.render(Context({'term1': '', 'term2': 0})) == ''
assert tmpl.render(Context({'term1': 0, 'term2': 0})) == ''
tmpl = Template('{{ term1|divide:term2|decimal:2 }}')
assert tmpl.render(Context({'term1': 2, 'term2': 3})) == '0.67'
def test_ceil():
# ceil
tmpl = Template('{{ value|ceil }}')
assert tmpl.render(Context({'value': 3.14})) == '4'
assert tmpl.render(Context({'value': 3.99})) == '4'
assert tmpl.render(Context({'value': -3.14})) == '-3'
assert tmpl.render(Context({'value': -3.99})) == '-3'
assert tmpl.render(Context({'value': 0})) == '0'
assert tmpl.render(Context({'value': '3.14'})) == '4'
assert tmpl.render(Context({'value': '3.99'})) == '4'
assert tmpl.render(Context({'value': '-3.14'})) == '-3'
assert tmpl.render(Context({'value': '-3.99'})) == '-3'
assert tmpl.render(Context({'value': '0'})) == '0'
assert tmpl.render(Context({'value': 'not a number'})) == '0'
assert tmpl.render(Context({'value': ''})) == '0'
assert tmpl.render(Context({'value': None})) == '0'
def test_floor():
# floor
tmpl = Template('{{ value|floor }}')
assert tmpl.render(Context({'value': 3.14})) == '3'
assert tmpl.render(Context({'value': 3.99})) == '3'
assert tmpl.render(Context({'value': -3.14})) == '-4'
assert tmpl.render(Context({'value': -3.99})) == '-4'
assert tmpl.render(Context({'value': 0})) == '0'
assert tmpl.render(Context({'value': '3.14'})) == '3'
assert tmpl.render(Context({'value': '3.99'})) == '3'
assert tmpl.render(Context({'value': '-3.14'})) == '-4'
assert tmpl.render(Context({'value': '-3.99'})) == '-4'
assert tmpl.render(Context({'value': '0'})) == '0'
assert tmpl.render(Context({'value': 'not a number'})) == '0'
assert tmpl.render(Context({'value': ''})) == '0'
assert tmpl.render(Context({'value': None})) == '0'
def test_abs():
tmpl = Template('{{ value|abs }}')
assert tmpl.render(Context({'value': 3.14})) == '3.14'
assert tmpl.render(Context({'value': -3.14})) == '3.14'
assert tmpl.render(Context({'value': 0})) == '0'
assert tmpl.render(Context({'value': '3.14'})) == '3.14'
assert tmpl.render(Context({'value': '-3.14'})) == '3.14'
assert tmpl.render(Context({'value': '0'})) == '0'
assert tmpl.render(Context({'value': 'not a number'})) == '0'
assert tmpl.render(Context({'value': ''})) == '0'
assert tmpl.render(Context({'value': None})) == '0'
def test_sum():
t = Template('{{ "2 29.5 9,5 .5"|split|sum }}')
assert t.render(Context()) == '41.5'
t = Template('{{ list|sum }}')
assert t.render(Context({'list': [1, 2, '3']})) == '6'
assert t.render(Context({'list': [1, 2, 'x']})) == '3'
assert t.render(Context({'list': [None, 2.0, 'x']})) == '2'
assert t.render(Context({'list': []})) == '0'
assert t.render(Context({'list': None})) == '' # list is not iterable
assert t.render(Context({'list': '123'})) == '' # consider string as not iterable
assert t.render(Context()) == ''