Package pytils :: Module numeral
[hide private]

Source Code for Module pytils.numeral

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