diff --git a/README.rst b/README.rst index 2a8f99f4..743b5439 100644 --- a/README.rst +++ b/README.rst @@ -78,6 +78,7 @@ Besides the numerical argument, there are two main optional arguments. * ``en`` (English, default) * ``am`` (Amharic) * ``ar`` (Arabic) +* ``az`` (Azerbaijani) * ``cz`` (Czech) * ``de`` (German) * ``dk`` (Danish) diff --git a/num2words/__init__.py b/num2words/__init__.py index fb9f7e91..1c438486 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_CZ, lang_DE, lang_DK, lang_EN, +from . import (lang_AM, lang_AR, lang_AZ, 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_IT, lang_JA, @@ -29,6 +29,7 @@ CONVERTER_CLASSES = { 'am': lang_AM.Num2Word_AM(), 'ar': lang_AR.Num2Word_AR(), + 'az': lang_AZ.Num2Word_AZ(), 'cz': lang_CZ.Num2Word_CZ(), 'en': lang_EN.Num2Word_EN(), 'en_IN': lang_EN_IN.Num2Word_EN_IN(), diff --git a/num2words/lang_AZ.py b/num2words/lang_AZ.py new file mode 100644 index 00000000..e225bfb7 --- /dev/null +++ b/num2words/lang_AZ.py @@ -0,0 +1,204 @@ +# -*- 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 + + +class Num2Word_AZ(Num2Word_Base): + DIGITS = { + 0: u"sıfır", + 1: u"bir", + 2: u"iki", + 3: u"üç", + 4: u"dörd", + 5: u"beş", + 6: u"altı", + 7: u"yeddi", + 8: u"səkkiz", + 9: u"doqquz", + } + + DECIMALS = { + 1: u"on", + 2: u"iyirmi", + 3: u"otuz", + 4: u"qırx", + 5: u"əlli", + 6: u"altmış", + 7: u"yetmiş", + 8: u"səksən", + 9: u"doxsan", + } + + POWERS_OF_TEN = { + 2: u"yüz", + 3: u"min", + 6: u"milyon", + 9: u"milyard", + 12: u"trilyon", + 15: u"katrilyon", + 18: u"kentilyon", + 21: u"sekstilyon", + 24: u"septilyon", + 27: u"oktilyon", + 30: u"nonilyon", + 33: u"desilyon", + 36: u"undesilyon", + 39: u"dodesilyon", + 42: u"tredesilyon", + 45: u"katordesilyon", + 48: u"kendesilyon", + 51: u"seksdesilyon", + 54: u"septendesilyon", + 57: u"oktodesilyon", + 60: u"novemdesilyon", + 63: u"vigintilyon", + } + + VOWELS = u"aıoueəiöü" + VOWEL_TO_CARDINAL_SUFFIX_MAP = { + **dict.fromkeys(["a", "ı"], "ıncı"), + **dict.fromkeys(["e", "ə", "i"], "inci"), + **dict.fromkeys(["o", "u"], "uncu"), + **dict.fromkeys(["ö", "ü"], "üncü"), + } + + VOWEL_TO_CARDINAL_NUM_SUFFIX_MAP = { + **dict.fromkeys(["a", "ı"], "cı"), + **dict.fromkeys(["e", "ə", "i"], "ci"), + **dict.fromkeys(["o", "u"], "cu"), + **dict.fromkeys(["ö", "ü"], "cü"), + } + + CURRENCY_INTEGRAL = ('manat', 'manat') + CURRENCY_FRACTION = ('qəpik', 'qəpik') + CURRENCY_FORMS = {'AZN': (CURRENCY_INTEGRAL, CURRENCY_FRACTION)} + + def setup(self): + super().setup() + + self.negword = u"mənfi" + self.pointword = u"nöqtə" + + def to_cardinal(self, value): + value_str = str(value) + parts = value_str.split(".") + integral_part = parts[0] + fraction_part = parts[1] if len(parts) > 1 else "" + + if integral_part.startswith("-"): + integral_part = integral_part[1:] # ignore minus sign here + + integral_part_in_words = self.int_to_word(integral_part) + fraction_part_in_words = "" if not fraction_part else self.int_to_word( + fraction_part, leading_zeros=True) + + value_in_words = integral_part_in_words + if fraction_part: + value_in_words = " ".join( + [ + integral_part_in_words, + self.pointword, + fraction_part_in_words + ] + ) + + if value < 0: + value_in_words = " ".join([self.negword, value_in_words]) + + return value_in_words + + def to_ordinal(self, value): + assert int(value) == value + + cardinal = self.to_cardinal(value) + last_vowel = self._last_vowel(cardinal) + assert last_vowel is not None + suffix = self.VOWEL_TO_CARDINAL_SUFFIX_MAP[last_vowel] + + if cardinal.endswith(tuple(self.VOWELS)): + cardinal = cardinal[:-1] + + ordinal = "".join([cardinal, suffix]) + + return ordinal + + def to_ordinal_num(self, value): + assert int(value) == value + + cardinal = self.to_cardinal(value) + last_vowel = self._last_vowel(cardinal) + assert last_vowel is not None + suffix = self.VOWEL_TO_CARDINAL_NUM_SUFFIX_MAP[last_vowel] + ordinal_num = "-".join([str(value), suffix]) + + return ordinal_num + + def to_year(self, value): + assert int(value) == value + + year = self.to_cardinal(abs(value)) + if value < 0: + year = " ".join(["e.ə.", year]) + + return year + + def pluralize(self, n, forms): + form = 0 if n == 1 else 1 + return forms[form] + + def int_to_word(self, num_str, leading_zeros=False): + words = [] + reversed_str = list(reversed(num_str)) + + for index, digit in enumerate(reversed_str): + digit_int = int(digit) + # calculate remainder after dividing index by 3 + # because number is parsed by three digit chunks + remainder_to_3 = index % 3 + if remainder_to_3 == 0: + if index > 0: + if set(reversed_str[index:index+3]) != {'0'}: + words.insert(0, self.POWERS_OF_TEN[index]) + if digit_int > 0: + # we say "min" not "bir min" + if not (digit_int == 1 and index == 3): + words.insert(0, self.DIGITS[digit_int]) + elif remainder_to_3 == 1: + if digit_int != 0: + words.insert(0, self.DECIMALS[digit_int]) + else: # remainder is 2 + if digit_int > 0: + words.insert(0, self.POWERS_OF_TEN[2]) # "yüz" + if digit_int > 1: + words.insert(0, self.DIGITS[digit_int]) + + if num_str == '0': + words.append(self.DIGITS[0]) + + if leading_zeros: + zeros_count = len(num_str) - len(str(int(num_str))) + words[:0] = zeros_count * [self.DIGITS[0]] + + return " ".join(words) + + def _last_vowel(self, value): + for char in value[::-1]: + if char in self.VOWELS: + return char diff --git a/tests/test_az.py b/tests/test_az.py new file mode 100644 index 00000000..af52cb27 --- /dev/null +++ b/tests/test_az.py @@ -0,0 +1,289 @@ +# -*- 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 unittest import TestCase + +from num2words import num2words + + +class Num2WordAZTest(TestCase): + lang = 'az' + + CARDINAL_TEST_CASES = ( + (0, 'sıfır',), + (1, 'bir',), + (2, 'iki',), + (3, 'üç',), + (4, 'dörd',), + (5, 'beş',), + (6, 'altı',), + (7, 'yeddi',), + (8, 'səkkiz',), + (9, 'doqquz',), + (10, 'on',), + (11, 'on bir',), + (20, 'iyirmi',), + (22, 'iyirmi iki',), + (30, 'otuz',), + (33, 'otuz üç',), + (40, 'qırx',), + (44, 'qırx dörd',), + (50, 'əlli',), + (55, 'əlli beş',), + (60, 'altmış',), + (66, 'altmış altı',), + (70, 'yetmiş',), + (77, 'yetmiş yeddi',), + (80, 'səksən',), + (88, 'səksən səkkiz',), + (90, 'doxsan',), + (99, 'doxsan doqquz',), + (100, 'yüz',), + (200, 'iki yüz',), + (678, 'altı yüz yetmiş səkkiz',), + (999, 'doqquz yüz doxsan doqquz',), + (1000, 'min',), + (100_000, 'yüz min',), + (328_914, 'üç yüz iyirmi səkkiz min doqquz yüz on dörd',), + (1_000_000, 'bir milyon',), + (1_000_000_000, 'bir milyard',), + (10**12, 'bir trilyon',), + (10**15, 'bir katrilyon',), + (10**18, 'bir kentilyon',), + (10**21, 'bir sekstilyon',), + (10**24, 'bir septilyon',), + (10**27, 'bir oktilyon',), + (10**30, 'bir nonilyon',), + (10**33, 'bir desilyon',), + (10**36, 'bir undesilyon',), + (10**39, 'bir dodesilyon',), + (10**42, 'bir tredesilyon',), + (10**45, 'bir katordesilyon',), + (10**48, 'bir kendesilyon',), + (10**51, 'bir seksdesilyon',), + (10**54, 'bir septendesilyon',), + (10**57, 'bir oktodesilyon',), + (10**60, 'bir novemdesilyon',), + (10**63, 'bir vigintilyon',), + (-0, 'sıfır',), + (-1, 'mənfi bir',), + (-2, 'mənfi iki',), + (-3, 'mənfi üç',), + (-4, 'mənfi dörd',), + (-5, 'mənfi beş',), + (-6, 'mənfi altı',), + (-7, 'mənfi yeddi',), + (-8, 'mənfi səkkiz',), + (-9, 'mənfi doqquz',), + (-10, 'mənfi on',), + (-11, 'mənfi on bir',), + (-20, 'mənfi iyirmi',), + (-22, 'mənfi iyirmi iki',), + (-30, 'mənfi otuz',), + (-33, 'mənfi otuz üç',), + (-40, 'mənfi qırx',), + (-44, 'mənfi qırx dörd',), + (-50, 'mənfi əlli',), + (-55, 'mənfi əlli beş',), + (-60, 'mənfi altmış',), + (-66, 'mənfi altmış altı',), + (-70, 'mənfi yetmiş',), + (-77, 'mənfi yetmiş yeddi',), + (-80, 'mənfi səksən',), + (-88, 'mənfi səksən səkkiz',), + (-90, 'mənfi doxsan',), + (-99, 'mənfi doxsan doqquz',), + (-100, 'mənfi yüz',), + (-200, 'mənfi iki yüz',), + (-678, 'mənfi altı yüz yetmiş səkkiz',), + (-999, 'mənfi doqquz yüz doxsan doqquz',), + (-1000, 'mənfi min',), + (-100_000, 'mənfi yüz min',), + (-328_914, 'mənfi üç yüz iyirmi səkkiz min doqquz yüz on dörd',), + (-1_000_000, 'mənfi bir milyon',), + (-1_000_000_000, 'mənfi bir milyard',), + (-10**12, 'mənfi bir trilyon',), + (-10**15, 'mənfi bir katrilyon',), + (-10**18, 'mənfi bir kentilyon',), + (-10**21, 'mənfi bir sekstilyon',), + (-10**24, 'mənfi bir septilyon',), + (-10**27, 'mənfi bir oktilyon',), + (-10**30, 'mənfi bir nonilyon',), + (-10**33, 'mənfi bir desilyon',), + (-10**36, 'mənfi bir undesilyon',), + (-10**39, 'mənfi bir dodesilyon',), + (-10**42, 'mənfi bir tredesilyon',), + (-10**45, 'mənfi bir katordesilyon',), + (-10**48, 'mənfi bir kendesilyon',), + (-10**51, 'mənfi bir seksdesilyon',), + (-10**54, 'mənfi bir septendesilyon',), + (-10**57, 'mənfi bir oktodesilyon',), + (-10**60, 'mənfi bir novemdesilyon',), + (-10**63, 'mənfi bir vigintilyon'), + ) + + CARDINAL_FRACTION_TEST_CASES = ( + (0.2, 'sıfır nöqtə iki',), + (0.02, 'sıfır nöqtə sıfır iki',), + (0.23, 'sıfır nöqtə iyirmi üç',), + (0.0023, 'sıfır nöqtə sıfır sıfır iyirmi üç',), + (1.43, 'bir nöqtə qırx üç',), + (-0.2, 'mənfi sıfır nöqtə iki',), + (-0.02, 'mənfi sıfır nöqtə sıfır iki',), + (-0.23, 'mənfi sıfır nöqtə iyirmi üç',), + (-0.0023, 'mənfi sıfır nöqtə sıfır sıfır iyirmi üç',), + (-1.43, 'mənfi bir nöqtə qırx üç',), + ) + + ORDINAL_TEST_CASES = ( + (0, 'sıfırıncı',), + (1, 'birinci',), + (2, 'ikinci',), + (3, 'üçüncü',), + (4, 'dördüncü',), + (5, 'beşinci',), + (6, 'altıncı',), + (7, 'yeddinci',), + (8, 'səkkizinci',), + (9, 'doqquzuncu',), + (10, 'onuncu',), + (11, 'on birinci',), + (20, 'iyirminci',), + (22, 'iyirmi ikinci',), + (30, 'otuzuncu',), + (33, 'otuz üçüncü',), + (40, 'qırxıncı',), + (44, 'qırx dördüncü',), + (50, 'əllinci',), + (55, 'əlli beşinci',), + (60, 'altmışıncı',), + (66, 'altmış altıncı',), + (70, 'yetmişinci',), + (77, 'yetmiş yeddinci',), + (80, 'səksəninci',), + (88, 'səksən səkkizinci',), + (90, 'doxsanıncı',), + (99, 'doxsan doqquzuncu',), + (100, 'yüzüncü',), + (1000, 'mininci',), + (328_914, 'üç yüz iyirmi səkkiz min doqquz yüz on dördüncü',), + (1_000_000, 'bir milyonuncu'), + ) + + ORDINAL_NUM_TEST_CASES = ( + (0, '0-cı',), + (1, '1-ci',), + (2, '2-ci',), + (3, '3-cü',), + (4, '4-cü',), + (5, '5-ci',), + (6, '6-cı',), + (7, '7-ci',), + (8, '8-ci',), + (9, '9-cu',), + (10, '10-cu',), + (11, '11-ci',), + (20, '20-ci',), + (22, '22-ci',), + (30, '30-cu',), + (33, '33-cü',), + (40, '40-cı',), + (44, '44-cü',), + (50, '50-ci',), + (55, '55-ci',), + (60, '60-cı',), + (66, '66-cı',), + (70, '70-ci',), + (77, '77-ci',), + (80, '80-ci',), + (88, '88-ci',), + (90, '90-cı',), + (99, '99-cu',), + (100, '100-cü',), + (1000, '1000-ci',), + (328_914, '328914-cü',), + (1_000_000, '1000000-cu'), + ) + + YEAR_TEST_CASES = ( + (167, 'yüz altmış yeddi'), + (1552, 'min beş yüz əlli iki'), + (1881, 'min səkkiz yüz səksən bir'), + (2022, 'iki min iyirmi iki'), + (-1, 'e.ə. bir'), + (-500, 'e.ə. beş yüz'), + (-5000, 'e.ə. beş min'), + ) + + CURRENCY_TEST_CASES = ( + (0.0, 'sıfır manat, sıfır qəpik'), + (0.01, 'sıfır manat, bir qəpik'), + (0.1, 'sıfır manat, on qəpik'), + (1.5, 'bir manat, əlli qəpik'), + (1.94, 'bir manat, doxsan dörd qəpik'), + (17.82, 'on yeddi manat, səksən iki qəpik'), + ) + + def test_cardinal(self): + """Test cardinal conversion for integer numbers.""" + + for number, expected in self.CARDINAL_TEST_CASES: + actual = num2words(number, lang=self.lang, to='cardinal') + + self.assertEqual(actual, expected) + + def test_cardinal_fracion(self): + """Test cardinal conversion for numbers with fraction.""" + + for number, expected in self.CARDINAL_FRACTION_TEST_CASES: + actual = num2words(number, lang=self.lang, to='cardinal') + + self.assertEqual(actual, expected) + + def test_ordinal(self): + """Test ordinal conversion.""" + + for number, expected in self.ORDINAL_TEST_CASES: + actual = num2words(number, lang=self.lang, to='ordinal') + + self.assertEqual(actual, expected) + + def test_ordinal_num(self): + """Test 'ordinal_num' conversion.""" + + for number, expected in self.ORDINAL_NUM_TEST_CASES: + actual = num2words(number, lang=self.lang, to='ordinal_num') + + self.assertEqual(actual, expected) + + def test_year(self): + """Test year conversion.""" + + for number, expected in self.YEAR_TEST_CASES: + actual = num2words(number, lang=self.lang, to='year') + + self.assertEqual(actual, expected) + + def test_currency(self): + """Test currency conversion.""" + + for number, expected in self.CURRENCY_TEST_CASES: + actual = num2words( + number, lang=self.lang, currency='AZN', to='currency') + + self.assertEqual(actual, expected)