Development of an internal social media platform with personalised dashboards for students
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

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