Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions babel/numbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1528,15 +1528,15 @@ def apply(
# Render scientific notation.
if self.exp_prec:
number = ''.join([
self._quantize_value(value, locale, frac_prec, group_separator, numbering_system=numbering_system),
self._quantize_value(value, locale, frac_prec, group_separator, numbering_system=numbering_system, is_negative=is_negative),
get_exponential_symbol(locale, numbering_system=numbering_system),
exp_sign, # type: ignore # exp_sign is always defined here
self._format_int(str(exp), self.exp_prec[0], self.exp_prec[1], locale, numbering_system=numbering_system), # type: ignore # exp is always defined here
]) # fmt: skip

# Is it a significant digits pattern?
elif '@' in self.pattern:
text = self._format_significant(value, self.int_prec[0], self.int_prec[1])
text = self._format_significant(value, self.int_prec[0], self.int_prec[1], is_negative=is_negative)
a, sep, b = text.partition(".")
number = self._format_int(a, 0, 1000, locale, numbering_system=numbering_system)
if sep:
Expand All @@ -1550,6 +1550,7 @@ def apply(
frac_prec,
group_separator,
numbering_system=numbering_system,
is_negative=is_negative,
)

retval = ''.join(
Expand Down Expand Up @@ -1591,10 +1592,17 @@ def apply(
# - Restore the original position of the decimal point, potentially
# padding with zeroes on either side
#
def _format_significant(self, value: decimal.Decimal, minimum: int, maximum: int) -> str:
def _format_significant(self, value: decimal.Decimal, minimum: int, maximum: int, *, is_negative: bool = False) -> str:
exp = value.adjusted()
scale = maximum - 1 - exp
digits = str(value.scaleb(scale).quantize(decimal.Decimal(1)))
# Round using the value's original sign so the directional rounding
# modes (ROUND_CEILING, ROUND_FLOOR) round toward the correct infinity
# for negative numbers. The magnitude alone is formatted here; the sign
# is rendered separately via the pattern's affixes.
scaled = value.scaleb(scale)
if is_negative:
scaled = scaled.copy_negate()
digits = str(scaled.quantize(decimal.Decimal(1)).copy_abs())
if scale <= 0:
result = digits + '0' * -scale
else:
Expand Down Expand Up @@ -1639,12 +1647,18 @@ def _quantize_value(
group_separator: bool,
*,
numbering_system: Literal["default"] | str,
is_negative: bool = False,
) -> str:
# If the number is +/-Infinity, we can't quantize it
if value.is_infinite():
return get_infinity_symbol(locale, numbering_system=numbering_system)
quantum = get_decimal_quantum(frac_prec[1])
rounded = value.quantize(quantum)
# Round using the value's original sign so the directional rounding
# modes (ROUND_CEILING, ROUND_FLOOR) round toward the correct infinity
# for negative numbers. The magnitude alone is formatted here; the sign
# is rendered separately via the pattern's affixes.
signed_value = value.copy_negate() if is_negative else value
rounded = signed_value.quantize(quantum).copy_abs()
a, sep, b = f"{rounded:f}".partition(".")
integer_part = a
if group_separator:
Expand Down
28 changes: 28 additions & 0 deletions tests/test_numbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,34 @@ def test_format_decimal_quantization():
'0.9999999999', locale=locale_code, decimal_quantization=False).endswith('9999999999') is True


@pytest.mark.parametrize('rounding, expected', [
(decimal.ROUND_CEILING, '-100'),
(decimal.ROUND_FLOOR, '-101'),
(decimal.ROUND_UP, '-101'),
(decimal.ROUND_DOWN, '-100'),
(decimal.ROUND_HALF_UP, '-101'),
(decimal.ROUND_HALF_EVEN, '-101'),
])
def test_format_decimal_negative_directional_rounding(rounding, expected):
# The directional rounding modes (ROUND_CEILING / ROUND_FLOOR) must round
# a negative number toward the correct infinity, matching decimal.quantize
# on the signed value, rather than rounding its magnitude.
# See https://github.com/python-babel/babel/issues/1096
with decimal.localcontext(decimal.Context(rounding=rounding)):
assert numbers.format_decimal(
decimal.Decimal('-100.75'), format='#', locale='en_US') == expected


def test_format_decimal_negative_directional_rounding_significant():
# The significant-digits path must honor the sign for directional rounding too.
with decimal.localcontext(decimal.Context(rounding=decimal.ROUND_CEILING)):
assert numbers.format_decimal(
decimal.Decimal('-100.75'), format='@@', locale='en_US') == '-100'
with decimal.localcontext(decimal.Context(rounding=decimal.ROUND_FLOOR)):
assert numbers.format_decimal(
decimal.Decimal('-100.75'), format='@@', locale='en_US') == '-110'


def test_format_currency():
assert (numbers.format_currency(1099.98, 'USD', locale='en_US')
== '$1,099.98')
Expand Down