Repository: python-babel/babel (python)

Issue:
format functions fail on infinity
``` python
>>> format_decimal(Decimal('Infinity'))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "babel/numbers.py", line 165, in format_decimal
    return pattern.apply(number, locale)
  File "babel/numbers.py", line 563, in apply
    self.frac_prec[1]))
  File "babel/numbers.py", line 380, in bankersround
    a, b = split_number(value)
  File "babel/numbers.py", line 330, in split_number
    for i in range(-exp):
TypeError: bad operand type for unary -: 'str'
```


Pull request description:
Support for formatting NaN, Infinity
This came up while I was looking for ways to improve type-safety - specifically that the type of `decimal_tuple.exponent` in `get_decimal_precision()` is `int | Literal['n', 'N', 'F']` and can't be compared with 0 or taken the absolute value of if it is not an int.

This fixes that type issue but I'm making this a separate PR since it also fixes NaN and Infinity cases in other areas, so it won't raise exceptions when formatting.

Fixes #132 

```py
>>> format_decimal(decimal.Decimal('Infinity'))
'∞'
>>> format_decimal(decimal.Decimal('-Infinity'))
'-∞'
>>> format_decimal(decimal.Decimal('NaN'))
'NaN'
```

Patch:
diff --git a/babel/numbers.py b/babel/numbers.py
index d107046..ee9a133 100644
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -126,7 +126,10 @@ def get_currency_name(
     """
     loc = Locale.parse(locale)
     if count is not None:
-        plural_form = loc.plural_form(count)
+        try:
+            plural_form = loc.plural_form(count)
+        except (OverflowError, ValueError):
+            plural_form = 'other'
         plural_names = loc._data['currency_names_plural']
         if currency in plural_names:
             currency_plural_names = plural_names[currency]
@@ -371,6 +374,17 @@ def get_group_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
     return Locale.parse(locale).number_symbols.get('group', ',')
 
 
+def get_infinity_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
+    """Return the symbol used by the locale to represent infinity.
+
+    >>> get_infinity_symbol('en_US')
+    u'∞'
+
+    :param locale: the `Locale` object or locale identifier
+    """
+    return Locale.parse(locale).number_symbols.get('infinity', '∞')
+
+
 def format_number(number: float | decimal.Decimal | str, locale: Locale | str | None = LC_NUMERIC) -> str:
     """Return the given number formatted for a specific locale.
 
@@ -400,7 +414,8 @@ def get_decimal_precision(number: decimal.Decimal) -> int:
     # Copied from: https://github.com/mahmoud/boltons/pull/59
     assert isinstance(number, decimal.Decimal)
     decimal_tuple = number.normalize().as_tuple()
-    if decimal_tuple.exponent >= 0:
+    # Note: DecimalTuple.exponent can be 'n' (qNaN), 'N' (sNaN), or 'F' (Infinity)
+    if not isinstance(decimal_tuple.exponent, int) or decimal_tuple.exponent >= 0:
         return 0
     return abs(decimal_tuple.exponent)
 
@@ -515,6 +530,8 @@ def _get_compact_format(
     """
     if not isinstance(number, decimal.Decimal):
         number = decimal.Decimal(str(number))
+    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):
         if abs(number) >= magnitude:
@@ -1287,6 +1304,9 @@ class NumberPattern:
         return value + ret
 
     def _quantize_value(self, value: decimal.Decimal, locale: Locale | str | None, frac_prec: tuple[int, int], group_separator: bool) -> str:
+        # If the number is +/-Infinity, we can't quantize it
+        if value.is_infinite():
+            return get_infinity_symbol(locale)
         quantum = get_decimal_quantum(frac_prec[1])
         rounded = value.quantize(quantum)
         a, sep, b = f"{rounded:f}".partition(".")


Test patch:
diff --git a/tests/test_numbers.py b/tests/test_numbers.py
index c8a04d6..d6028b3 100644
--- a/tests/test_numbers.py
+++ b/tests/test_numbers.py
@@ -111,6 +111,16 @@ class FormatDecimalTestCase(unittest.TestCase):
         number = decimal.Decimal("7E-7")
         assert numbers.format_decimal(number, format="@@@", locale='en_US') == '0.000000700'
 
+    def test_nan_and_infinity(self):
+        assert numbers.format_decimal(decimal.Decimal('Infinity'), locale='en_US') == '∞'
+        assert numbers.format_decimal(decimal.Decimal('-Infinity'), locale='en_US') == '-∞'
+        assert numbers.format_decimal(decimal.Decimal('NaN'), locale='en_US') == 'NaN'
+        assert numbers.format_compact_decimal(decimal.Decimal('Infinity'), locale='en_US', format_type="short") == '∞'
+        assert numbers.format_compact_decimal(decimal.Decimal('-Infinity'), locale='en_US', format_type="short") == '-∞'
+        assert numbers.format_compact_decimal(decimal.Decimal('NaN'), locale='en_US', format_type="short") == 'NaN'
+        assert numbers.format_currency(decimal.Decimal('Infinity'), 'USD', locale='en_US') == '$∞'
+        assert numbers.format_currency(decimal.Decimal('-Infinity'), 'USD', locale='en_US') == '-$∞'
+
     def test_group_separator(self):
         assert numbers.format_decimal(29567.12, locale='en_US', group_separator=False) == '29567.12'
         assert numbers.format_decimal(29567.12, locale='fr_CA', group_separator=False) == '29567,12'