diff options
author | Aarni Koskela <akx@iki.fi> | 2018-06-13 15:53:26 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-06-13 15:53:26 +0300 |
commit | 553a754a6bb5ee5dad6ecec10b7d5841315e9764 (patch) | |
tree | 7be9d2495b8fc3799643299c0adcd070bca90a9e | |
parent | a5ecaa321817d3705cbda1476f6e9f06daa1e847 (diff) | |
parent | 4c7e9b645cfec72672f836e04d8587ca66ba98c3 (diff) | |
download | babel-553a754a6bb5ee5dad6ecec10b7d5841315e9764.tar.gz |
Merge pull request #585 from django-ftl/implement_currency_name_format_type
numbers: implement currency formatting with long display names.
-rw-r--r-- | babel/numbers.py | 82 | ||||
-rw-r--r-- | docs/api/numbers.rst | 2 | ||||
-rwxr-xr-x | scripts/import_cldr.py | 12 | ||||
-rw-r--r-- | tests/test_numbers.py | 59 |
4 files changed, 154 insertions, 1 deletions
diff --git a/babel/numbers.py b/babel/numbers.py index 564d7ce..cc825c8 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -157,6 +157,36 @@ def get_currency_precision(currency): return precisions.get(currency, precisions['DEFAULT'])[0] +def get_currency_unit_pattern(currency, count=None, locale=LC_NUMERIC): + """ + Return the unit pattern used for long display of a currency value + for a given locale. + This is a string containing ``{0}`` where the numeric part + should be substituted and ``{1}`` where the currency long display + name should be substituted. + + >>> get_currency_unit_pattern('USD', locale='en_US', count=10) + u'{0} {1}' + + .. versionadded:: 2.7.0 + + :param currency: the currency code. + :param count: the optional count. If provided the unit + pattern for that number will be returned. + :param locale: the `Locale` object or locale identifier. + """ + loc = Locale.parse(locale) + if count is not None: + plural_form = loc.plural_form(count) + try: + return loc._data['currency_unit_patterns'][plural_form] + except LookupError: + # Fall back to 'other' + pass + + return loc._data['currency_unit_patterns']['other'] + + def get_territory_currencies(territory, start_date=None, end_date=None, tender=True, non_tender=False, include_details=False): @@ -442,6 +472,17 @@ def format_currency( ... UnknownCurrencyFormatError: "'unknown' is not a known currency format type" + You can also pass format_type='name' to use long display names. The order of + the number and currency name, along with the correct localized plural form + of the currency name, is chosen according to locale: + + >>> format_currency(1, 'USD', locale='en_US', format_type='name') + u'1.00 US dollar' + >>> format_currency(1099.98, 'USD', locale='en_US', format_type='name') + u'1,099.98 US dollars' + >>> format_currency(1099.98, 'USD', locale='ee', format_type='name') + u'us ga dollar 1,099.98' + By default the locale is allowed to truncate and round a high-precision number by forcing its format pattern onto the decimal part. You can bypass this behavior with the `decimal_quantization` parameter: @@ -459,7 +500,12 @@ def format_currency( :param format_type: the currency format type to use :param decimal_quantization: Truncate and round high-precision numbers to the format pattern. Defaults to `True`. + """ + if format_type == 'name': + return _format_currency_long_name(number, currency, format=format, + locale=locale, currency_digits=currency_digits, + decimal_quantization=decimal_quantization) locale = Locale.parse(locale) if format: pattern = parse_pattern(format) @@ -475,6 +521,42 @@ def format_currency( decimal_quantization=decimal_quantization) +def _format_currency_long_name( + number, currency, format=None, locale=LC_NUMERIC, currency_digits=True, + format_type='standard', decimal_quantization=True): + # Algorithm described here: + # https://www.unicode.org/reports/tr35/tr35-numbers.html#Currencies + locale = Locale.parse(locale) + # Step 1. + # There are no examples of items with explicit count (0 or 1) in current + # locale data. So there is no point implementing that. + # Step 2. + + # Correct number to numeric type, important for looking up plural rules: + if isinstance(number, string_types): + number_n = float(number) + else: + number_n = number + + # Step 3. + unit_pattern = get_currency_unit_pattern(currency, count=number_n, locale=locale) + + # Step 4. + display_name = get_currency_name(currency, count=number_n, locale=locale) + + # Step 5. + if not format: + format = locale.decimal_formats.get(format) + + pattern = parse_pattern(format) + + number_part = pattern.apply( + number, locale, currency=currency, currency_digits=currency_digits, + decimal_quantization=decimal_quantization) + + return unit_pattern.format(number_part, display_name) + + def format_percent( number, format=None, locale=LC_NUMERIC, decimal_quantization=True): """Return formatted percent value for a specific locale. diff --git a/docs/api/numbers.rst b/docs/api/numbers.rst index 1b21425..758ceba 100644 --- a/docs/api/numbers.rst +++ b/docs/api/numbers.rst @@ -38,6 +38,8 @@ Data Access .. autofunction:: get_currency_symbol +.. autofunction:: get_currency_unit_pattern + .. autofunction:: get_decimal_symbol .. autofunction:: get_plus_sign_symbol diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 60aa6c2..f1dd391 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -423,6 +423,7 @@ def _process_local_datas(sup, srcdir, destdir, force=False, dump_json=False): parse_percent_formats(data, tree) parse_currency_formats(data, tree) + parse_currency_unit_patterns(data, tree) parse_currency_names(data, tree) parse_unit_patterns(data, tree) parse_date_fields(data, tree) @@ -903,6 +904,17 @@ def parse_currency_formats(data, tree): currency_formats[type] = numbers.parse_pattern(pattern) +def parse_currency_unit_patterns(data, tree): + currency_unit_patterns = data.setdefault('currency_unit_patterns', {}) + for currency_formats_elem in tree.findall('.//currencyFormats'): + if _should_skip_number_elem(data, currency_formats_elem): # TODO: Support other number systems + continue + for unit_pattern_elem in currency_formats_elem.findall('./unitPattern'): + count = unit_pattern_elem.attrib['count'] + pattern = text_type(unit_pattern_elem.text) + currency_unit_patterns[count] = pattern + + def parse_day_period_rules(tree): """ Parse dayPeriodRule data into a dict. diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 493c1a7..32f4280 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -19,7 +19,7 @@ from datetime import date from babel import Locale, localedata, numbers from babel.numbers import ( list_currencies, validate_currency, UnknownCurrencyError, is_currency, normalize_currency, - get_currency_precision, get_decimal_precision) + get_currency_precision, get_decimal_precision, get_currency_unit_pattern) from babel.localedata import locale_identifiers from babel._compat import decimal @@ -228,6 +228,17 @@ def test_get_currency_precision(): assert get_currency_precision('JPY') == 0 +def test_get_currency_unit_pattern(): + assert get_currency_unit_pattern('USD', locale='en_US') == '{0} {1}' + assert get_currency_unit_pattern('USD', locale='es_GT') == '{1} {0}' + + # 'ro' locale various pattern according to count + assert get_currency_unit_pattern('USD', locale='ro', count=1) == '{0} {1}' + assert get_currency_unit_pattern('USD', locale='ro', count=2) == '{0} {1}' + assert get_currency_unit_pattern('USD', locale='ro', count=100) == '{0} de {1}' + assert get_currency_unit_pattern('USD', locale='ro') == '{0} de {1}' + + def test_get_territory_currencies(): assert numbers.get_territory_currencies('AT', date(1995, 1, 1)) == ['ATS'] assert numbers.get_territory_currencies('AT', date(2011, 1, 1)) == ['EUR'] @@ -415,6 +426,52 @@ def test_format_currency_quantization(): '0.9999999999', 'USD', locale=locale_code, decimal_quantization=False).find('9999999999') > -1 +def test_format_currency_long_display_name(): + assert (numbers.format_currency(1099.98, 'USD', locale='en_US', format_type='name') + == u'1,099.98 US dollars') + assert (numbers.format_currency(1.00, 'USD', locale='en_US', format_type='name') + == u'1.00 US dollar') + assert (numbers.format_currency(1.00, 'EUR', locale='en_US', format_type='name') + == u'1.00 euro') + assert (numbers.format_currency(2, 'EUR', locale='en_US', format_type='name') + == u'2.00 euros') + # This tests that '{1} {0}' unitPatterns are found: + assert (numbers.format_currency(1, 'USD', locale='sw', format_type='name') + == u'dola ya Marekani 1.00') + # This tests unicode chars: + assert (numbers.format_currency(1099.98, 'USD', locale='es_GT', format_type='name') + == u'dólares estadounidenses 1,099.98') + # Test for completely unknown currency, should fallback to currency code + assert (numbers.format_currency(1099.98, 'XAB', locale='en_US', format_type='name') + == u'1,099.98 XAB') + + # Test for finding different unit patterns depending on count + assert (numbers.format_currency(1, 'USD', locale='ro', format_type='name') + == u'1,00 dolar american') + assert (numbers.format_currency(2, 'USD', locale='ro', format_type='name') + == u'2,00 dolari americani') + assert (numbers.format_currency(100, 'USD', locale='ro', format_type='name') + == u'100,00 de dolari americani') + + +def test_format_currency_long_display_name_all(): + for locale_code in localedata.locale_identifiers(): + assert numbers.format_currency( + 1, 'USD', locale=locale_code, format_type='name').find('1') > -1 + assert numbers.format_currency( + '1', 'USD', locale=locale_code, format_type='name').find('1') > -1 + + +def test_format_currency_long_display_name_custom_format(): + assert (numbers.format_currency(1099.98, 'USD', locale='en_US', + format_type='name', format='##0') + == '1099.98 US dollars') + assert (numbers.format_currency(1099.98, 'USD', locale='en_US', + format_type='name', format='##0', + currency_digits=False) + == '1100 US dollars') + + def test_format_percent(): assert numbers.format_percent(0.34, locale='en_US') == u'34%' assert numbers.format_percent(0, locale='en_US') == u'0%' |