Skip to content

Commit acb0e57

Browse files
committed
gh-113804: Support "x" and "X" format types for floats
_Py_dg_dtoa_hex() is based on float.hex() with additional support for the precision setting and some format flags. Examples: ```pycon >>> f'{-0.1:x}' '-0x1.999999999999ap-4' >>> (-0.1).hex() '-0x1.999999999999ap-4' >>> f'{3.14159:+X}' '+0X1.921F9F01B866EP+1' >>> f'{3.14159:.3x}' '0x1.922p+1' ```
1 parent f19b93f commit acb0e57

File tree

11 files changed

+184
-92
lines changed

11 files changed

+184
-92
lines changed

Doc/library/string.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,30 @@ The available presentation types for :class:`float` and
588588
| | as altered by the other format modifiers. |
589589
+---------+----------------------------------------------------------+
590590

591+
Additionally, for :class:`float` available following representation types:
592+
593+
+---------+----------------------------------------------------------+
594+
| Type | Meaning |
595+
+=========+==========================================================+
596+
| ``'x'`` | Represent the number by a hexadecimal string in the |
597+
| | style ``[±]0xh.hhhhp±d``, where there is one hexadecimal |
598+
| | digit before the decimal-point character and the number |
599+
| | of hexadecimal digits after it is equal to the |
600+
| | precision; if the precision is missing, then the |
601+
| | precision is sufficient for an exact representation of |
602+
| | the value. If the precision is zero and the ``'#'`` |
603+
| | option is not specified, no decimal-point character |
604+
| | appears. The exponent ``d`` is written in decimal, it |
605+
| | always contains at least one digit, and it gives the |
606+
| | power of 2 by which to multiply the coefficient. |
607+
+---------+----------------------------------------------------------+
608+
| ``'X'`` | Same as ``'x'``, but uses ``0X`` prefix and ``'P'`` as |
609+
| | the exponent separator. |
610+
+---------+----------------------------------------------------------+
611+
612+
.. versionchanged:: 3.13
613+
Support ``'x'`` and ``'X'`` format types for :class:`float`.
614+
591615

592616
.. _formatexamples:
593617

Lib/test/test_complex.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,7 @@ def test_format(self):
814814
self.assertRaises(ValueError, (1.5+3j).__format__, '=20')
815815

816816
# integer presentation types are an error
817-
for t in 'bcdoxX':
817+
for t in 'bcdo':
818818
self.assertRaises(ValueError, (1.5+0.5j).__format__, t)
819819

820820
# make sure everything works in ''.format()

Lib/test/test_float.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,7 @@ def test_format(self):
705705

706706
# confirm format options expected to fail on floats, such as integer
707707
# presentation types
708-
for format_spec in 'sbcdoxX':
708+
for format_spec in 'sbcdo':
709709
self.assertRaises(ValueError, format, 0.0, format_spec)
710710
self.assertRaises(ValueError, format, 1.0, format_spec)
711711
self.assertRaises(ValueError, format, -1.0, format_spec)

Lib/test/test_random.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -599,16 +599,16 @@ def test_guaranteed_stable(self):
599599
'0x1.1ebb4352e4c4dp-1', '0x1.1a7422abf9c11p-1'])
600600
self.gen.seed("the quick brown fox", version=2)
601601
self.assertEqual([self.gen.random().hex() for i in range(4)],
602-
['0x1.1239ddfb11b7cp-3', '0x1.b3cbb5c51b120p-4',
602+
['0x1.1239ddfb11b7cp-3', '0x1.b3cbb5c51b12p-4',
603603
'0x1.8c4f55116b60fp-1', '0x1.63eb525174a27p-1'])
604604

605605
def test_bug_27706(self):
606606
# Verify that version 1 seeds are unaffected by hash randomization
607607

608608
self.gen.seed('nofar', version=1) # hash('nofar') == 5990528763808513177
609609
self.assertEqual([self.gen.random().hex() for i in range(4)],
610-
['0x1.8645314505ad7p-1', '0x1.afb1f82e40a40p-5',
611-
'0x1.2a59d2285e971p-1', '0x1.56977142a7880p-6'])
610+
['0x1.8645314505ad7p-1', '0x1.afb1f82e40a4p-5',
611+
'0x1.2a59d2285e971p-1', '0x1.56977142a788p-6'])
612612

613613
self.gen.seed('rachel', version=1) # hash('rachel') == -9091735575445484789
614614
self.assertEqual([self.gen.random().hex() for i in range(4)],
@@ -639,8 +639,8 @@ def test_bug_31482(self):
639639

640640
self.gen.seed(b'nofar', version=1) # hash('nofar') == 5990528763808513177
641641
self.assertEqual([self.gen.random().hex() for i in range(4)],
642-
['0x1.8645314505ad7p-1', '0x1.afb1f82e40a40p-5',
643-
'0x1.2a59d2285e971p-1', '0x1.56977142a7880p-6'])
642+
['0x1.8645314505ad7p-1', '0x1.afb1f82e40a4p-5',
643+
'0x1.2a59d2285e971p-1', '0x1.56977142a788p-6'])
644644

645645
self.gen.seed(b'rachel', version=1) # hash('rachel') == -9091735575445484789
646646
self.assertEqual([self.gen.random().hex() for i in range(4)],

Lib/test/test_str.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1321,10 +1321,6 @@ def __repr__(self):
13211321
self.assertRaises(ValueError, ("{[" + big + "]}").format, [0])
13221322

13231323
# test number formatter errors:
1324-
self.assertRaises(ValueError, '{0:x}'.format, 1j)
1325-
self.assertRaises(ValueError, '{0:x}'.format, 1.0)
1326-
self.assertRaises(ValueError, '{0:X}'.format, 1j)
1327-
self.assertRaises(ValueError, '{0:X}'.format, 1.0)
13281324
self.assertRaises(ValueError, '{0:o}'.format, 1j)
13291325
self.assertRaises(ValueError, '{0:o}'.format, 1.0)
13301326
self.assertRaises(ValueError, '{0:u}'.format, 1j)

Lib/test/test_strtod.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
# introduced in Python 2.7 and 3.1.
33

44
import random
5-
import unittest
65
import re
6+
import unittest
77
import sys
88
import test.support
99

@@ -98,7 +98,7 @@ def check_strtod(self, s):
9898
got = 'memory error'
9999
else:
100100
got = fs.hex()
101-
expected = strtod(s)
101+
expected = re.sub(r'\.?0+p', 'p', strtod(s))
102102
self.assertEqual(expected, got,
103103
"Incorrectly rounded str->float conversion for {}: "
104104
"expected {}, got {}".format(s, expected, got))

Lib/test/test_types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ def test(f, format_spec, result):
527527

528528
# confirm format options expected to fail on floats, such as integer
529529
# presentation types
530-
for format_spec in 'sbcdoxX':
530+
for format_spec in 'sbcdo':
531531
self.assertRaises(ValueError, format, 0.0, format_spec)
532532
self.assertRaises(ValueError, format, 1.0, format_spec)
533533
self.assertRaises(ValueError, format, -1.0, format_spec)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support "x" and "X" format types for floats. Patch by Sergey B Kirpichev.

Objects/floatobject.c

Lines changed: 4 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,14 +1141,7 @@ float_conjugate_impl(PyObject *self)
11411141
return float_float(self);
11421142
}
11431143

1144-
/* turn ASCII hex characters into integer values and vice versa */
1145-
1146-
static char
1147-
char_from_hex(int x)
1148-
{
1149-
assert(0 <= x && x < 16);
1150-
return Py_hexdigits[x];
1151-
}
1144+
/* turn ASCII hex characters into integer values */
11521145

11531146
static int
11541147
hex_from_char(char c) {
@@ -1217,10 +1210,6 @@ hex_from_char(char c) {
12171210

12181211
/* convert a float to a hexadecimal string */
12191212

1220-
/* TOHEX_NBITS is DBL_MANT_DIG rounded up to the next integer
1221-
of the form 4k+1. */
1222-
#define TOHEX_NBITS DBL_MANT_DIG + 3 - (DBL_MANT_DIG+2)%4
1223-
12241213
/*[clinic input]
12251214
float.hex
12261215
@@ -1236,54 +1225,13 @@ static PyObject *
12361225
float_hex_impl(PyObject *self)
12371226
/*[clinic end generated code: output=0ebc9836e4d302d4 input=bec1271a33d47e67]*/
12381227
{
1239-
double x, m;
1240-
int e, shift, i, si, esign;
1241-
/* Space for 1+(TOHEX_NBITS-1)/4 digits, a decimal point, and the
1242-
trailing NUL byte. */
1243-
char s[(TOHEX_NBITS-1)/4+3];
1228+
double x;
12441229

12451230
CONVERT_TO_DOUBLE(self, x);
12461231

1247-
if (Py_IS_NAN(x) || Py_IS_INFINITY(x))
1248-
return float_repr((PyFloatObject *)self);
1249-
1250-
if (x == 0.0) {
1251-
if (copysign(1.0, x) == -1.0)
1252-
return PyUnicode_FromString("-0x0.0p+0");
1253-
else
1254-
return PyUnicode_FromString("0x0.0p+0");
1255-
}
1256-
1257-
m = frexp(fabs(x), &e);
1258-
shift = 1 - Py_MAX(DBL_MIN_EXP - e, 0);
1259-
m = ldexp(m, shift);
1260-
e -= shift;
1261-
1262-
si = 0;
1263-
s[si] = char_from_hex((int)m);
1264-
si++;
1265-
m -= (int)m;
1266-
s[si] = '.';
1267-
si++;
1268-
for (i=0; i < (TOHEX_NBITS-1)/4; i++) {
1269-
m *= 16.0;
1270-
s[si] = char_from_hex((int)m);
1271-
si++;
1272-
m -= (int)m;
1273-
}
1274-
s[si] = '\0';
1275-
1276-
if (e < 0) {
1277-
esign = (int)'-';
1278-
e = -e;
1279-
}
1280-
else
1281-
esign = (int)'+';
1232+
char *buf = PyOS_double_to_string(x, 'x', -1, 0, NULL);
12821233

1283-
if (x < 0.0)
1284-
return PyUnicode_FromFormat("-0x%sp%c%d", s, esign, e);
1285-
else
1286-
return PyUnicode_FromFormat("0x%sp%c%d", s, esign, e);
1234+
return PyUnicode_FromString(buf);
12871235
}
12881236

12891237
/* Convert a hexadecimal string to a float. */

Python/formatter_unicode.c

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,7 +1100,7 @@ format_float_internal(PyObject *value,
11001100
add_pct = 1;
11011101
}
11021102

1103-
if (precision < 0)
1103+
if (precision < 0 && type != 'x' && type != 'X')
11041104
precision = default_precision;
11051105
else if (type == 'r')
11061106
type = 'g';
@@ -1282,7 +1282,7 @@ format_complex_internal(PyObject *value,
12821282
format the result. We take care of that later. */
12831283
type = 'g';
12841284

1285-
if (precision < 0)
1285+
if (precision < 0 && (type != 'x' || type != 'X'))
12861286
precision = default_precision;
12871287
else if (type == 'r')
12881288
type = 'g';
@@ -1574,6 +1574,8 @@ _PyFloat_FormatAdvancedWriter(_PyUnicodeWriter *writer,
15741574
case 'G':
15751575
case 'n':
15761576
case '%':
1577+
case 'x':
1578+
case 'X':
15771579
/* no conversion, already a float. do the formatting */
15781580
return format_float_internal(obj, &format, writer);
15791581

@@ -1612,6 +1614,8 @@ _PyComplex_FormatAdvancedWriter(_PyUnicodeWriter *writer,
16121614
case 'g':
16131615
case 'G':
16141616
case 'n':
1617+
case 'x':
1618+
case 'X':
16151619
/* no conversion, already a complex. do the formatting */
16161620
return format_complex_internal(obj, &format, writer);
16171621

0 commit comments

Comments
 (0)