From a545ecbd9ee57b6cc996be3f42fe210cec28ddcd Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 1 Aug 2024 10:30:29 +0300 Subject: [PATCH] mpf.__round__() returns mpf Probably this will look non-intuitive, but underlying floating-point arithmetic is binary, not decimal. Thus, round(x, n) sometimes will pick up a decimal representation with more than n digits. This is something common for CPython < 2.7 and < 3.1: Python 2.6.6 (r266:84292, Aug 12 2014, 07:57:07) [GCC 4.4.5] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> round(0.42754287856598971, 2) 0.42999999999999999 And with this patch as well: Python 3.12.5+ (heads/3.12:0181aa2e3e, Aug 29 2024, 14:55:08) [GCC 12.2.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> from mpmath import * >>> round(mp.mpf(0.42754287856598971), 2) mpf('0.42999999999999999') >>> mp.pretty = True >>> round(mp.mpf(0.42754287856598971), 2) 0.43 See also python/cpython#45921 Closes #455 --- mpmath/ctx_mp_python.py | 9 +++++++-- mpmath/tests/test_basic_ops.py | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/mpmath/ctx_mp_python.py b/mpmath/ctx_mp_python.py index 62a1f9a6..fbd90857 100644 --- a/mpmath/ctx_mp_python.py +++ b/mpmath/ctx_mp_python.py @@ -412,8 +412,13 @@ class _mpf(mpnumeric): def to_fixed(self, prec): return to_fixed(self._mpf_, prec) - def __round__(self, *args): - return round(float(self), *args) + def __round__(self, ndigits=0): + ctx = self.context + if ctx.isfinite(self): + frac = MPQ(*self.as_integer_ratio()) + res = round(frac, ndigits) + return ctx.convert(res) + return self class _constant(_mpf): diff --git a/mpmath/tests/test_basic_ops.py b/mpmath/tests/test_basic_ops.py index 119b3fc4..5fae4c40 100644 --- a/mpmath/tests/test_basic_ops.py +++ b/mpmath/tests/test_basic_ops.py @@ -1,13 +1,16 @@ import decimal +import math import operator import random import pytest +from hypothesis import example, given, settings +from hypothesis import strategies as st import mpmath from mpmath import (ceil, fadd, fdiv, floor, fmul, fneg, fp, frac, fsub, inf, isinf, isint, isnan, isnormal, iv, monitor, mp, mpc, mpf, - mpi, nan, ninf, nint, nint_distance, pi, workprec) + mpi, nan, ninf, nint, nint_distance, nstr, pi, workprec) from mpmath.libmp import (MPQ, finf, fnan, fninf, fnone, fone, from_float, from_int, from_pickable, from_str, mpf_add, mpf_mul, mpf_sub, round_down, round_nearest, round_up, to_int, @@ -607,3 +610,32 @@ def test_rand_precision(): def test_issue_260(): assert mpc(str(mpc(1j))) == mpc(1j) + + +@settings(max_examples=10000) +@given(st.floats(allow_nan=True, + allow_infinity=True, + allow_subnormal=True), + st.integers(min_value=0, max_value=15)) +@example(0.5, 0) +@example(-0.5, 0) +@example(1.5, 0) +@example(-1.5, 0) +@example(2.675, 2) +@example(math.inf, 3) +@example(-math.inf, 1) +def test_round_bulk(x, n): + mp.prec = fp.prec + m = mpf(x) + mr = round(m, n) + xr = round(x, n) + if isnan(x): + assert isnan(mr) + assert isnan(xr) + else: + assert float(mr) == xr + # mp context doesn't support negative zero + if not xr and math.copysign(1., xr) == -1.: + return + assert nstr(mr, n=14, base=16, strip_zeros=False, + show_zero_exponent=True, binary_exp=True) == xr.hex()