You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

humanize.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. import re
  2. from datetime import date, datetime
  3. from decimal import Decimal
  4. from django import template
  5. from django.conf import settings
  6. from django.template import defaultfilters
  7. from django.utils.formats import number_format
  8. from django.utils.safestring import mark_safe
  9. from django.utils.timezone import is_aware, utc
  10. from django.utils.translation import (
  11. gettext as _, gettext_lazy, ngettext, ngettext_lazy, npgettext_lazy,
  12. pgettext,
  13. )
  14. register = template.Library()
  15. @register.filter(is_safe=True)
  16. def ordinal(value):
  17. """
  18. Convert an integer to its ordinal as a string. 1 is '1st', 2 is '2nd',
  19. 3 is '3rd', etc. Works for any integer.
  20. """
  21. try:
  22. value = int(value)
  23. except (TypeError, ValueError):
  24. return value
  25. if value % 100 in (11, 12, 13):
  26. # Translators: Ordinal format for 11 (11th), 12 (12th), and 13 (13th).
  27. value = pgettext('ordinal 11, 12, 13', '{}th').format(value)
  28. else:
  29. templates = (
  30. # Translators: Ordinal format when value ends with 0, e.g. 80th.
  31. pgettext('ordinal 0', '{}th'),
  32. # Translators: Ordinal format when value ends with 1, e.g. 81st, except 11.
  33. pgettext('ordinal 1', '{}st'),
  34. # Translators: Ordinal format when value ends with 2, e.g. 82nd, except 12.
  35. pgettext('ordinal 2', '{}nd'),
  36. # Translators: Ordinal format when value ends with 3, e.g. 83th, except 13.
  37. pgettext('ordinal 3', '{}rd'),
  38. # Translators: Ordinal format when value ends with 4, e.g. 84th.
  39. pgettext('ordinal 4', '{}th'),
  40. # Translators: Ordinal format when value ends with 5, e.g. 85th.
  41. pgettext('ordinal 5', '{}th'),
  42. # Translators: Ordinal format when value ends with 6, e.g. 86th.
  43. pgettext('ordinal 6', '{}th'),
  44. # Translators: Ordinal format when value ends with 7, e.g. 87th.
  45. pgettext('ordinal 7', '{}th'),
  46. # Translators: Ordinal format when value ends with 8, e.g. 88th.
  47. pgettext('ordinal 8', '{}th'),
  48. # Translators: Ordinal format when value ends with 9, e.g. 89th.
  49. pgettext('ordinal 9', '{}th'),
  50. )
  51. value = templates[value % 10].format(value)
  52. # Mark value safe so i18n does not break with <sup> or <sub> see #19988
  53. return mark_safe(value)
  54. @register.filter(is_safe=True)
  55. def intcomma(value, use_l10n=True):
  56. """
  57. Convert an integer to a string containing commas every three digits.
  58. For example, 3000 becomes '3,000' and 45000 becomes '45,000'.
  59. """
  60. if settings.USE_L10N and use_l10n:
  61. try:
  62. if not isinstance(value, (float, Decimal)):
  63. value = int(value)
  64. except (TypeError, ValueError):
  65. return intcomma(value, False)
  66. else:
  67. return number_format(value, force_grouping=True)
  68. orig = str(value)
  69. new = re.sub(r"^(-?\d+)(\d{3})", r'\g<1>,\g<2>', orig)
  70. if orig == new:
  71. return new
  72. else:
  73. return intcomma(new, use_l10n)
  74. # A tuple of standard large number to their converters
  75. intword_converters = (
  76. (6, lambda number: (
  77. ngettext('%(value).1f million', '%(value).1f million', number),
  78. ngettext('%(value)s million', '%(value)s million', number),
  79. )),
  80. (9, lambda number: (
  81. ngettext('%(value).1f billion', '%(value).1f billion', number),
  82. ngettext('%(value)s billion', '%(value)s billion', number),
  83. )),
  84. (12, lambda number: (
  85. ngettext('%(value).1f trillion', '%(value).1f trillion', number),
  86. ngettext('%(value)s trillion', '%(value)s trillion', number),
  87. )),
  88. (15, lambda number: (
  89. ngettext('%(value).1f quadrillion', '%(value).1f quadrillion', number),
  90. ngettext('%(value)s quadrillion', '%(value)s quadrillion', number),
  91. )),
  92. (18, lambda number: (
  93. ngettext('%(value).1f quintillion', '%(value).1f quintillion', number),
  94. ngettext('%(value)s quintillion', '%(value)s quintillion', number),
  95. )),
  96. (21, lambda number: (
  97. ngettext('%(value).1f sextillion', '%(value).1f sextillion', number),
  98. ngettext('%(value)s sextillion', '%(value)s sextillion', number),
  99. )),
  100. (24, lambda number: (
  101. ngettext('%(value).1f septillion', '%(value).1f septillion', number),
  102. ngettext('%(value)s septillion', '%(value)s septillion', number),
  103. )),
  104. (27, lambda number: (
  105. ngettext('%(value).1f octillion', '%(value).1f octillion', number),
  106. ngettext('%(value)s octillion', '%(value)s octillion', number),
  107. )),
  108. (30, lambda number: (
  109. ngettext('%(value).1f nonillion', '%(value).1f nonillion', number),
  110. ngettext('%(value)s nonillion', '%(value)s nonillion', number),
  111. )),
  112. (33, lambda number: (
  113. ngettext('%(value).1f decillion', '%(value).1f decillion', number),
  114. ngettext('%(value)s decillion', '%(value)s decillion', number),
  115. )),
  116. (100, lambda number: (
  117. ngettext('%(value).1f googol', '%(value).1f googol', number),
  118. ngettext('%(value)s googol', '%(value)s googol', number),
  119. )),
  120. )
  121. @register.filter(is_safe=False)
  122. def intword(value):
  123. """
  124. Convert a large integer to a friendly text representation. Works best
  125. for numbers over 1 million. For example, 1000000 becomes '1.0 million',
  126. 1200000 becomes '1.2 million' and '1200000000' becomes '1.2 billion'.
  127. """
  128. try:
  129. value = int(value)
  130. except (TypeError, ValueError):
  131. return value
  132. if value < 1000000:
  133. return value
  134. def _check_for_i18n(value, float_formatted, string_formatted):
  135. """
  136. Use the i18n enabled defaultfilters.floatformat if possible
  137. """
  138. if settings.USE_L10N:
  139. value = defaultfilters.floatformat(value, 1)
  140. template = string_formatted
  141. else:
  142. template = float_formatted
  143. return template % {'value': value}
  144. for exponent, converters in intword_converters:
  145. large_number = 10 ** exponent
  146. if value < large_number * 1000:
  147. new_value = value / large_number
  148. return _check_for_i18n(new_value, *converters(new_value))
  149. return value
  150. @register.filter(is_safe=True)
  151. def apnumber(value):
  152. """
  153. For numbers 1-9, return the number spelled out. Otherwise, return the
  154. number. This follows Associated Press style.
  155. """
  156. try:
  157. value = int(value)
  158. except (TypeError, ValueError):
  159. return value
  160. if not 0 < value < 10:
  161. return value
  162. return (_('one'), _('two'), _('three'), _('four'), _('five'),
  163. _('six'), _('seven'), _('eight'), _('nine'))[value - 1]
  164. # Perform the comparison in the default time zone when USE_TZ = True
  165. # (unless a specific time zone has been applied with the |timezone filter).
  166. @register.filter(expects_localtime=True)
  167. def naturalday(value, arg=None):
  168. """
  169. For date values that are tomorrow, today or yesterday compared to
  170. present day return representing string. Otherwise, return a string
  171. formatted according to settings.DATE_FORMAT.
  172. """
  173. tzinfo = getattr(value, 'tzinfo', None)
  174. try:
  175. value = date(value.year, value.month, value.day)
  176. except AttributeError:
  177. # Passed value wasn't a date object
  178. return value
  179. today = datetime.now(tzinfo).date()
  180. delta = value - today
  181. if delta.days == 0:
  182. return _('today')
  183. elif delta.days == 1:
  184. return _('tomorrow')
  185. elif delta.days == -1:
  186. return _('yesterday')
  187. return defaultfilters.date(value, arg)
  188. # This filter doesn't require expects_localtime=True because it deals properly
  189. # with both naive and aware datetimes. Therefore avoid the cost of conversion.
  190. @register.filter
  191. def naturaltime(value):
  192. """
  193. For date and time values show how many seconds, minutes, or hours ago
  194. compared to current timestamp return representing string.
  195. """
  196. return NaturalTimeFormatter.string_for(value)
  197. class NaturalTimeFormatter:
  198. time_strings = {
  199. # Translators: delta will contain a string like '2 months' or '1 month, 2 weeks'
  200. 'past-day': gettext_lazy('%(delta)s ago'),
  201. # Translators: please keep a non-breaking space (U+00A0) between count
  202. # and time unit.
  203. 'past-hour': ngettext_lazy('an hour ago', '%(count)s hours ago', 'count'),
  204. # Translators: please keep a non-breaking space (U+00A0) between count
  205. # and time unit.
  206. 'past-minute': ngettext_lazy('a minute ago', '%(count)s minutes ago', 'count'),
  207. # Translators: please keep a non-breaking space (U+00A0) between count
  208. # and time unit.
  209. 'past-second': ngettext_lazy('a second ago', '%(count)s seconds ago', 'count'),
  210. 'now': gettext_lazy('now'),
  211. # Translators: please keep a non-breaking space (U+00A0) between count
  212. # and time unit.
  213. 'future-second': ngettext_lazy('a second from now', '%(count)s seconds from now', 'count'),
  214. # Translators: please keep a non-breaking space (U+00A0) between count
  215. # and time unit.
  216. 'future-minute': ngettext_lazy('a minute from now', '%(count)s minutes from now', 'count'),
  217. # Translators: please keep a non-breaking space (U+00A0) between count
  218. # and time unit.
  219. 'future-hour': ngettext_lazy('an hour from now', '%(count)s hours from now', 'count'),
  220. # Translators: delta will contain a string like '2 months' or '1 month, 2 weeks'
  221. 'future-day': gettext_lazy('%(delta)s from now'),
  222. }
  223. past_substrings = {
  224. # Translators: 'naturaltime-past' strings will be included in '%(delta)s ago'
  225. 'year': npgettext_lazy('naturaltime-past', '%d year', '%d years'),
  226. 'month': npgettext_lazy('naturaltime-past', '%d month', '%d months'),
  227. 'week': npgettext_lazy('naturaltime-past', '%d week', '%d weeks'),
  228. 'day': npgettext_lazy('naturaltime-past', '%d day', '%d days'),
  229. 'hour': npgettext_lazy('naturaltime-past', '%d hour', '%d hours'),
  230. 'minute': npgettext_lazy('naturaltime-past', '%d minute', '%d minutes'),
  231. }
  232. future_substrings = {
  233. # Translators: 'naturaltime-future' strings will be included in '%(delta)s from now'
  234. 'year': npgettext_lazy('naturaltime-future', '%d year', '%d years'),
  235. 'month': npgettext_lazy('naturaltime-future', '%d month', '%d months'),
  236. 'week': npgettext_lazy('naturaltime-future', '%d week', '%d weeks'),
  237. 'day': npgettext_lazy('naturaltime-future', '%d day', '%d days'),
  238. 'hour': npgettext_lazy('naturaltime-future', '%d hour', '%d hours'),
  239. 'minute': npgettext_lazy('naturaltime-future', '%d minute', '%d minutes'),
  240. }
  241. @classmethod
  242. def string_for(cls, value):
  243. if not isinstance(value, date): # datetime is a subclass of date
  244. return value
  245. now = datetime.now(utc if is_aware(value) else None)
  246. if value < now:
  247. delta = now - value
  248. if delta.days != 0:
  249. return cls.time_strings['past-day'] % {
  250. 'delta': defaultfilters.timesince(value, now, time_strings=cls.past_substrings),
  251. }
  252. elif delta.seconds == 0:
  253. return cls.time_strings['now']
  254. elif delta.seconds < 60:
  255. return cls.time_strings['past-second'] % {'count': delta.seconds}
  256. elif delta.seconds // 60 < 60:
  257. count = delta.seconds // 60
  258. return cls.time_strings['past-minute'] % {'count': count}
  259. else:
  260. count = delta.seconds // 60 // 60
  261. return cls.time_strings['past-hour'] % {'count': count}
  262. else:
  263. delta = value - now
  264. if delta.days != 0:
  265. return cls.time_strings['future-day'] % {
  266. 'delta': defaultfilters.timeuntil(value, now, time_strings=cls.future_substrings),
  267. }
  268. elif delta.seconds == 0:
  269. return cls.time_strings['now']
  270. elif delta.seconds < 60:
  271. return cls.time_strings['future-second'] % {'count': delta.seconds}
  272. elif delta.seconds // 60 < 60:
  273. count = delta.seconds // 60
  274. return cls.time_strings['future-minute'] % {'count': count}
  275. else:
  276. count = delta.seconds // 60 // 60
  277. return cls.time_strings['future-hour'] % {'count': count}