diff --git a/babel/numbers.py b/babel/numbers.py index 2ef9031aa..aa989229f 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -668,7 +668,8 @@ def _get_compact_format( if number.is_nan() or number.is_infinite(): return number, None format = None - for magnitude in sorted([int(m) for m in compact_format["other"]], reverse=True): + magnitudes = sorted([int(m) for m in compact_format["other"]], reverse=True) + for index, magnitude in enumerate(magnitudes): if abs(number) >= magnitude: # check the pattern using "other" as the amount format = compact_format["other"][str(magnitude)] @@ -678,17 +679,22 @@ def _get_compact_format( 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 = cast( - decimal.Decimal, - number / (magnitude // (10 ** (pattern.count("0") - 1))), - ) + divisor = magnitude // (10 ** (pattern.count("0") - 1)) + scaled = cast(decimal.Decimal, number / divisor) # round to the number of fraction digits requested - rounded = round(number, fraction_digits) + rounded = round(scaled, fraction_digits) + # Rounding can carry the value up into the next magnitude, e.g. + # 999_999 would otherwise render as "1000K". The CLDR algorithm + # selects the pattern from the rounded value, so redo the lookup + # one magnitude up to get "1M" instead. + if index > 0 and abs(rounded) * divisor >= magnitudes[index - 1]: + bumped = decimal.Decimal(magnitudes[index - 1]).copy_sign(number) + return _get_compact_format(bumped, compact_format, locale, fraction_digits) # if the remaining number is singular, use the singular format - plural_form = locale.plural_form(abs(number)) + plural_form = locale.plural_form(abs(scaled)) if plural_form not in compact_format: plural_form = "other" - if number == 1 and "1" in compact_format: + if scaled == 1 and "1" in compact_format: plural_form = "1" if str(magnitude) not in compact_format[plural_form]: plural_form = "other" # fall back to other as the implicit default diff --git a/tests/test_numbers_format_decimal.py b/tests/test_numbers_format_decimal.py index 356181b69..bb857a80a 100644 --- a/tests/test_numbers_format_decimal.py +++ b/tests/test_numbers_format_decimal.py @@ -175,3 +175,17 @@ def test_compact(): assert numbers.format_compact_decimal( 12345, format_type="short", locale='ar_EG', fraction_digits=2, numbering_system='latn', ) == '12.34\xa0ألف' + + +def test_compact_rounding_carries_to_next_magnitude(): + # Rounding can carry the value up into the next magnitude. The pattern has + # to be chosen from the rounded value, otherwise 999999 renders as the + # nonsensical "1000K" instead of "1M". + assert numbers.format_compact_decimal(999999, locale='en_US', format_type="short") == '1M' + assert numbers.format_compact_decimal(999999999, locale='en_US', format_type="short") == '1B' + assert numbers.format_compact_decimal(999999999999, locale='en_US', format_type="short") == '1T' + assert numbers.format_compact_decimal(-999999, locale='en_US', format_type="short") == '-1M' + assert numbers.format_compact_decimal(999999, locale='en_US', format_type="long") == '1 million' + # Values that do not carry over are unaffected. + assert numbers.format_compact_decimal(999499, locale='en_US', format_type="short") == '999K' + assert numbers.format_compact_decimal(999949, locale='en_US', format_type="short", fraction_digits=1) == '999.9K'