From a9dc7ba6e6045d8566f862439509e92ff60c9d7b Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Sun, 16 Oct 2022 18:12:58 -0600 Subject: [PATCH 01/13] feat: Added compact decimal formats --- babel/core.py | 12 ++++++++++++ babel/numbers.py | 36 +++++++++++++++++++++++++++++++++++- scripts/import_cldr.py | 2 -- tests/test_numbers.py | 27 +++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 3 deletions(-) diff --git a/babel/core.py b/babel/core.py index 9393c2394..220cbaf0a 100644 --- a/babel/core.py +++ b/babel/core.py @@ -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"] + + """ + return self._data['compact_decimal_formats'] + @property def currency_formats(self): """Locale patterns for currency number formatting. diff --git a/babel/numbers.py b/babel/numbers.py index b8971bcbc..79a0e19e3 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -375,8 +375,12 @@ def get_decimal_quantum(precision): return decimal.Decimal(10) ** (-precision) +class UnknownCompactFormat(KeyError): + """Exception raised when an unknown compact format is requested.""" + + 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, compact=None, compact_fraction_digits=0): u"""Return the given decimal number formatted for a specific locale. >>> format_decimal(1.2345, locale='en_US') @@ -408,6 +412,14 @@ 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', compact="short") + u'12K' + >>> format_decimal(12345, locale='en_US', compact="long") + u'12 thousand' + >>> format_decimal(12345, locale='en_US', compact="short", compact_fraction_digits=2) + u'12.35K' + >>> format_decimal(1234567, locale='ja_JP', compact="short") + u'123万' :param number: the number to format :param format: @@ -416,8 +428,30 @@ 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 compact: Compact format to use ("short" or "long"). Defaults to `None`. + :param compact_fraction_digits: Number of fraction digits to use in compact + format. Defaults to `0`. If this is set to + a value greater than `0`, the `decimal_quantization` + will be treated as `False`. """ locale = Locale.parse(locale) + if compact: + try: + compact_format = locale.compact_decimal_formats[compact] + for magnitude in sorted([int(m) for m in compact_format["other"]], reverse=True): + if number >= magnitude: + format = compact_format["other"][str(magnitude)] + pattern = parse_pattern(format).pattern + if pattern != "0" and number >= 1000: + number = number / (magnitude / (10 ** (pattern.count("0") - 1))) + if float(number) == 1.0 and "one" in compact_format: + format = compact_format["one"][str(magnitude)] + if compact_fraction_digits > 0: + decimal_quantization = False + number = round(number, compact_fraction_digits) + break + except KeyError as e: + raise UnknownCompactFormat("%r is not a known compact format" % e.args[0]) if not format: format = locale.decimal_formats.get(format) pattern = parse_pattern(format) diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 7fc8538d7..92dd27234 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -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'], {}) diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 78f779758..6b5494e67 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -121,6 +121,33 @@ 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): + assert numbers.format_decimal(1, locale='en_US', compact="short") == u'1' + assert numbers.format_decimal(999, locale='en_US', compact="short") == u'999' + assert numbers.format_decimal(1000, locale='en_US', compact="short") == u'1K' + assert numbers.format_decimal(9000, locale='en_US', compact="short") == u'9K' + assert numbers.format_decimal(9123, locale='en_US', compact="short", compact_fraction_digits=2) == u'9.12K' + assert numbers.format_decimal(10000, locale='en_US', compact="short") == u'10K' + assert numbers.format_decimal(10000, locale='en_US', compact="short", compact_fraction_digits=2) == u'10K' + assert numbers.format_decimal(1000000, locale='en_US', compact="short") == u'1M' + assert numbers.format_decimal(9000999, locale='en_US', compact="short") == u'9M' + assert numbers.format_decimal(9000900099, locale='en_US', compact="short", compact_fraction_digits=5) == u'9.0009B' + assert numbers.format_decimal(1, locale='en_US', compact="long") == u'1' + assert numbers.format_decimal(999, locale='en_US', compact="long") == u'999' + assert numbers.format_decimal(1000, locale='en_US', compact="long") == u'1 thousand' + assert numbers.format_decimal(9000, locale='en_US', compact="long") == u'9 thousand' + assert numbers.format_decimal(9000, locale='en_US', compact="long", compact_fraction_digits=2) == u'9 thousand' + assert numbers.format_decimal(10000, locale='en_US', compact="long") == u'10 thousand' + assert numbers.format_decimal(10000, locale='en_US', compact="long", compact_fraction_digits=2) == u'10 thousand' + assert numbers.format_decimal(1000000, locale='en_US', compact="long") == u'1 million' + assert numbers.format_decimal(9999999, locale='en_US', compact="long") == u'10 million' + assert numbers.format_decimal(9999999999, locale='en_US', compact="long", compact_fraction_digits=5) == u'10 billion' + assert numbers.format_decimal(1, locale='ja_JP', compact="short") == u'1' + assert numbers.format_decimal(999, locale='ja_JP', compact="short") == u'999' + assert numbers.format_decimal(1000, locale='ja_JP', compact="short") == u'1000' + assert numbers.format_decimal(9123, locale='ja_JP', compact="short") == u'9123' + assert numbers.format_decimal(10000, locale='ja_JP', compact="short") == u'1万' + assert numbers.format_decimal(1234567, locale='ja_JP', compact="long") == u'123万' class NumberParsingTestCase(unittest.TestCase): From 36739c328f7e47208302fd07cdf539d6b532302d Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Sun, 16 Oct 2022 18:22:05 -0600 Subject: [PATCH 02/13] refactor: reduce nesting --- babel/numbers.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/babel/numbers.py b/babel/numbers.py index 79a0e19e3..24f1f2fe9 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -438,20 +438,20 @@ def format_decimal( if compact: try: compact_format = locale.compact_decimal_formats[compact] - for magnitude in sorted([int(m) for m in compact_format["other"]], reverse=True): - if number >= magnitude: - format = compact_format["other"][str(magnitude)] - pattern = parse_pattern(format).pattern - if pattern != "0" and number >= 1000: - number = number / (magnitude / (10 ** (pattern.count("0") - 1))) - if float(number) == 1.0 and "one" in compact_format: - format = compact_format["one"][str(magnitude)] - if compact_fraction_digits > 0: - decimal_quantization = False - number = round(number, compact_fraction_digits) - break except KeyError as e: - raise UnknownCompactFormat("%r is not a known compact format" % e.args[0]) + raise UnknownCompactFormat("%r is not a known compact format" % e.args[0]) + for magnitude in sorted([int(m) for m in compact_format["other"]], reverse=True): + if number >= magnitude: + format = compact_format["other"][str(magnitude)] + pattern = parse_pattern(format).pattern + if pattern != "0" and number >= 1000: + number = number / (magnitude / (10 ** (pattern.count("0") - 1))) + if float(number) == 1.0 and "one" in compact_format: + format = compact_format["one"][str(magnitude)] + break + if compact_fraction_digits > 0: + decimal_quantization = False + number = round(number, compact_fraction_digits) if not format: format = locale.decimal_formats.get(format) pattern = parse_pattern(format) From 9128128f80a4993c8e289ef22b9ba01b72e0d71c Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Wed, 19 Oct 2022 01:48:10 +0000 Subject: [PATCH 03/13] Fix negative number formatting using abs --- babel/numbers.py | 9 +++++---- tests/test_numbers.py | 4 ++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/babel/numbers.py b/babel/numbers.py index 24f1f2fe9..c45cb7406 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -380,7 +380,8 @@ class UnknownCompactFormat(KeyError): def format_decimal( - number, format=None, locale=LC_NUMERIC, decimal_quantization=True, group_separator=True, compact=None, compact_fraction_digits=0): + number, format=None, locale=LC_NUMERIC, decimal_quantization=True, group_separator=True, + compact=None, compact_fraction_digits=0): u"""Return the given decimal number formatted for a specific locale. >>> format_decimal(1.2345, locale='en_US') @@ -439,12 +440,12 @@ def format_decimal( try: compact_format = locale.compact_decimal_formats[compact] except KeyError as e: - raise UnknownCompactFormat("%r is not a known compact format" % e.args[0]) + raise UnknownCompactFormat("%r is not a known compact format" % e.args[0]) from e for magnitude in sorted([int(m) for m in compact_format["other"]], reverse=True): - if number >= magnitude: + if abs(number) >= magnitude: format = compact_format["other"][str(magnitude)] pattern = parse_pattern(format).pattern - if pattern != "0" and number >= 1000: + if pattern != "0" and abs(number) >= 1000: number = number / (magnitude / (10 ** (pattern.count("0") - 1))) if float(number) == 1.0 and "one" in compact_format: format = compact_format["one"][str(magnitude)] diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 6b5494e67..2b1c68130 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -148,6 +148,10 @@ def test_compact(self): assert numbers.format_decimal(9123, locale='ja_JP', compact="short") == u'9123' assert numbers.format_decimal(10000, locale='ja_JP', compact="short") == u'1万' assert numbers.format_decimal(1234567, locale='ja_JP', compact="long") == u'123万' + assert numbers.format_decimal(-1, locale='en_US', compact="short") == u'-1' + assert numbers.format_decimal(-1234, locale='en_US', compact="short", compact_fraction_digits=2) == u'-1.23K' + assert numbers.format_decimal(-123456789, compact='short', locale='en_US') == u'-123M' + assert numbers.format_decimal(-123456789, compact='long', locale='en_US') == u'-123 million' class NumberParsingTestCase(unittest.TestCase): From 0ed0637140d2e5c8ac883369748428c165548ca3 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Wed, 19 Oct 2022 05:53:02 +0000 Subject: [PATCH 04/13] refactor and changed `compact` to `format_type` --- babel/numbers.py | 68 ++++++++++++++++++++++++++----------------- tests/test_numbers.py | 60 +++++++++++++++++++------------------- 2 files changed, 71 insertions(+), 57 deletions(-) diff --git a/babel/numbers.py b/babel/numbers.py index c45cb7406..9e3ed998b 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -375,13 +375,9 @@ def get_decimal_quantum(precision): return decimal.Decimal(10) ** (-precision) -class UnknownCompactFormat(KeyError): - """Exception raised when an unknown compact format is requested.""" - - def format_decimal( number, format=None, locale=LC_NUMERIC, decimal_quantization=True, group_separator=True, - compact=None, compact_fraction_digits=0): + 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') @@ -413,13 +409,13 @@ 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', compact="short") + >>> format_decimal(12345, locale='en_US', format_type="short") u'12K' - >>> format_decimal(12345, locale='en_US', compact="long") + >>> format_decimal(12345, locale='en_US', format_type="long") u'12 thousand' - >>> format_decimal(12345, locale='en_US', compact="short", compact_fraction_digits=2) + >>> format_decimal(12345, locale='en_US', format_type="short", compact_fraction_digits=2) u'12.35K' - >>> format_decimal(1234567, locale='ja_JP', compact="short") + >>> format_decimal(1234567, locale='ja_JP', format_type="short") u'123万' :param number: the number to format @@ -429,37 +425,55 @@ 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 compact: Compact format to use ("short" or "long"). Defaults to `None`. - :param compact_fraction_digits: Number of fraction digits to use in compact + :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`. """ locale = Locale.parse(locale) - if compact: - try: - compact_format = locale.compact_decimal_formats[compact] - except KeyError as e: - raise UnknownCompactFormat("%r is not a known compact format" % e.args[0]) from e - for magnitude in sorted([int(m) for m in compact_format["other"]], reverse=True): - if abs(number) >= magnitude: - format = compact_format["other"][str(magnitude)] - pattern = parse_pattern(format).pattern - if pattern != "0" and abs(number) >= 1000: - number = number / (magnitude / (10 ** (pattern.count("0") - 1))) - if float(number) == 1.0 and "one" in compact_format: - format = compact_format["one"][str(magnitude)] - break + if format_type in ("short", "long"): + number, format = _get_compact_format(number, format_type, locale, compact_fraction_digits) + # use the default decimal format if the number has no compact format + format_type = None + # if compact_fraction_digits is set, we don't want to truncate the fraction digits if compact_fraction_digits > 0: decimal_quantization = False - number = round(number, compact_fraction_digits) if not format: - format = locale.decimal_formats.get(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 1, use the singular format + if float(number) == 1.0 and "one" in compact_format: + format = compact_format["one"][str(magnitude)] + break + return number, format + + class UnknownCurrencyFormatError(KeyError): """Exception raised when an unknown currency format is requested.""" diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 2b1c68130..5ce599a90 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -122,36 +122,36 @@ def test_group_separator(self): assert numbers.format_percent(251234.1234, locale='sv_SE', group_separator=True) == u'25\xa0123\xa0412\xa0%' def test_compact(self): - assert numbers.format_decimal(1, locale='en_US', compact="short") == u'1' - assert numbers.format_decimal(999, locale='en_US', compact="short") == u'999' - assert numbers.format_decimal(1000, locale='en_US', compact="short") == u'1K' - assert numbers.format_decimal(9000, locale='en_US', compact="short") == u'9K' - assert numbers.format_decimal(9123, locale='en_US', compact="short", compact_fraction_digits=2) == u'9.12K' - assert numbers.format_decimal(10000, locale='en_US', compact="short") == u'10K' - assert numbers.format_decimal(10000, locale='en_US', compact="short", compact_fraction_digits=2) == u'10K' - assert numbers.format_decimal(1000000, locale='en_US', compact="short") == u'1M' - assert numbers.format_decimal(9000999, locale='en_US', compact="short") == u'9M' - assert numbers.format_decimal(9000900099, locale='en_US', compact="short", compact_fraction_digits=5) == u'9.0009B' - assert numbers.format_decimal(1, locale='en_US', compact="long") == u'1' - assert numbers.format_decimal(999, locale='en_US', compact="long") == u'999' - assert numbers.format_decimal(1000, locale='en_US', compact="long") == u'1 thousand' - assert numbers.format_decimal(9000, locale='en_US', compact="long") == u'9 thousand' - assert numbers.format_decimal(9000, locale='en_US', compact="long", compact_fraction_digits=2) == u'9 thousand' - assert numbers.format_decimal(10000, locale='en_US', compact="long") == u'10 thousand' - assert numbers.format_decimal(10000, locale='en_US', compact="long", compact_fraction_digits=2) == u'10 thousand' - assert numbers.format_decimal(1000000, locale='en_US', compact="long") == u'1 million' - assert numbers.format_decimal(9999999, locale='en_US', compact="long") == u'10 million' - assert numbers.format_decimal(9999999999, locale='en_US', compact="long", compact_fraction_digits=5) == u'10 billion' - assert numbers.format_decimal(1, locale='ja_JP', compact="short") == u'1' - assert numbers.format_decimal(999, locale='ja_JP', compact="short") == u'999' - assert numbers.format_decimal(1000, locale='ja_JP', compact="short") == u'1000' - assert numbers.format_decimal(9123, locale='ja_JP', compact="short") == u'9123' - assert numbers.format_decimal(10000, locale='ja_JP', compact="short") == u'1万' - assert numbers.format_decimal(1234567, locale='ja_JP', compact="long") == u'123万' - assert numbers.format_decimal(-1, locale='en_US', compact="short") == u'-1' - assert numbers.format_decimal(-1234, locale='en_US', compact="short", compact_fraction_digits=2) == u'-1.23K' - assert numbers.format_decimal(-123456789, compact='short', locale='en_US') == u'-123M' - assert numbers.format_decimal(-123456789, compact='long', locale='en_US') == u'-123 million' + 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' class NumberParsingTestCase(unittest.TestCase): From e4d915e792cef06c279c783e08bb3b336cb0ff2f Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Wed, 19 Oct 2022 06:03:28 +0000 Subject: [PATCH 05/13] refactor: relocate setting default format --- babel/numbers.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/babel/numbers.py b/babel/numbers.py index 9e3ed998b..12d9d4ea7 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -435,12 +435,9 @@ def format_decimal( locale = Locale.parse(locale) if format_type in ("short", "long"): number, format = _get_compact_format(number, format_type, locale, compact_fraction_digits) - # use the default decimal format if the number has no compact format - format_type = None # if compact_fraction_digits is set, we don't want to truncate the fraction digits - if compact_fraction_digits > 0: - decimal_quantization = False - if not format: + decimal_quantization = False if compact_fraction_digits > 0 else decimal_quantization + elif not format: format = locale.decimal_formats.get(format_type) pattern = parse_pattern(format) return pattern.apply( @@ -471,6 +468,7 @@ def _get_compact_format(number, format_type, locale, compact_fraction_digits=0): if float(number) == 1.0 and "one" in compact_format: format = compact_format["one"][str(magnitude)] break + format = format if format is not None else locale.decimal_formats.get(None) return number, format From 497d3745625c4548b6cd5cdc9b31215a81acbe2b Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Wed, 19 Oct 2022 13:30:01 +0000 Subject: [PATCH 06/13] Use plural_form instead of checking 1 --- babel/numbers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/babel/numbers.py b/babel/numbers.py index 12d9d4ea7..7e0ddd63a 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -465,8 +465,9 @@ def _get_compact_format(number, format_type, locale, compact_fraction_digits=0): # round to the number of fraction digits requested number = round(number, compact_fraction_digits) # if the remaining number is 1, use the singular format - if float(number) == 1.0 and "one" in compact_format: - format = compact_format["one"][str(magnitude)] + 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) return number, format From 99697065e1e8fabe81b6c72768e5f38234302c04 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Sun, 23 Oct 2022 01:17:07 +0000 Subject: [PATCH 07/13] Added pluralization test examples --- babel/numbers.py | 4 ++++ tests/test_numbers.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/babel/numbers.py b/babel/numbers.py index 7e0ddd63a..fc2164183 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -417,6 +417,10 @@ def format_decimal( 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: diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 5ce599a90..179f7eddd 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -152,6 +152,8 @@ def test_compact(self): 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): From 64519272c23cbadd8453d6747a9059eabf57e23d Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Sun, 23 Oct 2022 01:32:07 +0000 Subject: [PATCH 08/13] reword "1" to "singular" in comment --- babel/numbers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/babel/numbers.py b/babel/numbers.py index fc2164183..057972140 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -468,7 +468,7 @@ def _get_compact_format(number, format_type, locale, compact_fraction_digits=0): 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 1, use the singular format + # 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)] From acd1861877a882283405f07330b6e6ae12d2fcd7 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Mon, 31 Oct 2022 07:58:43 -0600 Subject: [PATCH 09/13] style: Use regular if for compact_fraction_digits Co-authored-by: Aarni Koskela --- babel/numbers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/babel/numbers.py b/babel/numbers.py index 057972140..a6eb4b3cf 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -440,7 +440,8 @@ def format_decimal( 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 + if compact_fraction_digits: + decimal_quantization = False elif not format: format = locale.decimal_formats.get(format_type) pattern = parse_pattern(format) From 519d3bb69dbcf9a0d39ccec3c8c7dbd602ddf9b5 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Mon, 31 Oct 2022 07:59:10 -0600 Subject: [PATCH 10/13] Use regular if for fallback Co-authored-by: Aarni Koskela --- babel/numbers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/babel/numbers.py b/babel/numbers.py index a6eb4b3cf..0e8df68f9 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -474,7 +474,8 @@ def _get_compact_format(number, format_type, locale, compact_fraction_digits=0): 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) + if format is None: # Did not find a format, fall back. + format = locale.decimal_formats.get(None) return number, format From bb58a0d5a78d63922296330eaa2e779fe984ae77 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Mon, 31 Oct 2022 14:59:49 +0000 Subject: [PATCH 11/13] refactor: move compact to a separate method --- babel/numbers.py | 61 +++++++++++++++++++++-------------------- tests/test_numbers.py | 64 +++++++++++++++++++++---------------------- 2 files changed, 64 insertions(+), 61 deletions(-) diff --git a/babel/numbers.py b/babel/numbers.py index 0e8df68f9..2424341c8 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -376,8 +376,7 @@ def get_decimal_quantum(precision): def format_decimal( - number, format=None, locale=LC_NUMERIC, decimal_quantization=True, group_separator=True, - format_type=None, compact_fraction_digits=0): + number, format=None, locale=LC_NUMERIC, decimal_quantization=True, group_separator=True): u"""Return the given decimal number formatted for a specific locale. >>> format_decimal(1.2345, locale='en_US') @@ -409,18 +408,6 @@ 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: @@ -429,27 +416,43 @@ 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`. """ locale = Locale.parse(locale) - 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 - if compact_fraction_digits: - decimal_quantization = False - elif not format: - format = locale.decimal_formats.get(format_type) + if not format: + format = locale.decimal_formats.get(format) 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): +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. + + >>> format_compact_decimal(12345, format_type="short") + u'12K' + >>> format_compact_decimal(12345, format_type="long") + u'12 thousand' + >>> format_compact_decimal(12345, format_type="short", 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. @@ -468,7 +471,7 @@ def _get_compact_format(number, format_type, locale, compact_fraction_digits=0): # 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) + 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" diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 179f7eddd..1b955c95e 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -122,38 +122,38 @@ def test_group_separator(self): assert numbers.format_percent(251234.1234, locale='sv_SE', group_separator=True) == u'25\xa0123\xa0412\xa0%' def test_compact(self): - 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 милион' + 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): From 6e768be7d755c7af35e929481dd411c32493ed81 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Mon, 31 Oct 2022 15:04:48 +0000 Subject: [PATCH 12/13] docs: Update documentation --- docs/api/numbers.rst | 2 ++ docs/numbers.rst | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/api/numbers.rst b/docs/api/numbers.rst index f9b0833a2..eac569206 100644 --- a/docs/api/numbers.rst +++ b/docs/api/numbers.rst @@ -13,6 +13,8 @@ Number Formatting .. autofunction:: format_decimal +.. autofunction:: format_compact_decimal + .. autofunction:: format_currency .. autofunction:: format_percent diff --git a/docs/numbers.rst b/docs/numbers.rst index ed3b60f13..cbe05cdef 100644 --- a/docs/numbers.rst +++ b/docs/numbers.rst @@ -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: From 6a296079281ec7c3f13ada736a7f14ede7d6b4f4 Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Mon, 31 Oct 2022 15:22:16 +0000 Subject: [PATCH 13/13] docs: Update docstring and doctests --- babel/numbers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/babel/numbers.py b/babel/numbers.py index 2424341c8..192e3ed6e 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -426,13 +426,13 @@ def format_decimal( 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. + u"""Return the given decimal number formatted for a specific locale in compact form. - >>> format_compact_decimal(12345, format_type="short") + >>> format_compact_decimal(12345, format_type="short", locale='en_US') u'12K' - >>> format_compact_decimal(12345, format_type="long") + >>> format_compact_decimal(12345, format_type="long", locale='en_US') u'12 thousand' - >>> format_compact_decimal(12345, format_type="short", fraction_digits=2) + >>> 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万'