Package pytils :: Module numeral
[hide private]
[frames] | no frames]

Source Code for Module pytils.numeral

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