Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions babel/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def get_global(key):

The keys available are:

- ``all_currencies``
- ``currency_fractions``
- ``language_aliases``
- ``likely_subtags``
Expand Down
109 changes: 97 additions & 12 deletions babel/numbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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:
Expand All @@ -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):
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 10 additions & 4 deletions scripts/import_cldr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'))
Expand Down Expand Up @@ -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'):
Expand Down
58 changes: 58 additions & 0 deletions tests/test_numbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
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


Expand Down Expand Up @@ -162,6 +166,55 @@ def test_can_parse_decimals(self):
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'
Expand All @@ -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']
Expand Down