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

feat: allow precision specification when bypassing decimal quantization #1058

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
54 changes: 47 additions & 7 deletions babel/numbers.py
Expand Up @@ -520,6 +520,7 @@ def format_decimal(
format: str | NumberPattern | None = None,
locale: Locale | str | None = LC_NUMERIC,
decimal_quantization: bool = True,
precision: int | None = None,
group_separator: bool = True,
*,
numbering_system: Literal["default"] | str = "latn",
Expand Down Expand Up @@ -560,11 +561,19 @@ def format_decimal(
>>> format_decimal(12345.67, locale='en_US', group_separator=True)
u'12,345.67'

When you bypass locale truncation, you can specify precision:

>>> format_decimal(1.2346, locale='en_US', decimal_quantization=False, precision=2)
u'1.23'
>>> format_decimal(0.3, locale='en_US', decimal_quantization=False, precision=2)
u'0.30'

: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 precision: Optionally specify precision when decimal_quantization is False.
:param group_separator: Boolean to switch group separator on/off in a locale's
number format.
:param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
Expand All @@ -576,7 +585,7 @@ def format_decimal(
format = locale.decimal_formats[format]
pattern = parse_pattern(format)
return pattern.apply(
number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator, numbering_system=numbering_system)
number, locale, decimal_quantization=decimal_quantization, precision=precision, group_separator=group_separator, numbering_system=numbering_system)


def format_compact_decimal(
Expand Down Expand Up @@ -674,6 +683,7 @@ def format_currency(
currency_digits: bool = True,
format_type: Literal["name", "standard", "accounting"] = "standard",
decimal_quantization: bool = True,
precision: int | None = None,
group_separator: bool = True,
*,
numbering_system: Literal["default"] | str = "latn",
Expand Down Expand Up @@ -755,6 +765,11 @@ def format_currency(
>>> format_currency(1099.9876, 'USD', locale='en_US', decimal_quantization=False)
u'$1,099.9876'

When you bypass locale truncation, you can specify precision:

>>> format_currency(1099.9876, 'USD', locale='en_US', decimal_quantization=False, precision=3)
u'$1,099.988'

:param number: the number to format
:param currency: the currency code
:param format: the format string to use
Expand All @@ -763,6 +778,7 @@ 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 precision: Optionally specify precision when decimal_quantization is False.
:param group_separator: Boolean to switch group separator on/off in a locale's
number format.
:param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
Expand All @@ -772,7 +788,7 @@ def format_currency(
if format_type == 'name':
return _format_currency_long_name(number, currency, format=format,
locale=locale, currency_digits=currency_digits,
decimal_quantization=decimal_quantization, group_separator=group_separator,
decimal_quantization=decimal_quantization, precision=precision, group_separator=group_separator,
numbering_system=numbering_system)
locale = Locale.parse(locale)
if format:
Expand All @@ -785,7 +801,7 @@ def format_currency(

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


def _format_currency_long_name(
Expand All @@ -796,6 +812,7 @@ def _format_currency_long_name(
currency_digits: bool = True,
format_type: Literal["name", "standard", "accounting"] = "standard",
decimal_quantization: bool = True,
precision: int | None = None,
group_separator: bool = True,
*,
numbering_system: Literal["default"] | str = "latn",
Expand Down Expand Up @@ -825,7 +842,7 @@ def _format_currency_long_name(

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

return unit_pattern.format(number_part, display_name)

Expand Down Expand Up @@ -887,6 +904,7 @@ def format_percent(
format: str | NumberPattern | None = None,
locale: Locale | str | None = LC_NUMERIC,
decimal_quantization: bool = True,
precision: int | None = None,
group_separator: bool = True,
*,
numbering_system: Literal["default"] | str = "latn",
Expand Down Expand Up @@ -922,11 +940,17 @@ def format_percent(
>>> format_percent(229291.1234, locale='pt_BR', group_separator=True)
u'22.929.112%'

When you bypass locale truncation, you can specify precision:

>>> format_percent(0.0111, locale='en_US', decimal_quantization=False, precision=3)
u'1.110%'

: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 precision: Optionally specify precision when decimal_quantization is False.
:param group_separator: Boolean to switch group separator on/off in a locale's
number format.
:param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
Expand All @@ -938,7 +962,7 @@ def format_percent(
format = locale.percent_formats[None]
pattern = parse_pattern(format)
return pattern.apply(
number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator,
number, locale, decimal_quantization=decimal_quantization, precision=precision, group_separator=group_separator,
numbering_system=numbering_system,
)

Expand All @@ -949,6 +973,7 @@ def format_scientific(
locale: Locale | str | None = LC_NUMERIC,
decimal_quantization: bool = True,
*,
precision: int | None = None,
numbering_system: Literal["default"] | str = "latn",
) -> str:
"""Return value formatted in scientific notation for a specific locale.
Expand All @@ -972,11 +997,17 @@ def format_scientific(
>>> format_scientific(1234.9876, u'#.##E0', locale='en_US', decimal_quantization=False)
u'1.2349876E3'

When you bypass locale truncation, you can specify precision:

>>> format_scientific(000.00100, locale='en_US', decimal_quantization=False, precision=3)
u'1.000E-3'

: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 precision: Optionally specify precision when decimal_quantization is False.
:param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
The special value "default" will use the default numbering system of the locale.
:raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
Expand All @@ -986,7 +1017,7 @@ def format_scientific(
format = locale.scientific_formats[None]
pattern = parse_pattern(format)
return pattern.apply(
number, locale, decimal_quantization=decimal_quantization, numbering_system=numbering_system)
number, locale, decimal_quantization=decimal_quantization, precision=precision, numbering_system=numbering_system)


class NumberFormatError(ValueError):
Expand Down Expand Up @@ -1346,6 +1377,7 @@ def apply(
currency: str | None = None,
currency_digits: bool = True,
decimal_quantization: bool = True,
precision: int | None = None,
force_frac: tuple[int, int] | None = None,
group_separator: bool = True,
*,
Expand All @@ -1371,6 +1403,8 @@ def apply(
strictly matching the CLDR definition for
the locale.
:type decimal_quantization: bool
:param precision: Optionally specify precision when decimal_quantization is False.
:type precision: int|None
:param force_frac: DEPRECATED - a forced override for `self.frac_prec`
for a single formatting invocation.
:param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
Expand Down Expand Up @@ -1407,14 +1441,20 @@ def apply(
else:
frac_prec = self.frac_prec

if decimal_quantization and precision is not None:
raise ValueError("To specify precision, decimal_quantization should be set to False.")

# Bump decimal precision to the natural precision of the number if it
# exceeds the one we're about to use. This adaptative precision is only
# triggered if the decimal quantization is disabled or if a scientific
# notation pattern has a missing mandatory fractional part (as in the
# default '#E0' pattern). This special case has been extensively
# discussed at https://github.com/python-babel/babel/pull/494#issuecomment-307649969 .
if not decimal_quantization or (self.exp_prec and frac_prec == (0, 0)):
frac_prec = (frac_prec[0], max([frac_prec[1], get_decimal_precision(value)]))
if not decimal_quantization and precision is not None:
frac_prec = (precision, precision)
else:
frac_prec = (frac_prec[0], max([frac_prec[1], get_decimal_precision(value)]))

# Render scientific notation.
if self.exp_prec:
Expand Down