# -*- encoding: utf-8 -*- from __future__ import unicode_literals import re from datetime import date, datetime from decimal import Decimal from django import template from django.conf import settings from django.template import defaultfilters from django.utils.encoding import force_text from django.utils.formats import number_format from django.utils.safestring import mark_safe from django.utils.timezone import is_aware, utc from django.utils.translation import pgettext, ugettext as _, ungettext register = template.Library() @register.filter(is_safe=True) def ordinal(value): """ Converts an integer to its ordinal as a string. 1 is '1st', 2 is '2nd', 3 is '3rd', etc. Works for any integer. """ try: value = int(value) except (TypeError, ValueError): return value suffixes = (_('th'), _('st'), _('nd'), _('rd'), _('th'), _('th'), _('th'), _('th'), _('th'), _('th')) if value % 100 in (11, 12, 13): # special case return mark_safe("%d%s" % (value, suffixes[0])) # Mark value safe so i18n does not break with or see #19988 return mark_safe("%d%s" % (value, suffixes[value % 10])) @register.filter(is_safe=True) def intcomma(value, use_l10n=True): """ Converts an integer to a string containing commas every three digits. For example, 3000 becomes '3,000' and 45000 becomes '45,000'. """ if settings.USE_L10N and use_l10n: try: if not isinstance(value, (float, Decimal)): value = int(value) except (TypeError, ValueError): return intcomma(value, False) else: return number_format(value, force_grouping=True) orig = force_text(value) new = re.sub("^(-?\d+)(\d{3})", '\g<1>,\g<2>', orig) if orig == new: return new else: return intcomma(new, use_l10n) # A tuple of standard large number to their converters intword_converters = ( (6, lambda number: ( ungettext('%(value).1f million', '%(value).1f million', number), ungettext('%(value)s million', '%(value)s million', number), )), (9, lambda number: ( ungettext('%(value).1f billion', '%(value).1f billion', number), ungettext('%(value)s billion', '%(value)s billion', number), )), (12, lambda number: ( ungettext('%(value).1f trillion', '%(value).1f trillion', number), ungettext('%(value)s trillion', '%(value)s trillion', number), )), (15, lambda number: ( ungettext('%(value).1f quadrillion', '%(value).1f quadrillion', number), ungettext('%(value)s quadrillion', '%(value)s quadrillion', number), )), (18, lambda number: ( ungettext('%(value).1f quintillion', '%(value).1f quintillion', number), ungettext('%(value)s quintillion', '%(value)s quintillion', number), )), (21, lambda number: ( ungettext('%(value).1f sextillion', '%(value).1f sextillion', number), ungettext('%(value)s sextillion', '%(value)s sextillion', number), )), (24, lambda number: ( ungettext('%(value).1f septillion', '%(value).1f septillion', number), ungettext('%(value)s septillion', '%(value)s septillion', number), )), (27, lambda number: ( ungettext('%(value).1f octillion', '%(value).1f octillion', number), ungettext('%(value)s octillion', '%(value)s octillion', number), )), (30, lambda number: ( ungettext('%(value).1f nonillion', '%(value).1f nonillion', number), ungettext('%(value)s nonillion', '%(value)s nonillion', number), )), (33, lambda number: ( ungettext('%(value).1f decillion', '%(value).1f decillion', number), ungettext('%(value)s decillion', '%(value)s decillion', number), )), (100, lambda number: ( ungettext('%(value).1f googol', '%(value).1f googol', number), ungettext('%(value)s googol', '%(value)s googol', number), )), ) @register.filter(is_safe=False) def intword(value): """ Converts a large integer to a friendly text representation. Works best for numbers over 1 million. For example, 1000000 becomes '1.0 million', 1200000 becomes '1.2 million' and '1200000000' becomes '1.2 billion'. """ try: value = int(value) except (TypeError, ValueError): return value if value < 1000000: return value def _check_for_i18n(value, float_formatted, string_formatted): """ Use the i18n enabled defaultfilters.floatformat if possible """ if settings.USE_L10N: value = defaultfilters.floatformat(value, 1) template = string_formatted else: template = float_formatted return template % {'value': value} for exponent, converters in intword_converters: large_number = 10 ** exponent if value < large_number * 1000: new_value = value / float(large_number) return _check_for_i18n(new_value, *converters(new_value)) return value @register.filter(is_safe=True) def apnumber(value): """ For numbers 1-9, returns the number spelled out. Otherwise, returns the number. This follows Associated Press style. """ try: value = int(value) except (TypeError, ValueError): return value if not 0 < value < 10: return value return (_('one'), _('two'), _('three'), _('four'), _('five'), _('six'), _('seven'), _('eight'), _('nine'))[value - 1] # Perform the comparison in the default time zone when USE_TZ = True # (unless a specific time zone has been applied with the |timezone filter). @register.filter(expects_localtime=True) def naturalday(value, arg=None): """ For date values that are tomorrow, today or yesterday compared to present day returns representing string. Otherwise, returns a string formatted according to settings.DATE_FORMAT. """ try: tzinfo = getattr(value, 'tzinfo', None) value = date(value.year, value.month, value.day) except AttributeError: # Passed value wasn't a date object return value except ValueError: # Date arguments out of range return value today = datetime.now(tzinfo).date() delta = value - today if delta.days == 0: return _('today') elif delta.days == 1: return _('tomorrow') elif delta.days == -1: return _('yesterday') return defaultfilters.date(value, arg) # This filter doesn't require expects_localtime=True because it deals properly # with both naive and aware datetimes. Therefore avoid the cost of conversion. @register.filter def naturaltime(value): """ For date and time values shows how many seconds, minutes or hours ago compared to current timestamp returns representing string. """ if not isinstance(value, date): # datetime is a subclass of date return value now = datetime.now(utc if is_aware(value) else None) if value < now: delta = now - value if delta.days != 0: return pgettext( 'naturaltime', '%(delta)s ago' ) % {'delta': defaultfilters.timesince(value, now)} elif delta.seconds == 0: return _('now') elif delta.seconds < 60: return ungettext( # Translators: please keep a non-breaking space (U+00A0) # between count and time unit. 'a second ago', '%(count)s seconds ago', delta.seconds ) % {'count': delta.seconds} elif delta.seconds // 60 < 60: count = delta.seconds // 60 return ungettext( # Translators: please keep a non-breaking space (U+00A0) # between count and time unit. 'a minute ago', '%(count)s minutes ago', count ) % {'count': count} else: count = delta.seconds // 60 // 60 return ungettext( # Translators: please keep a non-breaking space (U+00A0) # between count and time unit. 'an hour ago', '%(count)s hours ago', count ) % {'count': count} else: delta = value - now if delta.days != 0: return pgettext( 'naturaltime', '%(delta)s from now' ) % {'delta': defaultfilters.timeuntil(value, now)} elif delta.seconds == 0: return _('now') elif delta.seconds < 60: return ungettext( # Translators: please keep a non-breaking space (U+00A0) # between count and time unit. 'a second from now', '%(count)s seconds from now', delta.seconds ) % {'count': delta.seconds} elif delta.seconds // 60 < 60: count = delta.seconds // 60 return ungettext( # Translators: please keep a non-breaking space (U+00A0) # between count and time unit. 'a minute from now', '%(count)s minutes from now', count ) % {'count': count} else: count = delta.seconds // 60 // 60 return ungettext( # Translators: please keep a non-breaking space (U+00A0) # between count and time unit. 'an hour from now', '%(count)s hours from now', count ) % {'count': count}