Implement 'a'/'A' formating types for mpf.__format__
This commit is contained in:
@@ -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
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user