pandas-dev/pandas · #69647
BUG: fix repr of longdouble values outside float64 range (GH#17809)
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`) - Periodpandas/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, andpandas/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):