import re from datetime import date, datetime, timezone from decimal import Decimal from django import template from django.template import defaultfilters from django.utils.formats import number_format from django.utils.safestring import mark_safe from django.utils.timezone import is_aware from django.utils.translation import gettext as _ from django.utils.translation import ( gettext_lazy, ngettext, ngettext_lazy, npgettext_lazy, pgettext, round_away_from_one, ) register = template.Library() @register.filter(is_safe=True) def ordinal(value): """ Convert 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 if value % 100 in (11, 12, 13): # Translators: Ordinal format for 11 (11th), 12 (12th), and 13 (13th). value = pgettext("ordinal 11, 12, 13", "{}th").format(value) else: templates = ( # Translators: Ordinal format when value ends with 0, e.g. 80th. pgettext("ordinal 0", "{}th"), # Translators: Ordinal format when value ends with 1, e.g. 81st, except 11. pgettext("ordinal 1", "{}st"), # Translators: Ordinal format when value ends with 2, e.g. 82nd, except 12. pgettext("ordinal 2", "{}nd"), # Translators: Ordinal format when value ends with 3, e.g. 83rd, except 13. pgettext("ordinal 3", "{}rd"), # Translators: Ordinal format when value ends with 4, e.g. 84th. pgettext("ordinal 4", "{}th"), # Translators: Ordinal format when value ends with 5, e.g. 85th. pgettext("ordinal 5", "{}th"), # Translators: Ordinal format when value ends with 6, e.g. 86th. pgettext("ordinal 6", "{}th"), # Translators: Ordinal format when value ends with 7, e.g. 87th. pgettext("ordinal 7", "{}th"), # Translators: Ordinal format when value ends with 8, e.g. 88th. pgettext("ordinal 8", "{}th"), # Translators: Ordinal format when value ends with 9, e.g. 89th. pgettext("ordinal 9", "{}th"), ) value = templates[value % 10].format(value) # Mark value safe so i18n does not break with or see #19988 return mark_safe(value) @register.filter(is_safe=True) def intcomma(value, use_l10n=True): """ Convert an integer to a string containing commas every three digits. For example, 3000 becomes '3,000' and 45000 becomes '45,000'. """ if use_l10n: try: if not isinstance(value, (float, Decimal)): value = int(value) except (TypeError, ValueError): return intcomma(value, False) else: return number_format(value, use_l10n=True, force_grouping=True) orig = str(value) new = re.sub(r"^(-?\d+)(\d{3})", r"\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: ngettext("%(value)s million", "%(value)s million", number)), (9, lambda number: ngettext("%(value)s billion", "%(value)s billion", number)), (12, lambda number: ngettext("%(value)s trillion", "%(value)s trillion", number)), ( 15, lambda number: ngettext( "%(value)s quadrillion", "%(value)s quadrillion", number ), ), ( 18, lambda number: ngettext( "%(value)s quintillion", "%(value)s quintillion", number ), ), ( 21, lambda number: ngettext("%(value)s sextillion", "%(value)s sextillion", number), ), ( 24, lambda number: ngettext("%(value)s septillion", "%(value)s septillion", number), ), (27, lambda number: ngettext("%(value)s octillion", "%(value)s octillion", number)), (30, lambda number: ngettext("%(value)s nonillion", "%(value)s nonillion", number)), (33, lambda number: ngettext("%(value)s decillion", "%(value)s decillion", number)), (100, lambda number: ngettext("%(value)s googol", "%(value)s googol", number)), ) @register.filter(is_safe=False) def intword(value): """ Convert 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 abs_value = abs(value) if abs_value < 1000000: return value for exponent, converter in intword_converters: large_number = 10**exponent if abs_value < large_number * 1000: new_value = value / large_number rounded_value = round_away_from_one(new_value) return converter(abs(rounded_value)) % { "value": defaultfilters.floatformat(new_value, 1), } return value @register.filter(is_safe=True) def apnumber(value): """ For numbers 1-9, return the number spelled out. Otherwise, return 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 return representing string. Otherwise, return a string formatted according to settings.DATE_FORMAT. """ tzinfo = getattr(value, "tzinfo", None) try: value = date(value.year, value.month, value.day) except AttributeError: # Passed value wasn't a date object 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 show how many seconds, minutes, or hours ago compared to current timestamp return representing string. """ return NaturalTimeFormatter.string_for(value) class NaturalTimeFormatter: time_strings = { # Translators: delta will contain a string like '2 months' or '1 month, 2 weeks' "past-day": gettext_lazy("%(delta)s ago"), # Translators: please keep a non-breaking space (U+00A0) between count # and time unit. "past-hour": ngettext_lazy("an hour ago", "%(count)s hours ago", "count"), # Translators: please keep a non-breaking space (U+00A0) between count # and time unit. "past-minute": ngettext_lazy("a minute ago", "%(count)s minutes ago", "count"), # Translators: please keep a non-breaking space (U+00A0) between count # and time unit. "past-second": ngettext_lazy("a second ago", "%(count)s seconds ago", "count"), "now": gettext_lazy("now"), # Translators: please keep a non-breaking space (U+00A0) between count # and time unit. "future-second": ngettext_lazy( "a second from now", "%(count)s seconds from now", "count" ), # Translators: please keep a non-breaking space (U+00A0) between count # and time unit. "future-minute": ngettext_lazy( "a minute from now", "%(count)s minutes from now", "count" ), # Translators: please keep a non-breaking space (U+00A0) between count # and time unit. "future-hour": ngettext_lazy( "an hour from now", "%(count)s hours from now", "count" ), # Translators: delta will contain a string like '2 months' or '1 month, 2 weeks' "future-day": gettext_lazy("%(delta)s from now"), } past_substrings = { # Translators: 'naturaltime-past' strings will be included in '%(delta)s ago' "year": npgettext_lazy( "naturaltime-past", "%(num)d year", "%(num)d years", "num" ), "month": npgettext_lazy( "naturaltime-past", "%(num)d month", "%(num)d months", "num" ), "week": npgettext_lazy( "naturaltime-past", "%(num)d week", "%(num)d weeks", "num" ), "day": npgettext_lazy("naturaltime-past", "%(num)d day", "%(num)d days", "num"), "hour": npgettext_lazy( "naturaltime-past", "%(num)d hour", "%(num)d hours", "num" ), "minute": npgettext_lazy( "naturaltime-past", "%(num)d minute", "%(num)d minutes", "num" ), } future_substrings = { # Translators: 'naturaltime-future' strings will be included in # '%(delta)s from now'. "year": npgettext_lazy( "naturaltime-future", "%(num)d year", "%(num)d years", "num" ), "month": npgettext_lazy( "naturaltime-future", "%(num)d month", "%(num)d months", "num" ), "week": npgettext_lazy( "naturaltime-future", "%(num)d week", "%(num)d weeks", "num" ), "day": npgettext_lazy( "naturaltime-future", "%(num)d day", "%(num)d days", "num" ), "hour": npgettext_lazy( "naturaltime-future", "%(num)d hour", "%(num)d hours", "num" ), "minute": npgettext_lazy( "naturaltime-future", "%(num)d minute", "%(num)d minutes", "num" ), } @classmethod def string_for(cls, value): if not isinstance(value, date): # datetime is a subclass of date return value now = datetime.now(timezone.utc if is_aware(value) else None) if value < now: delta = now - value if delta.days != 0: return cls.time_strings["past-day"] % { "delta": defaultfilters.timesince( value, now, time_strings=cls.past_substrings ), } elif delta.seconds == 0: return cls.time_strings["now"] elif delta.seconds < 60: return cls.time_strings["past-second"] % {"count": delta.seconds} elif delta.seconds // 60 < 60: count = delta.seconds // 60 return cls.time_strings["past-minute"] % {"count": count} else: count = delta.seconds // 60 // 60 return cls.time_strings["past-hour"] % {"count": count} else: delta = value - now if delta.days != 0: return cls.time_strings["future-day"] % { "delta": defaultfilters.timeuntil( value, now, time_strings=cls.future_substrings ), } elif delta.seconds == 0: return cls.time_strings["now"] elif delta.seconds < 60: return cls.time_strings["future-second"] % {"count": delta.seconds} elif delta.seconds // 60 < 60: count = delta.seconds // 60 return cls.time_strings["future-minute"] % {"count": count} else: count = delta.seconds // 60 // 60 return cls.time_strings["future-hour"] % {"count": count}