Implement 'a'/'A' formating types for mpf.__format__

This commit is contained in:
Sergey B Kirpichev
2024-09-08 07:31:11 +03:00
parent b4b3b9cd77
commit 40c16e98c1
3 changed files with 134 additions and 4 deletions
+16
View File
@@ -398,6 +398,22 @@ class _mpf(mpnumeric):
'-1.23456'
>>> f'{x:.5Df}'
'-1.23457'
Format types 'a' and 'A' (use uppercase digits) allow to represent
floating-point number as a C99-style hexadecimal string
``[±][0x]h[.hhh]p±d``, where there is one hexadecimal digit before the
dot and the fractional part either is exact or the number of its
hexadecimal digits is equal to the specified precision. The exponent
``d`` is written in decimal, it always contains at least one digit, and
it gives the power of 2 by which to multiply the coefficient. If no
digits follow the decimal point, the decimal point is also removed
unless the ``#`` option is specified.
>>> f'{x:a}'
'-0x1.3c0ca2a5b1d5d0818d3359c99ff1a26f2b31063249p+0'
>>> f'{x:.10a}'
'-0x1.3c0ca2a5b2p+0'
"""
_, _, (prec, _) = s._ctxdata
+69 -3
View File
@@ -1404,7 +1404,7 @@ _FLOAT_FORMAT_SPECIFICATION_MATCHER = re.compile(r"""
(?P<thousands_separators>[,_])?
(?:\.(?P<precision>0|[1-9][0-9]*))?
(?P<rounding>[UDYZN])?
(?P<type>[eEfFgG%])?
(?P<type>[aAeEfFgG%])?
""", re.DOTALL | re.VERBOSE).fullmatch
_GMPY_ROUND_CHAR_DICT = {
@@ -1480,7 +1480,7 @@ def read_format_spec(format_spec):
format_dict['align'] = '='
format_dict['fill_char'] = '0'
if format_dict['precision'] < 0 and format_dict['type']:
if format_dict['precision'] < 0 and format_dict['type'].lower() not in ['', 'a']:
format_dict['precision'] = 6
else:
raise ValueError("Invalid format specifier '{}'".format(format_spec))
@@ -1620,13 +1620,66 @@ def format_scientific(s,
return sign, digits + sep + f'{exponent:+03d}'
def format_hexadecimal(s,
precision=None,
strip_zeros=False,
sign_spec='-',
base=16,
capitalize=False,
alternate=False,
rounding=round_nearest):
sep = 'P' if capitalize else 'p'
if precision < 0:
precision = s[1].bit_length()//4 + 1
# First, get the exponent to know how many digits we will need
dps = precision+1
sign, digits, exponent = to_digits_exp(
s, max(dps+10, int(s[3]/4)+10), base)
exponent *= 4
if sign != '-' and sign_spec != '-':
sign = sign_spec
# normalization
if int(digits[0], 16) > 1:
shift = math.floor(math.log2(int(digits[0], 16)))
exponent += shift
n = int(digits, 16) >> shift
digits = hex(n)[2:]
if digits != "0":
digits, exp_add = round_digits(s[0], digits, dps, base, rounding)
exponent += exp_add*4
# normalization
if digits[0] == "2":
exponent += 1
n = int(digits, 16) >> 1
digits = hex(n)[2:]
if s[1] and strip_zeros:
# Clean up trailing zeros
digits = digits.rstrip('0')
precision = len(digits)
if precision >= 1 and len(digits) > 1:
return sign, digits[0] + '.' + digits[1:] + sep + f'{exponent:+01d}'
if alternate:
return sign, digits + '.' + sep + f'{exponent:+01d}'
return sign, digits + sep + f'{exponent:+01d}'
_MAP_SPEC_STR = {finf: ('', 'inf'), fninf: ('-', 'inf'), fnan: ('', 'nan')}
def format_digits(num, format_dict, prec):
hack0 = True
capitalize = False
if format_dict['type'] in list('FGE'):
if format_dict['type'] in list('AFGE'):
capitalize = True
fmt_type = format_dict['type'].lower()
@@ -1696,6 +1749,19 @@ def format_digits(num, format_dict, prec):
if digits[0] in 'eE':
digits = '0' + digits
elif fmt_type == 'a':
sign, digits = format_hexadecimal(
num,
precision=precision,
strip_zeros=True,
sign_spec=format_dict['sign'],
base=16,
capitalize=capitalize,
alternate=format_dict['alternate'],
rounding=rounding
)
digits = ('0X' if capitalize else '0x') + digits
else: # fixed-point format
sign, digits = format_fixed(
num,
+49 -1
View File
@@ -1,3 +1,4 @@
import ctypes
import math
import platform
import random
@@ -5,7 +6,7 @@ import sys
import hypothesis.strategies as st
import pytest
from hypothesis import given, settings
from hypothesis import example, given, settings
from mpmath import fp, inf, mp, nan, ninf, workdps
from mpmath.libmp.libmpf import read_format_spec
@@ -887,3 +888,50 @@ def test_errors():
with pytest.raises(ValueError, match="Cannot specify both 0-padding "
"and a fill character"):
f"{mp.mpf('4'):q<03f}"
@settings(max_examples=10000)
@given(st.floats(allow_nan=True, allow_infinity=True,
allow_subnormal=False))
@example(float('nan'))
@example(float('inf'))
def test_hexadecimal_bulk(x):
if math.isnan(x):
assert math.isnan(float.fromhex(f"{mp.mpf(x):a}"))
else:
assert float.fromhex(f"{mp.mpf(x):a}") == x
try:
libc = ctypes.CDLL("libc.so.6")
except OSError:
libc = None
def float_print(d, i):
fmt = "%." + str(i) + "a\n"
a = ctypes.create_string_buffer(256)
libc.sprintf(a, bytes(fmt, 'utf-8'), ctypes.c_double(d))
return a.raw.decode('utf-8').split("\n")[0]
@pytest.mark.skipif(libc is None, reason='requires libc')
@settings(max_examples=10000)
@given(st.floats(allow_nan=False, allow_infinity=False,
allow_subnormal=False),
st.integers(min_value=0, max_value=15))
def test_hexadecimal_with_libc_bulk(x, p):
fmt = '.' + str(p) + 'a'
x_hex = float_print(x, p)
m_hex = format(mp.mpf(x), fmt)
assert mp.mpf(m_hex) == mp.mpf(x_hex)
def test_hexadecimal():
with workdps(1000):
x = mp.mpf('1.234567890123456789')
assert f'{x:.20a}' == '0x1.3c0ca428c59fb71a4194p+0'
assert f'{x:.0a}' == '0x1p+0'
assert f'{x:#.0a}' == '0x1.p+0'
assert f"{mp.mpf('1.234567890123456789'):+.0a}" == '+0x1p+0'