Repository: mpmath/mpmath
Pull request #834 (2024-08-04 14:00:37)

Correct formatting for `round_up` rounding mode
This PR improves the code in `round_digits`: now the first digit in the `stddigits` list is excluded from the list of digits that must be rounded up when the `round_up` rounding mode is used. When rounding to a 0 digit, all the digits following it are looked up, and if one of them is different from 0, the 0 is rounded up.

This fixes some wrongfully formatted floats as, for example, when running the following code in the master branch
```python
>>> from gmpy2 import mpfr
>>> from mpmath import mp
>>> print('{:.2Uf}'.format(mpfr('0.25')))
0.25
>>> print('{:.2Uf}'.format(mp.mpf('0.25')))
0.26
>>>
```



Patch:
diff --git a/mpmath/function_docs.py b/mpmath/function_docs.py
index 5963b33..3f3cce2 100644
--- a/mpmath/function_docs.py
+++ b/mpmath/function_docs.py
@@ -9850,7 +9850,7 @@
     >>> Y2 = lambda t,p: fp.conj(fp.spherharm(l2,m2,t,p))
     >>> l1 = l2 = 3; m1 = m2 = 2
     >>> fp.chop(fp.quad(lambda t,p: Y1(t,p)*Y2(t,p)*dS(t,p), *sphere))
-    1.0000000000000007
+    1.000000000000000...
     >>> m2 = 1    # m1 != m2
     >>> print(fp.chop(fp.quad(lambda t,p: Y1(t,p)*Y2(t,p)*dS(t,p), *sphere)))
     0.0
diff --git a/mpmath/libmp/libmpf.py b/mpmath/libmp/libmpf.py
index ddf81f4..ce768f8 100644
--- a/mpmath/libmp/libmpf.py
+++ b/mpmath/libmp/libmpf.py
@@ -1121,22 +1121,40 @@ def round_digits(digits, dps, base, rounding=round_nearest):
     elif rounding == round_nearest:
         rnd_digs = stddigits[(base//2 + base % 2):base]
     else:
-        rnd_digs = stddigits[:base]
+        rnd_digs = stddigits[1:base]
 
+    tie_down = False
     tie_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':
-                tie_up = True
-                break
-        if digits[dps-1] in stddigits[1:base:2]:
-            tie_up = True
-
-    if not tie_up:
-
+    if rounding == round_nearest:
+        # The first digit after dps is a 5 and we should determine whether we
+        # round it up or down.
+        if digits[dps] == rnd_digs[0]:
+            tie_down = True
+
+            # If the digit we round to is even, we may round down if all the
+            # following digits are 0.
+            for i in range(dps+1, len(digits)):
+                if digits[i] != '0':
+                    tie_down = False
+                    break
+            # If the digit we round to is odd, we round up no matter what.
+            if digits[dps-1] in stddigits[1:base:2]:
+                tie_down = False
+
+    elif rounding == round_up:
+        # If any digit following a 0 is different from zero, we round up.
+        if digits[dps] == '0':
+            for i in range(dps+1, len(digits)):
+                if digits[i] != '0':
+                    tie_up = True
+                    break
+
+    # Add or subtract a unit to the digit following the one we round to.
+    if tie_down:
         digits = digits[:dps] + stddigits[int(digits[dps], base) - 1]
+    elif tie_up:
+        digits = digits[:dps] + '1'
 
     # Rounding up kills some instances of "...99999"
     if digits[dps] in rnd_digs:
@@ -1386,13 +1404,14 @@ 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])
+    (?P<rounding>[UDYZN])?
+    (?P<type>[eEfFgG])?
 """, re.DOTALL | re.VERBOSE).fullmatch
 
 _GMPY_ROUND_CHAR_DICT = {
         'U': round_ceiling,
         'D': round_floor,
+        'Y': round_up,
         'Z': round_down,
         'N': round_nearest
         }
@@ -1434,7 +1453,7 @@ def read_format_spec(format_spec):
         'width': -1,
         'precision': 6,
         'rounding': round_nearest,
-        'type': 'f'
+        'type': 'g'
         }
 
     if match := _FLOAT_FORMAT_SPECIFICATION_MATCHER(format_spec):


Test patch:
diff --git a/mpmath/tests/__init__.py b/mpmath/tests/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/mpmath/tests/test_format.py b/mpmath/tests/test_format.py
index 4379296..e1d2fb6 100644
--- a/mpmath/tests/test_format.py
+++ b/mpmath/tests/test_format.py
@@ -539,6 +539,8 @@ def test_mpf_fmt():
         assert f"{mp.mpf('1e-50'):.50g}" == '1e-50'
         assert f"{mp.mpf('1e-50'):.50G}" == '1E-50'
 
+        assert f"{mp.mpf('1e-50'):}" == '1e-50'
+
         # thousands separator
         assert f"{mp.mpf('1e9'):,.0f}" == '1,000,000,000'
         assert f"{mp.mpf('123456789.0123456'):,.4f}" == '123,456,789.0123'
@@ -565,84 +567,168 @@ def test_mpf_fmt():
         assert f"{num:=.2Df}" == "-1.24"
         assert f"{num:=.2Zf}" == "-1.23"
         assert f"{num:=.2Nf}" == "-1.23"
+        assert f"{num:=.2Yf}" == "-1.24"
 
         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:=.3Yf}" == "-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"
+        assert f"{num:=.10Yf}" == "-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:=.2Yf}" == "1.24"
 
         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:=.3Yf}" == "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"
+        assert f"{num:=.10Yf}" == "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:=.2Ye}" == "-1.24e+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:=.3Ye}" == "-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"
+        assert f"{num:=.10Ye}" == "-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:=.2Ye}" == "1.24e+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:=.3Ye}" == "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"{num:=.10Ye}" == "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'):.2Yg}" == "1.3e+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'):.2Yg}" == "-1.3e+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'):.5Yg}" == "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"
+        assert f"{mp.mpf('-123.456'):.5Yg}" == "-123.46"
+
+        # Special cases were tying is relevant (cases involve exact floats)
+        assert f"{mp.mpf('0.25'):.1Nf}" == "0.2"
+        assert f"{mp.mpf('0.75'):.1Nf}" == "0.8"
+
+        num = mp.mpf('0.1')
+        assert f"{-num:=.2Df}" == "-0.11"
+        assert f"{-num:=.3Df}" == "-0.101"
+        assert f"{-num:=.4Df}" == "-0.1001"
+        assert f"{-num:=.5Df}" == "-0.10001"
+        assert f"{-num:=.6Df}" == "-0.100001"
+        assert f"{-num:=.7Df}" == "-0.1000001"
+
+        assert f"{-num:=.2De}" == "-1.01e-01"
+        assert f"{-num:=.3De}" == "-1.001e-01"
+        assert f"{-num:=.4De}" == "-1.0001e-01"
+        assert f"{-num:=.5De}" == "-1.00001e-01"
+        assert f"{-num:=.6De}" == "-1.000001e-01"
+        assert f"{-num:=.7De}" == "-1.0000001e-01"
+
+        assert f"{num:=.2Uf}" == "0.11"
+        assert f"{num:=.3Uf}" == "0.101"
+        assert f"{num:=.4Uf}" == "0.1001"
+        assert f"{num:=.5Uf}" == "0.10001"
+        assert f"{num:=.6Uf}" == "0.100001"
+        assert f"{num:=.7Uf}" == "0.1000001"
+
+        assert f"{num:=.2Ue}" == "1.01e-01"
+        assert f"{num:=.3Ue}" == "1.001e-01"
+        assert f"{num:=.4Ue}" == "1.0001e-01"
+        assert f"{num:=.5Ue}" == "1.00001e-01"
+        assert f"{num:=.6Ue}" == "1.000001e-01"
+        assert f"{num:=.7Ue}" == "1.0000001e-01"
+
+        num = mp.mpf('0.25')
+        assert f"{-num:=.2Df}" == "-0.25"
+        assert f"{-num:=.3Df}" == "-0.250"
+        assert f"{-num:=.4Df}" == "-0.2500"
+        assert f"{-num:=.5Df}" == "-0.25000"
+        assert f"{-num:=.6Df}" == "-0.250000"
+        assert f"{-num:=.7Df}" == "-0.2500000"
+
+        assert f"{-num:=.2De}" == "-2.50e-01"
+        assert f"{-num:=.3De}" == "-2.500e-01"
+        assert f"{-num:=.4De}" == "-2.5000e-01"
+        assert f"{-num:=.5De}" == "-2.50000e-01"
+        assert f"{-num:=.6De}" == "-2.500000e-01"
+        assert f"{-num:=.7De}" == "-2.5000000e-01"
+
+        assert f"{num:=.2Uf}" == "0.25"
+        assert f"{num:=.3Uf}" == "0.250"
+        assert f"{num:=.4Uf}" == "0.2500"
+        assert f"{num:=.5Uf}" == "0.25000"
+        assert f"{num:=.6Uf}" == "0.250000"
+        assert f"{num:=.7Uf}" == "0.2500000"
+
+        assert f"{num:=.2Ue}" == "2.50e-01"
+        assert f"{num:=.3Ue}" == "2.500e-01"
+        assert f"{num:=.4Ue}" == "2.5000e-01"
+        assert f"{num:=.5Ue}" == "2.50000e-01"
+        assert f"{num:=.6Ue}" == "2.500000e-01"
+        assert f"{num:=.7Ue}" == "2.5000000e-01"
+
+        # Changing the work precision changes the number printed (This is
+        # expected behavior)
+        with mp.workdps(20):
+            assert f"{mp.mpf('0.1'):=.4Uf}" == "0.1000"
+            assert f"{mp.mpf('-0.1'):=.4Df}" == "-0.1000"
 
 
 def test_errors():