├── LICENSE.txt ├── README.md ├── num2t4ru └── __init__.py ├── setup.py └── tests.py /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Простой python модуль для генерации названия числа. 2 | =================================================== 3 | 4 | Позволяет получить из числа, например, 100500 его "название" (генерация текста из числа, преобразование числа в текст), например "сто пятьсот". 5 | Позволяет так же использовать наименование единиц измерения например 100500 (рублей, рубля, рубль) > "сто пятьсот рублей" 6 | 7 | Примеры использования 8 | --------------------- 9 | 10 | ### Генерация названия int числа 11 | 12 | >>> from number_to_text import num2text 13 | >>> print num2text(100500) 14 | u"сто пятьсот" 15 | >>> print num2text(1234567891) 16 | u'один миллиард двести тридцать четыре миллиона пятьсот шестьдесят семь тысяч восемьсот девяносто один' 17 | 18 | ### Генерация названия int числа с единицами измерения 19 | 20 | >>> from number_to_text import num2text 21 | >>> male_units = ((u'рубль', u'рубля', u'рублей'), 'm') 22 | >>> female_units = ((u'копейка', u'копейки', u'копеек'), 'f') 23 | >>> # male_units это plural-формы для единицы измерения и ее род 'm' - мужской, 'f' - женский 24 | >>> num2text(101, male_units) # первая plural форма, мужской род 25 | u'сто один рубль' 26 | >>> num2text(102, male_units) # вторая plural форма, мужской род 27 | u'сто два рубля' 28 | >>> num2text(101, female_units) # первая plural форма, женский род 29 | u'сто одна копейка' 30 | >>> num2text(102, female_units) # вторая plural форма, женский род 31 | u'сто две копейки' 32 | >>> num2text(105, female_units) # третья plural форма, женский род 33 | u'сто пять копеек' 34 | 35 | ### Генерация названия дробного числа 36 | 37 | >>> from number_to_text import decimal2text 38 | >>> import Decimal 39 | >>> int_units = ((u'рубль', u'рубля', u'рублей'), 'm') 40 | >>> exp_units = ((u'копейка', u'копейки', u'копеек'), 'f') 41 | >>> decimal2text( 42 | decimal.Decimal('105.245'), 43 | int_units=int_units, 44 | exp_units=exp_units) 45 | u'сто пять рублей двадцать четыре копейки' 46 | >>> decimal2text( # можно задать число цифр после запятой (округление) 47 | decimal.Decimal('102.2450'), 48 | places=4, 49 | int_units=int_units, 50 | exp_units=exp_units) 51 | u'сто два рубля две тысячи четыреста пятьдесят копеек' xD 52 | 53 | Еще больше примеров можно найти в юнит-тестах. 54 | 55 | TODO 56 | ---- 57 | 58 | * Нучиться возвращать не строку а что-то более удобное для дальнейшей обработки. 59 | * Добваить в PyPi ??? 60 | * Отрефакторить 61 | * Больше комментариев в коде! 62 | -------------------------------------------------------------------------------- /num2t4ru/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on 04.07.2011 4 | Changed on 13.03.2016 by Artem Tiumentcev 5 | 6 | @author: Sergey Prokhorov 7 | ''' 8 | import decimal 9 | 10 | 11 | units = ( 12 | u'ноль', 13 | 14 | (u'один', u'одна'), 15 | (u'два', u'две'), 16 | 17 | u'три', u'четыре', u'пять', 18 | u'шесть', u'семь', u'восемь', u'девять' 19 | ) 20 | 21 | teens = ( 22 | u'десять', u'одиннадцать', 23 | u'двенадцать', u'тринадцать', 24 | u'четырнадцать', u'пятнадцать', 25 | u'шестнадцать', u'семнадцать', 26 | u'восемнадцать', u'девятнадцать' 27 | ) 28 | 29 | tens = ( 30 | teens, 31 | u'двадцать', u'тридцать', 32 | u'сорок', u'пятьдесят', 33 | u'шестьдесят', u'семьдесят', 34 | u'восемьдесят', u'девяносто' 35 | ) 36 | 37 | hundreds = ( 38 | u'сто', u'двести', 39 | u'триста', u'четыреста', 40 | u'пятьсот', u'шестьсот', 41 | u'семьсот', u'восемьсот', 42 | u'девятьсот' 43 | ) 44 | 45 | orders = (# plural forms and gender 46 | #((u'', u'', u''), 'm'), # ((u'рубль', u'рубля', u'рублей'), 'm'), # ((u'копейка', u'копейки', u'копеек'), 'f') 47 | ((u'тысяча', u'тысячи', u'тысяч'), 'f'), 48 | ((u'миллион', u'миллиона', u'миллионов'), 'm'), 49 | ((u'миллиард', u'миллиарда', u'миллиардов'), 'm'), 50 | ) 51 | 52 | minus = u'минус' 53 | 54 | 55 | def thousand(rest, sex): 56 | """Converts numbers from 19 to 999""" 57 | prev = 0 58 | plural = 2 59 | name = [] 60 | use_teens = rest % 100 >= 10 and rest % 100 <= 19 61 | if not use_teens: 62 | data = ((units, 10), (tens, 100), (hundreds, 1000)) 63 | else: 64 | data = ((teens, 10), (hundreds, 1000)) 65 | for names, x in data: 66 | cur = int(((rest - prev) % x) * 10 / x) 67 | prev = rest % x 68 | if x == 10 and use_teens: 69 | plural = 2 70 | name.append(teens[cur]) 71 | elif cur == 0: 72 | continue 73 | elif x == 10: 74 | name_ = names[cur] 75 | if isinstance(name_, tuple): 76 | name_ = name_[0 if sex == 'm' else 1] 77 | name.append(name_) 78 | if cur >= 2 and cur <= 4: 79 | plural = 1 80 | elif cur == 1: 81 | plural = 0 82 | else: 83 | plural = 2 84 | else: 85 | name.append(names[cur-1]) 86 | return plural, name 87 | 88 | 89 | def num2text(num, main_units=((u'', u'', u''), 'm')): 90 | """ 91 | http://ru.wikipedia.org/wiki/Gettext#.D0.9C.D0.BD.D0.BE.D0.B6.D0.B5.D1.81.\ 92 | D1.82.D0.B2.D0.B5.D0.BD.D0.BD.D1.8B.D0.B5_.D1.87.D0.B8.D1.81.D0.BB.D0.B0_2 93 | """ 94 | _orders = (main_units,) + orders 95 | if num == 0: 96 | return ' '.join((units[0], _orders[0][0][2])).strip() # ноль 97 | 98 | rest = abs(num) 99 | ord = 0 100 | name = [] 101 | while rest > 0: 102 | plural, nme = thousand(rest % 1000, _orders[ord][1]) 103 | if nme or ord == 0: 104 | name.append(_orders[ord][0][plural]) 105 | name += nme 106 | rest = int(rest / 1000) 107 | ord += 1 108 | if num < 0: 109 | name.append(minus) 110 | name.reverse() 111 | return ' '.join(name).strip() 112 | 113 | 114 | def decimal2text(value, places=2, 115 | int_units=(('', '', ''), 'm'), 116 | exp_units=(('', '', ''), 'm')): 117 | value = decimal.Decimal(value) 118 | q = decimal.Decimal(10) ** -places 119 | 120 | integral, exp = str(value.quantize(q)).split('.') 121 | return u'{} {}'.format( 122 | num2text(int(integral), int_units), 123 | num2text(int(exp), exp_units)) 124 | 125 | if __name__ == '__main__': 126 | import sys 127 | if len(sys.argv) > 1: 128 | try: 129 | num = sys.argv[1] 130 | if '.' in num: 131 | print(decimal2text( 132 | decimal.Decimal(num), 133 | int_units=((u'штука', u'штуки', u'штук'), 'f'), 134 | exp_units=((u'кусок', u'куска', u'кусков'), 'm'))) 135 | else: 136 | print(num2text( 137 | int(num), 138 | main_units=((u'штука', u'штуки', u'штук'), 'f'))) 139 | except ValueError: 140 | print (sys.stderr, "Invalid argument {}".format(sys.argv[1])) 141 | sys.exit() 142 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | here = os.path.abspath(os.path.dirname(__file__)) 5 | with open(os.path.join(here, 'README.md')) as f: 6 | README = f.read() 7 | 8 | 9 | setup( 10 | name='num2t4ru', 11 | version='2.0.0', 12 | author='Sergey Prokhorov', 13 | author_email='me@seriyps.ru', 14 | url='https://github.com/seriyps/ru_number_to_text', 15 | keywords='plural forms', 16 | license="Apache License 2.0", 17 | test_suite='tests', 18 | packages=find_packages(), 19 | long_description=open(os.path.join(os.path.dirname(__file__), 'README.md')).read(), 20 | ) 21 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on 13.03.2016 by Artem Tiumentcev 4 | 5 | @author: Sergey Prokhorov 6 | ''' 7 | import unittest 8 | 9 | from num2t4ru import num2text, decimal2text 10 | 11 | 12 | class TestStrToText(unittest.TestCase): 13 | 14 | def test_units(self): 15 | self.assertEqual(num2text(0), u'ноль') 16 | self.assertEqual(num2text(1), u'один') 17 | self.assertEqual(num2text(9), u'девять') 18 | 19 | def test_gender(self): 20 | self.assertEqual(num2text(1000), u'одна тысяча') 21 | self.assertEqual(num2text(2000), u'две тысячи') 22 | self.assertEqual(num2text(1000000), u'один миллион') 23 | self.assertEqual(num2text(2000000), u'два миллиона') 24 | 25 | def test_teens(self): 26 | self.assertEqual(num2text(10), u'десять') 27 | self.assertEqual(num2text(11), u'одиннадцать') 28 | self.assertEqual(num2text(19), u'девятнадцать') 29 | 30 | def test_tens(self): 31 | self.assertEqual(num2text(20), u'двадцать') 32 | self.assertEqual(num2text(90), u'девяносто') 33 | 34 | def test_hundreeds(self): 35 | self.assertEqual(num2text(100), u'сто') 36 | self.assertEqual(num2text(900), u'девятьсот') 37 | 38 | def test_orders(self): 39 | self.assertEqual(num2text(1000), u'одна тысяча') 40 | self.assertEqual(num2text(2000), u'две тысячи') 41 | self.assertEqual(num2text(5000), u'пять тысяч') 42 | self.assertEqual(num2text(1000000), u'один миллион') 43 | self.assertEqual(num2text(2000000), u'два миллиона') 44 | self.assertEqual(num2text(5000000), u'пять миллионов') 45 | self.assertEqual(num2text(1000000000), u'один миллиард') 46 | self.assertEqual(num2text(2000000000), u'два миллиарда') 47 | self.assertEqual(num2text(5000000000), u'пять миллиардов') 48 | 49 | def test_inter_oreders(self): 50 | self.assertEqual(num2text(1100), u'одна тысяча сто') 51 | self.assertEqual(num2text(2001), u'две тысячи один') 52 | self.assertEqual(num2text(5011), u'пять тысяч одиннадцать') 53 | self.assertEqual(num2text(1002000), u'один миллион две тысячи') 54 | self.assertEqual(num2text(2020000), u'два миллиона двадцать тысяч') 55 | self.assertEqual(num2text(5300600), u'пять миллионов триста тысяч шестьсот') 56 | self.assertEqual(num2text(1002000000), u'один миллиард два миллиона') 57 | self.assertEqual(num2text(2030000000), u'два миллиарда тридцать миллионов') 58 | self.assertEqual(num2text(1234567891), 59 | u'один миллиард двести тридцать четыре миллиона ' 60 | u'пятьсот шестьдесят семь тысяч ' 61 | u'восемьсот девяносто один') 62 | 63 | def test_main_units(self): 64 | male_units = ((u'рубль', u'рубля', u'рублей'), 'm') 65 | female_units = ((u'копейка', u'копейки', u'копеек'), 'f') 66 | self.assertEqual(num2text(101, male_units), u'сто один рубль') 67 | self.assertEqual(num2text(102, male_units), u'сто два рубля') 68 | self.assertEqual(num2text(105, male_units), u'сто пять рублей') 69 | 70 | self.assertEqual(num2text(101, female_units), u'сто одна копейка') 71 | self.assertEqual(num2text(102, female_units), u'сто две копейки') 72 | self.assertEqual(num2text(105, female_units), u'сто пять копеек') 73 | 74 | self.assertEqual(num2text(0, male_units), u'ноль рублей') 75 | self.assertEqual(num2text(0, female_units), u'ноль копеек') 76 | 77 | self.assertEqual(num2text(3000, male_units), u'три тысячи рублей') 78 | 79 | def test_decimal2text(self): 80 | int_units = ((u'рубль', u'рубля', u'рублей'), 'm') 81 | exp_units = ((u'копейка', u'копейки', u'копеек'), 'f') 82 | self.assertEqual( 83 | decimal2text( 84 | '105.245', 85 | int_units=int_units, 86 | exp_units=exp_units), 87 | u'сто пять рублей двадцать четыре копейки') 88 | self.assertEqual( 89 | decimal2text( 90 | '101.26', 91 | int_units=int_units, 92 | exp_units=exp_units), 93 | u'сто один рубль двадцать шесть копеек') 94 | self.assertEqual( 95 | decimal2text( 96 | '102.2450', 97 | places=4, 98 | int_units=int_units, 99 | exp_units=exp_units), 100 | u'сто два рубля две тысячи четыреста пятьдесят копеек') # xD 101 | self.assertEqual( 102 | decimal2text( 103 | '111', 104 | int_units=int_units, 105 | exp_units=exp_units), 106 | u'сто одиннадцать рублей ноль копеек') 107 | self.assertEqual( 108 | decimal2text( 109 | '3000.00', 110 | int_units=int_units, 111 | exp_units=exp_units), 112 | u'три тысячи рублей ноль копеек') 113 | 114 | def test_negative(self): 115 | self.assertEqual(num2text(-12345), 116 | u"минус двенадцать тысяч триста сорок пять") 117 | self.assertEqual( 118 | decimal2text('-123.45'), 119 | u'минус сто двадцать три сорок пять') 120 | 121 | if __name__ == '__main__': 122 | import sys 123 | if len(sys.argv) > 1: 124 | try: 125 | num = sys.argv[1] 126 | if '.' in num: 127 | print(decimal2text( 128 | num, 129 | int_units=((u'штука', u'штуки', u'штук'), 'f'), 130 | exp_units=((u'кусок', u'куска', u'кусков'), 'm'))) 131 | else: 132 | print(num2text( 133 | int(num), 134 | main_units=((u'штука', u'штуки', u'штук'), 'f'))) 135 | except ValueError: 136 | print (sys.stderr, "Invalid argument {}".format(sys.argv[1])) 137 | sys.exit() 138 | unittest.main() 139 | --------------------------------------------------------------------------------