Skip to content

Commit 05df10f

Browse files
authored
Add support for compact decimal formats (#909)
1 parent 03c8fae commit 05df10f

File tree

6 files changed

+105
-3
lines changed

6 files changed

+105
-3
lines changed

babel/core.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,18 @@ def decimal_formats(self):
564564
"""
565565
return self._data['decimal_formats']
566566

567+
@property
568+
def compact_decimal_formats(self):
569+
"""Locale patterns for compact decimal number formatting.
570+
571+
.. note:: The format of the value returned may change between
572+
Babel versions.
573+
574+
>>> Locale('en', 'US').compact_decimal_formats["short"]["one"]["1000"]
575+
<NumberPattern u'0K'>
576+
"""
577+
return self._data['compact_decimal_formats']
578+
567579
@property
568580
def currency_formats(self):
569581
"""Locale patterns for currency number formatting.

babel/numbers.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,63 @@ def format_decimal(
425425
number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator)
426426

427427

428+
def format_compact_decimal(number, *, format_type="short", locale=LC_NUMERIC, fraction_digits=0):
429+
u"""Return the given decimal number formatted for a specific locale in compact form.
430+
431+
>>> format_compact_decimal(12345, format_type="short", locale='en_US')
432+
u'12K'
433+
>>> format_compact_decimal(12345, format_type="long", locale='en_US')
434+
u'12 thousand'
435+
>>> format_compact_decimal(12345, format_type="short", locale='en_US', fraction_digits=2)
436+
u'12.35K'
437+
>>> format_compact_decimal(1234567, format_type="short", locale="ja_JP")
438+
u'123万'
439+
>>> format_compact_decimal(2345678, format_type="long", locale="mk")
440+
u'2 милиони'
441+
>>> format_compact_decimal(21098765, format_type="long", locale="mk")
442+
u'21 милион'
443+
444+
:param number: the number to format
445+
:param format_type: Compact format to use ("short" or "long")
446+
:param locale: the `Locale` object or locale identifier
447+
:param fraction_digits: Number of digits after the decimal point to use. Defaults to `0`.
448+
"""
449+
locale = Locale.parse(locale)
450+
number, format = _get_compact_format(number, format_type, locale, fraction_digits)
451+
pattern = parse_pattern(format)
452+
return pattern.apply(number, locale, decimal_quantization=False)
453+
454+
455+
def _get_compact_format(number, format_type, locale, fraction_digits=0):
456+
"""Returns the number after dividing by the unit and the format pattern to use.
457+
The algorithm is described here:
458+
https://www.unicode.org/reports/tr35/tr35-45/tr35-numbers.html#Compact_Number_Formats.
459+
"""
460+
format = None
461+
compact_format = locale.compact_decimal_formats[format_type]
462+
for magnitude in sorted([int(m) for m in compact_format["other"]], reverse=True):
463+
if abs(number) >= magnitude:
464+
# check the pattern using "other" as the amount
465+
format = compact_format["other"][str(magnitude)]
466+
pattern = parse_pattern(format).pattern
467+
# if the pattern is "0", we do not divide the number
468+
if pattern == "0":
469+
break
470+
# otherwise, we need to divide the number by the magnitude but remove zeros
471+
# equal to the number of 0's in the pattern minus 1
472+
number = number / (magnitude / (10 ** (pattern.count("0") - 1)))
473+
# round to the number of fraction digits requested
474+
number = round(number, fraction_digits)
475+
# if the remaining number is singular, use the singular format
476+
plural_form = locale.plural_form(abs(number))
477+
plural_form = plural_form if plural_form in compact_format else "other"
478+
format = compact_format[plural_form][str(magnitude)]
479+
break
480+
if format is None: # Did not find a format, fall back.
481+
format = locale.decimal_formats.get(None)
482+
return number, format
483+
484+
428485
class UnknownCurrencyFormatError(KeyError):
429486
"""Exception raised when an unknown currency format is requested."""
430487

docs/api/numbers.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Number Formatting
1313

1414
.. autofunction:: format_decimal
1515

16+
.. autofunction:: format_compact_decimal
17+
1618
.. autofunction:: format_currency
1719

1820
.. autofunction:: format_percent

docs/numbers.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ the ``babel.numbers`` module:
1212

1313
.. code-block:: pycon
1414
15-
>>> from babel.numbers import format_number, format_decimal, format_percent
15+
>>> from babel.numbers import format_number, format_decimal, format_compact_decimal, format_percent
1616
1717
Examples:
1818

scripts/import_cldr.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -770,8 +770,6 @@ def parse_decimal_formats(data, tree):
770770

771771
# These are mapped into a `compact_decimal_formats` dictionary
772772
# with the format {length: {count: {multiplier: pattern}}}.
773-
774-
# TODO: Add support for formatting them.
775773
compact_decimal_formats = data.setdefault('compact_decimal_formats', {})
776774
length_map = compact_decimal_formats.setdefault(length_type, {})
777775
length_count_map = length_map.setdefault(pattern_el.attrib['count'], {})

tests/test_numbers.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,39 @@ def test_group_separator(self):
121121
assert numbers.format_currency(101299.98, 'EUR', locale='en_US', group_separator=True, format_type='name') == u'101,299.98 euros'
122122
assert numbers.format_percent(251234.1234, locale='sv_SE', group_separator=True) == u'25\xa0123\xa0412\xa0%'
123123

124+
def test_compact(self):
125+
assert numbers.format_compact_decimal(1, locale='en_US', format_type="short") == u'1'
126+
assert numbers.format_compact_decimal(999, locale='en_US', format_type="short") == u'999'
127+
assert numbers.format_compact_decimal(1000, locale='en_US', format_type="short") == u'1K'
128+
assert numbers.format_compact_decimal(9000, locale='en_US', format_type="short") == u'9K'
129+
assert numbers.format_compact_decimal(9123, locale='en_US', format_type="short", fraction_digits=2) == u'9.12K'
130+
assert numbers.format_compact_decimal(10000, locale='en_US', format_type="short") == u'10K'
131+
assert numbers.format_compact_decimal(10000, locale='en_US', format_type="short", fraction_digits=2) == u'10K'
132+
assert numbers.format_compact_decimal(1000000, locale='en_US', format_type="short") == u'1M'
133+
assert numbers.format_compact_decimal(9000999, locale='en_US', format_type="short") == u'9M'
134+
assert numbers.format_compact_decimal(9000900099, locale='en_US', format_type="short", fraction_digits=5) == u'9.0009B'
135+
assert numbers.format_compact_decimal(1, locale='en_US', format_type="long") == u'1'
136+
assert numbers.format_compact_decimal(999, locale='en_US', format_type="long") == u'999'
137+
assert numbers.format_compact_decimal(1000, locale='en_US', format_type="long") == u'1 thousand'
138+
assert numbers.format_compact_decimal(9000, locale='en_US', format_type="long") == u'9 thousand'
139+
assert numbers.format_compact_decimal(9000, locale='en_US', format_type="long", fraction_digits=2) == u'9 thousand'
140+
assert numbers.format_compact_decimal(10000, locale='en_US', format_type="long") == u'10 thousand'
141+
assert numbers.format_compact_decimal(10000, locale='en_US', format_type="long", fraction_digits=2) == u'10 thousand'
142+
assert numbers.format_compact_decimal(1000000, locale='en_US', format_type="long") == u'1 million'
143+
assert numbers.format_compact_decimal(9999999, locale='en_US', format_type="long") == u'10 million'
144+
assert numbers.format_compact_decimal(9999999999, locale='en_US', format_type="long", fraction_digits=5) == u'10 billion'
145+
assert numbers.format_compact_decimal(1, locale='ja_JP', format_type="short") == u'1'
146+
assert numbers.format_compact_decimal(999, locale='ja_JP', format_type="short") == u'999'
147+
assert numbers.format_compact_decimal(1000, locale='ja_JP', format_type="short") == u'1000'
148+
assert numbers.format_compact_decimal(9123, locale='ja_JP', format_type="short") == u'9123'
149+
assert numbers.format_compact_decimal(10000, locale='ja_JP', format_type="short") == u'1万'
150+
assert numbers.format_compact_decimal(1234567, locale='ja_JP', format_type="long") == u'123万'
151+
assert numbers.format_compact_decimal(-1, locale='en_US', format_type="short") == u'-1'
152+
assert numbers.format_compact_decimal(-1234, locale='en_US', format_type="short", fraction_digits=2) == u'-1.23K'
153+
assert numbers.format_compact_decimal(-123456789, format_type='short', locale='en_US') == u'-123M'
154+
assert numbers.format_compact_decimal(-123456789, format_type='long', locale='en_US') == u'-123 million'
155+
assert numbers.format_compact_decimal(2345678, locale='mk', format_type='long') == u'2 милиони'
156+
assert numbers.format_compact_decimal(21098765, locale='mk', format_type='long') == u'21 милион'
124157

125158
class NumberParsingTestCase(unittest.TestCase):
126159

0 commit comments

Comments
 (0)