diff --git a/.coverage b/.coverage new file mode 100644 index 00000000..8d9df246 Binary files /dev/null and b/.coverage differ diff --git a/README.rst b/README.rst index 9b1e97f6..6beb1f0f 100644 --- a/README.rst +++ b/README.rst @@ -109,6 +109,8 @@ Besides the numerical argument, there are two main optional arguments, ``to:`` a * ``kn`` (Kannada) * ``ko`` (Korean) * ``kz`` (Kazakh) +* ``ckb`` (Central Kurdish) +* ``ku`` (Kurdish) * ``lt`` (Lithuanian) * ``lv`` (Latvian) * ``no`` (Norwegian) diff --git a/num2words/__init__.py b/num2words/__init__.py index 95dbcd7a..394eb2c6 100644 --- a/num2words/__init__.py +++ b/num2words/__init__.py @@ -25,7 +25,7 @@ lang_JA, lang_KN, lang_KO, lang_KZ, lang_LT, lang_LV, lang_NL, lang_NO, lang_PL, lang_PT, lang_PT_BR, lang_RO, lang_RU, lang_SK, lang_SL, lang_SR, lang_SV, lang_TE, lang_TG, lang_TH, - lang_TR, lang_UK, lang_VI) + lang_TR, lang_UK, lang_VI, lang_CKB, lang_KU) CONVERTER_CLASSES = { 'am': lang_AM.Num2Word_AM(), @@ -57,6 +57,8 @@ 'kn': lang_KN.Num2Word_KN(), 'ko': lang_KO.Num2Word_KO(), 'kz': lang_KZ.Num2Word_KZ(), + 'ckb': lang_CKB.Num2Word_CKB(), + 'ku': lang_KU.Num2Word_KU(), 'lt': lang_LT.Num2Word_LT(), 'lv': lang_LV.Num2Word_LV(), 'pl': lang_PL.Num2Word_PL(), diff --git a/num2words/lang_CKB.py b/num2words/lang_CKB.py new file mode 100644 index 00000000..065f847c --- /dev/null +++ b/num2words/lang_CKB.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024, Karwan Khalid. 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 decimal import Decimal +from math import floor +from typing import Union + +class Num2Word_CKB(object): + + SORANI_ONES = [ + "", "یه‌ک", "دوو", "سێ", "چوار", "پێنج", "شەش", "حەوت", "هەشت", + "نۆ", + "ده", + "یانزدە", "دوازدە", "سیانزدە", "چواردە", "پانزدە", + "شانزدە", "حەڤدە", "هەژدە", "نۆزدە", + ] + + SORANI_TENS = [ + "", + "ده", + "بیست", + "سی", + "چل", + "پەنجا", + "شەست", + "حەفتا", + "هەشتا", + "نەوەد", + ] + + SORANI_HUNDREDS = [ + "", + "سەد", + "دوو سەد", + "سێ سەد", + "چوار سەد", + "پێنج سەد", + "شەش سەد", + "حەفت سەد", + "هەشت سەد", + "نۆ سەد", + ] + + SORANI_BIG = [ + '', + ' هەزار', + ' میلیۆن', + " میلیار", + ' تریلیۆن', + " تریلیارد", + ] + + SORANI_FRAC = ["", "دەیەم", "سەدەم"] + SORANI_FRAC_BIG = ["", "هەزارم", "میلیۆنیم", "میلیاردیم"] + + SORANI_SEPERATOR = ' و ' + + MAXNUM = 10 ** 36 + + @staticmethod + def float2tuple(value: Union[int, float, Decimal]) -> tuple[int, int, int]: + if value is None: + raise ValueError("Value cannot be None") + pre = int(value) + + # Simple way of finding decimal places to update the precision + precision = abs(Decimal(str(value)).as_tuple().exponent) + + post = abs(value - pre) * 10**precision + if abs(round(post) - post) < 0.01: + post = int(round(post)) + else: + post = int(floor(post)) + return pre, post, precision + + @staticmethod + def cardinal3(number: int) -> str: + if number < 0: + raise ValueError("Number cannot be negative") + if number <= 19: + return Num2Word_CKB.SORANI_ONES[number] + if number < 100: + x, y = divmod(number, 10) + if y == 0: + return Num2Word_CKB.SORANI_TENS[x] + return Num2Word_CKB.SORANI_TENS[x] + Num2Word_CKB.SORANI_SEPERATOR + Num2Word_CKB.SORANI_ONES[y] + x, y = divmod(number, 100) + if y == 0: + return Num2Word_CKB.SORANI_HUNDREDS[x] + return Num2Word_CKB.SORANI_HUNDREDS[x] + Num2Word_CKB.SORANI_SEPERATOR + Num2Word_CKB.cardinal3(y) + + @staticmethod + def cardinalPos(number: int) -> str: + if number < 0: + raise ValueError("Number cannot be negative") + x = number + res = '' + for b in Num2Word_CKB.SORANI_BIG: + x, y = divmod(x, 1000) + if y == 0: + continue + yx = Num2Word_CKB.cardinal3(y) + b + if b == ' هەزار' and y == 1: + yx = 'هەزار' + if res == '': + res = yx + else: + res = yx + Num2Word_CKB.SORANI_SEPERATOR + res + return res + + @staticmethod + def fractional(number: int, level: int) -> str: + if number < 0: + raise ValueError("Number cannot be negative") + x = Num2Word_CKB.cardinalPos(number) + ld3, lm3 = divmod(level, 3) + ltext = (Num2Word_CKB.SORANI_FRAC[lm3] + " " + Num2Word_CKB.SORANI_FRAC_BIG[ld3]).strip() + return x + " " + ltext + + @staticmethod + def to_currency(value: Union[int, float, Decimal]) -> str: + if value is None: + raise ValueError("Value cannot be None") + return Num2Word_CKB.to_cardinal(value) + " دینار" + + @staticmethod + def to_ordinal(number: int) -> str: + if number < 0: + raise ValueError("Number cannot be negative") + if number == 0: + return "سفر" + r = Num2Word_CKB.to_cardinal(number) + if r[-1] == 'ک': + return r + 'ەم' + return r + 'یەم' + + @staticmethod + def to_year(value: Union[int, float, Decimal]) -> str: + if value is None: + raise ValueError("Value cannot be None") + return 'ساڵی ' + Num2Word_CKB.to_cardinal(value) + + @staticmethod + def to_ordinal_num(value: int) -> str: + if value is None: + raise ValueError("Value cannot be None") + return str(value)+"یەم" + def _int2word(number): + if number is None: + raise ValueError("Value cannot be None") + if number < 0: + return "سالب " + Num2Word_CKB.to_cardinal(-number) + if number == 0: + return "سفر" + x, y, level = Num2Word_CKB.float2tuple(number) + if y == 0: + return Num2Word_CKB.cardinalPos(x) + if x == 0: + return Num2Word_CKB.fractional(y, level) + return Num2Word_CKB.cardinalPos(x) + Num2Word_CKB.SEPARATOR + Num2Word_CKB.fractional(y, level) + + @staticmethod + def to_cardinal(number): + n = str(number).replace(',', '.') + if '.' in n: + left, right = n.split('.') + leading_zero_count = len(right) - len(right.lstrip('0')) + decimal_part = (("سفر" + ' ') * leading_zero_count + + Num2Word_CKB._int2word(int(right))) + return '%s%s %s' % ( + Num2Word_CKB._int2word(int(left)), + " پۆینت", + decimal_part + ) + else: + return "%s" % ( Num2Word_CKB._int2word(int(n))) + + + diff --git a/num2words/lang_KU.py b/num2words/lang_KU.py new file mode 100644 index 00000000..8f193309 --- /dev/null +++ b/num2words/lang_KU.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024, Karwan Khalid. 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 decimal import Decimal +from math import floor +from typing import Union + +class Num2Word_KU(object): + + ONES = [ + "", "Yek", "du", "sê", "çar", "pênc", "şeş", "heft", "heşt", + "neh", "deh", "yanzdeh", "dwanzdeh", "sêzdeh", "çardeh", "panzdeh", + "şanzdeh", "hivdeh", "hijdeh", "nozdeh", + ] + + TENS = [ + "", "deh", "bîst", "sî", "çil", "pêncî", "şêst", "heftê", "heştê", + "nod", + ] + + HUNDREDS = [ + "", "sed", "du sed", "sê sed", "çar sed", "pênc sed", "şeş sed", + "heft sed", "heşt sed", "neh sed", + ] + + BIG = [ + '', ' hezar', ' milyon', " milyar", ' trilyion', " trlitar", + ] + + FRAC = ["", "dehem", "sedhem"] + FRAC_BIG = ["", "hezarem", "milyonem", "milyarem"] + + SEPARATOR = ' û ' + + MAXNUM = 10 ** 36 + + @staticmethod + def float2tuple(value: Union[int, float, Decimal]) -> tuple[int, int, int]: + if value is None: + raise ValueError("Value cannot be None") + pre = int(value) + # Simple way of finding decimal places to update the precision + precision = abs(Decimal(str(value)).as_tuple().exponent) + post = abs(value - pre) * 10**precision + if abs(round(post) - post) < 0.01: + post = int(round(post)) + else: + post = int(floor(post)) + return pre, post, precision + + @staticmethod + def cardinal3(number: int) -> str: + if number < 0: + raise ValueError("Number cannot be negative") + if number <= 19: + return Num2Word_KU.ONES[number] + if number < 100: + x, y = divmod(number, 10) + if y == 0: + return Num2Word_KU.TENS[x] + return Num2Word_KU.TENS[x] + Num2Word_KU.SEPARATOR + Num2Word_KU.ONES[y] + x, y = divmod(number, 100) + if y == 0: + return Num2Word_KU.HUNDREDS[x] + return Num2Word_KU.HUNDREDS[x] + Num2Word_KU.SEPARATOR + Num2Word_KU.cardinal3(y) + + @staticmethod + def cardinal_pos(number: int) -> str: + if number < 0: + raise ValueError("Number cannot be negative") + x = number + res = '' + for b in Num2Word_KU.BIG: + x, y = divmod(x, 1000) + if y == 0: + continue + yx = Num2Word_KU.cardinal3(y) + b + if b == ' hezar' and y == 1: + yx = 'hezar' + if res == '': + res = yx + else: + res = yx + Num2Word_KU.SEPARATOR + res + return res + + @staticmethod + def fractional(number: int, level: int) -> str: + if number < 0: + raise ValueError("Number cannot be negative") + x = Num2Word_KU.cardinal_pos(number) + ld3, lm3 = divmod(level, 3) + ltext = (Num2Word_KU.FRAC[lm3] + " " + Num2Word_KU.FRAC_BIG[ld3]).strip() + return x + " " + ltext + + @staticmethod + def to_currency(value: Union[int, float, Decimal]) -> str: + if value is None: + raise ValueError("Value cannot be None") + return Num2Word_KU.to_cardinal(value) + " lira" + + @staticmethod + def to_ordinal(number: int) -> str: + if number < 0: + raise ValueError("Number cannot be negative") + r = Num2Word_KU.to_cardinal(number) + if r[-1] == 'e' and r[-2] == 's': + return r[:-1] + 'wm' + return r + 'yem' + + @staticmethod + def to_year(value: Union[int, float, Decimal]) -> str: + if value is None: + raise ValueError("Value cannot be None") + return 'sala ' + Num2Word_KU.to_cardinal(value) + + @staticmethod + def to_ordinal_num(value: int) -> str: + if value is None: + raise ValueError("Value cannot be None") + return str(value)+"yem" + def _int2word(number): + if number is None: + raise ValueError("Value cannot be None") + if number < 0: + return "negatîf " + Num2Word_KU.to_cardinal(-number) + if number == 0: + return "sifir" + x, y, level = Num2Word_KU.float2tuple(number) + if y == 0: + return Num2Word_KU.cardinal_pos(x) + if x == 0: + return Num2Word_KU.fractional(y, level) + return Num2Word_KU.cardinal_pos(x) + Num2Word_KU.SEPARATOR + Num2Word_KU.fractional(y, level) + + @staticmethod + def to_cardinal(number): + n = str(number).replace(',', '.') + if '.' in n: + left, right = n.split('.') + leading_zero_count = len(right) - len(right.lstrip('0')) + decimal_part = (("sifir" + ' ') * leading_zero_count + + Num2Word_KU._int2word(int(right))) + return '%s%s %s' % ( + Num2Word_KU._int2word(int(left)), + " point", + decimal_part + ) + else: + return "%s" % ( Num2Word_KU._int2word(int(n))) + + + diff --git a/tests/test_ckb.py b/tests/test_ckb.py new file mode 100644 index 00000000..f4fafec9 --- /dev/null +++ b/tests/test_ckb.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024, Karwan Khalid. 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 unittest import TestCase + +from num2words import num2words + + +class Num2WordsCKBTest(TestCase): + def test_and_join_199(self): + self.assertEqual(num2words(199, lang='ckb'), "سەد و نەوەد و نۆ") + + def test_ordinal(self): + self.assertEqual( + num2words(0, lang='ckb', to='ordinal'), + 'سفر' + ) + self.assertEqual( + num2words(1, lang='ckb', to='ordinal'), + 'یه‌کەم' + ) + self.assertEqual( + num2words(13, lang='ckb', to='ordinal'), + 'سیانزدەیەم' + ) + self.assertEqual( + num2words(23, lang='ckb', to='ordinal'), + 'بیست و سێیەم' + ) + + def test_cardinal(self): + self.assertEqual(num2words(130000, lang='ckb'), "سەد و سی هەزار") + self.assertEqual(num2words(242, lang='ckb'), "دوو سەد و چل و دوو") + self.assertEqual(num2words(800, lang='ckb'), "هەشت سەد") + self.assertEqual(num2words(-203, lang='ckb'), "سالب دوو سەد و سێ") + + + def test_year(self): + self.assertEqual(num2words(1398, lang='ckb', to='year'), + "ساڵی هەزار و سێ سەد و نەوەد و هەشت") + self.assertEqual(num2words(1399, lang='ckb', to='year'), + "ساڵی هەزار و سێ سەد و نەوەد و نۆ") + self.assertEqual( + num2words(1400, lang='ckb', to='year'), "ساڵی هەزار و چوار سەد") + + def test_currency(self): + self.assertEqual( + num2words(1000, lang='ckb', to='currency'), 'هەزار دینار') + self.assertEqual( + num2words(1500000, lang='ckb', to='currency'), + 'یه‌ک میلیۆن و پێنج سەد هەزار دینار' + ) + + def test_ordinal_num(self): + self.assertEqual(num2words(10, lang='ckb', to='ordinal_num'), '10یەم') + self.assertEqual(num2words(21, lang='ckb', to='ordinal_num'), '21یەم') + self.assertEqual(num2words(102, lang='ckb', to='ordinal_num'), '102یەم') + self.assertEqual(num2words(73, lang='ckb', to='ordinal_num'), '73یەم') + + def test_cardinal_for_float_number(self): + self.assertEqual(num2words(12.5, lang='ckb'), "دوازدە پۆینت پێنج") + self.assertEqual(num2words(0.75, lang='ckb'), "سفر پۆینت حەفتا و پێنج") + self.assertEqual(num2words(12.51, lang='ckb'), + "دوازدە پۆینت پەنجا و یه‌ک") + + def test_overflow(self): + with self.assertRaises(OverflowError): + num2words("1000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000") diff --git a/tests/test_ku.py b/tests/test_ku.py new file mode 100644 index 00000000..07cafcad --- /dev/null +++ b/tests/test_ku.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024, Karwan Khalid. 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 unittest import TestCase + +from num2words import num2words + + +class Num2WordsKUTest(TestCase): + def test_and_join_199(self): + self.assertEqual(num2words(199, lang='ku'), "sed û nod û neh") + + def test_ordinal(self): + self.assertEqual( + num2words(0, lang='ku', to='ordinal'), + 'sifiryem' + ) + self.assertEqual( + num2words(1, lang='ku', to='ordinal'), + 'Yekyem' + ) + self.assertEqual( + num2words(13, lang='ku', to='ordinal'), + 'sêzdehyem' + ) + self.assertEqual( + num2words(23, lang='ku', to='ordinal'), + 'bîst û sêyem' + ) + + def test_cardinal(self): + self.assertEqual(num2words(130000, lang='ku'), "sed û sî hezar") + self.assertEqual(num2words(242, lang='ku'), "du sed û çil û du") + self.assertEqual(num2words(800, lang='ku'), "heşt sed") + self.assertEqual(num2words(-203, lang='ku'), "negatîf du sed û sê") + + + def test_year(self): + self.assertEqual(num2words(1398, lang='ku', to='year'), + "sala hezar û sê sed û nod û heşt") + self.assertEqual(num2words(1399, lang='ku', to='year'), + "sala hezar û sê sed û nod û neh") + self.assertEqual( + num2words(1400, lang='ku', to='year'), "sala hezar û çar sed") + + def test_currency(self): + self.assertEqual( + num2words(1000, lang='ku', to='currency'), 'hezar lira') + self.assertEqual( + num2words(1500000, lang='ku', to='currency'), + 'Yek milyon û pênc sed hezar lira' + ) + + def test_ordinal_num(self): + self.assertEqual(num2words(10, lang='ku', to='ordinal_num'), '10yem') + self.assertEqual(num2words(21, lang='ku', to='ordinal_num'), '21yem') + self.assertEqual(num2words(102, lang='ku', to='ordinal_num'), '102yem') + self.assertEqual(num2words(73, lang='ku', to='ordinal_num'), '73yem') + + def test_cardinal_for_float_number(self): + self.assertEqual(num2words(12.5, lang='ku'), "dwanzdeh point pênc") + self.assertEqual(num2words(0.75, lang='ku'), "sifir point heftê û pênc") + self.assertEqual(num2words(12.51, lang='ku'), + "dwanzdeh point pêncî û Yek") + + def test_overflow(self): + with self.assertRaises(OverflowError): + num2words("1000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000")