From b409813eff5142e1856e5e57c221b72d8d5b978b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 30 Jul 2013 02:13:45 +0200 Subject: [PATCH] Added support for territory currency lookups. The main usecase of this is to figure out at what point in time did a country use a certain currency. The default behavior is to use the current date. This fixes #42 --- CHANGES | 4 ++ babel/numbers.py | 88 +++++++++++++++++++++++++++++++++++++++++- docs/api/numbers.rst | 2 + scripts/import_cldr.py | 28 ++++++++++++++ tests/test_numbers.py | 23 +++++++++++ 5 files changed, 144 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 7dbaf25cc..147124e4f 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,10 @@ Version 2.0 (release date to be decided, codename to be selected) +- Added support for looking up currencies that belong to a territory + through the :func:`babel.numbers.get_territory_currencies` + function. + Version 1.4 ----------- diff --git a/babel/numbers.py b/babel/numbers.py index 0f387190a..344625e19 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -21,8 +21,9 @@ from decimal import Decimal, InvalidOperation import math import re +from datetime import date as date_, datetime as datetime_ -from babel.core import default_locale, Locale +from babel.core import default_locale, Locale, get_global from babel._compat import range_type @@ -63,6 +64,91 @@ def get_currency_symbol(currency, locale=LC_NUMERIC): return Locale.parse(locale).currency_symbols.get(currency, currency) +def get_territory_currencies(territory, start_date=None, end_date=None, + tender=True, non_tender=False, + include_details=False): + """Returns the list of currencies for the given territory that are valid at + the given date range. In addition to that the currency database + distinguishes between tender and non-tender currencies. By default only + tender currencies are returned. + + The return value is a list of all currencies roughly ordered by the time + of when the currency became active. The longer the currency is being in + use the more to the left of the list it will be. + + The start date defaults to today. If no end date is given it will be the + same as the start date. Otherwise a range can be defined. For instance + this can be used to find the currencies in use in Austria between 1995 and + 2011: + + >>> from datetime import date + >>> get_territory_currencies('AT', date(1995, 1, 1), date(2011, 1, 1)) + ['ATS', 'EUR'] + + Likewise it's also possible to find all the currencies in use on a + single date: + + >>> get_territory_currencies('AT', date(1995, 1, 1)) + ['ATS'] + >>> get_territory_currencies('AT', date(2011, 1, 1)) + ['EUR'] + + By default the return value only includes tender currencies. This + however can be changed: + + >>> get_territory_currencies('US') + ['USD'] + >>> get_territory_currencies('US', tender=False, non_tender=True) + ['USN', 'USS'] + + .. versionadded:: 2.0 + + :param territory: the name of the territory to find the currency fo + :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. + :param non_tender: controls whether non-tender currencies should be + included. + :param include_details: if set to `True`, instead of returning currency + codes the return value will be dictionaries + with detail information. In that case each + dictionary will have the keys ``'currency'``, + ``'from'``, ``'to'``, and ``'tender'``. + """ + currencies = get_global('territory_currencies') + if start_date is None: + start_date = date_.today() + elif isinstance(start_date, datetime_): + start_date = start_date.date() + if end_date is None: + end_date = start_date + elif isinstance(end_date, datetime_): + end_date = end_date.date() + + curs = currencies.get(territory.upper(), ()) + # TODO: validate that the territory exists + + def _is_active(start, end): + return (start is None or start <= end_date) and \ + (end is None or end >= start_date) + + result = [] + for currency_code, start, end, is_tender in curs: + if ((is_tender and tender) or \ + (not is_tender and non_tender)) and _is_active(start, end): + if include_details: + result.append({ + 'currency': currency_code, + 'from': start, + 'to': end, + 'tender': is_tender, + }) + else: + result.append(currency_code) + + return result + + def get_decimal_symbol(locale=LC_NUMERIC): """Return the symbol used by the locale to separate decimal fractions. diff --git a/docs/api/numbers.rst b/docs/api/numbers.rst index de3573e71..207ae0bca 100644 --- a/docs/api/numbers.rst +++ b/docs/api/numbers.rst @@ -43,3 +43,5 @@ Data Access .. autofunction:: get_plus_sign_symbol .. autofunction:: get_minus_sign_symbol + +.. autofunction:: get_territory_currencies diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 84b2b1ddf..c189e4f3e 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -21,6 +21,8 @@ except ImportError: from xml.etree import ElementTree +from datetime import date + # Make sure we're using Babel source, and not some previously installed version sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..')) @@ -95,6 +97,18 @@ def _translate_alias(ctxt, path): return keys +def _parse_currency_date(s): + if not s: + return None + parts = s.split('-', 2) + return date(*map(int, parts + [1] * (3 - len(parts)))) + + +def _currency_sort_key(tup): + code, start, end, tender = tup + return int(not tender), start or date(1, 1, 1) + + def main(): parser = OptionParser(usage='%prog path/to/cldr') options, args = parser.parse_args() @@ -128,6 +142,7 @@ def main(): script_aliases = global_data.setdefault('script_aliases', {}) variant_aliases = global_data.setdefault('variant_aliases', {}) likely_subtags = global_data.setdefault('likely_subtags', {}) + territory_currencies = global_data.setdefault('territory_currencies', {}) # create auxiliary zone->territory map from the windows zones (we don't set # the 'zones_territories' map directly here, because there are some zones @@ -186,6 +201,19 @@ def main(): for likely_subtag in sup_likely.findall('.//likelySubtags/likelySubtag'): likely_subtags[likely_subtag.attrib['from']] = likely_subtag.attrib['to'] + # Currencies in territories + for region in sup.findall('.//currencyData/region'): + region_code = region.attrib['iso3166'] + region_currencies = [] + for currency in region.findall('./currency'): + 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')) + region_currencies.sort(key=_currency_sort_key) + territory_currencies[region_code] = region_currencies + outfile = open(global_path, 'wb') try: pickle.dump(global_data, outfile, 2) diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 6db4b6795..99e0d1bda 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -15,6 +15,8 @@ import unittest import pytest +from datetime import date + from babel import numbers @@ -180,6 +182,27 @@ def test_get_currency_symbol(): assert numbers.get_currency_symbol('USD', 'en_US') == u'$' +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'] + + assert numbers.get_territory_currencies('US', date(2013, 1, 1)) == ['USD'] + assert sorted(numbers.get_territory_currencies('US', date(2013, 1, 1), + non_tender=True)) == ['USD', 'USN', 'USS'] + + assert numbers.get_territory_currencies('US', date(2013, 1, 1), + include_details=True) == [{ + 'currency': 'USD', + 'from': date(1792, 1, 1), + 'to': None, + 'tender': True + }] + + assert numbers.get_territory_currencies('LS', date(2013, 1, 1)) == ['ZAR', 'LSL'] + + assert numbers.get_territory_currencies('QO', date(2013, 1, 1)) == [] + + def test_get_decimal_symbol(): assert numbers.get_decimal_symbol('en_US') == u'.'