diff options
author | Jonah Lawrence <jonah@freshidea.com> | 2022-11-04 09:16:24 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-04 17:16:24 +0200 |
commit | 5fcc2535f96bfce9c1a1ecf9d19a976b9bf6ab6b (patch) | |
tree | a7871ea693786fe78eb10c5ff92e30926a71bfbb | |
parent | 3add2c141783b1f590c753f475b8cba64d15cd0c (diff) | |
download | babel-5fcc2535f96bfce9c1a1ecf9d19a976b9bf6ab6b.tar.gz |
feat: Support for short compact currency formats (#926)
Co-authored-by: Jun Omae (大前 潤) <42682+jun66j5@users.noreply.github.com>
-rw-r--r-- | babel/core.py | 12 | ||||
-rw-r--r-- | babel/numbers.py | 53 | ||||
-rw-r--r-- | babel/support.py | 9 | ||||
-rw-r--r-- | docs/api/numbers.rst | 2 | ||||
-rwxr-xr-x | scripts/import_cldr.py | 21 | ||||
-rw-r--r-- | tests/test_numbers.py | 29 | ||||
-rw-r--r-- | tests/test_support.py | 5 |
7 files changed, 119 insertions, 12 deletions
diff --git a/babel/core.py b/babel/core.py index 220cbaf..2a01c30 100644 --- a/babel/core.py +++ b/babel/core.py @@ -591,6 +591,18 @@ class Locale: return self._data['currency_formats'] @property + def compact_currency_formats(self): + """Locale patterns for compact currency number formatting. + + .. note:: The format of the value returned may change between + Babel versions. + + >>> Locale('en', 'US').compact_currency_formats["short"]["one"]["1000"] + <NumberPattern u'¤0K'> + """ + return self._data['compact_currency_formats'] + + @property def percent_formats(self): """Locale patterns for percent number formatting. diff --git a/babel/numbers.py b/babel/numbers.py index 8a341c4..8baf110 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -440,18 +440,21 @@ def format_compact_decimal(number, *, format_type="short", locale=LC_NUMERIC, fr :param fraction_digits: Number of digits after the decimal point to use. Defaults to `0`. """ locale = Locale.parse(locale) - number, format = _get_compact_format(number, format_type, locale, fraction_digits) + compact_format = locale.compact_decimal_formats[format_type] + number, format = _get_compact_format(number, compact_format, locale, fraction_digits) + # Did not find a format, fall back. + if format is None: + format = locale.decimal_formats.get(None) pattern = parse_pattern(format) return pattern.apply(number, locale, decimal_quantization=False) -def _get_compact_format(number, format_type, locale, fraction_digits=0): +def _get_compact_format(number, compact_format, locale, fraction_digits=0): """Returns the number after dividing by the unit and the format pattern to use. The algorithm is described here: https://www.unicode.org/reports/tr35/tr35-45/tr35-numbers.html#Compact_Number_Formats. """ format = None - compact_format = locale.compact_decimal_formats[format_type] for magnitude in sorted([int(m) for m in compact_format["other"]], reverse=True): if abs(number) >= magnitude: # check the pattern using "other" as the amount @@ -470,8 +473,6 @@ def _get_compact_format(number, format_type, locale, fraction_digits=0): plural_form = plural_form if plural_form in compact_format else "other" format = compact_format[plural_form][str(magnitude)] break - if format is None: # Did not find a format, fall back. - format = locale.decimal_formats.get(None) return number, format @@ -624,6 +625,44 @@ def _format_currency_long_name( return unit_pattern.format(number_part, display_name) +def format_compact_currency(number, currency, *, format_type="short", locale=LC_NUMERIC, fraction_digits=0): + u"""Format a number as a currency value in compact form. + + >>> format_compact_currency(12345, 'USD', locale='en_US') + u'$12K' + >>> format_compact_currency(123456789, 'USD', locale='en_US', fraction_digits=2) + u'$123.46M' + >>> format_compact_currency(123456789, 'EUR', locale='de_DE', fraction_digits=1) + '123,5\xa0Mio.\xa0€' + + :param number: the number to format + :param currency: the currency code + :param format_type: the compact format type to use. Defaults to "short". + :param locale: the `Locale` object or locale identifier + :param fraction_digits: Number of digits after the decimal point to use. Defaults to `0`. + """ + locale = Locale.parse(locale) + try: + compact_format = locale.compact_currency_formats[format_type] + except KeyError as error: + raise UnknownCurrencyFormatError(f"{format_type!r} is not a known compact currency format type") from error + number, format = _get_compact_format(number, compact_format, locale, fraction_digits) + # Did not find a format, fall back. + if format is None or "¤" not in str(format): + # find first format that has a currency symbol + for magnitude in compact_format['other']: + format = compact_format['other'][magnitude].pattern + if '¤' not in format: + continue + # remove characters that are not the currency symbol, 0's or spaces + format = re.sub(r'[^0\s\¤]', '', format) + # compress adjacent spaces into one + format = re.sub(r'(\s)\s+', r'\1', format).strip() + break + pattern = parse_pattern(format) + return pattern.apply(number, locale, currency=currency, currency_digits=False, decimal_quantization=False) + + def format_percent( number, format=None, locale=LC_NUMERIC, decimal_quantization=True, group_separator=True): """Return formatted percent value for a specific locale. @@ -1082,6 +1121,10 @@ class NumberPattern: retval = retval.replace(u'¤¤', currency.upper()) retval = retval.replace(u'¤', get_currency_symbol(currency, locale)) + # remove single quotes around text, except for doubled single quotes + # which are replaced with a single quote + retval = re.sub(r"'([^']*)'", lambda m: m.group(1) or "'", retval) + return retval # diff --git a/babel/support.py b/babel/support.py index 3efc003..50f2752 100644 --- a/babel/support.py +++ b/babel/support.py @@ -17,7 +17,7 @@ import locale from babel.core import Locale from babel.dates import format_date, format_datetime, format_time, \ format_timedelta -from babel.numbers import format_decimal, format_currency, \ +from babel.numbers import format_decimal, format_currency, format_compact_currency, \ format_percent, format_scientific, format_compact_decimal @@ -124,6 +124,13 @@ class Format: """ return format_currency(number, currency, locale=self.locale) + def compact_currency(self, number, currency, format_type='short', fraction_digits=0): + """Return a number in the given currency formatted for the locale + using the compact number format. + """ + return format_compact_currency(number, currency, format_type=format_type, + fraction_digits=fraction_digits, locale=self.locale) + def percent(self, number, format=None): """Return a number formatted as percentage for the locale. diff --git a/docs/api/numbers.rst b/docs/api/numbers.rst index eac5692..d3ab8b1 100644 --- a/docs/api/numbers.rst +++ b/docs/api/numbers.rst @@ -17,6 +17,8 @@ Number Formatting .. autofunction:: format_currency +.. autofunction:: format_compact_currency + .. autofunction:: format_percent .. autofunction:: format_scientific diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 92dd272..097840c 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -915,10 +915,6 @@ def parse_currency_formats(data, tree): curr_length_type = length_elem.attrib.get('type') for elem in length_elem.findall('currencyFormat'): type = elem.attrib.get('type') - if curr_length_type: - # Handle `<currencyFormatLength type="short">`, etc. - # TODO(3.x): use nested dicts instead of colon-separated madness - type = '%s:%s' % (type, curr_length_type) if _should_skip_elem(elem, type, currency_formats): continue for child in elem.iter(): @@ -928,8 +924,21 @@ def parse_currency_formats(data, tree): child.attrib['path']) ) elif child.tag == 'pattern': - pattern = str(child.text) - currency_formats[type] = numbers.parse_pattern(pattern) + pattern_type = child.attrib.get('type') + pattern = numbers.parse_pattern(str(child.text)) + if pattern_type: + # This is a compact currency format, see: + # https://www.unicode.org/reports/tr35/tr35-45/tr35-numbers.html#Compact_Number_Formats + + # These are mapped into a `compact_currency_formats` dictionary + # with the format {length: {count: {multiplier: pattern}}}. + compact_currency_formats = data.setdefault('compact_currency_formats', {}) + length_map = compact_currency_formats.setdefault(curr_length_type, {}) + length_count_map = length_map.setdefault(child.attrib['count'], {}) + length_count_map[pattern_type] = pattern + else: + # Regular currency format + currency_formats[type] = pattern def parse_currency_unit_patterns(data, tree): diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 1b955c9..bb6c4e8 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -422,6 +422,27 @@ def test_format_currency_format_type(): == u'1.099,98') +def test_format_compact_currency(): + assert numbers.format_compact_currency(1, 'USD', locale='en_US', format_type="short") == u'$1' + assert numbers.format_compact_currency(999, 'USD', locale='en_US', format_type="short") == u'$999' + assert numbers.format_compact_currency(123456789, 'USD', locale='en_US', format_type="short") == u'$123M' + assert numbers.format_compact_currency(123456789, 'USD', locale='en_US', fraction_digits=2, format_type="short") == u'$123.46M' + assert numbers.format_compact_currency(-123456789, 'USD', locale='en_US', fraction_digits=2, format_type="short") == u'-$123.46M' + assert numbers.format_compact_currency(1, 'JPY', locale='ja_JP', format_type="short") == u'¥1' + assert numbers.format_compact_currency(1234, 'JPY', locale='ja_JP', format_type="short") == u'¥1234' + assert numbers.format_compact_currency(123456, 'JPY', locale='ja_JP', format_type="short") == u'¥12万' + assert numbers.format_compact_currency(123456, 'JPY', locale='ja_JP', format_type="short", fraction_digits=2) == u'¥12.35万' + assert numbers.format_compact_currency(123, 'EUR', locale='yav', format_type="short") == '123\xa0€' + assert numbers.format_compact_currency(12345, 'EUR', locale='yav', format_type="short") == '12K\xa0€' + assert numbers.format_compact_currency(123456789, 'EUR', locale='de_DE', fraction_digits=1) == '123,5\xa0Mio.\xa0€' + + +def test_format_compact_currency_invalid_format_type(): + with pytest.raises(numbers.UnknownCurrencyFormatError): + numbers.format_compact_currency(1099.98, 'USD', locale='en_US', + format_type='unknown') + + @pytest.mark.parametrize('input_value, expected_value', [ ('10000', '$10,000.00'), ('1', '$1.00'), @@ -696,3 +717,11 @@ def test_parse_decimal_nbsp_heuristics(): def test_very_small_decimal_no_quantization(): assert numbers.format_decimal(decimal.Decimal('1E-7'), locale='en', decimal_quantization=False) == '0.0000001' + + +def test_single_quotes_in_pattern(): + assert numbers.format_decimal(123, "'@0.#'00'@01'", locale='en') == '@0.#120@01' + + assert numbers.format_decimal(123, "'$'''0", locale='en') == "$'123" + + assert numbers.format_decimal(12, "'#'0 o''clock", locale='en') == "#12 o'clock" diff --git a/tests/test_support.py b/tests/test_support.py index 93ad37e..9447107 100644 --- a/tests/test_support.py +++ b/tests/test_support.py @@ -334,6 +334,11 @@ def test_format_compact_decimal(): assert fmt.compact_decimal(1234567, format_type='long', fraction_digits=2) == '1.23 million' +def test_format_compact_currency(): + fmt = support.Format('en_US') + assert fmt.compact_currency(1234567, "USD", format_type='short', fraction_digits=2) == '$1.23M' + + def test_format_percent(): fmt = support.Format('en_US') assert fmt.percent(0.34) == '34%' |