Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support for short compact currency formats #926

Merged
merged 17 commits into from
Nov 4, 2022
Merged
Show file tree
Hide file tree
Changes from 8 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
12 changes: 12 additions & 0 deletions babel/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,18 @@ def currency_formats(self):
"""
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.
Expand Down
41 changes: 36 additions & 5 deletions babel/numbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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


Expand Down Expand Up @@ -624,6 +625,36 @@ 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 compact currency value.

>>> 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)
u"123,5\xa0Mio'.'\xa0€"
DenverCoder1 marked this conversation as resolved.
Show resolved Hide resolved

: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 e:
raise UnknownCurrencyFormatError(
"%r is not a known currency format type" % format_type) from e
DenverCoder1 marked this conversation as resolved.
Show resolved Hide resolved
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):
format = "¤0"
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.
Expand Down
9 changes: 8 additions & 1 deletion babel/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
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


Expand Down Expand Up @@ -124,6 +124,13 @@ def currency(self, number, currency):
"""
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.

Expand Down
2 changes: 2 additions & 0 deletions docs/api/numbers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Number Formatting

.. autofunction:: format_currency

.. autofunction:: format_compact_currency

.. autofunction:: format_percent

.. autofunction:: format_scientific
Expand Down
21 changes: 15 additions & 6 deletions scripts/import_cldr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
akx marked this conversation as resolved.
Show resolved Hide resolved
type = '%s:%s' % (type, curr_length_type)
if _should_skip_elem(elem, type, currency_formats):
continue
for child in elem.iter():
Expand All @@ -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):
Expand Down
11 changes: 11 additions & 0 deletions tests/test_numbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,17 @@ def test_compact(self):
assert numbers.format_compact_decimal(2345678, locale='mk', format_type='long') == u'2 милиони'
assert numbers.format_compact_decimal(21098765, locale='mk', format_type='long') == u'21 милион'

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万'


class NumberParsingTestCase(unittest.TestCase):

def test_can_parse_decimals(self):
Expand Down
5 changes: 5 additions & 0 deletions tests/test_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -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%'
Expand Down