summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--babel/core.py1
-rw-r--r--babel/numbers.py109
-rwxr-xr-xscripts/import_cldr.py14
-rw-r--r--tests/test_numbers.py58
4 files changed, 166 insertions, 16 deletions
diff --git a/babel/core.py b/babel/core.py
index bb59b74..5140f49 100644
--- a/babel/core.py
+++ b/babel/core.py
@@ -45,6 +45,7 @@ def get_global(key):
The keys available are:
+ - ``all_currencies``
- ``currency_fractions``
- ``language_aliases``
- ``likely_subtags``
diff --git a/babel/numbers.py b/babel/numbers.py
index a5d30ef..9a60d72 100644
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -20,14 +20,89 @@
# - 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)
diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py
index b804119..7b9e773 100755
--- a/scripts/import_cldr.py
+++ b/scripts/import_cldr.py
@@ -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'):
diff --git a/tests/test_numbers.py b/tests/test_numbers.py
index 2593cc0..5bcd171 100644
--- a/tests/test_numbers.py
+++ b/tests/test_numbers.py
@@ -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']