base.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. import math
  4. from collections import OrderedDict
  5. from decimal import Decimal
  6. from .compat import to_s
  7. from .currency import parse_currency_parts, prefix_currency
  8. class Num2Word_Base(object):
  9. CURRENCY_FORMS = {}
  10. CURRENCY_ADJECTIVES = {}
  11. def __init__(self):
  12. self.is_title = False
  13. self.precision = 2
  14. self.exclude_title = []
  15. self.negword = "(-) "
  16. self.pointword = "(.)"
  17. self.errmsg_nonnum = "type(%s) not in [long, int, float]"
  18. self.errmsg_floatord = "Cannot treat float %s as ordinal."
  19. self.errmsg_negord = "Cannot treat negative num %s as ordinal."
  20. self.errmsg_toobig = "abs(%s) must be less than %s."
  21. self.setup()
  22. # uses cards
  23. if any(hasattr(self, field) for field in
  24. ['high_numwords', 'mid_numwords', 'low_numwords']):
  25. self.cards = OrderedDict()
  26. self.set_numwords()
  27. self.MAXVAL = 1000 * list(self.cards.keys())[0]
  28. def set_numwords(self):
  29. self.set_high_numwords(self.high_numwords)
  30. self.set_mid_numwords(self.mid_numwords)
  31. self.set_low_numwords(self.low_numwords)
  32. # self.set_labeled_numbers(self.labeled_numbers)
  33. def set_high_numwords(self, *args):
  34. raise NotImplementedError
  35. # def set_labeled_numbers(self, labeled_numbers):
  36. # for key, val in labeled_numbers.items():
  37. # self.cards[key] = val
  38. def set_mid_numwords(self, mid):
  39. for key, val in mid:
  40. self.cards[key] = val
  41. def set_low_numwords(self, numwords):
  42. for word, n in zip(numwords, range(len(numwords) - 1, -1, -1)):
  43. self.cards[n] = word
  44. def splitnum(self, value):
  45. for elem in self.cards:
  46. if elem > value:
  47. continue
  48. out = []
  49. if value == 0:
  50. div, mod = 1, 0
  51. else:
  52. div, mod = divmod(value, elem)
  53. if div == 1:
  54. out.append((self.cards[1], 1))
  55. else:
  56. if div == value: # The system tallies, eg Roman Numerals
  57. return [(div * self.cards[elem], div*elem)]
  58. out.append(self.splitnum(div))
  59. out.append((self.cards[elem], elem))
  60. if mod:
  61. out.append(self.splitnum(mod))
  62. return out
  63. def parse_minus(self, num_str):
  64. """Detach minus and return it as symbol with new num_str."""
  65. if num_str.startswith('-'):
  66. # Extra spacing to compensate if there is no minus.
  67. return '%s ' % self.negword, num_str[1:]
  68. return '', num_str
  69. def str_to_number(self, value):
  70. return Decimal(value)
  71. def to_cardinal(self, value):
  72. try:
  73. assert int(value) == value
  74. except (ValueError, TypeError, AssertionError):
  75. return self.to_cardinal_float(value)
  76. out = ""
  77. if value < 0:
  78. value = abs(value)
  79. out = self.negword
  80. if value >= self.MAXVAL:
  81. raise OverflowError(self.errmsg_toobig % (value, self.MAXVAL))
  82. val = self.splitnum(value)
  83. words, num = self.clean(val)
  84. return self.title(out + words)
  85. def float2tuple(self, value):
  86. pre = int(value)
  87. # Simple way of finding decimal places to update the precision
  88. self.precision = abs(Decimal(str(value)).as_tuple().exponent)
  89. post = abs(value - pre) * 10**self.precision
  90. if abs(round(post) - post) < 0.01:
  91. # We generally floor all values beyond our precision (rather than
  92. # rounding), but in cases where we have something like 1.239999999,
  93. # which is probably due to python's handling of floats, we actually
  94. # want to consider it as 1.24 instead of 1.23
  95. post = int(round(post))
  96. else:
  97. post = int(math.floor(post))
  98. return pre, post
  99. def to_cardinal_float(self, value):
  100. try:
  101. float(value) == value
  102. except (ValueError, TypeError, AssertionError, AttributeError):
  103. raise TypeError(self.errmsg_nonnum % value)
  104. pre, post = self.float2tuple(float(value))
  105. post = str(post)
  106. post = '0' * (self.precision - len(post)) + post
  107. out = [self.to_cardinal(pre)]
  108. if self.precision:
  109. out.append(self.title(self.pointword))
  110. for i in range(self.precision):
  111. curr = int(post[i])
  112. out.append(to_s(self.to_cardinal(curr)))
  113. return " ".join(out)
  114. def merge(self, curr, next):
  115. raise NotImplementedError
  116. def clean(self, val):
  117. out = val
  118. while len(val) != 1:
  119. out = []
  120. left, right = val[:2]
  121. if isinstance(left, tuple) and isinstance(right, tuple):
  122. out.append(self.merge(left, right))
  123. if val[2:]:
  124. out.append(val[2:])
  125. else:
  126. for elem in val:
  127. if isinstance(elem, list):
  128. if len(elem) == 1:
  129. out.append(elem[0])
  130. else:
  131. out.append(self.clean(elem))
  132. else:
  133. out.append(elem)
  134. val = out
  135. return out[0]
  136. def title(self, value):
  137. if self.is_title:
  138. out = []
  139. value = value.split()
  140. for word in value:
  141. if word in self.exclude_title:
  142. out.append(word)
  143. else:
  144. out.append(word[0].upper() + word[1:])
  145. value = " ".join(out)
  146. return value
  147. def verify_ordinal(self, value):
  148. if not value == int(value):
  149. raise TypeError(self.errmsg_floatord % value)
  150. if not abs(value) == value:
  151. raise TypeError(self.errmsg_negord % value)
  152. def to_ordinal(self, value):
  153. return self.to_cardinal(value)
  154. def to_ordinal_num(self, value):
  155. return value
  156. # Trivial version
  157. def inflect(self, value, text):
  158. text = text.split("/")
  159. if value == 1:
  160. return text[0]
  161. return "".join(text)
  162. # //CHECK: generalise? Any others like pounds/shillings/pence?
  163. def to_splitnum(self, val, hightxt="", lowtxt="", jointxt="",
  164. divisor=100, longval=True, cents=True):
  165. out = []
  166. if isinstance(val, float):
  167. high, low = self.float2tuple(val)
  168. else:
  169. try:
  170. high, low = val
  171. except TypeError:
  172. high, low = divmod(val, divisor)
  173. if high:
  174. hightxt = self.title(self.inflect(high, hightxt))
  175. out.append(self.to_cardinal(high))
  176. if low:
  177. if longval:
  178. if hightxt:
  179. out.append(hightxt)
  180. if jointxt:
  181. out.append(self.title(jointxt))
  182. elif hightxt:
  183. out.append(hightxt)
  184. if low:
  185. if cents:
  186. out.append(self.to_cardinal(low))
  187. else:
  188. out.append("%02d" % low)
  189. if lowtxt and longval:
  190. out.append(self.title(self.inflect(low, lowtxt)))
  191. return " ".join(out)
  192. def to_year(self, value, **kwargs):
  193. return self.to_cardinal(value)
  194. def pluralize(self, n, forms):
  195. """
  196. Should resolve gettext form:
  197. http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html
  198. """
  199. raise NotImplementedError
  200. def _money_verbose(self, number, currency):
  201. return self.to_cardinal(number)
  202. def _cents_verbose(self, number, currency):
  203. return self.to_cardinal(number)
  204. def _cents_terse(self, number, currency):
  205. return "%02d" % number
  206. def to_currency(self, val, currency='EUR', cents=True, separator=',',
  207. adjective=False):
  208. """
  209. Args:
  210. val: Numeric value
  211. currency (str): Currency code
  212. cents (bool): Verbose cents
  213. separator (str): Cent separator
  214. adjective (bool): Prefix currency name with adjective
  215. Returns:
  216. str: Formatted string
  217. """
  218. left, right, is_negative = parse_currency_parts(val)
  219. try:
  220. cr1, cr2 = self.CURRENCY_FORMS[currency]
  221. except KeyError:
  222. raise NotImplementedError(
  223. 'Currency code "%s" not implemented for "%s"' %
  224. (currency, self.__class__.__name__))
  225. if adjective and currency in self.CURRENCY_ADJECTIVES:
  226. cr1 = prefix_currency(self.CURRENCY_ADJECTIVES[currency], cr1)
  227. minus_str = "%s " % self.negword if is_negative else ""
  228. money_str = self._money_verbose(left, currency)
  229. cents_str = self._cents_verbose(right, currency) \
  230. if cents else self._cents_terse(right, currency)
  231. return u'%s%s %s%s %s %s' % (
  232. minus_str,
  233. money_str,
  234. self.pluralize(left, cr1),
  235. separator,
  236. cents_str,
  237. self.pluralize(right, cr2)
  238. )
  239. def setup(self):
  240. pass