Repository: mpmath/mpmath
Pull request #823 (2024-07-26 13:41:39)

Rounding types
Adds support for rounding types, following the gmpy2 library.


Patch:
diff --git a/.gitignore b/.gitignore
index 7698467..b434fda 100644
--- a/.gitignore
+++ b/.gitignore
@@ -62,3 +62,6 @@ htmlcov/
 
 # PyCharm files
 .idea/
+
+# Ignore files cached by Hypothesis (including examples directory so far)
+.hypothesis/
diff --git a/docs/basics.rst b/docs/basics.rst
index 680257b..3d40bd4 100644
--- a/docs/basics.rst
+++ b/docs/basics.rst
@@ -53,7 +53,7 @@ The ``mpf`` type is analogous to Python's built-in ``float``. It holds a real nu
     >>> mpf(mpf(2))
     mpf('2.0')
     >>> mpf("inf")
-    mpf('+inf')
+    mpf('inf')
 
 The ``mpc`` type represents a complex number in rectangular form as a pair of ``mpf`` instances. It can be constructed from a Python ``complex``, a real number, or a pair of real numbers:
 
diff --git a/docs/contexts.rst b/docs/contexts.rst
index 354e1a0..77ab898 100644
--- a/docs/contexts.rst
+++ b/docs/contexts.rst
@@ -193,12 +193,12 @@ Some transcendental functions are supported::
     >>> iv.exp(0)
     [1.0, 1.0]
     >>> iv.exp(['-inf','inf'])
-    [0.0, +inf]
+    [0.0, inf]
     >>>
     >>> iv.exp(['-inf',0])
     [0.0, 1.0]
     >>> iv.exp([0,'inf'])
-    [1.0, +inf]
+    [1.0, inf]
     >>> iv.exp([0,1])
     [1.0, 2.7182818284590455349]
     >>>
@@ -207,7 +207,7 @@ Some transcendental functions are supported::
     >>> iv.log([0,1])
     [-inf, 0.0]
     >>> iv.log([0,'inf'])
-    [-inf, +inf]
+    [-inf, inf]
     >>> iv.log(2)
     [0.69314718055994528623, 0.69314718055994539725]
     >>>
diff --git a/mpmath/ctx_mp_python.py b/mpmath/ctx_mp_python.py
index e1a7bf6..ba45e44 100644
--- a/mpmath/ctx_mp_python.py
+++ b/mpmath/ctx_mp_python.py
@@ -681,7 +681,7 @@ def convert(ctx, x, strings=True):
         prec, rounding = ctx._prec_rounding
         if isinstance(x, numbers.Rational):
             p, q = x.numerator, x.denominator
-            return ctx.make_mpf(from_rational(p, q, prec))
+            return ctx.make_mpf(from_rational(p, q, prec, rounding))
         if strings and isinstance(x, str):
             try:
                 _mpf_ = from_str(x, prec, rounding)
@@ -702,9 +702,10 @@ def npconvert(ctx, x):
         scalar.
         """
         import numpy as np
-        if isinstance(x, np.integer): return ctx.make_mpf(from_int(int(x)))
-        if isinstance(x, np.floating): return ctx.make_mpf(from_npfloat(x))
-        if isinstance(x, np.complexfloating):
+        if isinstance(x, np.ndarray) and x.ndim == 0: x = x.item()
+        if isinstance(x, (np.integer, int)): return ctx.make_mpf(from_int(int(x)))
+        if isinstance(x, (np.floating, float)): return ctx.make_mpf(from_npfloat(x))
+        if isinstance(x, (np.complexfloating, complex)):
             return ctx.make_mpc((from_npfloat(x.real), from_npfloat(x.imag)))
         raise TypeError("cannot create mpf from " + repr(x))
 
@@ -1094,7 +1095,7 @@ def mag(ctx, x):
             >>> mag(0.01), int(ceil(log(0.01,2)))
             (-6, -6)
             >>> mag(0), mag(inf), mag(-inf), mag(nan)
-            (-inf, +inf, +inf, nan)
+            (-inf, inf, inf, nan)
 
         """
         if hasattr(x, "_mpf_"):
diff --git a/mpmath/function_docs.py b/mpmath/function_docs.py
index db483ee..5963b33 100644
--- a/mpmath/function_docs.py
+++ b/mpmath/function_docs.py
@@ -420,7 +420,7 @@
     >>> sqrt(1+1j)
     (1.09868411346781 + 0.455089860562227j)
     >>> sqrt(inf)
-    +inf
+    inf
 
 Square root evaluation is fast at huge precision::
 
@@ -489,7 +489,7 @@
     >>> exp(-1)
     0.3678794411714423215955238
     >>> exp(inf)
-    +inf
+    inf
     >>> exp(-inf)
     0.0
 
@@ -565,7 +565,7 @@
     >>> cosh(1)
     1.543080634815243778477906
     >>> cosh(-inf), cosh(+inf)
-    (+inf, +inf)
+    (inf, inf)
 
 The hyperbolic cosine is an even, convex function with
 a global minimum at `x = 0`, having a Maclaurin series
@@ -595,7 +595,7 @@
     >>> sinh(1)
     1.175201193643801456882382
     >>> sinh(-inf), sinh(+inf)
-    (-inf, +inf)
+    (-inf, inf)
 
 The hyperbolic sine is an odd function, with a Maclaurin
 series that starts::
@@ -718,7 +718,7 @@
     >>> iv.tan([0,1])
     [0.0, 1.55740772465490223050697482944]
     >>> iv.tan([0,2])  # Interval includes a singularity
-    [-inf, +inf]
+    [-inf, inf]
 """
 
 sec = r"""
@@ -746,7 +746,7 @@
     >>> iv.sec([0,1])
     [1.0, 1.85081571768092561791175326276]
     >>> iv.sec([0,2])  # Interval includes a singularity
-    [-inf, +inf]
+    [-inf, inf]
 """
 
 csc = r"""
@@ -771,9 +771,9 @@
 
     >>> iv.dps = 25; iv.pretty = True
     >>> iv.csc([0,1])  # Interval includes a singularity
-    [1.18839510577812121626159943988, +inf]
+    [1.18839510577812121626159943988, inf]
     >>> iv.csc([0,2])
-    [1.0, +inf]
+    [1.0, inf]
 """
 
 cot = r"""
@@ -799,9 +799,9 @@
 
     >>> iv.dps = 25; iv.pretty = True
     >>> iv.cot([0,1])  # Interval includes a singularity
-    [0.642092615934330703006419974862, +inf]
+    [0.642092615934330703006419974862, inf]
     >>> iv.cot([1,2])
-    [-inf, +inf]
+    [-inf, inf]
 """
 
 acos = r"""
@@ -1627,7 +1627,7 @@
     >>> log(0)
     -inf
     >>> log(inf)
-    +inf
+    inf
 
 The natural logarithm is the antiderivative of `1/x`::
 
@@ -1957,7 +1957,7 @@
     4 24.0
     5 120.0
     >>> fac(inf)
-    +inf
+    inf
     >>> fac(0.5), sqrt(pi)/2
     (0.886226925452758, 0.886226925452758)
 
@@ -2013,7 +2013,7 @@
     4 6.0
     5 24.0
     >>> gamma(inf)
-    +inf
+    inf
     >>> gamma(0)
     Traceback (most recent call last):
       ...
@@ -2100,7 +2100,7 @@
 while higher orders tend to zero::
 
     >>> psi(0,inf), psi(1,inf), psi(2,inf)
-    (+inf, 0.0, 0.0)
+    (inf, 0.0, 0.0)
 
 Evaluation for a complex argument::
 
@@ -2181,7 +2181,7 @@
 The infinite harmonic series `1 + 1/2 + 1/3 + \ldots` diverges::
 
     >>> harmonic(inf)
-    +inf
+    inf
 
 :func:`~mpmath.harmonic` is evaluated using the digamma function rather
 than by summing the harmonic series term by term. It can therefore
@@ -2462,7 +2462,7 @@
     >>> beta(-3, 3)
     -0.333333333333333
     >>> beta(-2, 3)
-    +inf
+    inf
     >>> beta(inf, 1)
     0.0
     >>> beta(inf, 0)
@@ -2868,7 +2868,7 @@
     >>> hyper([1,2,3,4], [5,6,7], 1)   # 4F3 at finite-valued branch point
     1.141783505526870731311423
     >>> hyper([4,5,6,7], [1,2,3], 1)   # 4F3 at pole
-    +inf
+    inf
     >>> hyper([1,2,3,4,5], [6,7,8,9], 10)    # 5F4
     (1.543998916527972259717257 - 0.5876309929580408028816365j)
     >>> hyper([1,2,3,4,5,6], [7,8,9,10,11], 1j)   # 6F5
@@ -2879,7 +2879,7 @@
     >>> hyper(['1/3',1,'3/2',2], ['1/5','11/6','41/8'], 1)
     2.219433352235586121250027
     >>> hyper(['1/3',1,'3/2',2], ['1/5','11/6','5/4'], 1)
-    +inf
+    inf
     >>> eps1 = extradps(6)(lambda: 1 - mpf('1e-6'))()
     >>> hyper(['1/3',1,'3/2',2], ['1/5','11/6','5/4'], eps1)
     2923978034.412973409330956
@@ -3268,7 +3268,7 @@
     >>> hyp2f1(-2.5, 3, 4, 1)
     0.06926406926406926406926407
     >>> hyp2f1(2, 3, 4, 1)
-    +inf
+    inf
 
 Evaluation for huge arguments::
 
@@ -3395,7 +3395,7 @@
 The `U` function may be singular at `z = 0`::
 
     >>> hyperu(1.5, 2, 0)
-    +inf
+    inf
     >>> hyperu(1.5, -2, 0)
     0.1719434921288400112603671
 
@@ -3588,7 +3588,7 @@
     >>> gammainc(-3, -4, -5)
     (-0.2214577048967798566234192 + 0.0j)
     >>> lower_gamma(-3, 5)
-    +inf
+    inf
 
 If `z` is an integer, the recurrence reduces the incomplete gamma
 function to `P(a) \exp(-a) + Q(b) \exp(-b)` where `P` and
@@ -3768,7 +3768,7 @@
     >>> erfi(-1)
     -1.65042575879754
     >>> erfi(inf)
-    +inf
+    inf
     >>> erfi(-inf)
     -inf
 
@@ -3820,7 +3820,7 @@
     >>> erfinv(0)
     0.0
     >>> erfinv(1)
-    +inf
+    inf
     >>> erfinv(-1)
     -inf
 
@@ -4023,7 +4023,7 @@
     >>> ei(1)
     1.89511781635594
     >>> ei(inf)
-    +inf
+    inf
     >>> ei(-inf)
     0.0
 
@@ -4130,7 +4130,7 @@
     >>> findroot(li, 2)
     1.45136923488338105028396848589
     >>> li(inf)
-    +inf
+    inf
     >>> li(2, offset=True)
     0.0
     >>> li(1, offset=True)
@@ -4335,7 +4335,7 @@
     >>> chi(1)
     0.8378669409802082408946786
     >>> chi(inf)
-    +inf
+    inf
     >>> findroot(chi, 0.5)
     0.5238225713898644064509583
     >>> chi(2+3j)
@@ -4367,7 +4367,7 @@
     >>> shi(-1)
     -1.057250875375728514571842
     >>> shi(inf)
-    +inf
+    inf
     >>> shi(2+3j)
     (-0.1931890762719198291678095 + 2.645432555362369624818525j)
 
@@ -4650,11 +4650,11 @@
     0.3333333332991690159427932
     0.3333333333333333333333333
     >>> airyai(+inf,-2); airyai(+inf,-3)
-    +inf
-    +inf
+    inf
+    inf
     >>> airyai(-1000000,-2); airyai(-inf,-2)
     666666.4078472650651209742
-    +inf
+    inf
     >>> airyai(-1000000,-3); airyai(-inf,-3)
     -333333074513.7520264995733
     -inf
@@ -4714,7 +4714,7 @@
     >>> airybi(-1)
     0.10399738949694461188869
     >>> airybi(inf); airybi(-inf)
-    +inf
+    inf
     0.0
 
 Evaluation is supported for large magnitudes of the argument::
@@ -4832,10 +4832,10 @@
     0.0
     >>> airybi(10,-1); airybi(+inf,-1)
     147809803.1074067161675853
-    +inf
+    inf
     >>> airybi(+inf,-2); airybi(+inf,-3)
-    +inf
-    +inf
+    inf
+    inf
     >>> airybi(-1000000,-2); airybi(-inf,-2)
     0.4482883750599908479851085
     0.4482883573538263579148237
@@ -4846,7 +4846,7 @@
     -inf
     >>> airybi(-100000,-4); airybi(-inf,-4)
     2241411040.437759489540248
-    +inf
+    inf
 
 """
 
@@ -5013,7 +5013,7 @@
     >>> ellipk(-inf)
     0.0
     >>> ellipk(1)
-    +inf
+    inf
     >>> ellipk(-1)
     1.31102877714605990523242
     >>> ellipk(2)
@@ -6158,9 +6158,9 @@
 The point `x = 0` is a singularity (logarithmic if `n = 0`)::
 
     >>> besselk(0,0)
-    +inf
+    inf
     >>> besselk(1,0)
-    +inf
+    inf
     >>> for n in range(-4, 5):
     ...     print(besselk(n, '1e-1000'))
     ...
@@ -6326,17 +6326,17 @@
     >>> lambertw(e)
     1.0
     >>> lambertw(inf)
-    +inf
+    inf
     >>> lambertw(0, k=-1)
     -inf
     >>> lambertw(0, k=3)
     -inf
     >>> lambertw(inf, k=2)
-    (+inf + 12.56637061435917295385057j)
+    (inf + 12.56637061435917295385057j)
     >>> lambertw(inf, k=3)
-    (+inf + 18.84955592153875943077586j)
+    (inf + 18.84955592153875943077586j)
     >>> lambertw(-inf, k=3)
-    (+inf + 21.9911485751285526692385j)
+    (inf + 21.9911485751285526692385j)
 
 The `k = 0` and `k = -1` branches join at `z = -1/e` where
 `W(z) = -1` for both branches. Since `-1/e` can only be represented
@@ -6401,7 +6401,7 @@
     >>> barnesg(8)
     24883200.0
     >>> barnesg(inf)
-    +inf
+    inf
     >>> barnesg(0), barnesg(-1), barnesg(-2)
     (0.0, 0.0, 0.0)
 
@@ -6730,7 +6730,7 @@
     1.200973602347074224816022
     1.200973602347074224816022
     >>> loggamma(inf)
-    +inf
+    inf
 
 Huge arguments are permitted::
 
@@ -6802,7 +6802,7 @@
     >>> siegeltheta(0)
     0.0
     >>> siegeltheta(inf)
-    +inf
+    inf
     >>> siegeltheta(-inf)
     -inf
     >>> siegeltheta(1)
@@ -7174,7 +7174,7 @@
 Euler constants::
 
     >>> primezeta(1)
-    +inf
+    inf
     >>> extradps(25)(lambda x: primezeta(1+x)+log(x))(+eps)
     -0.31571845205389007685
     >>> mertens-euler
@@ -8104,7 +8104,7 @@
 Evaluation at zero::
 
     >>> whitm(1,-1,0); whitm(1,-0.5,0); whitm(1,0,0)
-    +inf
+    inf
     nan
     0.0
 
@@ -8158,11 +8158,11 @@
     >>> for m in [-1, -0.5, 0, 0.5, 1]:
     ...     whitw(1, m, 0)
     ...
-    +inf
+    inf
     nan
     0.0
     nan
-    +inf
+    inf
 
 We can verify that :func:`~mpmath.whitw` numerically satisfies the
 differential equation for arbitrarily chosen values::
@@ -9154,7 +9154,7 @@
     1.202056903159594285399738
     1.202056903159594285399738
     >>> dirichlet(1, [1])
-    +inf
+    inf
 
 The alternating zeta function::
 
@@ -9753,7 +9753,7 @@
     >>> eulerpoly(5, -inf)
     -inf
     >>> eulerpoly(6, -inf)
-    +inf
+    inf
 
 Computing Euler numbers::
 
@@ -9997,7 +9997,7 @@
     0.2988589049025509052765491
     0.2988589049025509052765491
     >>> scorerhi(+inf); scorerhi(-inf)
-    +inf
+    inf
     0.0
     >>> scorerhi(1)
     0.9722051551424333218376886
diff --git a/mpmath/functions/elliptic.py b/mpmath/functions/elliptic.py
index 6e7e906..6c0c0ed 100644
--- a/mpmath/functions/elliptic.py
+++ b/mpmath/functions/elliptic.py
@@ -241,7 +241,7 @@ def kfrom(ctx, q=None, m=None, k=None, tau=None, qbar=None):
         >>> kfrom(q=1)
         1
         >>> kfrom(q=-1)
-        (0.0 + +infj)
+        (0.0 + infj)
     """
     if k is not None:
         return ctx.convert(k)
@@ -662,10 +662,10 @@ def elliprf(ctx, x, y, z):
         >>> elliprf(2,2,2)**2
         0.5
         >>> elliprf(1,0,0); elliprf(0,0,1); elliprf(0,1,0); elliprf(0,0,0)
-        +inf
-        +inf
-        +inf
-        +inf
+        inf
+        inf
+        inf
+        inf
 
     Representing complete elliptic integrals in terms of `R_F`::
 
@@ -783,7 +783,7 @@ def elliprc(ctx, x, y, pv=True):
         3.141592653589793238462643
         3.141592653589793238462643
         >>> elliprc(1,0)
-        +inf
+        inf
         >>> elliprc(5,5)**2
         0.2
         >>> elliprc(1,inf); elliprc(inf,1); elliprc(inf,inf)
@@ -856,9 +856,9 @@ def elliprj(ctx, x, y, z, p, integration=1):
         1.380226776765915172432054
         1.380226776765915172432054
         >>> elliprj(1,3,2,0); elliprj(0,1,1,0); elliprj(0,0,0,0)
-        +inf
-        +inf
-        +inf
+        inf
+        inf
+        inf
         >>> elliprj(1,inf,1,0); elliprj(1,1,1,inf)
         0.0
         0.0
@@ -1050,8 +1050,8 @@ def ellipf(ctx, phi, m):
         1.415737208425956198892166
         1.415737208425956198892166
         >>> ellipf(pi/2+eps, 1); ellipf(-pi/2-eps, 1)
-        +inf
-        +inf
+        inf
+        inf
         >>> ellipf(1.5, 1)
         3.340677542798311003320813
 
@@ -1166,9 +1166,9 @@ def ellipe(ctx, *args):
         >>> ellipe(2)
         (0.5990701173677961037199612 + 0.5990701173677961037199612j)
         >>> ellipe(inf)
-        (0.0 + +infj)
+        (0.0 + infj)
         >>> ellipe(-inf)
-        +inf
+        inf
 
     Verifying the defining integral and hypergeometric
     representation::
@@ -1325,9 +1325,9 @@ def ellippi(ctx, *args):
         >>> ellippi(2,inf)
         0.0
         >>> abs(ellippi(1,5))
-        +inf
+        inf
         >>> abs(ellippi(0.25,1))
-        +inf
+        inf
 
     Evaluation in terms of simpler functions::
 
@@ -1373,13 +1373,13 @@ def ellippi(ctx, *args):
     Some degenerate cases::
 
         >>> ellippi(1,1)
-        +inf
+        inf
         >>> ellippi(1,0)
-        +inf
+        inf
         >>> ellippi(1,2,0)
-        +inf
+        inf
         >>> ellippi(1,2,1)
-        +inf
+        inf
         >>> ellippi(1,0,1)
         0.0
 
diff --git a/mpmath/functions/zeta.py b/mpmath/functions/zeta.py
index 4960ff9..9aab919 100644
--- a/mpmath/functions/zeta.py
+++ b/mpmath/functions/zeta.py
@@ -953,7 +953,7 @@ def secondzeta(ctx, s, a = 0.015, **kwargs):
         >>> secondzeta(-8)
         -0.67236328125
         >>> secondzeta(-7)
-        +inf
+        inf
 
     **Implementation notes**
 
diff --git a/mpmath/libmp/libmpf.py b/mpmath/libmp/libmpf.py
index 9054b6a..ddf81f4 100644
--- a/mpmath/libmp/libmpf.py
+++ b/mpmath/libmp/libmpf.py
@@ -43,12 +43,12 @@ class ComplexResult(ValueError):
 def prec_to_dps(n):
     """Return number of accurate decimals that can be represented
     with a precision of n bits."""
-    return max(1, int(round(int(n)/3.3219280948873626)-1))
+    return max(1, int(round(int(n)/blog2_10)-1))
 
 def dps_to_prec(n):
     """Return the number of bits required to represent n decimals
     accurately."""
-    return max(1, int(round((int(n)+1)*3.3219280948873626)))
+    return max(1, int(round((int(n)+1)*blog2_10)))
 
 def repr_dps(n):
     """Return the number of decimal digits required to represent
@@ -79,6 +79,7 @@ def repr_dps(n):
 fninf = (1, MPZ_ZERO, -789, -3)
 
 math_float_inf = math.inf
+blog2_10 = 3.3219280948873626
 
 
 #----------------------------------------------------------------------------#
@@ -361,7 +362,7 @@ def from_Decimal(x, prec=0, rnd=round_fast):
     if x.is_nan(): return fnan
     if x.is_infinite(): return fninf if x.is_signed() else finf
     if not prec:
-        prec = int(len(x.as_tuple()[1])*3.3219280948873626)
+        prec = int(len(x.as_tuple()[1])*blog2_10)
     return from_str(str(x), prec, rnd)
 
 def to_float(s, strict=False, rnd=round_fast):
@@ -1062,7 +1063,7 @@ def to_digits_exp(s, dps, base=10):
         return '', '0', 0
 
     if base == 10:
-        blog2 = 3.3219280948873626
+        blog2 = blog2_10
     elif pow(2, blog2 := int(math.log2(base))) == base:
         pass
     else:
@@ -1102,28 +1103,39 @@ def to_digits_exp(s, dps, base=10):
     exponent += len(digits) - fixdps - 1
     return sign, digits, exponent
 
-def round_digits(digits, dps, base):
+def round_digits(digits, dps, base, rounding=round_nearest):
     '''
     Returns the rounded digits, and the number of places the decimal point was
     shifted.
+
+    Supports three kinds of rounding: up, down, or nearest.
     '''
 
     assert len(digits) > dps
+    assert rounding in (round_nearest, round_up, round_down)
+
     exponent = 0
 
-    rnd_digs = stddigits[(base//2 + base % 2):base]
+    if rounding == round_down:
+        return digits[:dps], 0
+    elif rounding == round_nearest:
+        rnd_digs = stddigits[(base//2 + base % 2):base]
+    else:
+        rnd_digs = stddigits[:base]
+
+    tie_up = False
 
-    round_up = False
     # The first digit after dps is a 5.
     if digits[dps] == rnd_digs[0]:
         for i in range(dps+1, len(digits)):
             if digits[i] != '0':
-                round_up = True
+                tie_up = True
                 break
         if digits[dps-1] in stddigits[1:base:2]:
-            round_up = True
+            tie_up = True
+
+    if not tie_up:
 
-    if not round_up:
         digits = digits[:dps] + stddigits[int(digits[dps], base) - 1]
 
     # Rounding up kills some instances of "...99999"
@@ -1194,7 +1206,7 @@ def to_str(s, dps, strip_zeros=True, min_fixed=None, max_fixed=None,
             if show_zero_exponent:
                 t += sep + '+0'
             return prefix + t
-        if s == finf: return '+inf'
+        if s == finf: return 'inf'
         if s == fninf: return '-inf'
         if s == fnan: return 'nan'
         raise ValueError
@@ -1360,8 +1372,6 @@ def from_str(x, prec=0, rnd=round_fast, base=0):
 #                              String formatting                             #
 #----------------------------------------------------------------------------#
 
-blog2_10 = 3.3219280948873626
-
 _FLOAT_FORMAT_SPECIFICATION_MATCHER = re.compile(r"""
     (?:
         (?P<fill_char>.)?
@@ -1376,9 +1386,16 @@ def from_str(x, prec=0, rnd=round_fast, base=0):
     (?P<width>0|[1-9][0-9]*)?
     (?P<thousands_separators>[,_])?
     (?:\.(?P<precision>0|[1-9][0-9]*))?
+    (?P<rounding>[UDZN])?
     (?P<type>[eEfFgG])
 """, re.DOTALL | re.VERBOSE).fullmatch
 
+_GMPY_ROUND_CHAR_DICT = {
+        'U': round_ceiling,
+        'D': round_floor,
+        'Z': round_down,
+        'N': round_nearest
+        }
 
 def calc_padding(nchars, width, align):
     '''
@@ -1416,6 +1433,7 @@ def read_format_spec(format_spec):
         'thousands_separators': '',
         'width': -1,
         'precision': 6,
+        'rounding': round_nearest,
         'type': 'f'
         }
 
@@ -1430,8 +1448,12 @@ def read_format_spec(format_spec):
             or format_dict['thousands_separators']
         format_dict['width'] = int(match['width'] or format_dict['width'])
         format_dict['precision'] = int(match['precision'] or format_dict['precision'])
+        rounding_char = match['rounding']
         format_dict['type'] = match['type'] or format_dict['type']
 
+        if rounding_char is not None:
+            format_dict['rounding'] = _GMPY_ROUND_CHAR_DICT[rounding_char]
+
         if match['zeropad'] and match['fill_char']:
             raise ValueError('Cannot specify both 0-padding and a fill '
                              'character')
@@ -1454,7 +1476,8 @@ def format_fixed(s,
                  sep_range=3,
                  base=10,
                  alternate=False,
-                 no_neg_0=False):
+                 no_neg_0=False,
+                 rounding=round_nearest):
     '''
     Format a number into fixed point.
     Returns the sign character, and the string that represents the number in
@@ -1491,7 +1514,7 @@ def format_fixed(s,
         if no_neg_0:
             sign = '' if sign_spec == '-' else sign_spec
     else:
-        digits, exp_add = round_digits(digits, dps, base)
+        digits, exp_add = round_digits(digits, dps, base, rounding)
         exponent += exp_add
 
         # Here we prepend the corresponding 0s to the digits string, according
@@ -1544,7 +1567,8 @@ def format_scientific(s,
                       base=10,
                       capitalize=False,
                       alternate=False,
-                      no_neg_0=False):
+                      rounding=round_nearest):
+
 
     sep = 'E' if capitalize else 'e'
 
@@ -1556,7 +1580,7 @@ def format_scientific(s,
     if sign != '-' and sign_spec != '-':
         sign = sign_spec
 
-    digits, exp_add = round_digits(digits, dps, base)
+    digits, exp_add = round_digits(digits, dps, base, rounding)
     exponent += exp_add
 
     if strip_zeros:
@@ -1594,6 +1618,13 @@ def format_mpf(num, format_spec):
     strip_last_zero = False
     strip_zeros = False
 
+    if format_dict['rounding'] == round_ceiling:
+        rounding = round_down if num[0] else round_up
+    elif format_dict['rounding'] == round_floor:
+        rounding = round_up if num[0] else round_down
+    else:
+        rounding = format_dict['rounding']
+
     if fmt_type == 'g':
         if not format_dict['alternate']:
             strip_last_zero = True
@@ -1621,7 +1652,8 @@ def format_mpf(num, format_spec):
                 sep_range=3,
                 base=10,
                 alternate=format_dict['alternate'],
-                no_neg_0=format_dict['no_neg_0']
+                no_neg_0=format_dict['no_neg_0'],
+                rounding=rounding
                 )
     else:  # The format type is scientific
         sign, digits = format_scientific(
@@ -1632,7 +1664,7 @@ def format_mpf(num, format_spec):
                 base=10,
                 capitalize=capitalize,
                 alternate=format_dict['alternate'],
-                no_neg_0=format_dict['no_neg_0']
+                rounding=rounding
                 )
 
     nchars = len(digits) + len(sign)
diff --git a/pyproject.toml b/pyproject.toml
index 79c9226..bb3aca9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,7 +31,8 @@ Homepage = 'https://mpmath.org/'
 Documentation = 'http://mpmath.org/doc/current/'
 [project.optional-dependencies]
 tests = ['pytest>=6', 'numpy; python_version<"3.13"', 'packaging',
-         'matplotlib; python_version<"3.13"', 'pexpect', 'ipython']
+         'matplotlib; python_version<"3.13"', 'pexpect', 'ipython',
+         'hypothesis']
 develop = ['mpmath[tests]', 'flake518>=1.5; python_version>="3.9"',
            'pytest-cov', 'wheel', 'build']
 gmpy = ['gmpy2>=2.2; platform_python_implementation!="PyPy"']


Test patch:
diff --git a/mpmath/tests/test_convert.py b/mpmath/tests/test_convert.py
index 76cdfb5..73c84ca 100644
--- a/mpmath/tests/test_convert.py
+++ b/mpmath/tests/test_convert.py
@@ -41,7 +41,7 @@ def test_basic_string():
     assert str(mpf("-2163048125l")) == '-2163048125.0'
     assert str(mpf("-2163048125L/1088391168")) == '-1.98738118113799'
     assert str(mpf("2163048125/1088391168l")) == '1.98738118113799'
-    assert str(mpf('inf')) == '+inf'
+    assert str(mpf('inf')) == 'inf'
 
     # issue 613
     assert str(mpf('2_5_0_0.0')) == '2500.0'
@@ -277,7 +277,10 @@ def test_compatibility():
     assert sqrt(Fraction(2, 3)).ae(sqrt(mpf('2/3')))
     assert sqrt(Decimal(2)/Decimal(3)).ae(sqrt(mpf('2/3')))
     mp.prec = oldprec
-    pytest.raises(TypeError, lambda: mpmathify(np.array(1)))
+    assert mpmathify(np.array(123)) == mpf(123)
+    assert mpmathify(np.array(1.25)) == mpf(1.25)
+    assert mpmathify(np.array(0.5+1j)) == mpc(0.5+1j)
+    pytest.raises(TypeError, lambda: mpmathify(np.array([1])))
 
 def test_issue465():
     assert mpf(Fraction(1, 3)) == mpf('0.33333333333333331')
diff --git a/mpmath/tests/test_format.py b/mpmath/tests/test_format.py
index e208d1b..4379296 100644
--- a/mpmath/tests/test_format.py
+++ b/mpmath/tests/test_format.py
@@ -75,6 +75,8 @@ def test_mpf_fmt_cpython():
     float.__format__, when dps is default.
     '''
 
+    # 'f' code formatting.
+
     # zeros
     assert f'{mp.mpf(0):.0f}' == '0'
     assert f'{mp.mpf(0):.1f}' == '0.0'
@@ -82,6 +84,22 @@ def test_mpf_fmt_cpython():
     assert f'{mp.mpf(0):.3f}' == '0.000'
     assert f'{mp.mpf(0):.50f}' == '0.00000000000000000000000000000000000000000000000000'
 
+    # nan, infs
+    assert f'{inf:f}' == 'inf'
+    assert f'{inf:+f}' == '+inf'
+    assert f'{inf:F}' == 'INF'
+    assert f'{inf:+F}' == '+INF'
+
+    assert f'{ninf:f}' == '-inf'
+    assert f'{ninf:+f}' == '-inf'
+    assert f'{ninf:F}' == '-INF'
+    assert f'{ninf:+F}' == '-INF'
+
+    assert f'{nan:f}' == 'nan'
+    assert f'{nan:+f}' == '+nan'
+    assert f'{nan:F}' == 'NAN'
+    assert f'{nan:+F}' == '+NAN'
+
     # precision 0;  result should never include a .
     assert f'{mp.mpf(1.5):.0f}' == '2'
     assert f'{mp.mpf(2.5):.0f}' == '2'
@@ -165,6 +183,22 @@ def test_mpf_fmt_cpython():
     assert f'{mp.mpf(0):.10e}' == '0.0000000000e+00'
     assert f'{mp.mpf(0):.50e}' == '0.00000000000000000000000000000000000000000000000000e+00'
 
+    # nan, infs
+    assert f'{inf:e}' == 'inf'
+    assert f'{inf:+e}' == '+inf'
+    assert f'{inf:E}' == 'INF'
+    assert f'{inf:+E}' == '+INF'
+
+    assert f'{ninf:e}' == '-inf'
+    assert f'{ninf:+e}' == '-inf'
+    assert f'{ninf:E}' == '-INF'
+    assert f'{ninf:+E}' == '-INF'
+
+    assert f'{nan:e}' == 'nan'
+    assert f'{nan:+e}' == '+nan'
+    assert f'{nan:E}' == 'NAN'
+    assert f'{nan:+E}' == '+NAN'
+
     # precision 0.  no decimal point in the output
     assert f'{mp.mpf(0.01):.0e}' == '1e-02'
     assert f'{mp.mpf(0.1):.0e}' == '1e-01'
@@ -287,6 +321,22 @@ def test_mpf_fmt_cpython():
     assert f'{mp.mpf(0):.50g}' == '0'
     assert f'{mp.mpf(0):.100g}' == '0'
 
+    # nan, infs
+    assert f'{inf:g}' == 'inf'
+    assert f'{inf:+g}' == '+inf'
+    assert f'{inf:G}' == 'INF'
+    assert f'{inf:+G}' == '+INF'
+
+    assert f'{ninf:g}' == '-inf'
+    assert f'{ninf:+g}' == '-inf'
+    assert f'{ninf:G}' == '-INF'
+    assert f'{ninf:+G}' == '-INF'
+
+    assert f'{nan:g}' == 'nan'
+    assert f'{nan:+g}' == '+nan'
+    assert f'{nan:G}' == 'NAN'
+    assert f'{nan:+G}' == '+NAN'
+
     # precision 0 doesn't make a lot of sense for the 'g' code (what does
     # it mean to have no significant digits?); in practice, it's interpreted
     # as identical to precision 1
@@ -422,6 +472,11 @@ def test_mpf_float():
             num = -num
         assert fmt_str.format(num) == fmt_str.format(mp.mpf(num))
 
+    # Test the same random formats with special numbers
+    for num in (inf, -inf, nan, 0):
+        fmt_str = random_fmt()
+        assert fmt_str.format(fp.mpf(num)) == fmt_str.format(mp.mpf(num))
+
 
 def test_mpf_fmt():
     '''
@@ -504,57 +559,90 @@ def test_mpf_fmt():
         assert f"{mp.mpf('0.24'):=+020.2e}" == '+000000000002.40e-01'
         assert f"{mp.mpf('0.24'):=+020.2g}" == '+0000000000000000.24'
 
-
-def test_mpf_fmt_special():
-    assert f'{inf:f}' == 'inf'
-    assert f'{inf:+f}' == '+inf'
-    assert f'{inf:F}' == 'INF'
-    assert f'{inf:+F}' == '+INF'
-
-    assert f'{ninf:f}' == '-inf'
-    assert f'{ninf:+f}' == '-inf'
-    assert f'{ninf:F}' == '-INF'
-    assert f'{ninf:+F}' == '-INF'
-
-    assert f'{nan:f}' == 'nan'
-    assert f'{nan:+f}' == '+nan'
-    assert f'{nan:F}' == 'NAN'
-    assert f'{nan:+F}' == '+NAN'
-
-    assert f'{inf:e}' == 'inf'
-    assert f'{inf:+e}' == '+inf'
-    assert f'{inf:E}' == 'INF'
-    assert f'{inf:+E}' == '+INF'
-
-    assert f'{ninf:e}' == '-inf'
-    assert f'{ninf:+e}' == '-inf'
-    assert f'{ninf:E}' == '-INF'
-    assert f'{ninf:+E}' == '-INF'
-
-    assert f'{nan:e}' == 'nan'
-    assert f'{nan:+e}' == '+nan'
-    assert f'{nan:E}' == 'NAN'
-    assert '{:+E}'.format(nan) == '+NAN'
-
-    assert f'{inf:g}' == 'inf'
-    assert f'{inf:+g}' == '+inf'
-    assert f'{inf:G}' == 'INF'
-    assert f'{inf:+G}' == '+INF'
-
-    assert f'{ninf:g}' == '-inf'
-    assert f'{ninf:+g}' == '-inf'
-    assert f'{ninf:G}' == '-INF'
-    assert f'{ninf:+G}' == '-INF'
-
-    assert f'{nan:g}' == 'nan'
-    assert f'{nan:+g}' == '+nan'
-    assert f'{nan:G}' == 'NAN'
-    assert f'{nan:+G}' == '+NAN'
-
-    # Test the same random formats with special numbers
-    for num in (float('inf'), -float('inf'), float('nan'), 0):
-        fmt_str = random_fmt()
-        assert fmt_str.format(fp.mpf(num)) == fmt_str.format(mp.mpf(num))
+        # Tests for different kinds of rounding
+        num = mp.mpf('-1.23456789999901234567')
+        assert f"{num:=.2Uf}" == "-1.23"
+        assert f"{num:=.2Df}" == "-1.24"
+        assert f"{num:=.2Zf}" == "-1.23"
+        assert f"{num:=.2Nf}" == "-1.23"
+
+        assert f"{num:=.3Uf}" == "-1.234"
+        assert f"{num:=.3Df}" == "-1.235"
+        assert f"{num:=.3Zf}" == "-1.234"
+        assert f"{num:=.3Nf}" == "-1.235"
+
+        assert f"{num:=.10Uf}" == "-1.2345678999"
+        assert f"{num:=.10Df}" == "-1.2345679000"
+        assert f"{num:=.10Zf}" == "-1.2345678999"
+        assert f"{num:=.10Nf}" == "-1.2345679000"
+
+        num = mp.mpf('1.23456789999901234567')
+        assert f"{num:=.2Uf}" == "1.24"
+        assert f"{num:=.2Df}" == "1.23"
+        assert f"{num:=.2Zf}" == "1.23"
+        assert f"{num:=.2Nf}" == "1.23"
+
+        assert f"{num:=.3Uf}" == "1.235"
+        assert f"{num:=.3Df}" == "1.234"
+        assert f"{num:=.3Zf}" == "1.234"
+        assert f"{num:=.3Nf}" == "1.235"
+
+        assert f"{num:=.10Uf}" == "1.2345679000"
+        assert f"{num:=.10Df}" == "1.2345678999"
+        assert f"{num:=.10Zf}" == "1.2345678999"
+        assert f"{num:=.10Nf}" == "1.2345679000"
+
+        num = mp.mpf('-123.456789999901234567')
+        assert f"{num:=.2Ue}" == "-1.23e+02"
+        assert f"{num:=.2De}" == "-1.24e+02"
+        assert f"{num:=.2Ze}" == "-1.23e+02"
+        assert f"{num:=.2Ne}" == "-1.23e+02"
+
+        assert f"{num:=.3Ue}" == "-1.234e+02"
+        assert f"{num:=.3De}" == "-1.235e+02"
+        assert f"{num:=.3Ze}" == "-1.234e+02"
+        assert f"{num:=.3Ne}" == "-1.235e+02"
+
+        assert f"{num:=.10Ue}" == "-1.2345678999e+02"
+        assert f"{num:=.10De}" == "-1.2345679000e+02"
+        assert f"{num:=.10Ze}" == "-1.2345678999e+02"
+        assert f"{num:=.10Ne}" == "-1.2345679000e+02"
+
+        num = mp.mpf('123456.789999901234567')
+        assert f"{num:=.2Ue}" == "1.24e+05"
+        assert f"{num:=.2De}" == "1.23e+05"
+        assert f"{num:=.2Ze}" == "1.23e+05"
+        assert f"{num:=.2Ne}" == "1.23e+05"
+
+        assert f"{num:=.3Ue}" == "1.235e+05"
+        assert f"{num:=.3De}" == "1.234e+05"
+        assert f"{num:=.3Ze}" == "1.234e+05"
+        assert f"{num:=.3Ne}" == "1.235e+05"
+
+        assert f"{num:=.10Ue}" == "1.2345679000e+05"
+        assert f"{num:=.10De}" == "1.2345678999e+05"
+        assert f"{num:=.10Ze}" == "1.2345678999e+05"
+        assert f"{num:=.10Ne}" == "1.2345679000e+05"
+
+        assert f"{mp.mpf('123.456'):.2Ug}" == "1.3e+02"
+        assert f"{mp.mpf('123.456'):.2Dg}" == "1.2e+02"
+        assert f"{mp.mpf('123.456'):.2Zg}" == "1.2e+02"
+        assert f"{mp.mpf('123.456'):.2Ng}" == "1.2e+02"
+
+        assert f"{mp.mpf('-123.456'):.2Ug}" == "-1.2e+02"
+        assert f"{mp.mpf('-123.456'):.2Dg}" == "-1.3e+02"
+        assert f"{mp.mpf('-123.456'):.2Zg}" == "-1.2e+02"
+        assert f"{mp.mpf('-123.456'):.2Ng}" == "-1.2e+02"
+
+        assert f"{mp.mpf('123.456'):.5Ug}" == "123.46"
+        assert f"{mp.mpf('123.456'):.5Dg}" == "123.45"
+        assert f"{mp.mpf('123.456'):.5Zg}" == "123.45"
+        assert f"{mp.mpf('123.456'):.5Ng}" == "123.46"
+
+        assert f"{mp.mpf('-123.456'):.5Ug}" == "-123.45"
+        assert f"{mp.mpf('-123.456'):.5Dg}" == "-123.46"
+        assert f"{mp.mpf('-123.456'):.5Zg}" == "-123.45"
+        assert f"{mp.mpf('-123.456'):.5Ng}" == "-123.46"
 
 
 def test_errors():