pandas-dev/pandas · #69647

BUG: fix repr of longdouble values outside float64 range (GH#17809)

jbrockmendel · merged Oct 3, 20263 files · 73 + / 10 −
doc/source/whatsnew/v3.2.0.rst1 + / 0 −
@@ -180,6 +180,7 @@ MultiIndex I/O ^^^ - Bug in :meth:`DataFrame.to_parquet` with ``engine="pyarrow"`` where a misspelled keyword argument truncated an existing file at the destination or was reported as an error about the path (:issue:`45815`)+- Bug in the repr of a :class:`Series` or :class:`DataFrame` with ``longdouble`` (e.g. ``float128``) dtype showing values outside the ``float64`` range as ``0`` or ``inf`` (:issue:`17809`) -  Period
pandas/io/formats/format.py38 + / 10 −
@@ -23,6 +23,7 @@ from typing import (     TYPE_CHECKING,     Any,+    Literal,     cast, ) import warnings@@ -1373,6 +1374,26 @@ def formatter(value):          return formatter +    def _fixed_width_format(self, kind: Literal["f", "e"]) -> Callable:+        # np.longdouble.__format__ casts to float64, which turns values outside+        # its range into 0 or inf, so format those with numpy (GH#17809)+        if (+            self.values.dtype == np.longdouble+            and np.finfo(np.longdouble).precision > np.finfo(np.float64).precision+        ):+            return partial(+                _format_longdouble,+                kind=kind,+                digits=self.digits,+                leading_space=self.leading_space,+            )++        if self.leading_space is True:+            fmt_str = "{value: .{digits:d}{kind}}"+        else:+            fmt_str = "{value:.{digits:d}{kind}}"+        return partial(fmt_str.format, digits=self.digits, kind=kind)+     def get_result_as_array(self) -> np.ndarray:         """         Returns the float values converted into strings using@@ -1462,11 +1483,7 @@ def format_values_with(float_format):         float_format: FloatFormatType | None         if self.float_format is None:             if self.fixed_width:-                if self.leading_space is True:-                    fmt_str = "{value: .{digits:d}f}"-                else:-                    fmt_str = "{value:.{digits:d}f}"-                float_format = partial(fmt_str.format, digits=self.digits)+                float_format = self._fixed_width_format("f")             else:                 float_format = self.float_format         else:@@ -1495,11 +1512,7 @@ def format_values_with(float_format):         has_small_values = ((abs_vals < 10 ** (-self.digits)) & (abs_vals > 0)).any()          if has_small_values or (too_long and has_large_values):-            if self.leading_space is True:-                fmt_str = "{value: .{digits:d}e}"-            else:-                fmt_str = "{value:.{digits:d}e}"-            float_format = partial(fmt_str.format, digits=self.digits)+            float_format = self._fixed_width_format("e")             formatted_values = format_values_with(float_format)          return formatted_values@@ -1864,6 +1877,21 @@ def _make_fixed_width(     return result, max_len  +def _format_longdouble(+    value, kind: Literal["f", "e"], digits: int, leading_space: bool | None+) -> str:+    np_format = (+        np.format_float_scientific if kind == "e" else np.format_float_positional+    )+    # at precision 0, numpy keeps the trailing decimal point+    # (e.g. "2.") where str.format drops it; strip it to match+    trim: Literal["-", "k"] = "-" if digits == 0 else "k"+    result = np_format(value, precision=digits, unique=False, trim=trim)+    if leading_space is True and not result.startswith("-"):+        result = " " + result+    return result++ def _trim_zeros_complex(str_complexes: ArrayLike, decimal: str = ".") -> list[str]:     """     Separates the real and imaginary parts from the complex number, and
pandas/tests/io/formats/test_format.py34 + / 0 −
@@ -2175,6 +2175,40 @@ def test_too_long(self):             df = pd.DataFrame({"x": [12345.6789, 2e6]})             assert str(df) == "            x\n0  1.2346e+04\n1  2.0000e+06" +    @pytest.mark.skipif(+        np.finfo(np.longdouble).precision <= np.finfo(np.float64).precision,+        reason="longdouble is no wider than float64 on this platform",+    )+    @pytest.mark.parametrize(+        "values, expected",+        [+            (+                ["1e-300", "1e-400", "-1e-500"],+                "0    1.000000e-300\n1    1.000000e-400\n2   -1.000000e-500",+            ),+            (["1.5", "1e400"], "0     1.500000e+00\n1    1.000000e+400"),+            (["1.5", "-2.25"], "0    1.50\n1   -2.25"),+        ],+    )+    def test_longdouble_outside_float64_range(self, values, expected):+        # GH#17809+        ser = pd.Series(np.array(values, dtype=np.longdouble))+        assert str(ser) == f"{expected}\ndtype: {ser.dtype}"++    @pytest.mark.skipif(+        np.finfo(np.longdouble).precision <= np.finfo(np.float64).precision,+        reason="longdouble is no wider than float64 on this platform",+    )+    def test_longdouble_outside_float64_range_precision_zero(self):+        # GH#17809: at display.precision=0, numpy keeps a trailing decimal+        # point that str.format drops; match float64's output+        with pd.option_context("display.precision", 0):+            ser = pd.Series(np.array(["1.5", "2.25"], dtype=np.longdouble))+            assert str(ser) == f"0    2\n1    2\ndtype: {ser.dtype}"++            ser = pd.Series(np.array(["1.5", "1e-400"], dtype=np.longdouble))+            assert str(ser) == f"0     2e+00\n1    1e-400\ndtype: {ser.dtype}"+  class TestTimedelta64Formatter:     def test_days(self):