# -*- coding: utf-8 -*- # Copyright (c) 2003, Taro Ogawa. All Rights Reserved. # Copyright (c) 2013, Savoir-faire Linux inc. All Rights Reserved. # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301 USA from __future__ import unicode_literals from .base import Num2Word_Base from .utils import get_digits, splitbyx GENDER_PLURAL_INDEXES = { 'm': 0, 'masculine': 0, 'м': 0, 'мужской': 0, 'f': 1, 'feminine': 1, 'ж': 0, 'женский': 0, 'n': 2, 'neuter': 2, 'с': 0, 'средний': 0, 'p': 3, 'plural': 3 } CASE_INDEXES = { 'n': 0, 'nominative': 0, 'и': 0, 'именительный': 0, 'g': 1, 'genitive': 1, 'р': 1, 'родительный': 1, 'd': 2, 'dative': 2, 'д': 2, 'дательный': 2, 'a': 3, 'accusative': 3, 'в': 3, 'винительный': 3, 'i': 4, 'instrumental': 4, 'т': 4, 'творительный': 4, 'p': 5, 'prepositional': 5, 'п': 5, 'предложный': 5 } # Default values D_CASE = 'n' D_PLURAL = False D_GENDER = 'm' D_ANIMATE = True def get_num_element(cases_dict, num, **kwargs): return case_classifier_element(cases_dict[num], **kwargs) def case_classifier_element(classifier, case=D_CASE, plural=D_PLURAL, gender=D_GENDER, animate=D_ANIMATE): case = classifier[CASE_INDEXES[case]] if isinstance(case, str): return case if plural: gender = case[GENDER_PLURAL_INDEXES['plural']] else: gender = case[GENDER_PLURAL_INDEXES[gender]] if isinstance(gender, str): return gender if animate: return gender[0] return gender[1] # format: # {n : [case_1 .. case_5]} # case: text or [gender_1 .. gender_3 plural_4] # gender: text or [animate, inanimate] ONES = { 0: ['ноль', 'ноля', 'нолю', 'ноль', 'нолём', 'ноле'], 1: [['один', 'одна', 'одно', 'одни'], ['одного', 'одной', 'одного', 'одних'], ['одному', 'одной', 'одному', 'одним'], [['одного', 'один'], 'одну', 'одно', ['одних', 'одни']], ['одним', 'одной', 'одним', 'одними'], ['одном', 'одной', 'одном', 'одних']], 2: [['два', 'две', 'два', 'двое'], ['двух'] * 3 + ['двоих'], ['двум'] * 3 + ['двоим'], [['двух', 'два'], ['двух', 'две'], 'два', 'двоих'], ['двумя'] * 3 + ['двоими'], ['двух'] * 3 + ['двоих']], 3: [['три'] * 3 + ['трое'], ['трёх'] * 3 + ['троих'], ['трём'] * 3 + ['троим'], [['трёх', 'три'], ['трёх', 'три'], 'три', 'троих'], ['тремя'] * 3 + ['троими'], ['трёх'] * 3 + ['троих']], 4: [['четыре'] * 3 + ['четверо'], ['четырёх'] * 3 + ['четверых'], ['четырём'] * 3 + ['четверым'], [['четырёх', 'четыре'], ['четырёх', 'четыре'], 'четыре', 'четверых'], ['четырьмя'] * 3 + ['четверыми'], ['четырёх'] * 3 + ['четверых']], 5: ['пять', 'пяти', 'пяти', 'пять', 'пятью', 'пяти'], 6: ['шесть', 'шести', 'шести', 'шесть', 'шестью', 'шести'], 7: ['семь', 'семи', 'семи', 'семь', 'семью', 'семи'], 8: ['восемь', 'восьми', 'восьми', 'восемь', 'восемью', 'восьми'], 9: ['девять', 'девяти', 'девяти', 'девять', 'девятью', 'девяти'] } ONES_ORD_PREFIXES = {0: 'нулев', 1: 'перв', 2: 'втор', 4: 'четвёрт', 5: 'пят', 6: 'шест', 7: 'седьм', 8: 'восьм', 9: 'девят'} ONES_ORD_POSTFIXES_GROUPS = {0: 0, 1: 1, 2: 0, 4: 1, 5: 1, 6: 0, 7: 0, 8: 0, 9: 1} CASE_POSTFIXES = [[{0: 'ой', 1: 'ый'}, 'ая', 'ое', 'ые'], ['ого', 'ой', 'ого', 'ых'], ['ому', 'ой', 'ому', 'ым'], [['ого', {0: 'ой', 1: 'ый'}], 'ую', 'ое', ['ых', 'ые']], ['ым', 'ой', 'ым', 'ыми'], ['ом', 'ой', 'ом', 'ых']] def get_cases(prefix, post_group): return [[ prefix + postfix if isinstance(postfix, str) else [prefix + animate if isinstance(animate, str) else prefix + animate[post_group] for animate in postfix] if isinstance(postfix, list) else prefix + postfix[post_group] for postfix in case] for case in CASE_POSTFIXES] def get_ord_classifier(prefixes, post_groups): if isinstance(post_groups, int): post_groups = {n: post_groups for n, i in prefixes.items()} return { num: get_cases(prefix, post_groups[num]) for num, prefix in prefixes.items() } ONES_ORD = { 3: [['третий', 'третья', 'третье', 'третьи'], ['третьего', 'третьей', 'третьего', 'третьих'], ['третьему', 'третьей', 'третьему', 'третьим'], [['третьего', 'третий'], 'третью', 'третье', ['третьих', 'третьи']], ['третьим', 'третьей', 'третьим', 'третьими'], ['третьем', 'третьей', 'третьем', 'третьих']], } ONES_ORD.update( get_ord_classifier(ONES_ORD_PREFIXES, ONES_ORD_POSTFIXES_GROUPS) ) TENS_PREFIXES = {1: 'один', 2: 'две', 3: 'три', 4: 'четыр', 5: 'пят', 6: 'шест', 7: 'сем', 8: 'восем', 9: 'девят'} TENS_POSTFIXES = ['надцать', 'надцати', 'надцати', 'надцать', 'надцатью', 'надцати'] TENS = {0: ['десять', 'десяти', 'десяти', 'десять', 'десятью', 'десяти']} TENS.update({ num: [prefix + postfix for postfix in TENS_POSTFIXES] for num, prefix in TENS_PREFIXES.items() }) TENS_ORD_PREFIXES = {0: "десят"} TENS_ORD_PREFIXES.update({ num: prefix + 'надцат' for num, prefix in TENS_PREFIXES.items() }) TENS_ORD = get_ord_classifier(TENS_ORD_PREFIXES, 1) TWENTIES = { 2: ['двадцать', 'двадцати', 'двадцати', 'двадцать', 'двадцатью', 'двадцати'], 3: ['тридцать', 'тридцати', 'тридцати', 'тридцать', 'тридцатью', 'тридцати'], 4: ['сорок', 'сорока', 'сорока', 'сорок', 'сорока', 'сорока'], 5: ['пятьдесят', 'пятидесяти', 'пятидесяти', 'пятьдесят', 'пятьюдесятью', 'пятидесяти'], 6: ['шестьдесят', 'шестидесяти', 'шестидесяти', 'шестьдесят', 'шестьюдесятью', 'шестидесяти'], 7: ['семьдесят', 'семидесяти', 'семидесяти', 'семьдесят', 'семьюдесятью', 'семидесяти'], 8: ['восемьдесят', 'восьмидесяти', 'восьмидесяти', 'восемьдесят', 'восемьюдесятью', 'восьмидесяти'], 9: ['девяносто', 'девяноста', 'девяноста', 'девяносто', 'девяноста', 'девяноста'], } TWENTIES_ORD_PREFIXES = {2: 'двадцат', 3: 'тридцат', 4: 'сороков', 5: 'пятидесят', 6: 'шестидесят', 7: 'семидесят', 8: 'восьмидесят', 9: 'девяност'} TWENTIES_ORD_POSTFIXES_GROUPS = {2: 1, 3: 1, 4: 0, 5: 1, 6: 1, 7: 1, 8: 1, 9: 1} TWENTIES_ORD = get_ord_classifier(TWENTIES_ORD_PREFIXES, TWENTIES_ORD_POSTFIXES_GROUPS) HUNDREDS = { 1: ['сто', 'ста', 'ста', 'сто', 'ста', 'ста'], 2: ['двести', 'двухсот', 'двумстам', 'двести', 'двумястами', 'двухстах'], 3: ['триста', 'трёхсот', 'трёмстам', 'триста', 'тремястами', 'трёхстах'], 4: ['четыреста', 'четырёхсот', 'четырёмстам', 'четыреста', 'четырьмястами', 'четырёхстах'], 5: ['пятьсот', 'пятисот', 'пятистам', 'пятьсот', 'пятьюстами', 'пятистах'], 6: ['шестьсот', 'шестисот', 'шестистам', 'шестьсот', 'шестьюстами', 'шестистах'], 7: ['семьсот', 'семисот', 'семистам', 'семьсот', 'семьюстами', 'семистах'], 8: ['восемьсот', 'восьмисот', 'восьмистам', 'восемьсот', 'восемьюстами', 'восьмистах'], 9: ['девятьсот', 'девятисот', 'девятистам', 'девятьсот', 'девятьюстами', 'девятистах'], } HUNDREDS_ORD_PREFIXES = { num: case[1] if num != 1 else 'сот' for num, case in HUNDREDS.items() } HUNDREDS_ORD = get_ord_classifier(HUNDREDS_ORD_PREFIXES, 1) THOUSANDS_PREFIXES = {2: 'миллион', 3: 'миллиард', 4: 'триллион', 5: 'квадриллион', 6: 'квинтиллион', 7: 'секстиллион', 8: 'септиллион', 9: 'октиллион', 10: 'нониллион'} THOUSANDS_POSTFIXES = [('', 'а', 'ов'), ('а', 'ов', 'ов'), ('у', 'ам', 'ам'), ('', 'а', 'ов'), ('ом', 'ами', 'ами'), ('е', 'ах', 'ах')] THOUSANDS = { 1: [['тысяча', 'тысячи', 'тысяч'], ['тысячи', 'тысяч', 'тысяч'], ['тысяче', 'тысячам', 'тысячам'], ['тысячу', 'тысячи', 'тысяч'], ['тысячей', 'тысячами', 'тысячами'], ['тысяче', 'тысячах', 'тысячах']] } THOUSANDS.update({ num: [ [prefix + postfix for postfix in case] for case in THOUSANDS_POSTFIXES ] for num, prefix in THOUSANDS_PREFIXES.items() }) def get_thousands_elements(num, case): return THOUSANDS[num][CASE_INDEXES[case]] THOUSANDS_ORD_PREFIXES = {1: 'тысячн'} THOUSANDS_ORD_PREFIXES.update({ num: prefix + 'н' for num, prefix in THOUSANDS_PREFIXES.items() }) THOUSANDS_ORD = get_ord_classifier(THOUSANDS_ORD_PREFIXES, 1) class Num2Word_RU(Num2Word_Base): CURRENCY_FORMS = { 'RUB': ( ('рубль', 'рубля', 'рублей'), ('копейка', 'копейки', 'копеек') ), 'EUR': ( ('евро', 'евро', 'евро'), ('цент', 'цента', 'центов') ), 'USD': ( ('доллар', 'доллара', 'долларов'), ('цент', 'цента', 'центов') ), 'UAH': ( ('гривна', 'гривны', 'гривен'), ('копейка', 'копейки', 'копеек') ), 'KZT': ( ('тенге', 'тенге', 'тенге'), ('тиын', 'тиына', 'тиынов') ), 'BYN': ( ('белорусский рубль', 'белорусских рубля', 'белорусских рублей'), ('копейка', 'копейки', 'копеек') ), 'UZS': ( ('сум', 'сума', 'сумов'), ('тийин', 'тийина', 'тийинов') ), 'PLN': ( ('польский злотый', 'польских слотых', 'польских злотых'), ('грош', 'гроша', 'грошей') ), } def setup(self): self.negword = "минус" self.pointword = ('целая', 'целых', 'целых') self.pointword_ord = get_cases("цел", 1) def to_cardinal(self, number, case=D_CASE, plural=D_PLURAL, gender=D_GENDER, animate=D_ANIMATE): n = str(number).replace(',', '.') if '.' in n: left, right = n.split('.') decimal_part = self._int2word(int(right), cardinal=True, gender='f') return u'%s %s %s %s' % ( self._int2word(int(left), cardinal=True, gender='f'), self.pluralize(int(left), self.pointword), decimal_part, self.__decimal_bitness(right) ) else: return self._int2word(int(n), cardinal=True, case=case, plural=plural, gender=gender, animate=animate) def __decimal_bitness(self, n): if n[-1] == "1" and n[-2:] != "11": return self._int2word(10 ** len(n), cardinal=False, gender='f') return self._int2word(10 ** len(n), cardinal=False, case='g', plural=True) def pluralize(self, n, forms): if n % 100 in (11, 12, 13, 14): return forms[2] if n % 10 == 1: return forms[0] if n % 10 in (2, 3, 4): return forms[1] return forms[2] def to_ordinal(self, number, case=D_CASE, plural=D_PLURAL, gender=D_GENDER, animate=D_ANIMATE): self.verify_ordinal(number) n = str(number).replace(',', '.') return self._int2word(int(n), cardinal=False, case=case, plural=plural, gender=gender, animate=animate) def _money_verbose(self, number, currency): if currency == 'UAH': return self._int2word(number, gender='f') return self._int2word(number, gender='m') def _cents_verbose(self, number, currency): if currency in ('UAH', 'RUB', 'BYN'): return self._int2word(number, gender='f') return self._int2word(number, gender='m') def _int2word(self, n, feminine=False, cardinal=True, case=D_CASE, plural=D_PLURAL, gender=D_GENDER, animate=D_ANIMATE): """ n: number feminine: not used - for backward compatibility cardinal:True - cardinal False - ordinal case: 'n' - nominative 'g' - genitive 'd' - dative 'a' - accusative 'i' - instrumental 'p' - prepositional plural: True - plural False - singular gender: 'f' - masculine 'm' - feminine 'n' - neuter animate: True - animate False - inanimate """ # For backward compatibility if feminine: gender = 'f' kwargs = {'case': case, 'plural': plural, 'gender': gender, 'animate': animate} if n < 0: return ' '.join([self.negword, self._int2word(abs(n), cardinal=cardinal, **kwargs)]) if n == 0: return get_num_element(ONES, 0, **kwargs) if cardinal else \ get_num_element(ONES_ORD, 0, **kwargs) words = [] chunks = list(splitbyx(str(n), 3)) ord_join = chunks[-1] == 0 # join in one word if ending on 'тысячный' i = len(chunks) rightest_nonzero_chunk_i = i - 1 - max( [i for i, e in enumerate(chunks) if e != 0]) for x in chunks: chunk_words = [] i -= 1 if x == 0: continue n1, n2, n3 = get_digits(x) if cardinal: chunk_words.extend( self.__chunk_cardianl(n3, n2, n1, i, **kwargs) ) if i > 0: chunk_words.append( self.pluralize(x, get_thousands_elements(i, case))) # ordinal, not joined like 'двухтысячный' elif not (ord_join and rightest_nonzero_chunk_i == i): chunk_words.extend( self.__chunk_ordinal(n3, n2, n1, i, **kwargs) ) if i > 0: t_case = case if rightest_nonzero_chunk_i == i else 'n' chunk_words.append( self.pluralize(x, get_thousands_elements(i, t_case))) # ordinal, joined else: chunk_words.extend( self.__chunk_ordinal_join(n3, n2, n1, i, **kwargs) ) if i > 0: chunk_words.append( get_num_element(THOUSANDS_ORD, i, **kwargs)) chunk_words = [''.join(chunk_words)] words.extend(chunk_words) return ' '.join(words) def __chunk_cardianl(self, hundreds, tens, ones, chunk_num, **kwargs): words = [] if hundreds > 0: words.append(get_num_element(HUNDREDS, hundreds, **kwargs)) if tens > 1: words.append(get_num_element(TWENTIES, tens, **kwargs)) if tens == 1: words.append(get_num_element(TENS, ones, **kwargs)) elif ones > 0: if chunk_num == 0: w_ones = get_num_element(ONES, ones, **kwargs) elif chunk_num == 1: # Thousands are feminine f_kwargs = kwargs.copy() f_kwargs['gender'] = 'f' w_ones = get_num_element(ONES, ones, **f_kwargs) else: w_ones = get_num_element(ONES, ones, **kwargs) words.append(w_ones) return words def __chunk_ordinal(self, hundreds, tens, ones, chunk_num, **kwargs): words = [] if hundreds > 0: if tens == 0 and ones == 0: words.append(get_num_element(HUNDREDS_ORD, hundreds, **kwargs)) else: words.append(get_num_element(HUNDREDS, hundreds)) if tens > 1: if ones == 0: words.append(get_num_element(TWENTIES_ORD, tens, **kwargs)) else: words.append(get_num_element(TWENTIES, tens)) if tens == 1: words.append(get_num_element(TENS_ORD, ones, **kwargs)) elif ones > 0: if chunk_num == 0: w_ones = get_num_element(ONES_ORD, ones, **kwargs) # тысячный, миллионнный и т.д. elif chunk_num > 0 and ones == 1 and hundreds == 0 and tens == 0: w_ones = None elif chunk_num == 1: # Thousands are feminine w_ones = get_num_element(ONES, ones, gender='f') else: w_ones = get_num_element(ONES, ones) if w_ones: words.append(w_ones) return words def __chunk_ordinal_join(self, hundreds, tens, ones, chunk_num, **kwargs): words = [] if hundreds > 1: words.append(get_num_element(HUNDREDS, hundreds, case='g')) elif hundreds == 1: words.append(get_num_element(HUNDREDS, hundreds)) # стО, not стА if tens == 9: words.append(get_num_element(TWENTIES, tens)) # девяностО, not А elif tens > 1: words.append(get_num_element(TWENTIES, tens, case='g')) if tens == 1: words.append(get_num_element(TENS, ones, case='g')) elif ones > 0: if chunk_num == 0: w_ones = get_num_element(ONES_ORD, ones, **kwargs) # тысячный, миллионнный и т.д., двадцатиодномиллионный elif chunk_num > 0 and ones == 1 and tens != 1: if tens == 0 and hundreds == 0: w_ones = None else: w_ones = get_num_element(ONES, 1, gender='n') else: w_ones = get_num_element(ONES, ones, case='g') if w_ones: words.append(w_ones) return words