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
58 changes: 55 additions & 3 deletions babel/numbers.py
Expand Up @@ -376,7 +376,8 @@ def get_decimal_quantum(precision):


def format_decimal(
number, format=None, locale=LC_NUMERIC, decimal_quantization=True, group_separator=True):
number, format=None, locale=LC_NUMERIC, decimal_quantization=True, group_separator=True,
format_type=None, compact_fraction_digits=0):
u"""Return the given decimal number formatted for a specific locale.

>>> format_decimal(1.2345, locale='en_US')
Expand Down Expand Up @@ -408,6 +409,18 @@ def format_decimal(
u'12345,67'
>>> format_decimal(12345.67, locale='en_US', group_separator=True)
u'12,345.67'
>>> format_decimal(12345, locale='en_US', format_type="short")
u'12K'
>>> format_decimal(12345, locale='en_US', format_type="long")
u'12 thousand'
>>> format_decimal(12345, locale='en_US', format_type="short", compact_fraction_digits=2)
u'12.35K'
>>> format_decimal(1234567, locale='ja_JP', format_type="short")
u'123万'
>>> format_decimal(2345678, locale='mk', format_type='long')
u'2 милиони'
>>> format_decimal(21098765, locale='mk', format_type='long')
u'21 милион'

:param number: the number to format
:param format:
Expand All @@ -416,15 +429,54 @@ def format_decimal(
the format pattern. Defaults to `True`.
:param group_separator: Boolean to switch group separator on/off in a locale's
number format.
:param format_type: Format to use (`None`, "short" or "long"). The standard decimal
format is `None`. Defaults to `None`.
:param compact_fraction_digits: Number of fraction digits to use in "short" or "long"
format. Defaults to `0`. If this is set to
a value greater than `0`, the `decimal_quantization`
will be treated as `False`.
DenverCoder1 marked this conversation as resolved.
Show resolved Hide resolved
"""
locale = Locale.parse(locale)
if not format:
format = locale.decimal_formats.get(format)
if format_type in ("short", "long"):
number, format = _get_compact_format(number, format_type, locale, compact_fraction_digits)
# if compact_fraction_digits is set, we don't want to truncate the fraction digits
decimal_quantization = False if compact_fraction_digits > 0 else decimal_quantization
DenverCoder1 marked this conversation as resolved.
Show resolved Hide resolved
elif not format:
format = locale.decimal_formats.get(format_type)
pattern = parse_pattern(format)
return pattern.apply(
number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator)


def _get_compact_format(number, format_type, locale, compact_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, compact_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
format = format if format is not None else locale.decimal_formats.get(None)
DenverCoder1 marked this conversation as resolved.
Show resolved Hide resolved
return number, format


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

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_decimal(1, locale='en_US', format_type="short") == u'1'
assert numbers.format_decimal(999, locale='en_US', format_type="short") == u'999'
assert numbers.format_decimal(1000, locale='en_US', format_type="short") == u'1K'
assert numbers.format_decimal(9000, locale='en_US', format_type="short") == u'9K'
assert numbers.format_decimal(9123, locale='en_US', format_type="short", compact_fraction_digits=2) == u'9.12K'
assert numbers.format_decimal(10000, locale='en_US', format_type="short") == u'10K'
assert numbers.format_decimal(10000, locale='en_US', format_type="short", compact_fraction_digits=2) == u'10K'
assert numbers.format_decimal(1000000, locale='en_US', format_type="short") == u'1M'
assert numbers.format_decimal(9000999, locale='en_US', format_type="short") == u'9M'
assert numbers.format_decimal(9000900099, locale='en_US', format_type="short", compact_fraction_digits=5) == u'9.0009B'
assert numbers.format_decimal(1, locale='en_US', format_type="long") == u'1'
assert numbers.format_decimal(999, locale='en_US', format_type="long") == u'999'
assert numbers.format_decimal(1000, locale='en_US', format_type="long") == u'1 thousand'
assert numbers.format_decimal(9000, locale='en_US', format_type="long") == u'9 thousand'
assert numbers.format_decimal(9000, locale='en_US', format_type="long", compact_fraction_digits=2) == u'9 thousand'
assert numbers.format_decimal(10000, locale='en_US', format_type="long") == u'10 thousand'
assert numbers.format_decimal(10000, locale='en_US', format_type="long", compact_fraction_digits=2) == u'10 thousand'
assert numbers.format_decimal(1000000, locale='en_US', format_type="long") == u'1 million'
assert numbers.format_decimal(9999999, locale='en_US', format_type="long") == u'10 million'
assert numbers.format_decimal(9999999999, locale='en_US', format_type="long", compact_fraction_digits=5) == u'10 billion'
assert numbers.format_decimal(1, locale='ja_JP', format_type="short") == u'1'
assert numbers.format_decimal(999, locale='ja_JP', format_type="short") == u'999'
assert numbers.format_decimal(1000, locale='ja_JP', format_type="short") == u'1000'
assert numbers.format_decimal(9123, locale='ja_JP', format_type="short") == u'9123'
assert numbers.format_decimal(10000, locale='ja_JP', format_type="short") == u'1万'
assert numbers.format_decimal(1234567, locale='ja_JP', format_type="long") == u'123万'
assert numbers.format_decimal(-1, locale='en_US', format_type="short") == u'-1'
assert numbers.format_decimal(-1234, locale='en_US', format_type="short", compact_fraction_digits=2) == u'-1.23K'
assert numbers.format_decimal(-123456789, format_type='short', locale='en_US') == u'-123M'
assert numbers.format_decimal(-123456789, format_type='long', locale='en_US') == u'-123 million'
assert numbers.format_decimal(2345678, locale='mk', format_type='long') == u'2 милиони'
assert numbers.format_decimal(21098765, locale='mk', format_type='long') == u'21 милион'

class NumberParsingTestCase(unittest.TestCase):

Expand Down