diff --git a/num2words/lang_AR.py b/num2words/lang_AR.py index a0027a83..7195dd47 100644 --- a/num2words/lang_AR.py +++ b/num2words/lang_AR.py @@ -17,10 +17,14 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301 USA +import decimal +import math import re from decimal import Decimal from math import floor +from .base import Num2Word_Base + CURRENCY_SR = [("ريال", "ريالان", "ريالات", "ريالاً"), ("هللة", "هللتان", "هللات", "هللة")] CURRENCY_EGP = [("جنيه", "جنيهان", "جنيهات", "جنيهاً"), @@ -37,11 +41,13 @@ ] -class Num2Word_AR(object): - errmsg_too_big = "Too large" - max_num = 10 ** 36 +class Num2Word_AR(Num2Word_Base): + errmsg_toobig = "abs(%s) must be less than %s." + MAXVAL = 10**51 def __init__(self): + super().__init__() + self.number = 0 self.arabicPrefixText = "" self.arabicSuffixText = "" @@ -75,26 +81,40 @@ def __init__(self): "", "مائة", "مئتان", "ثلاثمائة", "أربعمائة", "خمسمائة", "ستمائة", "سبعمائة", "ثمانمائة", "تسعمائة" ] + self.arabicAppendedTwos = [ "مئتا", "ألفا", "مليونا", "مليارا", "تريليونا", "كوادريليونا", - "كوينتليونا", "سكستيليونا" + "كوينتليونا", "سكستيليونا", "سبتيليونا", "أوكتيليونا ", + "نونيليونا", "ديسيليونا", "أندسيليونا", "دوديسيليونا", + "تريديسيليونا", "كوادريسيليونا", "كوينتينيليونا" ] self.arabicTwos = [ "مئتان", "ألفان", "مليونان", "ملياران", "تريليونان", - "كوادريليونان", "كوينتليونان", "سكستيليونان" + "كوادريليونان", "كوينتليونان", "سكستيليونان", "سبتيليونان", + "أوكتيليونان ", "نونيليونان ", "ديسيليونان", "أندسيليونان", + "دوديسيليونان", "تريديسيليونان", "كوادريسيليونان", "كوينتينيليونان" ] self.arabicGroup = [ "مائة", "ألف", "مليون", "مليار", "تريليون", "كوادريليون", - "كوينتليون", "سكستيليون" + "كوينتليون", "سكستيليون", "سبتيليون", "أوكتيليون", "نونيليون", + "ديسيليون", "أندسيليون", "دوديسيليون", "تريديسيليون", + "كوادريسيليون", "كوينتينيليون" ] self.arabicAppendedGroup = [ "", "ألفاً", "مليوناً", "ملياراً", "تريليوناً", "كوادريليوناً", - "كوينتليوناً", "سكستيليوناً" + "كوينتليوناً", "سكستيليوناً", "سبتيليوناً", "أوكتيليوناً", + "نونيليوناً", "ديسيليوناً", "أندسيليوناً", "دوديسيليوناً", + "تريديسيليوناً", "كوادريسيليوناً", "كوينتينيليوناً" ] self.arabicPluralGroups = [ "", "آلاف", "ملايين", "مليارات", "تريليونات", "كوادريليونات", - "كوينتليونات", "سكستيليونات" + "كوينتليونات", "سكستيليونات", "سبتيليونات", "أوكتيليونات", + "نونيليونات", "ديسيليونات", "أندسيليونات", "دوديسيليونات", + "تريديسيليونات", "كوادريسيليونات", "كوينتينيليونات" ] + assert len(self.arabicAppendedGroup) == len(self.arabicGroup) + assert len(self.arabicPluralGroups) == len(self.arabicGroup) + assert len(self.arabicAppendedTwos) == len(self.arabicTwos) def number_to_arabic(self, arabic_prefix_text, arabic_suffix_text): self.arabicPrefixText = arabic_prefix_text @@ -102,7 +122,6 @@ def number_to_arabic(self, arabic_prefix_text, arabic_suffix_text): self.extract_integer_and_decimal_parts() def extract_integer_and_decimal_parts(self): - re.split('\\.', str(self.number)) splits = re.split('\\.', str(self.number)) self.integer_value = int(splits[0]) @@ -129,8 +148,9 @@ def decimal_value(self, decimal_part): else: result = decimal_part - for i in range(len(result), self.partPrecision): - result += '0' + # The following is useless (never happens) + # for i in range(len(result), self.partPrecision): + # result += '0' return result def digit_feminine_status(self, digit, group_level): @@ -138,13 +158,13 @@ def digit_feminine_status(self, digit, group_level): if self.isCurrencyPartNameFeminine: return self.arabicFeminineOnes[int(digit)] else: + # Note: this never happens return self.arabicOnes[int(digit)] elif group_level == 0: if self.isCurrencyNameFeminine: return self.arabicFeminineOnes[int(digit)] else: return self.arabicOnes[int(digit)] - else: return self.arabicOnes[int(digit)] @@ -159,28 +179,37 @@ def process_arabic_group(self, group_number, group_level, ret_val = "{}".format(self.arabicAppendedTwos[0]) else: ret_val = "{}".format(self.arabicHundreds[int(hundreds)]) + if ret_val != "" and tens != 0: + ret_val += " و " if tens > 0: if tens < 20: + # if int(group_level) >= len(self.arabicTwos): + # raise OverflowError(self.errmsg_toobig % + # (self.number, self.MAXVAL)) + assert int(group_level) < len(self.arabicTwos) if tens == 2 and int(hundreds) == 0 and group_level > 0: - if self.integer_value in [2000, 2000000, 2000000000, - 2000000000000, 2000000000000000, - 2000000000000000000]: + pow = int(math.log10(self.integer_value)) + if self.integer_value > 10 and pow % 3 == 0 and \ + self.integer_value == 2 * (10 ** pow): ret_val = "{}".format( self.arabicAppendedTwos[int(group_level)]) else: ret_val = "{}".format( self.arabicTwos[int(group_level)]) else: - if ret_val != "": - ret_val += " و " if tens == 1 and group_level > 0 and hundreds == 0: + # Note: this never happens + # (hundreds == 0 only if group_number is 0) ret_val += "" elif (tens == 1 or tens == 2) and ( group_level == 0 or group_level == -1) and \ hundreds == 0 and remaining_number == 0: + # Note: this never happens (idem) ret_val += "" + elif tens == 1 and group_level > 0: + ret_val += self.arabicGroup[int(group_level)] else: ret_val += self.digit_feminine_status(int(tens), group_level) @@ -188,9 +217,6 @@ def process_arabic_group(self, group_number, group_level, ones = tens % 10 tens = (tens / 10) - 2 if ones > 0: - if ret_val != "" and tens < 4: - ret_val += " و " - ret_val += self.digit_feminine_status(ones, group_level) if ret_val != "" and ones != 0: ret_val += " و " @@ -199,8 +225,23 @@ def process_arabic_group(self, group_number, group_level, return ret_val + # We use this instead of built-in `abs` function, + # because `abs` suffers from loss of precision for big numbers + def abs(self, number): + return number if number >= 0 else -number + + # We use this instead of `"{:09d}".format(number)`, + # because the string conversion suffers from loss of + # precision for big numbers + def to_str(self, number): + integer = int(number) + if integer == number: + return str(integer) + decimal = round((number - integer) * 10**9) + return str(integer) + "." + "{:09d}".format(decimal).rstrip("0") + def convert(self, value): - self.number = "{:.9f}".format(value) + self.number = self.to_str(value) self.number_to_arabic(self.arabicPrefixText, self.arabicSuffixText) return self.convert_to_arabic() @@ -218,9 +259,16 @@ def convert_to_arabic(self): while temp_number > Decimal(0): - number_to_process = int( - Decimal(str(temp_number)) % Decimal(str(1000))) - temp_number = int(Decimal(temp_number) / Decimal(1000)) + temp_number_dec = Decimal(str(temp_number)) + try: + number_to_process = int(temp_number_dec % Decimal(str(1000))) + except decimal.InvalidOperation: + decimal.getcontext().prec = len( + temp_number_dec.as_tuple().digits + ) + number_to_process = int(temp_number_dec % Decimal(str(1000))) + + temp_number = int(temp_number_dec / Decimal(1000)) group_description = \ self.process_arabic_group(number_to_process, @@ -229,8 +277,13 @@ def convert_to_arabic(self): if group_description != '': if group > 0: if ret_val != "": - ret_val = "{} و {}".format("", ret_val) - if number_to_process != 2: + ret_val = "{}و {}".format("", ret_val) + if number_to_process != 2 and number_to_process != 1: + # if group >= len(self.arabicGroup): + # raise OverflowError(self.errmsg_toobig % + # (self.number, self.MAXVAL) + # ) + assert group < len(self.arabicGroup) if number_to_process % 100 != 1: if 3 <= number_to_process <= 10: ret_val = "{} {}".format( @@ -294,8 +347,8 @@ def convert_to_arabic(self): return formatted_number def validate_number(self, number): - if number >= self.max_num: - raise OverflowError(self.errmsg_too_big) + if number >= self.MAXVAL: + raise OverflowError(self.errmsg_toobig % (number, self.MAXVAL)) return number def set_currency_prefer(self, currency): @@ -329,7 +382,7 @@ def to_ordinal(self, number, prefix=''): self.currency_unit = ('', '', '', '') self.arabicPrefixText = prefix self.arabicSuffixText = "" - return "{}".format(self.convert(abs(number)).strip()) + return "{}".format(self.convert(self.abs(number)).strip()) def to_year(self, value): value = self.validate_number(value) @@ -339,6 +392,7 @@ def to_ordinal_num(self, value): return self.to_ordinal(value).strip() def to_cardinal(self, number): + self.isCurrencyNameFeminine = False number = self.validate_number(number) minus = '' if number < 0: @@ -349,4 +403,4 @@ def to_cardinal(self, number): self.arabicPrefixText = "" self.arabicSuffixText = "" self.arabicOnes = ARABIC_ONES - return minus + self.convert(value=abs(number)).strip() + return minus + self.convert(value=self.abs(number)).strip() diff --git a/num2words/lang_EO.py b/num2words/lang_EO.py index 473e74d1..ff47e69d 100644 --- a/num2words/lang_EO.py +++ b/num2words/lang_EO.py @@ -60,7 +60,7 @@ def setup(self): self.pointword = "komo" self.errmsg_nonnum = u"Sole nombroj povas esti konvertita en vortojn." self.errmsg_toobig = ( - u"Tro granda nombro por esti konvertita en vortojn." + u"Tro granda nombro por esti konvertita en vortojn (abs(%s) > %s)." ) self.exclude_title = ["kaj", "komo", "minus"] self.mid_numwords = [(1000, "mil"), (100, "cent"), (90, "naŭdek"), diff --git a/num2words/lang_FA.py b/num2words/lang_FA.py index f652f255..3597b08c 100644 --- a/num2words/lang_FA.py +++ b/num2words/lang_FA.py @@ -78,8 +78,9 @@ class Num2Word_FA(object): - errmsg_too_big = "Too large" - max_num = 10 ** 36 + # Those are unused + errmsg_toobig = "Too large" + MAXNUM = 10 ** 36 def __init__(self): self.number = 0 diff --git a/num2words/lang_FR.py b/num2words/lang_FR.py index 0c440b3f..f843205c 100644 --- a/num2words/lang_FR.py +++ b/num2words/lang_FR.py @@ -37,7 +37,9 @@ def setup(self): self.errmsg_nonnum = ( u"Seulement des nombres peuvent être convertis en mots." ) - self.errmsg_toobig = u"Nombre trop grand pour être converti en mots." + self.errmsg_toobig = ( + u"Nombre trop grand pour être converti en mots (abs(%s) > %s)." + ) self.exclude_title = ["et", "virgule", "moins"] self.mid_numwords = [(1000, "mille"), (100, "cent"), (80, "quatre-vingts"), (60, "soixante"), diff --git a/num2words/lang_ID.py b/num2words/lang_ID.py index 9a2bbfcb..54e636f9 100644 --- a/num2words/lang_ID.py +++ b/num2words/lang_ID.py @@ -44,8 +44,8 @@ class Num2Word_ID(): errmsg_floatord = "Cannot treat float number as ordinal" errmsg_negord = "Cannot treat negative number as ordinal" - errmsg_toobig = "Too large" - max_num = 10 ** 36 + errmsg_toobig = "Number is too large to convert to words (abs(%s) > %s)." + MAXVAL = 10 ** 36 def split_by_koma(self, number): return str(number).split('.') @@ -169,8 +169,8 @@ def join(self, word_blocks, float_part): return ' '.join(word_list) + float_part def to_cardinal(self, number): - if number >= self.max_num: - raise OverflowError(self.errmsg_toobig % (number, self.max_num)) + if number >= self.MAXVAL: + raise OverflowError(self.errmsg_toobig % (number, self.MAXVAL)) minus = '' if number < 0: minus = 'min ' diff --git a/num2words/lang_RO.py b/num2words/lang_RO.py index ec1deda3..feb96105 100644 --- a/num2words/lang_RO.py +++ b/num2words/lang_RO.py @@ -34,7 +34,8 @@ def setup(self): self.pointword = "virgulă" self.exclude_title = ["și", "virgulă", "minus"] self.errmsg_toobig = ( - "Numărul e prea mare pentru a fi convertit în cuvinte." + "Numărul e prea mare pentru a \ +fi convertit în cuvinte (abs(%s) > %s)." ) self.mid_numwords = [(1000, "mie/i"), (100, "sută/e"), (90, "nouăzeci"), (80, "optzeci"), diff --git a/num2words/lang_SL.py b/num2words/lang_SL.py index fb0e2876..617b0335 100644 --- a/num2words/lang_SL.py +++ b/num2words/lang_SL.py @@ -31,7 +31,9 @@ def setup(self): self.negword = "minus " self.pointword = "celih" self.errmsg_nonnum = "Only numbers may be converted to words." - self.errmsg_toobig = "Number is too large to convert to words." + self.errmsg_toobig = ( + "Number is too large to convert to words (abs(%s) > %s)." + ) self.exclude_title = [] self.mid_numwords = [(1000, "tisoč"), (900, "devetsto"), diff --git a/tests/test_ar.py b/tests/test_ar.py index 5e1ea410..91e648a6 100644 --- a/tests/test_ar.py +++ b/tests/test_ar.py @@ -42,13 +42,13 @@ def test_default_currency(self): self.assertEqual(num2words(20000.12, to='currency', lang='ar'), 'عشرون ألف ريال و اثنتا عشرة هللة') self.assertEqual(num2words(1000000, to='currency', lang='ar'), - 'واحد مليون ريال') - val = 'تسعمائة و ثلاثة و عشرون ألفاً و أربعمائة و أحد عشر ريالاً' + 'مليون ريال') + val = 'تسعمائة و ثلاثة و عشرون ألفاً و أربعمائة و أحد عشر ريالاً' self.assertEqual(num2words(923411, to='currency', lang='ar'), val) self.assertEqual(num2words(63411, to='currency', lang='ar'), - 'ثلاثة و ستون ألفاً و أربعمائة و أحد عشر ريالاً') + 'ثلاثة و ستون ألفاً و أربعمائة و أحد عشر ريالاً') self.assertEqual(num2words(1000000.99, to='currency', lang='ar'), - 'واحد مليون ريال و تسع و تسعون هللة') + 'مليون ريال و تسع و تسعون هللة') def test_currency_parm(self): self.assertEqual( @@ -62,12 +62,13 @@ def test_currency_parm(self): 'عشرون ألف جنيه و اثنتا عشرة قرش') self.assertEqual( num2words(923411, to='currency', lang='ar', currency="SR"), - 'تسعمائة و ثلاثة و عشرون ألفاً و أربعمائة و أحد عشر ريالاً') + 'تسعمائة و ثلاثة و عشرون ألفاً و أربعمائة و أحد عشر ريالاً') self.assertEqual( num2words(1000000.99, to='currency', lang='ar', currency="KWD"), - 'واحد مليون دينار و تسع و تسعون فلس') + 'مليون دينار و تسع و تسعون فلس') def test_ordinal(self): + self.assertEqual(num2words(1, to='ordinal', lang='ar'), 'اول') self.assertEqual(num2words(2, to='ordinal', lang='ar'), 'ثاني') self.assertEqual(num2words(3, to='ordinal', lang='ar'), 'ثالث') @@ -82,21 +83,82 @@ def test_ordinal(self): 'مائة و اثنان') self.assertEqual( num2words(923411, to='ordinal_num', lang='ar'), - 'تسعمائة و ثلاثة و عشرون ألفاً و أربعمائة و أحد عشر') + 'تسعمائة و ثلاثة و عشرون ألفاً و أربعمائة و أحد عشر') + + # See https://github.com/savoirfairelinux/num2words/issues/403 + self.assertEqual(num2words(23, lang="ar"), 'ثلاثة و عشرون') + self.assertEqual(num2words(23, to='ordinal', + lang="ar"), 'ثلاث و عشرون') + self.assertEqual(num2words(23, lang="ar"), 'ثلاثة و عشرون') def test_cardinal(self): + self.assertEqual(num2words(0, to='cardinal', lang='ar'), 'صفر') self.assertEqual(num2words(12, to='cardinal', lang='ar'), 'اثنا عشر') + self.assertEqual(num2words(12.3, to='cardinal', lang='ar'), + 'اثنا عشر , ثلاثون') + self.assertEqual(num2words(12.01, to='cardinal', lang='ar'), + 'اثنا عشر , إحدى') + self.assertEqual(num2words(12.02, to='cardinal', lang='ar'), + 'اثنا عشر , اثنتان') + self.assertEqual(num2words(12.03, to='cardinal', lang='ar'), + 'اثنا عشر , ثلاث') + self.assertEqual(num2words(12.34, to='cardinal', lang='ar'), + 'اثنا عشر , أربع و ثلاثون') + # Not implemented + self.assertEqual(num2words(12.345, to='cardinal', lang='ar'), + num2words(12.34, to='cardinal', lang='ar')) self.assertEqual(num2words(-8324, to='cardinal', lang='ar'), - 'سالب ثمانية آلاف و ثلاثمائة و أربعة و عشرون') + 'سالب ثمانية آلاف و ثلاثمائة و أربعة و عشرون') + + self.assertEqual(num2words(200, to='cardinal', lang='ar'), + 'مئتا') + self.assertEqual(num2words(700, to='cardinal', lang='ar'), + 'سبعمائة') + self.assertEqual(num2words(101010, to='cardinal', lang='ar'), + 'مائة و ألف ألف و عشرة') + self.assertEqual( num2words(3431.12, to='cardinal', lang='ar'), - 'ثلاثة آلاف و أربعمائة و واحد و ثلاثون , اثنتا عشرة') + 'ثلاثة آلاف و أربعمائة و واحد و ثلاثون , اثنتا عشرة') self.assertEqual(num2words(431, to='cardinal', lang='ar'), 'أربعمائة و واحد و ثلاثون') self.assertEqual(num2words(94231, to='cardinal', lang='ar'), - 'أربعة و تسعون ألفاً و مئتان و واحد و ثلاثون') + 'أربعة و تسعون ألفاً و مئتان و واحد و ثلاثون') self.assertEqual(num2words(1431, to='cardinal', lang='ar'), - 'واحد ألف و أربعمائة و واحد و ثلاثون') + 'ألف و أربعمائة و واحد و ثلاثون') + self.assertEqual(num2words(740, to='cardinal', lang='ar'), + 'سبعمائة و أربعون') + self.assertEqual(num2words(741, to='cardinal', lang='ar'), + # 'سبعة مائة و واحد و أربعون' + 'سبعمائة و واحد و أربعون' + ) + self.assertEqual(num2words(262, to='cardinal', lang='ar'), + 'مئتان و اثنان و ستون' + ) + self.assertEqual(num2words(798, to='cardinal', lang='ar'), + 'سبعمائة و ثمانية و تسعون' + ) + self.assertEqual(num2words(710, to='cardinal', lang='ar'), + 'سبعمائة و عشرة') + self.assertEqual(num2words(711, to='cardinal', lang='ar'), + # 'سبعة مائة و إحدى عشر' + 'سبعمائة و أحد عشر' + ) + self.assertEqual(num2words(700, to='cardinal', lang='ar'), + 'سبعمائة') + self.assertEqual(num2words(701, to='cardinal', lang='ar'), + 'سبعمائة و واحد') + + self.assertEqual( + num2words(1258888, to='cardinal', lang='ar'), + 'مليون و مئتان و ثمانية و خمسون ألفاً و ثمانمائة و ثمانية و ثمانون' + ) + + self.assertEqual(num2words(1100, to='cardinal', lang='ar'), + 'ألف و مائة') + + self.assertEqual(num2words(1000000521, to='cardinal', lang='ar'), + 'مليار و خمسمائة و واحد و عشرون') def test_prefix_and_suffix(self): self.assertEqual(num2words(645, to='currency', @@ -107,7 +169,38 @@ def test_year(self): self.assertEqual(num2words(2000, to='year', lang='ar'), 'ألفا') def test_max_numbers(self): - with self.assertRaises(Exception) as context: - num2words(10 ** 36, to='year', lang='ar') - self.assertTrue('Too large' in str(context.exception)) + for number in 10**51, 10**51 + 2: + + with self.assertRaises(OverflowError) as context: + num2words(number, lang='ar') + + self.assertTrue('must be less' in str(context.exception)) + + def test_big_numbers(self): + self.assertEqual( + num2words(1000000045000000000000003000000002000000300, + to='cardinal', lang='ar'), + 'تريديسيليون و خمسة و أربعون ديسيليوناً\ + و ثلاثة كوينتليونات و ملياران و ثلاثمائة' + ) + self.assertEqual( + num2words(-1000000000000000000000003000000002000000302, + to='cardinal', lang='ar'), + 'سالب تريديسيليون و ثلاثة كوينتليونات \ +و ملياران و ثلاثمائة و اثنان' + ) + self.assertEqual( + num2words(9999999999999999999999999999999999999999999999992, + to='cardinal', lang='ar'), + 'تسعة كوينتينيليونات و تسعمائة و\ + تسعة و تسعون كوادريسيليوناً و تسعمائة و تسعة\ + و تسعون تريديسيليوناً و تسعمائة و تسعة و تسعون دوديسيليوناً و تسعمائة\ + و تسعة و تسعون أندسيليوناً و تسعمائة و تسعة و تسعون ديسيليوناً\ + و تسعمائة و تسعة و تسعون نونيليوناً و تسعمائة و تسعة و تسعون\ + أوكتيليوناً و تسعمائة و تسعة و تسعون سبتيليوناً و تسعمائة و تسعة\ + و تسعون سكستيليوناً و تسعمائة و تسعة و تسعون كوينتليوناً و تسعمائة و\ + تسعة و تسعون كوادريليوناً و تسعمائة و تسعة و تسعون تريليوناً\ + و تسعمائة و تسعة و تسعون ملياراً و تسعمائة و تسعة و تسعون مليوناً\ + و تسعمائة و تسعة و تسعون ألفاً و تسعمائة و اثنان و تسعون' + ) diff --git a/tests/test_fr.py b/tests/test_fr.py index 538be341..873473ae 100644 --- a/tests/test_fr.py +++ b/tests/test_fr.py @@ -149,7 +149,7 @@ ) -class Num2WordsENTest(TestCase): +class Num2WordsFRTest(TestCase): def test_ordinal_special_joins(self): # ref https://github.com/savoirfairelinux/num2words/issues/18 self.assertEqual( @@ -203,3 +203,10 @@ def test_currency_usd(self): num2words(test[0], lang='fr', to='currency', currency='USD'), test[1] ) + + def test_max_numbers(self): + + with self.assertRaises(OverflowError) as context: + num2words(10 ** 700, lang='fr') + + self.assertTrue('trop grand' in str(context.exception))