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

Added group_separator feature in number formatting #726

Merged
merged 4 commits into from Sep 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
58 changes: 41 additions & 17 deletions babel/numbers.py
Expand Up @@ -373,7 +373,7 @@ def get_decimal_quantum(precision):


def format_decimal(
number, format=None, locale=LC_NUMERIC, decimal_quantization=True):
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')
Expand Down Expand Up @@ -401,19 +401,25 @@ def format_decimal(
u'1.235'
>>> format_decimal(1.2346, locale='en_US', decimal_quantization=False)
u'1.2346'
>>> format_decimal(12345.67, locale='fr_CA', group_separator=False)
u'12345,67'
>>> format_decimal(12345.67, locale='en_US', group_separator=True)
u'12,345.67'

:param number: the number to format
:param format:
:param locale: the `Locale` object or locale identifier
:param decimal_quantization: Truncate and round high-precision numbers to
the format pattern. Defaults to `True`.
:param group_separator: Boolean to switch group separator on/off in a locale's
number format.
"""
locale = Locale.parse(locale)
if not format:
format = locale.decimal_formats.get(format)
pattern = parse_pattern(format)
return pattern.apply(
number, locale, decimal_quantization=decimal_quantization)
number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator)


class UnknownCurrencyFormatError(KeyError):
Expand All @@ -422,7 +428,7 @@ class UnknownCurrencyFormatError(KeyError):

def format_currency(
number, currency, format=None, locale=LC_NUMERIC, currency_digits=True,
format_type='standard', decimal_quantization=True):
format_type='standard', decimal_quantization=True, group_separator=True):
u"""Return formatted currency value.

>>> format_currency(1099.98, 'USD', locale='en_US')
Expand Down Expand Up @@ -472,6 +478,12 @@ def format_currency(
...
UnknownCurrencyFormatError: "'unknown' is not a known currency format type"

>>> format_currency(101299.98, 'USD', locale='en_US', group_separator=False)
u'$101299.98'

>>> format_currency(101299.98, 'USD', locale='en_US', group_separator=True)
u'$101,299.98'

You can also pass format_type='name' to use long display names. The order of
the number and currency name, along with the correct localized plural form
of the currency name, is chosen according to locale:
Expand Down Expand Up @@ -500,12 +512,14 @@ def format_currency(
:param format_type: the currency format type to use
:param decimal_quantization: Truncate and round high-precision numbers to
the format pattern. Defaults to `True`.
:param group_separator: Boolean to switch group separator on/off in a locale's
number format.

"""
if format_type == 'name':
return _format_currency_long_name(number, currency, format=format,
locale=locale, currency_digits=currency_digits,
decimal_quantization=decimal_quantization)
decimal_quantization=decimal_quantization, group_separator=group_separator)
locale = Locale.parse(locale)
if format:
pattern = parse_pattern(format)
Expand All @@ -518,12 +532,12 @@ def format_currency(

return pattern.apply(
number, locale, currency=currency, currency_digits=currency_digits,
decimal_quantization=decimal_quantization)
decimal_quantization=decimal_quantization, group_separator=group_separator)


def _format_currency_long_name(
number, currency, format=None, locale=LC_NUMERIC, currency_digits=True,
format_type='standard', decimal_quantization=True):
format_type='standard', decimal_quantization=True, group_separator=True):
# Algorithm described here:
# https://www.unicode.org/reports/tr35/tr35-numbers.html#Currencies
locale = Locale.parse(locale)
Expand Down Expand Up @@ -552,13 +566,13 @@ def _format_currency_long_name(

number_part = pattern.apply(
number, locale, currency=currency, currency_digits=currency_digits,
decimal_quantization=decimal_quantization)
decimal_quantization=decimal_quantization, group_separator=group_separator)

return unit_pattern.format(number_part, display_name)


def format_percent(
number, format=None, locale=LC_NUMERIC, decimal_quantization=True):
number, format=None, locale=LC_NUMERIC, decimal_quantization=True, group_separator=True):
"""Return formatted percent value for a specific locale.

>>> format_percent(0.34, locale='en_US')
Expand All @@ -582,18 +596,26 @@ def format_percent(
>>> format_percent(23.9876, locale='en_US', decimal_quantization=False)
u'2,398.76%'

>>> format_percent(229291.1234, locale='pt_BR', group_separator=False)
u'22929112%'

>>> format_percent(229291.1234, locale='pt_BR', group_separator=True)
u'22.929.112%'

:param number: the percent number to format
:param format:
:param locale: the `Locale` object or locale identifier
:param decimal_quantization: Truncate and round high-precision numbers to
the format pattern. Defaults to `True`.
:param group_separator: Boolean to switch group separator on/off in a locale's
number format.
"""
locale = Locale.parse(locale)
if not format:
format = locale.percent_formats.get(format)
pattern = parse_pattern(format)
return pattern.apply(
number, locale, decimal_quantization=decimal_quantization)
number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator)


def format_scientific(
Expand Down Expand Up @@ -913,6 +935,7 @@ def apply(
currency_digits=True,
decimal_quantization=True,
force_frac=None,
group_separator=True,
):
"""Renders into a string a number following the defined pattern.

Expand Down Expand Up @@ -952,8 +975,8 @@ def apply(
if self.exp_prec:
value, exp, exp_sign = self.scientific_notation_elements(value, locale)

# Adjust the precision of the fractionnal part and force it to the
# currency's if neccessary.
# Adjust the precision of the fractional part and force it to the
# currency's if necessary.
if force_frac:
# TODO (3.x?): Remove this parameter
warnings.warn('The force_frac parameter to NumberPattern.apply() is deprecated.', DeprecationWarning)
Expand All @@ -975,7 +998,7 @@ def apply(
# Render scientific notation.
if self.exp_prec:
number = ''.join([
self._quantize_value(value, locale, frac_prec),
self._quantize_value(value, locale, frac_prec, group_separator),
get_exponential_symbol(locale),
exp_sign,
self._format_int(
Expand All @@ -993,7 +1016,7 @@ def apply(

# A normal number pattern.
else:
number = self._quantize_value(value, locale, frac_prec)
number = self._quantize_value(value, locale, frac_prec, group_separator)

retval = ''.join([
self.prefix[is_negative],
Expand Down Expand Up @@ -1060,13 +1083,14 @@ def _format_int(self, value, min, max, locale):
gsize = self.grouping[1]
return value + ret

def _quantize_value(self, value, locale, frac_prec):
def _quantize_value(self, value, locale, frac_prec, group_separator):
quantum = get_decimal_quantum(frac_prec[1])
rounded = value.quantize(quantum)
a, sep, b = "{:f}".format(rounded).partition(".")
number = (self._format_int(a, self.int_prec[0],
self.int_prec[1], locale) +
self._format_frac(b or '0', locale, frac_prec))
integer_part = a
if group_separator:
integer_part = self._format_int(a, self.int_prec[0], self.int_prec[1], locale)
number = integer_part + self._format_frac(b or '0', locale, frac_prec)
return number

def _format_frac(self, value, locale, force_frac=None):
Expand Down
30 changes: 30 additions & 0 deletions tests/test_numbers.py
Expand Up @@ -153,6 +153,36 @@ def test_formatting_of_very_small_decimals(self):
fmt = numbers.format_decimal(number, format="@@@", locale='en_US')
self.assertEqual('0.000000700', fmt)

def test_group_separator(self):
self.assertEqual('29567.12', numbers.format_decimal(29567.12,
locale='en_US', group_separator=False))
self.assertEqual('29567,12', numbers.format_decimal(29567.12,
locale='fr_CA', group_separator=False))
self.assertEqual('29567,12', numbers.format_decimal(29567.12,
locale='pt_BR', group_separator=False))
self.assertEqual(u'$1099.98', numbers.format_currency(1099.98, 'USD',
locale='en_US', group_separator=False))
self.assertEqual(u'101299,98\xa0€', numbers.format_currency(101299.98, 'EUR',
locale='fr_CA', group_separator=False))
self.assertEqual('101299.98 euros', numbers.format_currency(101299.98, 'EUR',
locale='en_US', group_separator=False, format_type='name'))
self.assertEqual(u'25123412\xa0%', numbers.format_percent(251234.1234, locale='sv_SE', group_separator=False))

self.assertEqual(u'29,567.12', numbers.format_decimal(29567.12,
locale='en_US', group_separator=True))
self.assertEqual(u'29\u202f567,12', numbers.format_decimal(29567.12,
locale='fr_CA', group_separator=True))
self.assertEqual(u'29.567,12', numbers.format_decimal(29567.12,
locale='pt_BR', group_separator=True))
self.assertEqual(u'$1,099.98', numbers.format_currency(1099.98, 'USD',
locale='en_US', group_separator=True))
self.assertEqual(u'101\u202f299,98\xa0\u20ac', numbers.format_currency(101299.98, 'EUR',
locale='fr_CA', group_separator=True))
self.assertEqual(u'101,299.98 euros', numbers.format_currency(101299.98, 'EUR',
locale='en_US', group_separator=True,
format_type='name'))
self.assertEqual(u'25\xa0123\xa0412\xa0%', numbers.format_percent(251234.1234, locale='sv_SE', group_separator=True))


class NumberParsingTestCase(unittest.TestCase):

Expand Down