Repository: mpmath/mpmath (python)

Issue:
Support formatting mpf/mpc in binary
This already supported by ``to_str()``, but probably could be done more efficiently:
```pycon
>>> f'{gmpy2.mpfr(1.1):.15b}'
'1.000110011001101p+0'
>>> mpmath.libmp.to_str(x._mpf_, 16, base=2, show_zero_exponent=True, binary_exp=True)[2:]
'1.000110011001101p+0'
```

For compatibility with gmpy2/MPFR we could utilize same "b" format type.

Pull request description:
Support 'b' (binary) format type for mpf/mpc
Closes #866

Patch:
diff --git a/mpmath/ctx_mp_python.py b/mpmath/ctx_mp_python.py
index a1e4c37..8fb3c5f 100644
--- a/mpmath/ctx_mp_python.py
+++ b/mpmath/ctx_mp_python.py
@@ -414,6 +414,13 @@ def __format__(s, format_spec):
             >>> f'{x:.10a}'
             '-0x1.3c0ca2a5b2p+0'
 
+        Format type 'b' allows format number in binary:
+
+            >>> f'{x:.15b}'
+            '-1.001111000000110p+0'
+
+        Alternate form (``#`` option) adds ``0b`` prefix.
+
         """
 
         _, _, (prec, _) = s._ctxdata
@@ -679,10 +686,8 @@ def __format__(s, format_spec):
         ``mpc`` objects allow for formatting similar to Python
         :external:class:`complex`, specified in :external:ref:`formatspec`.
         All of Python's format types are supported, with the exception of
-        ``n``.  Additionally, as for :func:`mpmath.mpf.__format__`, four
-        different kinds of rounding are supported and format types 'a'
-        and 'A' (use uppercase digits) allow to represent components as
-        C99-style hexadecimal strings.
+        ``n``.  Also available additional options and formating types,
+        accepted by :func:`mpmath.mpf.__format__`.
 
         """
 
diff --git a/mpmath/libmp/libmpf.py b/mpmath/libmp/libmpf.py
index ed51c6c..87a56ce 100644
--- a/mpmath/libmp/libmpf.py
+++ b/mpmath/libmp/libmpf.py
@@ -1401,7 +1401,7 @@ def from_str(x, prec=0, rnd=round_fast, base=0):
     (?P<thousands_separators>[,_])?
     (?:\.(?P<precision>0|[1-9][0-9]*))?
     (?P<rounding>[UDYZN])?
-    (?P<type>[aAeEfFgG%])?
+    (?P<type>[aAbeEfFgG%])?
 """, re.DOTALL | re.VERBOSE).fullmatch
 
 _GMPY_ROUND_CHAR_DICT = {
@@ -1477,7 +1477,7 @@ def read_format_spec(format_spec):
             format_dict['align'] = '='
             format_dict['fill_char'] = '0'
 
-        if format_dict['precision'] < 0 and format_dict['type'].lower() not in ['', 'a']:
+        if format_dict['precision'] < 0 and format_dict['type'].lower() not in ['', 'a', 'b']:
             format_dict['precision'] = 6
     else:
         raise ValueError("Invalid format specifier '{}'".format(format_spec))
@@ -1668,6 +1668,29 @@ def format_hexadecimal(s,
     return sign, digits + sep + f'{exponent:+01d}'
 
 
+def format_binary(s,
+                  precision=-1,
+                  sign_spec='-',
+                  rounding=round_nearest):
+    sign = '-' if s[0] else ''
+    if sign != '-' and sign_spec != '-':
+        sign = sign_spec
+
+    prec = precision + 1 if precision >= 0 else s[1].bit_length()
+    s = normalize(*s, prec, rounding)
+
+    digits = bin(s[1])[2:]
+    digits = digits + '0'*(precision + 1 - len(digits))
+    if len(digits) > 1:
+        digits = digits[0] + '.' + digits[1:]
+
+    exponent = s[2]
+    if s[1]:
+        exponent += s[1].bit_length() - 1
+
+    return sign, digits + f'p{exponent:+01d}'
+
+
 _MAP_SPEC_STR = {finf: ('', 'inf'), fninf: ('-', 'inf'), fnan: ('', 'nan')}
 
 
@@ -1755,6 +1778,16 @@ def format_digits(num, format_dict, prec):
                 )
         digits = ('0X' if capitalize else '0x') + digits
 
+    elif fmt_type == 'b':
+        sign, digits = format_binary(
+                num,
+                precision=precision,
+                sign_spec=format_dict['sign'],
+                rounding=rounding
+                )
+        if format_dict['alternate']:
+            digits = '0b' + digits
+
     else:  # fixed-point format
         sign, digits = format_fixed(
                 num,


Test patch:
diff --git a/mpmath/tests/test_format.py b/mpmath/tests/test_format.py
index ed17bd2..7bc406c 100644
--- a/mpmath/tests/test_format.py
+++ b/mpmath/tests/test_format.py
@@ -839,6 +839,35 @@ def test_hexadecimal_with_libc_bulk(x, p):
     assert mp.mpf(m_hex) == mp.mpf(x_hex)
 
 
+@settings(max_examples=10000)
+@given(st.floats(allow_nan=False, allow_infinity=False,
+                 allow_subnormal=False),
+       st.integers(min_value=-3, max_value=15))
+def test_binary_with_gmpy2_bulk(x, p):
+    gmpy2 = pytest.importorskip('gmpy2')
+    if not x and math.copysign(1, x) == -1:
+        return  # skip negative zero
+    if p >= 0:
+        fmt = '.' + str(p) + 'b'
+    else:
+        fmt = 'b'
+    g_bin = format(gmpy2.mpfr(x), fmt)
+    m_bin = format(mp.mpf(x), fmt)
+    assert m_bin == g_bin
+
+
+def test_binary_fmt():
+    x = mp.mpf(3)
+    assert f'{x:b}' == '1.1p+1'
+    assert f'{x:.2b}' == '1.10p+1'
+    assert f'{x:+.2b}' == '+1.10p+1'
+    assert f'{x:#.2b}' == '0b1.10p+1'
+
+    x = mp.mpf(0)
+    assert f'{x:.2b}' == '0.00p+0'
+    assert f'{x:b}' == '0p+0'
+
+
 def test_hexadecimal():
     with workdps(1000):
         x = mp.mpf('1.234567890123456789')