]> git.ipfire.org Git - thirdparty/babel.git/commitdiff
Add currency utilities and helpers. 491/head
authorKevin Deldycke <kevin@deldycke.com>
Fri, 7 Apr 2017 14:09:14 +0000 (16:09 +0200)
committerKevin Deldycke <kevin@deldycke.com>
Wed, 17 May 2017 16:00:10 +0000 (18:00 +0200)
babel/core.py
babel/numbers.py
scripts/import_cldr.py
tests/test_numbers.py

index bb59b7413f8365c889528d1074c85af06b88820f..5140f49d79b80dc6c16995fff5a56a726bf2cf31 100644 (file)
@@ -45,6 +45,7 @@ def get_global(key):
 
     The keys available are:
 
+    - ``all_currencies``
     - ``currency_fractions``
     - ``language_aliases``
     - ``likely_subtags``
index a5d30ef267f939677f432ad4fcff55c2938b52a5..9a60d721df7681f05448290cff1c66ed0a67ecf2 100644 (file)
 #  - http://www.unicode.org/reports/tr35/ (Appendix G.6)
 import re
 from datetime import date as date_, datetime as datetime_
+from itertools import chain
 
 from babel.core import default_locale, Locale, get_global
-from babel._compat import decimal
+from babel._compat import decimal, string_types
+from babel.localedata import locale_identifiers
 
 
 LC_NUMERIC = default_locale('LC_NUMERIC')
 
 
+class UnknownCurrencyError(Exception):
+    """Exception thrown when a currency is requested for which no data is available.
+    """
+
+    def __init__(self, identifier):
+        """Create the exception.
+        :param identifier: the identifier string of the unsupported currency
+        """
+        Exception.__init__(self, 'Unknown currency %r.' % identifier)
+
+        #: The identifier of the locale that could not be found.
+        self.identifier = identifier
+
+
+def list_currencies(locale=None):
+    """ Return a `set` of normalized currency codes.
+
+    .. versionadded:: 2.5.0
+
+    :param locale: filters returned currency codes by the provided locale.
+                   Expected to be a locale instance or code. If no locale is
+                   provided, returns the list of all currencies from all
+                   locales.
+    """
+    # Get locale-scoped currencies.
+    if locale:
+        currencies = Locale.parse(locale).currencies.keys()
+    else:
+        currencies = get_global('all_currencies')
+    return set(currencies)
+
+
+def validate_currency(currency, locale=None):
+    """ Check the currency code is recognized by Babel.
+
+    Accepts a ``locale`` parameter for fined-grained validation, working as
+    the one defined above in ``list_currencies()`` method.
+
+    Raises a `ValueError` exception if the currency is unknown to Babel.
+    """
+    if currency not in list_currencies(locale):
+        raise UnknownCurrencyError(currency)
+
+
+def is_currency(currency, locale=None):
+    """ Returns `True` only if a currency is recognized by Babel.
+
+    This method always return a Boolean and never raise.
+    """
+    if not currency or not isinstance(currency, string_types):
+        return False
+    try:
+        validate_currency(currency, locale)
+    except UnknownCurrencyError:
+        return False
+    return True
+
+
+def normalize_currency(currency, locale=None):
+    """Returns the normalized sting of any currency code.
+
+    Accepts a ``locale`` parameter for fined-grained validation, working as
+    the one defined above in ``list_currencies()`` method.
+
+    Returns None if the currency is unknown to Babel.
+    """
+    if isinstance(currency, string_types):
+        currency = currency.upper()
+    if not is_currency(currency, locale):
+        return
+    return currency
+
+
 def get_currency_name(currency, count=None, locale=LC_NUMERIC):
     """Return the name used by the locale for the specified currency.
 
@@ -36,10 +111,10 @@ def get_currency_name(currency, count=None, locale=LC_NUMERIC):
 
     .. versionadded:: 0.9.4
 
-    :param currency: the currency code
+    :param currency: the currency code.
     :param count: the optional count.  If provided the currency name
                   will be pluralized to that number if possible.
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier.
     """
     loc = Locale.parse(locale)
     if count is not None:
@@ -56,12 +131,26 @@ def get_currency_symbol(currency, locale=LC_NUMERIC):
     >>> get_currency_symbol('USD', locale='en_US')
     u'$'
 
-    :param currency: the currency code
-    :param locale: the `Locale` object or locale identifier
+    :param currency: the currency code.
+    :param locale: the `Locale` object or locale identifier.
     """
     return Locale.parse(locale).currency_symbols.get(currency, currency)
 
 
+def get_currency_precision(currency):
+    """Return currency's precision.
+
+    Precision is the number of decimals found after the decimal point in the
+    currency's format pattern.
+
+    .. versionadded:: 2.5.0
+
+    :param currency: the currency code.
+    """
+    precisions = get_global('currency_fractions')
+    return precisions.get(currency, precisions['DEFAULT'])[0]
+
+
 def get_territory_currencies(territory, start_date=None, end_date=None,
                              tender=True, non_tender=False,
                              include_details=False):
@@ -102,7 +191,7 @@ def get_territory_currencies(territory, start_date=None, end_date=None,
 
     .. versionadded:: 2.0
 
-    :param territory: the name of the territory to find the currency fo
+    :param territory: the name of the territory to find the currency for.
     :param start_date: the start date.  If not given today is assumed.
     :param end_date: the end date.  If not given the start date is assumed.
     :param tender: controls whether tender currencies should be included.
@@ -326,12 +415,8 @@ def format_currency(number, currency, format=None, locale=LC_NUMERIC,
             raise UnknownCurrencyFormatError("%r is not a known currency format"
                                              " type" % format_type)
     if currency_digits:
-        fractions = get_global('currency_fractions')
-        try:
-            digits = fractions[currency][0]
-        except KeyError:
-            digits = fractions['DEFAULT'][0]
-        frac = (digits, digits)
+        precision = get_currency_precision(currency)
+        frac = (precision, precision)
     else:
         frac = None
     return pattern.apply(number, locale, currency=currency, force_frac=frac)
index b8041193f8ec3940ea0231c9438b7e75100b2cc5..7b9e7734ba5821e9b2bbae43134aad5ef7301b7e 100755 (executable)
@@ -12,6 +12,7 @@
 # individuals. For the exact contribution history, see the revision
 # history and logs, available at http://babel.edgewall.org/log/.
 
+import collections
 from optparse import OptionParser
 import os
 import re
@@ -218,6 +219,7 @@ def parse_global(srcdir, sup):
     likely_subtags = global_data.setdefault('likely_subtags', {})
     territory_currencies = global_data.setdefault('territory_currencies', {})
     parent_exceptions = global_data.setdefault('parent_exceptions', {})
+    all_currencies = collections.defaultdict(set)
     currency_fractions = global_data.setdefault('currency_fractions', {})
     territory_languages = global_data.setdefault('territory_languages', {})
     bcp47_timezone = parse(os.path.join(srcdir, 'bcp47', 'timezone.xml'))
@@ -286,14 +288,18 @@ def parse_global(srcdir, sup):
         region_code = region.attrib['iso3166']
         region_currencies = []
         for currency in region.findall('./currency'):
+            cur_code = currency.attrib['iso4217']
             cur_start = _parse_currency_date(currency.attrib.get('from'))
             cur_end = _parse_currency_date(currency.attrib.get('to'))
-            region_currencies.append((currency.attrib['iso4217'],
-                                      cur_start, cur_end,
-                                      currency.attrib.get(
-                                          'tender', 'true') == 'true'))
+            cur_tender = currency.attrib.get('tender', 'true') == 'true'
+            # Tie region to currency.
+            region_currencies.append((cur_code, cur_start, cur_end, cur_tender))
+            # Keep a reverse index of currencies to territorie.
+            all_currencies[cur_code].add(region_code)
         region_currencies.sort(key=_currency_sort_key)
         territory_currencies[region_code] = region_currencies
+    global_data['all_currencies'] = dict([
+        (currency, tuple(sorted(regions))) for currency, regions in all_currencies.items()])
 
     # Explicit parent locales
     for paternity in sup.findall('.//parentLocales/parentLocale'):
index 2593cc017309088faa5610729c22d7db68c333e6..5bcd1717d1b6cfd40cb5a85aafb26a034a9a6847 100644 (file)
@@ -17,6 +17,10 @@ import pytest
 from datetime import date
 
 from babel import numbers
+from babel.numbers import (
+    list_currencies, validate_currency, UnknownCurrencyError, is_currency, normalize_currency, get_currency_precision)
+from babel.core import Locale
+from babel.localedata import locale_identifiers
 from babel._compat import decimal
 
 
@@ -162,6 +166,55 @@ class NumberParsingTestCase(unittest.TestCase):
                           lambda: numbers.parse_decimal('2,109,998', locale='de'))
 
 
+def test_list_currencies():
+    assert isinstance(list_currencies(), set)
+    assert list_currencies().issuperset(['BAD', 'BAM', 'KRO'])
+
+    assert isinstance(list_currencies(locale='fr'), set)
+    assert list_currencies('fr').issuperset(['BAD', 'BAM', 'KRO'])
+
+    with pytest.raises(ValueError) as excinfo:
+        list_currencies('yo!')
+    assert excinfo.value.args[0] == "expected only letters, got 'yo!'"
+
+    assert list_currencies(locale='pa_Arab') == set(['PKR', 'INR', 'EUR'])
+    assert list_currencies(locale='kok') == set([])
+
+    assert len(list_currencies()) == 296
+
+
+def test_validate_currency():
+    validate_currency('EUR')
+
+    with pytest.raises(UnknownCurrencyError) as excinfo:
+        validate_currency('FUU')
+    assert excinfo.value.args[0] == "Unknown currency 'FUU'."
+
+
+def test_is_currency():
+    assert is_currency('EUR') == True
+    assert is_currency('eUr') == False
+    assert is_currency('FUU') == False
+    assert is_currency('') == False
+    assert is_currency(None) == False
+    assert is_currency('   EUR    ') == False
+    assert is_currency('   ') == False
+    assert is_currency([]) == False
+    assert is_currency(set()) == False
+
+
+def test_normalize_currency():
+    assert normalize_currency('EUR') == 'EUR'
+    assert normalize_currency('eUr') == 'EUR'
+    assert normalize_currency('FUU') == None
+    assert normalize_currency('') == None
+    assert normalize_currency(None) == None
+    assert normalize_currency('   EUR    ') == None
+    assert normalize_currency('   ') == None
+    assert normalize_currency([]) == None
+    assert normalize_currency(set()) == None
+
+
 def test_get_currency_name():
     assert numbers.get_currency_name('USD', locale='en_US') == u'US Dollar'
     assert numbers.get_currency_name('USD', count=2, locale='en_US') == u'US dollars'
@@ -171,6 +224,11 @@ def test_get_currency_symbol():
     assert numbers.get_currency_symbol('USD', 'en_US') == u'$'
 
 
+def test_get_currency_precision():
+    assert get_currency_precision('EUR') == 2
+    assert get_currency_precision('JPY') == 0
+
+
 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']