numeral.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. # -*- coding: utf-8 -*-
  2. # -*- test-case-name: pytils.test.test_numeral -*-
  3. """
  4. Plural forms and in-word representation for numerals.
  5. """
  6. from __future__ import division
  7. from decimal import Decimal
  8. from pytils.utils import check_length, check_positive, split_values
  9. FRACTIONS = (
  10. ("десятая", "десятых", "десятых"),
  11. ("сотая", "сотых", "сотых"),
  12. ("тысячная", "тысячных", "тысячных"),
  13. ("десятитысячная", "десятитысячных", "десятитысячных"),
  14. ("стотысячная", "стотысячных", "стотысячных"),
  15. ("миллионная", "милллионных", "милллионных"),
  16. ("десятимиллионная", "десятимилллионных", "десятимиллионных"),
  17. ("стомиллионная", "стомилллионных", "стомиллионных"),
  18. ("миллиардная", "миллиардных", "миллиардных"),
  19. ) #: Forms (1, 2, 5) for fractions
  20. ONES = {
  21. 0: ("", "", ""),
  22. 1: ("один", "одна", "одно"),
  23. 2: ("два", "две", "два"),
  24. 3: ("три", "три", "три"),
  25. 4: ("четыре", "четыре", "четыре"),
  26. 5: ("пять", "пять", "пять"),
  27. 6: ("шесть", "шесть", "шесть"),
  28. 7: ("семь", "семь", "семь"),
  29. 8: ("восемь", "восемь", "восемь"),
  30. 9: ("девять", "девять", "девять"),
  31. } #: Forms (MALE, FEMALE, NEUTER) for ones
  32. TENS = {
  33. 0: "",
  34. # 1 - особый случай
  35. 10: "десять",
  36. 11: "одиннадцать",
  37. 12: "двенадцать",
  38. 13: "тринадцать",
  39. 14: "четырнадцать",
  40. 15: "пятнадцать",
  41. 16: "шестнадцать",
  42. 17: "семнадцать",
  43. 18: "восемнадцать",
  44. 19: "девятнадцать",
  45. 2: "двадцать",
  46. 3: "тридцать",
  47. 4: "сорок",
  48. 5: "пятьдесят",
  49. 6: "шестьдесят",
  50. 7: "семьдесят",
  51. 8: "восемьдесят",
  52. 9: "девяносто",
  53. } #: Tens
  54. HUNDREDS = {
  55. 0: "",
  56. 1: "сто",
  57. 2: "двести",
  58. 3: "триста",
  59. 4: "четыреста",
  60. 5: "пятьсот",
  61. 6: "шестьсот",
  62. 7: "семьсот",
  63. 8: "восемьсот",
  64. 9: "девятьсот",
  65. } #: Hundreds
  66. MALE = 1 #: sex - male
  67. FEMALE = 2 #: sex - female
  68. NEUTER = 3 #: sex - neuter
  69. def _get_float_remainder(fvalue, signs=9):
  70. """
  71. Get remainder of float, i.e. 2.05 -> '05'
  72. @param fvalue: input value
  73. @type fvalue: C{integer types}, C{float} or C{Decimal}
  74. @param signs: maximum number of signs
  75. @type signs: C{integer types}
  76. @return: remainder
  77. @rtype: C{str}
  78. @raise ValueError: fvalue is negative
  79. @raise ValueError: signs overflow
  80. """
  81. check_positive(fvalue)
  82. if isinstance(fvalue, int):
  83. return "0"
  84. if isinstance(fvalue, Decimal) and fvalue.as_tuple()[2] == 0:
  85. # Decimal.as_tuple() -> (sign, digit_tuple, exponent)
  86. # если экспонента "0" -- значит дробной части нет
  87. return "0"
  88. signs = min(signs, len(FRACTIONS))
  89. # нужно remainder в строке, потому что дробные X.0Y
  90. # будут "ломаться" до X.Y
  91. remainder = str(fvalue).split('.')[1]
  92. iremainder = int(remainder)
  93. orig_remainder = remainder
  94. factor = len(str(remainder)) - signs
  95. if factor > 0:
  96. # после запятой цифр больше чем signs, округляем
  97. iremainder = int(round(iremainder / (10.0**factor)))
  98. format = "%%0%dd" % min(len(remainder), signs)
  99. remainder = format % iremainder
  100. if len(remainder) > signs:
  101. # при округлении цифр вида 0.998 ругаться
  102. raise ValueError("Signs overflow: I can't round only fractional part \
  103. of %s to fit %s in %d signs" % \
  104. (str(fvalue), orig_remainder, signs))
  105. return remainder
  106. def choose_plural(amount, variants):
  107. """
  108. Choose proper case depending on amount
  109. @param amount: amount of objects
  110. @type amount: C{integer types}
  111. @param variants: variants (forms) of object in such form:
  112. (1 object, 2 objects, 5 objects).
  113. @type variants: 3-element C{sequence} of C{unicode}
  114. or C{unicode} (three variants with delimeter ',')
  115. @return: proper variant
  116. @rtype: C{str}
  117. @raise ValueError: variants' length lesser than 3
  118. """
  119. if isinstance(variants, str):
  120. variants = split_values(variants)
  121. check_length(variants, 3)
  122. amount = abs(amount)
  123. if amount % 10 == 1 and amount % 100 != 11:
  124. variant = 0
  125. elif amount % 10 >= 2 and amount % 10 <= 4 and \
  126. (amount % 100 < 10 or amount % 100 >= 20):
  127. variant = 1
  128. else:
  129. variant = 2
  130. return variants[variant]
  131. def get_plural(amount, variants, absence=None):
  132. """
  133. Get proper case with value
  134. @param amount: amount of objects
  135. @type amount: C{integer types}
  136. @param variants: variants (forms) of object in such form:
  137. (1 object, 2 objects, 5 objects).
  138. @type variants: 3-element C{sequence} of C{str}
  139. or C{str} (three variants with delimeter ',')
  140. @param absence: if amount is zero will return it
  141. @type absence: C{str}
  142. @return: amount with proper variant
  143. @rtype: C{str}
  144. """
  145. if amount or absence is None:
  146. return "%d %s" % (amount, choose_plural(amount, variants))
  147. else:
  148. return absence
  149. def _get_plural_legacy(amount, extra_variants):
  150. """
  151. Get proper case with value (legacy variant, without absence)
  152. @param amount: amount of objects
  153. @type amount: C{integer types}
  154. @param variants: variants (forms) of object in such form:
  155. (1 object, 2 objects, 5 objects, 0-object variant).
  156. 0-object variant is similar to C{absence} in C{get_plural}
  157. @type variants: 3-element C{sequence} of C{str}
  158. or C{str} (three variants with delimeter ',')
  159. @return: amount with proper variant
  160. @rtype: C{str}
  161. """
  162. absence = None
  163. if isinstance(extra_variants, str):
  164. extra_variants = split_values(extra_variants)
  165. if len(extra_variants) == 4:
  166. variants = extra_variants[:3]
  167. absence = extra_variants[3]
  168. else:
  169. variants = extra_variants
  170. return get_plural(amount, variants, absence)
  171. def rubles(amount, zero_for_kopeck=False):
  172. """
  173. Get string for money
  174. @param amount: amount of money
  175. @type amount: C{integer types}, C{float} or C{Decimal}
  176. @param zero_for_kopeck: If false, then zero kopecks ignored
  177. @type zero_for_kopeck: C{bool}
  178. @return: in-words representation of money's amount
  179. @rtype: C{str}
  180. @raise ValueError: amount is negative
  181. """
  182. check_positive(amount)
  183. pts = []
  184. amount = round(amount, 2)
  185. pts.append(sum_string(int(amount), 1, ("рубль", "рубля", "рублей")))
  186. remainder = _get_float_remainder(amount, 2)
  187. iremainder = int(remainder)
  188. if iremainder != 0 or zero_for_kopeck:
  189. # если 3.1, то это 10 копеек, а не одна
  190. if iremainder < 10 and len(remainder) == 1:
  191. iremainder *= 10
  192. pts.append(sum_string(iremainder, 2,
  193. ("копейка", "копейки", "копеек")))
  194. return " ".join(pts)
  195. def in_words_int(amount, gender=MALE):
  196. """
  197. Integer in words
  198. @param amount: numeral
  199. @type amount: C{integer types}
  200. @param gender: gender (MALE, FEMALE or NEUTER)
  201. @type gender: C{int}
  202. @return: in-words reprsentation of numeral
  203. @rtype: C{str}
  204. @raise ValueError: amount is negative
  205. """
  206. check_positive(amount)
  207. return sum_string(amount, gender)
  208. def in_words_float(amount, _gender=FEMALE):
  209. """
  210. Float in words
  211. @param amount: float numeral
  212. @type amount: C{float} or C{Decimal}
  213. @return: in-words reprsentation of float numeral
  214. @rtype: C{str}
  215. @raise ValueError: when ammount is negative
  216. """
  217. check_positive(amount)
  218. pts = []
  219. # преобразуем целую часть
  220. pts.append(sum_string(int(amount), 2,
  221. ("целая", "целых", "целых")))
  222. # теперь то, что после запятой
  223. remainder = _get_float_remainder(amount)
  224. signs = len(str(remainder)) - 1
  225. pts.append(sum_string(int(remainder), 2, FRACTIONS[signs]))
  226. return " ".join(pts)
  227. def in_words(amount, gender=None):
  228. """
  229. Numeral in words
  230. @param amount: numeral
  231. @type amount: C{integer types}, C{float} or C{Decimal}
  232. @param gender: gender (MALE, FEMALE or NEUTER)
  233. @type gender: C{int}
  234. @return: in-words reprsentation of numeral
  235. @rtype: C{str}
  236. raise ValueError: when amount is negative
  237. """
  238. check_positive(amount)
  239. if isinstance(amount, Decimal) and amount.as_tuple()[2] == 0:
  240. # если целое,
  241. # т.е. Decimal.as_tuple -> (sign, digits tuple, exponent), exponent=0
  242. # то как целое
  243. amount = int(amount)
  244. if gender is None:
  245. args = (amount,)
  246. else:
  247. args = (amount, gender)
  248. # если целое
  249. if isinstance(amount, int):
  250. return in_words_int(*args)
  251. # если дробное
  252. elif isinstance(amount, (float, Decimal)):
  253. return in_words_float(*args)
  254. # ни float, ни int, ни Decimal
  255. else:
  256. # до сюда не должно дойти
  257. raise TypeError(
  258. "amount should be number type (int, long, float, Decimal), got %s"
  259. % type(amount))
  260. def sum_string(amount, gender, items=None):
  261. """
  262. Get sum in words
  263. @param amount: amount of objects
  264. @type amount: C{integer types}
  265. @param gender: gender of object (MALE, FEMALE or NEUTER)
  266. @type gender: C{int}
  267. @param items: variants of object in three forms:
  268. for one object, for two objects and for five objects
  269. @type items: 3-element C{sequence} of C{str} or
  270. just C{str} (three variants with delimeter ',')
  271. @return: in-words representation objects' amount
  272. @rtype: C{str}
  273. @raise ValueError: items isn't 3-element C{sequence} or C{unicode}
  274. @raise ValueError: amount bigger than 10**11
  275. @raise ValueError: amount is negative
  276. """
  277. if isinstance(items, str):
  278. items = split_values(items)
  279. if items is None:
  280. items = ("", "", "")
  281. try:
  282. one_item, two_items, five_items = items
  283. except ValueError:
  284. raise ValueError("Items must be 3-element sequence")
  285. check_positive(amount)
  286. if amount == 0:
  287. if five_items:
  288. return "ноль %s" % five_items
  289. else:
  290. return "ноль"
  291. into = ''
  292. tmp_val = amount
  293. # единицы
  294. into, tmp_val = _sum_string_fn(into, tmp_val, gender, items)
  295. # тысячи
  296. into, tmp_val = _sum_string_fn(into, tmp_val, FEMALE,
  297. ("тысяча", "тысячи", "тысяч"))
  298. # миллионы
  299. into, tmp_val = _sum_string_fn(into, tmp_val, MALE,
  300. ("миллион", "миллиона", "миллионов"))
  301. # миллиарды
  302. into, tmp_val = _sum_string_fn(into, tmp_val, MALE,
  303. ("миллиард", "миллиарда", "миллиардов"))
  304. if tmp_val == 0:
  305. return into
  306. else:
  307. raise ValueError("Cannot operand with numbers bigger than 10**11")
  308. def _sum_string_fn(into, tmp_val, gender, items=None):
  309. """
  310. Make in-words representation of single order
  311. @param into: in-words representation of lower orders
  312. @type into: C{str}
  313. @param tmp_val: temporary value without lower orders
  314. @type tmp_val: C{integer types}
  315. @param gender: gender (MALE, FEMALE or NEUTER)
  316. @type gender: C{int}
  317. @param items: variants of objects
  318. @type items: 3-element C{sequence} of C{str}
  319. @return: new into and tmp_val
  320. @rtype: C{tuple}
  321. @raise ValueError: tmp_val is negative
  322. """
  323. if items is None:
  324. items = ("", "", "")
  325. one_item, two_items, five_items = items
  326. check_positive(tmp_val)
  327. if tmp_val == 0:
  328. return into, tmp_val
  329. words = []
  330. rest = tmp_val % 1000
  331. tmp_val = tmp_val // 1000
  332. if rest == 0:
  333. # последние три знака нулевые
  334. if into == "":
  335. into = "%s " % five_items
  336. return into, tmp_val
  337. # начинаем подсчет с rest
  338. end_word = five_items
  339. # сотни
  340. words.append(HUNDREDS[rest // 100])
  341. # десятки
  342. rest = rest % 100
  343. rest1 = rest // 10
  344. # особый случай -- tens=1
  345. tens = rest1 == 1 and TENS[rest] or TENS[rest1]
  346. words.append(tens)
  347. # единицы
  348. if rest1 < 1 or rest1 > 1:
  349. amount = rest % 10
  350. end_word = choose_plural(amount, items)
  351. words.append(ONES[amount][gender-1])
  352. words.append(end_word)
  353. # добавляем то, что уже было
  354. words.append(into)
  355. # убираем пустые подстроки
  356. words = filter(lambda x: len(x) > 0, words)
  357. # склеиваем и отдаем
  358. return " ".join(words).strip(), tmp_val