diff options
-rw-r--r-- | babel/core.py | 1 | ||||
-rw-r--r-- | babel/numbers.py | 109 | ||||
-rwxr-xr-x | scripts/import_cldr.py | 14 | ||||
-rw-r--r-- | tests/test_numbers.py | 58 |
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'] |