diff --git a/README.rst b/README.rst index c24bb099..e8102f76 100644 --- a/README.rst +++ b/README.rst @@ -79,6 +79,7 @@ Besides the numerical argument, there are two main optional arguments. * ``am`` (Amharic) * ``ar`` (Arabic) * ``az`` (Azerbaijani) +* ``by`` (Belarusian) * ``cz`` (Czech) * ``de`` (German) * ``dk`` (Danish) diff --git a/bin/num2words b/bin/num2words index 06ff0443..359d1bdb 100755 --- a/bin/num2words +++ b/bin/num2words @@ -55,7 +55,7 @@ import sys from docopt import docopt import num2words -__version__ = "0.5.12" +__version__ = "0.5.13" __license__ = "LGPL" diff --git a/num2words/__init__.py b/num2words/__init__.py index 6aa20d2d..87918622 100644 --- a/num2words/__init__.py +++ b/num2words/__init__.py @@ -17,7 +17,7 @@ from __future__ import unicode_literals -from . import (lang_AM, lang_AR, lang_AZ, lang_CZ, lang_DE, lang_DK, lang_EN, +from . import (lang_AM, lang_AR, lang_AZ, lang_BY, lang_CZ, lang_DE, lang_DK, lang_EN, lang_EN_IN, lang_EO, lang_ES, lang_ES_CO, lang_ES_NI, lang_ES_VE, lang_FA, lang_FI, lang_FR, lang_FR_BE, lang_FR_CH, lang_FR_DZ, lang_HE, lang_HU, lang_ID, lang_IS, lang_IT, @@ -30,6 +30,7 @@ 'am': lang_AM.Num2Word_AM(), 'ar': lang_AR.Num2Word_AR(), 'az': lang_AZ.Num2Word_AZ(), + 'by': lang_BY.Num2Word_BY(), 'cz': lang_CZ.Num2Word_CZ(), 'en': lang_EN.Num2Word_EN(), 'en_IN': lang_EN_IN.Num2Word_EN_IN(), diff --git a/num2words/lang_BY.py b/num2words/lang_BY.py new file mode 100644 index 00000000..26435202 --- /dev/null +++ b/num2words/lang_BY.py @@ -0,0 +1,328 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2003, Taro Ogawa. All Rights Reserved. +# Copyright (c) 2013, Savoir-faire Linux inc. All Rights Reserved. +# Copyright (c) 2022, Sergei Ruzki. 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 + +ZERO = 'нуль' + +ONES_FEMININE = { + 1: 'адна', + 2: 'дзве', + 3: 'тры', + 4: 'чатыры', + 5: 'пяць', + 6: 'шэсць', + 7: 'сем', + 8: 'восем', + 9: 'дзевяць', +} + +ONES = { + 'f': { + 1: 'адна', + 2: 'дзве', + 3: 'тры', + 4: 'чатыры', + 5: 'пяць', + 6: 'шэсць', + 7: 'сем', + 8: 'восем', + 9: 'дзевяць', + }, + 'm': { + 1: 'адзін', + 2: 'два', + 3: 'тры', + 4: 'чатыры', + 5: 'пяць', + 6: 'шэсць', + 7: 'сем', + 8: 'восем', + 9: 'дзевяць', + }, + 'n': { + 1: 'адно', + 2: 'два', + 3: 'тры', + 4: 'чатыры', + 5: 'пяць', + 6: 'шэсць', + 7: 'сем', + 8: 'восем', + 9: 'дзевяць', + } +} + +TENS = { + 0: 'дзесяць', + 1: 'адзінаццаць', + 2: 'дванаццаць', + 3: 'трынаццаць', + 4: 'чатырнаццаць', + 5: 'пятнаццаць', + 6: 'шастнаццаць', + 7: 'семнаццаць', + 8: 'васямнаццаць', + 9: 'дзевятнаццаць', +} + +TWENTIES = { + 2: 'дваццаць', + 3: 'трыццаць', + 4: 'сорак', + 5: 'пяцьдзясят', + 6: 'шэсцьдзясят', + 7: 'семдзесят', + 8: 'восемдзесят', + 9: 'дзевяноста', +} + +HUNDREDS = { + 1: 'сто', + 2: 'дзвесце', + 3: 'трыста', + 4: 'чатырыста', + 5: 'пяцьсот', + 6: 'шэсцьсот', + 7: 'семсот', + 8: 'восемсот', + 9: 'дзевяцьсот', +} + +THOUSANDS = { + 1: ('тысяча', 'тысячы', 'тысяч'), # 10^3 + 2: ('мільён', 'мільёны', 'мільёнаў'), # 10^6 + 3: ('мільярд', 'мільярды', 'мільярдаў'), # 10^9 + 4: ('трыльён', 'трыльёны', 'трыльёнаў'), # 10^12 + 5: ('квадрыльён', 'квадрыльёны', 'квадрыльёнаў'), # 10^15 + 6: ('квінтыльён', 'квінтыльёны', 'квінтыльёнаў'), # 10^18 + 7: ('секстыльён', 'секстыльёны', 'секстыльёнаў'), # 10^21 + 8: ('сэптыльён', 'сэптыльёны', 'сэптыльёнаў'), # 10^24 + 9: ('актыльён', 'актыльёны', 'актыльёнаў'), # 10^27 + 10: ('нанільён', 'нанільёны', 'нанільёнаў'), # 10^30 +} + + +class Num2Word_BY(Num2Word_Base): + CURRENCY_FORMS = { + 'RUB': ( + ('расійскі рубель', 'расійскія рублі', 'расійскіх рублёў'), ('капейка', 'капейкі', 'капеек') + ), + 'EUR': ( + ('эўра', 'эўра', 'эўра'), ('цэнт', 'цэнты', 'цэнтаў') + ), + 'USD': ( + ('долар', 'долары', 'долараў'), ('цэнт', 'цэнты', 'цэнтаў') + ), + 'UAH': ( + ('грыўна', 'грыўны', 'грыўнаў'), ('капейка', 'капейкі', 'капеек') + ), + 'KZT': ( + ('тэнге', 'тэнге', 'тэнге'), ('тыйін', 'тыйіны', 'тыйінаў') + ), + 'BYN': ( + ('беларускі рубель', 'беларускія рублі', 'беларускіх рублёў'), + ('капейка', 'капейкі', 'капеек') + ), + 'UZS': ( + ('сум', 'сума', 'сумаў'), ('тыйін', 'тыйіны', 'тыйінаў') + ), + } + + def setup(self): + self.negword = 'мінус' + self.pointword = 'коска' + self.ords = {'нуль': 'нулявы', + 'адзін': 'першы', + 'два': 'другі', + 'тры': 'трэці', + 'чатыры': 'чацьвёрты', + 'пяць': 'пяты', + 'шесць': 'шасты', + 'сем': 'сёмы', + 'восем': 'восьмы', + 'девяць': 'дзявяты', + 'сто': 'соты', + 'тысяча': 'тысячны'} + + self.ords_adjective = { + 'адзін': 'адна', + 'адна': 'адна', + 'дзве': 'двух', + 'тры': 'трох', + 'чатыры': 'четырох', + 'пяць': 'пяці', + 'шесць': 'шасці', + 'сем': 'сямі', + 'восем': 'васьмі', + 'дзевяць': 'дзевяті', + 'сто': 'ста'} + + def to_cardinal(self, number, gender='m'): + n = str(number).replace(',', '.') + if '.' in n: + left, right = n.split('.') + leading_zero_count = len(right) - len(right.lstrip('0')) + decimal_part = ((ZERO + ' ') * leading_zero_count + + self._int2word(int(right), gender)) + return u'%s %s %s' % ( + self._int2word(int(left), gender), + self.pointword, + decimal_part + ) + else: + return self._int2word(int(n), gender) + + def pluralize(self, n, forms): + if n % 100 < 10 or n % 100 > 20: + if n % 10 == 1: + form = 0 + elif 5 > n % 10 > 1: + form = 1 + else: + form = 2 + else: + form = 2 + return forms[form] + + def to_ordinal(self, number, gender='m'): + self.verify_ordinal(number) + outwords = self.to_cardinal(number, gender).split(' ') + lastword = outwords[-1].lower() + try: + if len(outwords) > 1: + if outwords[-2] in self.ords_adjective: + outwords[-2] = self.ords_adjective.get( + outwords[-2], outwords[-2]) + elif outwords[-2] == 'дзесяць': + outwords[-2] = outwords[-2][:-1] + 'і' + if len(outwords) == 3: + if outwords[-3] in ['адзін', 'адна']: + outwords[-3] = '' + lastword = self.ords[lastword] + except KeyError: + if lastword[:-3] in self.ords_adjective: + lastword = self.ords_adjective.get( + lastword[:-3], lastword) + 'соты' + elif lastword[-5:] == 'шэсць': + lastword = 'шосты' + elif lastword[-7:] == 'дзесяць': + lastword = 'дзясяты' + elif lastword[-9:] == 'семдзесят': + lastword = 'сямідзясяты' + elif lastword[-1] == 'ь' or lastword[-2] == 'ц': + lastword = lastword[:-2] + 'ты' + elif lastword[-1] == 'к': + lastword = lastword.replace('о', 'а') + 'авы' + + elif lastword[-2] == 'ч' or lastword[-1] == 'ч': + if lastword[-2] == 'ч': + lastword = lastword[:-1] + 'ны' + if lastword[-1] == 'ч': + lastword = lastword + 'ны' + + if 'дву' in lastword[-2]: + lastword[-2].replace('дву', 'дзву') + + elif lastword[-1] == 'н' or lastword[-2] == 'н': + lastword = lastword[:lastword.rfind('н') + 1] + 'ны' + elif lastword[-1] == 'д' or lastword[-2] == 'д': + lastword = lastword[:lastword.rfind('д') + 1] + 'ны' + + if gender == 'f': + if lastword[-1:] in ['i', 'ы']: + lastword = lastword[:-2] + 'ая' + else: + lastword = lastword[:-2] + 'ая' + if gender == 'n': + if lastword[-2:] == 'ий': + lastword = lastword[:-2] + 'ье' + else: + lastword = lastword[:-2] + 'ое' + + outwords[-1] = self.title(lastword) + if len(outwords) == 2 and 'адна' in outwords[-2]: + outwords[-2] = outwords[-1] + del outwords[-1] + + if len(outwords) > 1 and 'тысяч' in outwords[-1]: + outwords[-2] = outwords[-2] + outwords[-1] + del outwords[-1] + + return ' '.join(outwords).strip() + + def _money_verbose(self, number, currency): + gender = 'm' + if currency == 'UAH': + gender = 'f' + + return self._int2word(number, gender) + + def _cents_verbose(self, number, currency): + if currency in ('UAH', 'RUB', 'BYN'): + gender = 'f' + else: + gender = 'm' + + return self._int2word(number, gender) + + def _int2word(self, n, gender='m'): + if isinstance(gender, bool) and gender: + gender = 'f' + if n < 0: + return ' '.join([self.negword, self._int2word(abs(n), gender)]) + + if n == 0: + return ZERO + + words = [] + chunks = list(splitbyx(str(n), 3)) + i = len(chunks) + for x in chunks: + i -= 1 + + if x == 0: + continue + + n1, n2, n3 = get_digits(x) + + if n3 > 0: + words.append(HUNDREDS[n3]) + + if n2 > 1: + words.append(TWENTIES[n2]) + + if n2 == 1: + words.append(TENS[n1]) + elif n1 > 0: + if i == 0: + ones = ONES[gender] + elif i == 1: + ones = ONES['f'] # Thousands are feminine + else: + ones = ONES['m'] + + words.append(ones[n1]) + + if i > 0: + words.append(self.pluralize(x, THOUSANDS[i])) + + return ' '.join(words) diff --git a/tests/test_by.py b/tests/test_by.py new file mode 100644 index 00000000..1442c60d --- /dev/null +++ b/tests/test_by.py @@ -0,0 +1,317 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2003, Taro Ogawa. All Rights Reserved. +# Copyright (c) 2013, Savoir-faire Linux inc. All Rights Reserved. +# Copyright (c) 2023, Sergei Ruzki/Ivan Shakh 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 unittest import TestCase + +from num2words import num2words + + +class Num2WordsBYTest(TestCase): + + def test_cardinal(self): + self.assertEqual(num2words(100, lang='by'), "сто") + self.assertEqual(num2words(101, lang='by'), "сто адзін") + self.assertEqual(num2words(110, lang='by'), "сто дзесяць") + self.assertEqual(num2words(115, lang='by'), "сто пятнаццаць") + self.assertEqual(num2words(123, lang='by'), "сто дваццаць тры") + self.assertEqual(num2words(1000, lang='by'), "адна тысяча") + self.assertEqual(num2words(1001, lang='by'), "адна тысяча адзін") + self.assertEqual(num2words(2012, lang='by'), "дзве тысячы дванаццаць") + self.assertEqual( + num2words(12519.85, lang='by'), + "дванаццаць тысяч пяцьсот дзевятнаццаць коска восемдзесят пяць") + self.assertEqual( + num2words(1234567890, lang='by'), + "адзін мільярд дзвесце трыццаць чатыры мільёны пяцьсот " + "шэсцьдзясят сем тысяч восемсот дзевяноста") + self.assertEqual( + num2words(461407892039002157189883901676, lang='by'), + "чатырыста шэсцьдзясят адзін " + "актыльён чатырыста сем сэптыльёнаў восемсот дзевяноста " + "два секстыльёны трыццаць дзевяць квінтыльёнаў два квадрыльёны " + "сто пяцьдзясят сем трыльёнаў сто восемдзесят дзевяць мільярдаў " + "восемсот восемдзесят тры мільёны дзевяцьсот адна тысяча " + "шэсцьсот семдзесят шэсць") + self.assertEqual( + num2words(94234693663034822824384220291, lang='by'), + "дзевяноста чатыры актыльёны " + "дзвесце трыццаць чатыры сэптыльёны шэсцьсот дзевяноста тры " + "секстыльёны шэсцьсот шэсцьдзясят тры квінтыльёны трыццаць " + "чатыры квадрыльёны восемсот дваццаць два трыльёны восемсот " + "дваццаць чатыры мільярды трыста восемдзесят чатыры мільёны " + "дзвесце дваццаць тысяч дзвесце дзевяноста адзін") + self.assertEqual(num2words(5, lang='by'), "пяць") + self.assertEqual(num2words(15, lang='by'), "пятнаццаць") + self.assertEqual(num2words(154, lang='by'), "сто пяцьдзясят чатыры") + self.assertEqual( + num2words(1135, lang='by'), "адна тысяча сто трыццаць пяць" + ) + self.assertEqual( + num2words(418531, lang='by'), + "чатырыста васямнаццаць тысяч пяцьсот трыццаць адзін" + ) + self.assertEqual( + num2words(1000139, lang='by'), "адзін мільён сто трыццаць дзевяць" + ) + self.assertEqual(num2words(-1, lang='by'), "мінус адзін") + self.assertEqual(num2words(-15, lang='by'), "мінус пятнаццаць") + self.assertEqual(num2words(-100, lang='by'), "мінус сто") + + def test_floating_point(self): + self.assertEqual(num2words(5.2, lang='by'), "пяць коска два") + self.assertEqual( + num2words(10.02, lang='by'), + "дзесяць коска нуль два" + ) + self.assertEqual( + num2words(15.007, lang='by'), + "пятнаццаць коска нуль нуль сем" + ) + self.assertEqual( + num2words(561.42, lang='by'), + "пяцьсот шэсцьдзясят адзін коска сорак два" + ) + + def test_to_ordinal(self): + self.assertEqual( + num2words(1, lang='by', to='ordinal'), + 'першы' + ) + self.assertEqual( + num2words(5, lang='by', to='ordinal'), + 'пяты' + ) + self.assertEqual( + num2words(10, lang='by', to='ordinal'), + 'дзясяты' + ) + + self.assertEqual( + num2words(13, lang='by', to='ordinal'), + 'трынаццаты' + ) + self.assertEqual( + num2words(20, lang='by', to='ordinal'), + 'дваццаты' + ) + self.assertEqual( + num2words(23, lang='by', to='ordinal'), + 'дваццаць трэці' + ) + self.assertEqual( + num2words(40, lang='by', to='ordinal'), + 'саракавы' + ) + self.assertEqual( + num2words(61, lang='by', to='ordinal'), + 'шэсцьдзясят першы' + ) + self.assertEqual( + num2words(70, lang='by', to='ordinal'), + 'сямідзясяты' + ) + self.assertEqual( + num2words(100, lang='by', to='ordinal'), + 'соты' + ) + self.assertEqual( + num2words(136, lang='by', to='ordinal'), + 'сто трыццаць шосты' + ) + self.assertEqual( + num2words(500, lang='by', to='ordinal'), + 'пяцісоты' + ) + self.assertEqual( + num2words(1000, lang='by', to='ordinal'), + 'тысячны' + ) + self.assertEqual( + num2words(1001, lang='by', to='ordinal'), + 'тысяча першы' + ) + self.assertEqual( + num2words(2000, lang='by', to='ordinal'), + 'двухтысячны' + ) + self.assertEqual( + num2words(10000, lang='by', to='ordinal'), + 'дзесяцітысячны' + ) + self.assertEqual( + num2words(1000000, lang='by', to='ordinal'), + 'мільённы' + ) + self.assertEqual( + num2words(1000000000, lang='by', to='ordinal'), + 'мільярдны' + ) + + def test_to_currency(self): + self.assertEqual( + num2words(1.0, lang='by', to='currency', currency='EUR'), + 'адзін эўра, нуль цэнтаў' + ) + self.assertEqual( + num2words(1.0, lang='by', to='currency', currency='RUB'), + 'адзін расійскі рубель, нуль капеек' + ) + self.assertEqual( + num2words(1.0, lang='by', to='currency', currency='BYN'), + 'адзін беларускі рубель, нуль капеек' + ) + self.assertEqual( + num2words(1.0, lang='by', to='currency', currency='UAH'), + 'адна грыўна, нуль капеек' + ) + self.assertEqual( + num2words(1234.56, lang='by', to='currency', currency='EUR'), + 'адна тысяча дзвесце трыццаць чатыры эўра, пяцьдзясят шэсць цэнтаў' + ) + self.assertEqual( + num2words(1234.56, lang='by', to='currency', currency='RUB'), + 'адна тысяча дзвесце трыццаць чатыры расійскія рублі, пяцьдзясят шэсць капеек' + ) + self.assertEqual( + num2words(1234.56, lang='by', to='currency', currency='BYN'), + 'адна тысяча дзвесце трыццаць чатыры беларускія рублі, пяцьдзясят шэсць капеек' + ) + self.assertEqual( + num2words(1234.56, lang='by', to='currency', currency='UAH'), + 'адна тысяча дзвесце трыццаць чатыры грыўны, пяцьдзясят шэсць капеек' + ) + self.assertEqual( + num2words(10111, lang='by', to='currency', currency='EUR', + separator=' і'), + 'сто адзін эўра і адзінаццаць цэнтаў' + ) + self.assertEqual( + num2words(10111, lang='by', to='currency', currency='RUB', + separator=' і'), + 'сто адзін расійскі рубель і адзінаццаць капеек' + ) + self.assertEqual( + num2words(10111, lang='by', to='currency', currency='BYN', + separator=' і'), + 'сто адзін беларускі рубель і адзінаццаць капеек' + ) + self.assertEqual( + num2words(10111, lang='by', to='currency', currency='UAH', + separator=' і'), + 'сто адна грыўна і адзінаццаць капеек' + ) + self.assertEqual( + num2words(10121, lang='by', to='currency', currency='EUR', + separator=' і'), + 'сто адзін эўра і дваццаць адзін цэнт' + ) + self.assertEqual( + num2words(10121, lang='by', to='currency', currency='RUB', + separator=' і'), + 'сто адзін расійскі рубель і дваццаць адна капейка' + ) + self.assertEqual( + num2words(10121, lang='by', to='currency', currency='BYN', + separator=' і'), + 'сто адзін беларускі рубель і дваццаць адна капейка' + ) + self.assertEqual( + num2words(10121, lang='by', to='currency', currency='UAH', + separator=' і'), + 'сто адна грыўна і дваццаць адна капейка' + ) + self.assertEqual( + num2words(10122, lang='by', to='currency', currency='EUR', + separator=' і'), + 'сто адзін эўра і дваццаць два цэнты' + ) + self.assertEqual( + num2words(10122, lang='by', to='currency', currency='RUB', + separator=' і'), + 'сто адзін расійскі рубель і дваццаць дзве капейкі' + ) + self.assertEqual( + num2words(10122, lang='by', to='currency', currency='BYN', + separator=' і'), + 'сто адзін беларускі рубель і дваццаць дзве капейкі' + ) + self.assertEqual( + num2words(10122, lang='by', to='currency', currency='UAH', + separator=' і'), + 'сто адна грыўна і дваццаць дзве капейкі' + ) + self.assertEqual( + num2words(10122, lang='by', to='currency', currency='KZT', + separator=' і'), + 'сто адзін тэнге і дваццаць два тыйіны' + ) + self.assertEqual( + num2words(-1251985, lang='by', to='currency', currency='EUR', + cents=False), + 'мінус дванаццаць тысяч пяцьсот дзевятнаццаць эўра, 85 цэнтаў' + ) + self.assertEqual( + num2words(-1251985, lang='by', to='currency', currency='RUB', + cents=False), + 'мінус дванаццаць тысяч пяцьсот дзевятнаццаць расійскіх рублёў, 85 капеек' + ) + self.assertEqual( + num2words(-1251985, lang='by', to='currency', currency='BYN', + cents=False), + 'мінус дванаццаць тысяч пяцьсот дзевятнаццаць беларускіх рублёў, 85 капеек' + ) + self.assertEqual( + num2words(-1251985, lang='by', to='currency', currency='UAH', + cents=False), + 'мінус дванаццаць тысяч пяцьсот дзевятнаццаць грыўнаў, 85 капеек' + ) + self.assertEqual( + num2words('38.4', lang='by', to='currency', separator=' і', + cents=False, currency='EUR'), + "трыццаць восем эўра і 40 цэнтаў" + ) + self.assertEqual( + num2words('38.4', lang='by', to='currency', separator=' і', + cents=False, currency='RUB'), + "трыццаць восем расійскіх рублёў і 40 капеек" + ) + self.assertEqual( + num2words('38.4', lang='by', to='currency', separator=' і', + cents=False, currency='UAH'), + "трыццаць восем грыўнаў і 40 капеек" + ) + self.assertEqual( + num2words('1230.56', lang='by', to='currency', currency='USD'), + 'адна тысяча дзвесце трыццаць долараў, пяцьдзясят шэсць цэнтаў' + ) + self.assertEqual( + num2words('1231.56', lang='by', to='currency', currency='USD'), + 'адна тысяча дзвесце трыццаць адзін долар, пяцьдзясят шэсць цэнтаў' + ) + self.assertEqual( + num2words('1234.56', lang='by', to='currency', currency='USD'), + 'адна тысяча дзвесце трыццаць чатыры долары, пяцьдзясят шэсць ' + 'цэнтаў' + ) + self.assertEqual( + num2words(10122, lang='by', to='currency', currency='UZS', + separator=' і'), + 'сто адзін сум і дваццаць два тыйіны' + )