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

Add support for compact decimal formats #909

Merged
merged 13 commits into from Oct 31, 2022
12 changes: 12 additions & 0 deletions babel/core.py
Expand Up @@ -564,6 +564,18 @@ def decimal_formats(self):
"""
return self._data['decimal_formats']

@property
def compact_decimal_formats(self):
"""Locale patterns for compact decimal number formatting.

.. note:: The format of the value returned may change between
Babel versions.

>>> Locale('en', 'US').compact_decimal_formats["short"]["one"]["1000"]
<NumberPattern u'0K'>
"""
return self._data['compact_decimal_formats']

@property
def currency_formats(self):
"""Locale patterns for currency number formatting.
Expand Down
57 changes: 57 additions & 0 deletions babel/numbers.py
Expand Up @@ -425,6 +425,63 @@ def format_decimal(
number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator)


def format_compact_decimal(number, *, format_type="short", locale=LC_NUMERIC, fraction_digits=0):
u"""Return the given decimal number formatted for a specific locale in compact form.

>>> format_compact_decimal(12345, format_type="short", locale='en_US')
u'12K'
>>> format_compact_decimal(12345, format_type="long", locale='en_US')
u'12 thousand'
>>> format_compact_decimal(12345, format_type="short", locale='en_US', fraction_digits=2)
u'12.35K'
>>> format_compact_decimal(1234567, format_type="short", locale="ja_JP")
u'123万'
>>> format_compact_decimal(2345678, format_type="long", locale="mk")
u'2 милиони'
>>> format_compact_decimal(21098765, format_type="long", locale="mk")
u'21 милион'

:param number: the number to format
:param format_type: Compact format to use ("short" or "long")
: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)
number, format = _get_compact_format(number, format_type, locale, fraction_digits)
pattern = parse_pattern(format)
return pattern.apply(number, locale, decimal_quantization=False)


def _get_compact_format(number, format_type, 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
format = compact_format["other"][str(magnitude)]
pattern = parse_pattern(format).pattern
# if the pattern is "0", we do not divide the number
if pattern == "0":
break
# otherwise, we need to divide the number by the magnitude but remove zeros
# equal to the number of 0's in the pattern minus 1
number = number / (magnitude / (10 ** (pattern.count("0") - 1)))
# round to the number of fraction digits requested
number = round(number, fraction_digits)
# if the remaining number is singular, use the singular format
plural_form = locale.plural_form(abs(number))
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


class UnknownCurrencyFormatError(KeyError):
"""Exception raised when an unknown currency format is requested."""

Expand Down
2 changes: 2 additions & 0 deletions docs/api/numbers.rst
Expand Up @@ -13,6 +13,8 @@ Number Formatting

.. autofunction:: format_decimal

.. autofunction:: format_compact_decimal

.. autofunction:: format_currency

.. autofunction:: format_percent
Expand Down
2 changes: 1 addition & 1 deletion docs/numbers.rst
Expand Up @@ -12,7 +12,7 @@ the ``babel.numbers`` module:

.. code-block:: pycon

>>> from babel.numbers import format_number, format_decimal, format_percent
>>> from babel.numbers import format_number, format_decimal, format_compact_decimal, format_percent

Examples:

Expand Down
2 changes: 0 additions & 2 deletions scripts/import_cldr.py
Expand Up @@ -770,8 +770,6 @@ def parse_decimal_formats(data, tree):

# These are mapped into a `compact_decimal_formats` dictionary
# with the format {length: {count: {multiplier: pattern}}}.

# TODO: Add support for formatting them.
compact_decimal_formats = data.setdefault('compact_decimal_formats', {})
length_map = compact_decimal_formats.setdefault(length_type, {})
length_count_map = length_map.setdefault(pattern_el.attrib['count'], {})
Expand Down
33 changes: 33 additions & 0 deletions tests/test_numbers.py
Expand Up @@ -121,6 +121,39 @@ def test_group_separator(self):
assert numbers.format_currency(101299.98, 'EUR', locale='en_US', group_separator=True, format_type='name') == u'101,299.98 euros'
assert numbers.format_percent(251234.1234, locale='sv_SE', group_separator=True) == u'25\xa0123\xa0412\xa0%'

def test_compact(self):
DenverCoder1 marked this conversation as resolved.
Show resolved Hide resolved
assert numbers.format_compact_decimal(1, locale='en_US', format_type="short") == u'1'
assert numbers.format_compact_decimal(999, locale='en_US', format_type="short") == u'999'
assert numbers.format_compact_decimal(1000, locale='en_US', format_type="short") == u'1K'
assert numbers.format_compact_decimal(9000, locale='en_US', format_type="short") == u'9K'
assert numbers.format_compact_decimal(9123, locale='en_US', format_type="short", fraction_digits=2) == u'9.12K'
assert numbers.format_compact_decimal(10000, locale='en_US', format_type="short") == u'10K'
assert numbers.format_compact_decimal(10000, locale='en_US', format_type="short", fraction_digits=2) == u'10K'
assert numbers.format_compact_decimal(1000000, locale='en_US', format_type="short") == u'1M'
assert numbers.format_compact_decimal(9000999, locale='en_US', format_type="short") == u'9M'
assert numbers.format_compact_decimal(9000900099, locale='en_US', format_type="short", fraction_digits=5) == u'9.0009B'
assert numbers.format_compact_decimal(1, locale='en_US', format_type="long") == u'1'
assert numbers.format_compact_decimal(999, locale='en_US', format_type="long") == u'999'
assert numbers.format_compact_decimal(1000, locale='en_US', format_type="long") == u'1 thousand'
assert numbers.format_compact_decimal(9000, locale='en_US', format_type="long") == u'9 thousand'
assert numbers.format_compact_decimal(9000, locale='en_US', format_type="long", fraction_digits=2) == u'9 thousand'
assert numbers.format_compact_decimal(10000, locale='en_US', format_type="long") == u'10 thousand'
assert numbers.format_compact_decimal(10000, locale='en_US', format_type="long", fraction_digits=2) == u'10 thousand'
assert numbers.format_compact_decimal(1000000, locale='en_US', format_type="long") == u'1 million'
assert numbers.format_compact_decimal(9999999, locale='en_US', format_type="long") == u'10 million'
assert numbers.format_compact_decimal(9999999999, locale='en_US', format_type="long", fraction_digits=5) == u'10 billion'
assert numbers.format_compact_decimal(1, locale='ja_JP', format_type="short") == u'1'
assert numbers.format_compact_decimal(999, locale='ja_JP', format_type="short") == u'999'
assert numbers.format_compact_decimal(1000, locale='ja_JP', format_type="short") == u'1000'
assert numbers.format_compact_decimal(9123, locale='ja_JP', format_type="short") == u'9123'
assert numbers.format_compact_decimal(10000, locale='ja_JP', format_type="short") == u'1万'
assert numbers.format_compact_decimal(1234567, locale='ja_JP', format_type="long") == u'123万'
assert numbers.format_compact_decimal(-1, locale='en_US', format_type="short") == u'-1'
assert numbers.format_compact_decimal(-1234, locale='en_US', format_type="short", fraction_digits=2) == u'-1.23K'
assert numbers.format_compact_decimal(-123456789, format_type='short', locale='en_US') == u'-123M'
assert numbers.format_compact_decimal(-123456789, format_type='long', locale='en_US') == u'-123 million'
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 милион'

class NumberParsingTestCase(unittest.TestCase):

Expand Down