mirror of https://github.com/python/cpython
bpo-45995: add "z" format specifer to coerce negative 0 to zero (GH-30049)
Add "z" format specifier to coerce negative 0 to zero. See https://github.com/python/cpython/issues/90153 (originally https://bugs.python.org/issue45995) for discussion. This covers `str.format()` and f-strings. Old-style string interpolation is not supported. Co-authored-by: Mark Dickinson <dickinsm@gmail.com>
This commit is contained in:
parent
dd207a6ac5
commit
b0b836b20c
|
@ -309,7 +309,7 @@ non-empty format specification typically modifies the result.
|
|||
The general form of a *standard format specifier* is:
|
||||
|
||||
.. productionlist:: format-spec
|
||||
format_spec: [[`fill`]`align`][`sign`][#][0][`width`][`grouping_option`][.`precision`][`type`]
|
||||
format_spec: [[`fill`]`align`][`sign`][z][#][0][`width`][`grouping_option`][.`precision`][`type`]
|
||||
fill: <any character>
|
||||
align: "<" | ">" | "=" | "^"
|
||||
sign: "+" | "-" | " "
|
||||
|
@ -380,6 +380,15 @@ following:
|
|||
+---------+----------------------------------------------------------+
|
||||
|
||||
|
||||
.. index:: single: z; in string formatting
|
||||
|
||||
The ``'z'`` option coerces negative zero floating-point values to positive
|
||||
zero after rounding to the format precision. This option is only valid for
|
||||
floating-point presentation types.
|
||||
|
||||
.. versionchanged:: 3.11
|
||||
Added the ``'z'`` option (see also :pep:`682`).
|
||||
|
||||
.. index:: single: # (hash); in string formatting
|
||||
|
||||
The ``'#'`` option causes the "alternate form" to be used for the
|
||||
|
|
|
@ -14,12 +14,14 @@ extern "C" {
|
|||
* F_BLANK ' '
|
||||
* F_ALT '#'
|
||||
* F_ZERO '0'
|
||||
* F_NO_NEG_0 'z'
|
||||
*/
|
||||
#define F_LJUST (1<<0)
|
||||
#define F_SIGN (1<<1)
|
||||
#define F_BLANK (1<<2)
|
||||
#define F_ALT (1<<3)
|
||||
#define F_ZERO (1<<4)
|
||||
#define F_NO_NEG_0 (1<<5)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ PyAPI_FUNC(double) _Py_parse_inf_or_nan(const char *p, char **endptr);
|
|||
#define Py_DTSF_ADD_DOT_0 0x02 /* if the result is an integer add ".0" */
|
||||
#define Py_DTSF_ALT 0x04 /* "alternate" formatting. it's format_code
|
||||
specific */
|
||||
#define Py_DTSF_NO_NEG_0 0x08 /* negative zero result is coerced to 0 */
|
||||
|
||||
/* PyOS_double_to_string's "type", if non-NULL, will be set to one of: */
|
||||
#define Py_DTST_FINITE 0
|
||||
|
|
|
@ -3795,6 +3795,10 @@ class Decimal(object):
|
|||
# represented in fixed point; rescale them to 0e0.
|
||||
if not self and self._exp > 0 and spec['type'] in 'fF%':
|
||||
self = self._rescale(0, rounding)
|
||||
if not self and spec['no_neg_0'] and self._sign:
|
||||
adjusted_sign = 0
|
||||
else:
|
||||
adjusted_sign = self._sign
|
||||
|
||||
# figure out placement of the decimal point
|
||||
leftdigits = self._exp + len(self._int)
|
||||
|
@ -3825,7 +3829,7 @@ class Decimal(object):
|
|||
|
||||
# done with the decimal-specific stuff; hand over the rest
|
||||
# of the formatting to the _format_number function
|
||||
return _format_number(self._sign, intpart, fracpart, exp, spec)
|
||||
return _format_number(adjusted_sign, intpart, fracpart, exp, spec)
|
||||
|
||||
def _dec_from_triple(sign, coefficient, exponent, special=False):
|
||||
"""Create a decimal instance directly, without any validation,
|
||||
|
@ -6143,7 +6147,7 @@ _exact_half = re.compile('50*$').match
|
|||
#
|
||||
# A format specifier for Decimal looks like:
|
||||
#
|
||||
# [[fill]align][sign][#][0][minimumwidth][,][.precision][type]
|
||||
# [[fill]align][sign][z][#][0][minimumwidth][,][.precision][type]
|
||||
|
||||
_parse_format_specifier_regex = re.compile(r"""\A
|
||||
(?:
|
||||
|
@ -6151,6 +6155,7 @@ _parse_format_specifier_regex = re.compile(r"""\A
|
|||
(?P<align>[<>=^])
|
||||
)?
|
||||
(?P<sign>[-+ ])?
|
||||
(?P<no_neg_0>z)?
|
||||
(?P<alt>\#)?
|
||||
(?P<zeropad>0)?
|
||||
(?P<minimumwidth>(?!0)\d+)?
|
||||
|
|
|
@ -6119,7 +6119,7 @@ topics = {'assert': 'The "assert" statement\n'
|
|||
'The general form of a *standard format specifier* is:\n'
|
||||
'\n'
|
||||
' format_spec ::= '
|
||||
'[[fill]align][sign][#][0][width][grouping_option][.precision][type]\n'
|
||||
'[[fill]align][sign][z][#][0][width][grouping_option][.precision][type]\n'
|
||||
' fill ::= <any character>\n'
|
||||
' align ::= "<" | ">" | "=" | "^"\n'
|
||||
' sign ::= "+" | "-" | " "\n'
|
||||
|
@ -6221,6 +6221,15 @@ topics = {'assert': 'The "assert" statement\n'
|
|||
' '
|
||||
'+-----------+------------------------------------------------------------+\n'
|
||||
'\n'
|
||||
'The "\'z\'" option coerces negative zero floating-point '
|
||||
'values to positive\n'
|
||||
'zero after rounding to the format precision. This option '
|
||||
'is only valid for\n'
|
||||
'floating-point presentation types.\n'
|
||||
'\n'
|
||||
'Changed in version 3.11: Added the "\'z\'" option (see also '
|
||||
'**PEP 682**).\n'
|
||||
'\n'
|
||||
'The "\'#\'" option causes the “alternate form” to be used '
|
||||
'for the\n'
|
||||
'conversion. The alternate form is defined differently for '
|
||||
|
|
|
@ -1072,6 +1072,57 @@ class FormatTest(unittest.TestCase):
|
|||
(',e', '123456', '1.23456e+5'),
|
||||
(',E', '123456', '1.23456E+5'),
|
||||
|
||||
# negative zero: default behavior
|
||||
('.1f', '-0', '-0.0'),
|
||||
('.1f', '-.0', '-0.0'),
|
||||
('.1f', '-.01', '-0.0'),
|
||||
|
||||
# negative zero: z option
|
||||
('z.1f', '0.', '0.0'),
|
||||
('z6.1f', '0.', ' 0.0'),
|
||||
('z6.1f', '-1.', ' -1.0'),
|
||||
('z.1f', '-0.', '0.0'),
|
||||
('z.1f', '.01', '0.0'),
|
||||
('z.1f', '-.01', '0.0'),
|
||||
('z.2f', '0.', '0.00'),
|
||||
('z.2f', '-0.', '0.00'),
|
||||
('z.2f', '.001', '0.00'),
|
||||
('z.2f', '-.001', '0.00'),
|
||||
|
||||
('z.1e', '0.', '0.0e+1'),
|
||||
('z.1e', '-0.', '0.0e+1'),
|
||||
('z.1E', '0.', '0.0E+1'),
|
||||
('z.1E', '-0.', '0.0E+1'),
|
||||
|
||||
('z.2e', '-0.001', '-1.00e-3'), # tests for mishandled rounding
|
||||
('z.2g', '-0.001', '-0.001'),
|
||||
('z.2%', '-0.001', '-0.10%'),
|
||||
|
||||
('zf', '-0.0000', '0.0000'), # non-normalized form is preserved
|
||||
|
||||
('z.1f', '-00000.000001', '0.0'),
|
||||
('z.1f', '-00000.', '0.0'),
|
||||
('z.1f', '-.0000000000', '0.0'),
|
||||
|
||||
('z.2f', '-00000.000001', '0.00'),
|
||||
('z.2f', '-00000.', '0.00'),
|
||||
('z.2f', '-.0000000000', '0.00'),
|
||||
|
||||
('z.1f', '.09', '0.1'),
|
||||
('z.1f', '-.09', '-0.1'),
|
||||
|
||||
(' z.0f', '-0.', ' 0'),
|
||||
('+z.0f', '-0.', '+0'),
|
||||
('-z.0f', '-0.', '0'),
|
||||
(' z.0f', '-1.', '-1'),
|
||||
('+z.0f', '-1.', '-1'),
|
||||
('-z.0f', '-1.', '-1'),
|
||||
|
||||
('z>6.1f', '-0.', 'zz-0.0'),
|
||||
('z>z6.1f', '-0.', 'zzz0.0'),
|
||||
('x>z6.1f', '-0.', 'xxx0.0'),
|
||||
('🖤>z6.1f', '-0.', '🖤🖤🖤0.0'), # multi-byte fill char
|
||||
|
||||
# issue 6850
|
||||
('a=-7.0', '0.12345', 'aaaa0.1'),
|
||||
|
||||
|
@ -1086,6 +1137,15 @@ class FormatTest(unittest.TestCase):
|
|||
# bytes format argument
|
||||
self.assertRaises(TypeError, Decimal(1).__format__, b'-020')
|
||||
|
||||
def test_negative_zero_format_directed_rounding(self):
|
||||
with self.decimal.localcontext() as ctx:
|
||||
ctx.rounding = ROUND_CEILING
|
||||
self.assertEqual(format(self.decimal.Decimal('-0.001'), 'z.2f'),
|
||||
'0.00')
|
||||
|
||||
def test_negative_zero_bad_format(self):
|
||||
self.assertRaises(ValueError, format, self.decimal.Decimal('1.23'), 'fz')
|
||||
|
||||
def test_n_format(self):
|
||||
Decimal = self.decimal.Decimal
|
||||
|
||||
|
|
|
@ -701,18 +701,16 @@ class FormatTestCase(unittest.TestCase):
|
|||
# conversion to string should fail
|
||||
self.assertRaises(ValueError, format, 3.0, "s")
|
||||
|
||||
# other format specifiers shouldn't work on floats,
|
||||
# in particular int specifiers
|
||||
for format_spec in ([chr(x) for x in range(ord('a'), ord('z')+1)] +
|
||||
[chr(x) for x in range(ord('A'), ord('Z')+1)]):
|
||||
if not format_spec in 'eEfFgGn%':
|
||||
self.assertRaises(ValueError, format, 0.0, format_spec)
|
||||
self.assertRaises(ValueError, format, 1.0, format_spec)
|
||||
self.assertRaises(ValueError, format, -1.0, format_spec)
|
||||
self.assertRaises(ValueError, format, 1e100, format_spec)
|
||||
self.assertRaises(ValueError, format, -1e100, format_spec)
|
||||
self.assertRaises(ValueError, format, 1e-100, format_spec)
|
||||
self.assertRaises(ValueError, format, -1e-100, format_spec)
|
||||
# confirm format options expected to fail on floats, such as integer
|
||||
# presentation types
|
||||
for format_spec in 'sbcdoxX':
|
||||
self.assertRaises(ValueError, format, 0.0, format_spec)
|
||||
self.assertRaises(ValueError, format, 1.0, format_spec)
|
||||
self.assertRaises(ValueError, format, -1.0, format_spec)
|
||||
self.assertRaises(ValueError, format, 1e100, format_spec)
|
||||
self.assertRaises(ValueError, format, -1e100, format_spec)
|
||||
self.assertRaises(ValueError, format, 1e-100, format_spec)
|
||||
self.assertRaises(ValueError, format, -1e-100, format_spec)
|
||||
|
||||
# issue 3382
|
||||
self.assertEqual(format(NAN, 'f'), 'nan')
|
||||
|
|
|
@ -546,6 +546,80 @@ class FormatTest(unittest.TestCase):
|
|||
with self.assertRaisesRegex(ValueError, str_err):
|
||||
"{a:%ЫйЯЧ}".format(a='a')
|
||||
|
||||
def test_negative_zero(self):
|
||||
## default behavior
|
||||
self.assertEqual(f"{-0.:.1f}", "-0.0")
|
||||
self.assertEqual(f"{-.01:.1f}", "-0.0")
|
||||
self.assertEqual(f"{-0:.1f}", "0.0") # integers do not distinguish -0
|
||||
|
||||
## z sign option
|
||||
self.assertEqual(f"{0.:z.1f}", "0.0")
|
||||
self.assertEqual(f"{0.:z6.1f}", " 0.0")
|
||||
self.assertEqual(f"{-1.:z6.1f}", " -1.0")
|
||||
self.assertEqual(f"{-0.:z.1f}", "0.0")
|
||||
self.assertEqual(f"{.01:z.1f}", "0.0")
|
||||
self.assertEqual(f"{-0:z.1f}", "0.0") # z is allowed for integer input
|
||||
self.assertEqual(f"{-.01:z.1f}", "0.0")
|
||||
self.assertEqual(f"{0.:z.2f}", "0.00")
|
||||
self.assertEqual(f"{-0.:z.2f}", "0.00")
|
||||
self.assertEqual(f"{.001:z.2f}", "0.00")
|
||||
self.assertEqual(f"{-.001:z.2f}", "0.00")
|
||||
|
||||
self.assertEqual(f"{0.:z.1e}", "0.0e+00")
|
||||
self.assertEqual(f"{-0.:z.1e}", "0.0e+00")
|
||||
self.assertEqual(f"{0.:z.1E}", "0.0E+00")
|
||||
self.assertEqual(f"{-0.:z.1E}", "0.0E+00")
|
||||
|
||||
self.assertEqual(f"{-0.001:z.2e}", "-1.00e-03") # tests for mishandled
|
||||
# rounding
|
||||
self.assertEqual(f"{-0.001:z.2g}", "-0.001")
|
||||
self.assertEqual(f"{-0.001:z.2%}", "-0.10%")
|
||||
|
||||
self.assertEqual(f"{-00000.000001:z.1f}", "0.0")
|
||||
self.assertEqual(f"{-00000.:z.1f}", "0.0")
|
||||
self.assertEqual(f"{-.0000000000:z.1f}", "0.0")
|
||||
|
||||
self.assertEqual(f"{-00000.000001:z.2f}", "0.00")
|
||||
self.assertEqual(f"{-00000.:z.2f}", "0.00")
|
||||
self.assertEqual(f"{-.0000000000:z.2f}", "0.00")
|
||||
|
||||
self.assertEqual(f"{.09:z.1f}", "0.1")
|
||||
self.assertEqual(f"{-.09:z.1f}", "-0.1")
|
||||
|
||||
self.assertEqual(f"{-0.: z.0f}", " 0")
|
||||
self.assertEqual(f"{-0.:+z.0f}", "+0")
|
||||
self.assertEqual(f"{-0.:-z.0f}", "0")
|
||||
self.assertEqual(f"{-1.: z.0f}", "-1")
|
||||
self.assertEqual(f"{-1.:+z.0f}", "-1")
|
||||
self.assertEqual(f"{-1.:-z.0f}", "-1")
|
||||
|
||||
self.assertEqual(f"{0.j:z.1f}", "0.0+0.0j")
|
||||
self.assertEqual(f"{-0.j:z.1f}", "0.0+0.0j")
|
||||
self.assertEqual(f"{.01j:z.1f}", "0.0+0.0j")
|
||||
self.assertEqual(f"{-.01j:z.1f}", "0.0+0.0j")
|
||||
|
||||
self.assertEqual(f"{-0.:z>6.1f}", "zz-0.0") # test fill, esp. 'z' fill
|
||||
self.assertEqual(f"{-0.:z>z6.1f}", "zzz0.0")
|
||||
self.assertEqual(f"{-0.:x>z6.1f}", "xxx0.0")
|
||||
self.assertEqual(f"{-0.:🖤>z6.1f}", "🖤🖤🖤0.0") # multi-byte fill char
|
||||
|
||||
def test_specifier_z_error(self):
|
||||
error_msg = re.compile("Invalid format specifier '.*z.*'")
|
||||
with self.assertRaisesRegex(ValueError, error_msg):
|
||||
f"{0:z+f}" # wrong position
|
||||
with self.assertRaisesRegex(ValueError, error_msg):
|
||||
f"{0:fz}" # wrong position
|
||||
|
||||
error_msg = re.escape("Negative zero coercion (z) not allowed")
|
||||
with self.assertRaisesRegex(ValueError, error_msg):
|
||||
f"{0:zd}" # can't apply to int presentation type
|
||||
with self.assertRaisesRegex(ValueError, error_msg):
|
||||
f"{'x':zs}" # can't apply to string
|
||||
|
||||
error_msg = re.escape("unsupported format character 'z'")
|
||||
with self.assertRaisesRegex(ValueError, error_msg):
|
||||
"%z.1f" % 0 # not allowed in old style string interpolation
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
@ -524,18 +524,16 @@ class TypesTests(unittest.TestCase):
|
|||
self.assertRaises(TypeError, 3.0.__format__, None)
|
||||
self.assertRaises(TypeError, 3.0.__format__, 0)
|
||||
|
||||
# other format specifiers shouldn't work on floats,
|
||||
# in particular int specifiers
|
||||
for format_spec in ([chr(x) for x in range(ord('a'), ord('z')+1)] +
|
||||
[chr(x) for x in range(ord('A'), ord('Z')+1)]):
|
||||
if not format_spec in 'eEfFgGn%':
|
||||
self.assertRaises(ValueError, format, 0.0, format_spec)
|
||||
self.assertRaises(ValueError, format, 1.0, format_spec)
|
||||
self.assertRaises(ValueError, format, -1.0, format_spec)
|
||||
self.assertRaises(ValueError, format, 1e100, format_spec)
|
||||
self.assertRaises(ValueError, format, -1e100, format_spec)
|
||||
self.assertRaises(ValueError, format, 1e-100, format_spec)
|
||||
self.assertRaises(ValueError, format, -1e-100, format_spec)
|
||||
# confirm format options expected to fail on floats, such as integer
|
||||
# presentation types
|
||||
for format_spec in 'sbcdoxX':
|
||||
self.assertRaises(ValueError, format, 0.0, format_spec)
|
||||
self.assertRaises(ValueError, format, 1.0, format_spec)
|
||||
self.assertRaises(ValueError, format, -1.0, format_spec)
|
||||
self.assertRaises(ValueError, format, 1e100, format_spec)
|
||||
self.assertRaises(ValueError, format, -1e100, format_spec)
|
||||
self.assertRaises(ValueError, format, 1e-100, format_spec)
|
||||
self.assertRaises(ValueError, format, -1e-100, format_spec)
|
||||
|
||||
# Alternate float formatting
|
||||
test(1.0, '.0e', '1e+00')
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
Add a "z" option to the string formatting specification that coerces negative
|
||||
zero floating-point values to positive zero after rounding to the format
|
||||
precision. Contributed by John Belmonte.
|
|
@ -3183,6 +3183,56 @@ dotsep_as_utf8(const char *s)
|
|||
return utf8;
|
||||
}
|
||||
|
||||
/* copy of libmpdec _mpd_round() */
|
||||
static void
|
||||
_mpd_round(mpd_t *result, const mpd_t *a, mpd_ssize_t prec,
|
||||
const mpd_context_t *ctx, uint32_t *status)
|
||||
{
|
||||
mpd_ssize_t exp = a->exp + a->digits - prec;
|
||||
|
||||
if (prec <= 0) {
|
||||
mpd_seterror(result, MPD_Invalid_operation, status);
|
||||
return;
|
||||
}
|
||||
if (mpd_isspecial(a) || mpd_iszero(a)) {
|
||||
mpd_qcopy(result, a, status);
|
||||
return;
|
||||
}
|
||||
|
||||
mpd_qrescale_fmt(result, a, exp, ctx, status);
|
||||
if (result->digits > prec) {
|
||||
mpd_qrescale_fmt(result, result, exp+1, ctx, status);
|
||||
}
|
||||
}
|
||||
|
||||
/* Locate negative zero "z" option within a UTF-8 format spec string.
|
||||
* Returns pointer to "z", else NULL.
|
||||
* The portion of the spec we're working with is [[fill]align][sign][z] */
|
||||
static const char *
|
||||
format_spec_z_search(char const *fmt, Py_ssize_t size) {
|
||||
char const *pos = fmt;
|
||||
char const *fmt_end = fmt + size;
|
||||
/* skip over [[fill]align] (fill may be multi-byte character) */
|
||||
pos += 1;
|
||||
while (pos < fmt_end && *pos & 0x80) {
|
||||
pos += 1;
|
||||
}
|
||||
if (pos < fmt_end && strchr("<>=^", *pos) != NULL) {
|
||||
pos += 1;
|
||||
} else {
|
||||
/* fill not present-- skip over [align] */
|
||||
pos = fmt;
|
||||
if (pos < fmt_end && strchr("<>=^", *pos) != NULL) {
|
||||
pos += 1;
|
||||
}
|
||||
}
|
||||
/* skip over [sign] */
|
||||
if (pos < fmt_end && strchr("+- ", *pos) != NULL) {
|
||||
pos += 1;
|
||||
}
|
||||
return pos < fmt_end && *pos == 'z' ? pos : NULL;
|
||||
}
|
||||
|
||||
static int
|
||||
dict_get_item_string(PyObject *dict, const char *key, PyObject **valueobj, const char **valuestr)
|
||||
{
|
||||
|
@ -3220,11 +3270,16 @@ dec_format(PyObject *dec, PyObject *args)
|
|||
PyObject *fmtarg;
|
||||
PyObject *context;
|
||||
mpd_spec_t spec;
|
||||
char *fmt;
|
||||
char const *fmt;
|
||||
char *fmt_copy = NULL;
|
||||
char *decstring = NULL;
|
||||
uint32_t status = 0;
|
||||
int replace_fillchar = 0;
|
||||
int no_neg_0 = 0;
|
||||
Py_ssize_t size;
|
||||
mpd_t *mpd = MPD(dec);
|
||||
mpd_uint_t dt[MPD_MINALLOC_MAX];
|
||||
mpd_t tmp = {MPD_STATIC|MPD_STATIC_DATA,0,0,0,MPD_MINALLOC_MAX,dt};
|
||||
|
||||
|
||||
CURRENT_CONTEXT(context);
|
||||
|
@ -3233,19 +3288,39 @@ dec_format(PyObject *dec, PyObject *args)
|
|||
}
|
||||
|
||||
if (PyUnicode_Check(fmtarg)) {
|
||||
fmt = (char *)PyUnicode_AsUTF8AndSize(fmtarg, &size);
|
||||
fmt = PyUnicode_AsUTF8AndSize(fmtarg, &size);
|
||||
if (fmt == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
/* NOTE: If https://github.com/python/cpython/pull/29438 lands, the
|
||||
* format string manipulation below can be eliminated by enhancing
|
||||
* the forked mpd_parse_fmt_str(). */
|
||||
if (size > 0 && fmt[0] == '\0') {
|
||||
/* NUL fill character: must be replaced with a valid UTF-8 char
|
||||
before calling mpd_parse_fmt_str(). */
|
||||
replace_fillchar = 1;
|
||||
fmt = dec_strdup(fmt, size);
|
||||
if (fmt == NULL) {
|
||||
fmt = fmt_copy = dec_strdup(fmt, size);
|
||||
if (fmt_copy == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
fmt[0] = '_';
|
||||
fmt_copy[0] = '_';
|
||||
}
|
||||
/* Strip 'z' option, which isn't understood by mpd_parse_fmt_str().
|
||||
* NOTE: fmt is always null terminated by PyUnicode_AsUTF8AndSize() */
|
||||
char const *z_position = format_spec_z_search(fmt, size);
|
||||
if (z_position != NULL) {
|
||||
no_neg_0 = 1;
|
||||
size_t z_index = z_position - fmt;
|
||||
if (fmt_copy == NULL) {
|
||||
fmt = fmt_copy = dec_strdup(fmt, size);
|
||||
if (fmt_copy == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
/* Shift characters (including null terminator) left,
|
||||
overwriting the 'z' option. */
|
||||
memmove(fmt_copy + z_index, fmt_copy + z_index + 1, size - z_index);
|
||||
size -= 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
@ -3311,8 +3386,45 @@ dec_format(PyObject *dec, PyObject *args)
|
|||
}
|
||||
}
|
||||
|
||||
if (no_neg_0 && mpd_isnegative(mpd) && !mpd_isspecial(mpd)) {
|
||||
/* Round into a temporary (carefully mirroring the rounding
|
||||
of mpd_qformat_spec()), and check if the result is negative zero.
|
||||
If so, clear the sign and format the resulting positive zero. */
|
||||
mpd_ssize_t prec;
|
||||
mpd_qcopy(&tmp, mpd, &status);
|
||||
if (spec.prec >= 0) {
|
||||
switch (spec.type) {
|
||||
case 'f':
|
||||
mpd_qrescale(&tmp, &tmp, -spec.prec, CTX(context), &status);
|
||||
break;
|
||||
case '%':
|
||||
tmp.exp += 2;
|
||||
mpd_qrescale(&tmp, &tmp, -spec.prec, CTX(context), &status);
|
||||
break;
|
||||
case 'g':
|
||||
prec = (spec.prec == 0) ? 1 : spec.prec;
|
||||
if (tmp.digits > prec) {
|
||||
_mpd_round(&tmp, &tmp, prec, CTX(context), &status);
|
||||
}
|
||||
break;
|
||||
case 'e':
|
||||
if (!mpd_iszero(&tmp)) {
|
||||
_mpd_round(&tmp, &tmp, spec.prec+1, CTX(context), &status);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (status & MPD_Errors) {
|
||||
PyErr_SetString(PyExc_ValueError, "unexpected error when rounding");
|
||||
goto finish;
|
||||
}
|
||||
if (mpd_iszero(&tmp)) {
|
||||
mpd_set_positive(&tmp);
|
||||
mpd = &tmp;
|
||||
}
|
||||
}
|
||||
|
||||
decstring = mpd_qformat_spec(MPD(dec), &spec, CTX(context), &status);
|
||||
decstring = mpd_qformat_spec(mpd, &spec, CTX(context), &status);
|
||||
if (decstring == NULL) {
|
||||
if (status & MPD_Malloc_error) {
|
||||
PyErr_NoMemory();
|
||||
|
@ -3335,7 +3447,7 @@ finish:
|
|||
Py_XDECREF(grouping);
|
||||
Py_XDECREF(sep);
|
||||
Py_XDECREF(dot);
|
||||
if (replace_fillchar) PyMem_Free(fmt);
|
||||
if (fmt_copy) PyMem_Free(fmt_copy);
|
||||
if (decstring) mpd_free(decstring);
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -415,6 +415,7 @@ formatfloat(PyObject *v, int flags, int prec, int type,
|
|||
PyObject *result;
|
||||
double x;
|
||||
size_t len;
|
||||
int dtoa_flags = 0;
|
||||
|
||||
x = PyFloat_AsDouble(v);
|
||||
if (x == -1.0 && PyErr_Occurred()) {
|
||||
|
@ -426,8 +427,13 @@ formatfloat(PyObject *v, int flags, int prec, int type,
|
|||
if (prec < 0)
|
||||
prec = 6;
|
||||
|
||||
p = PyOS_double_to_string(x, type, prec,
|
||||
(flags & F_ALT) ? Py_DTSF_ALT : 0, NULL);
|
||||
if (flags & F_ALT) {
|
||||
dtoa_flags |= Py_DTSF_ALT;
|
||||
}
|
||||
if (flags & F_NO_NEG_0) {
|
||||
dtoa_flags |= Py_DTSF_NO_NEG_0;
|
||||
}
|
||||
p = PyOS_double_to_string(x, type, prec, dtoa_flags, NULL);
|
||||
|
||||
if (p == NULL)
|
||||
return NULL;
|
||||
|
@ -706,6 +712,7 @@ _PyBytes_FormatEx(const char *format, Py_ssize_t format_len,
|
|||
case ' ': flags |= F_BLANK; continue;
|
||||
case '#': flags |= F_ALT; continue;
|
||||
case '0': flags |= F_ZERO; continue;
|
||||
case 'z': flags |= F_NO_NEG_0; continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -14372,7 +14372,7 @@ formatfloat(PyObject *v, struct unicode_format_arg_t *arg,
|
|||
double x;
|
||||
Py_ssize_t len;
|
||||
int prec;
|
||||
int dtoa_flags;
|
||||
int dtoa_flags = 0;
|
||||
|
||||
x = PyFloat_AsDouble(v);
|
||||
if (x == -1.0 && PyErr_Occurred())
|
||||
|
@ -14383,9 +14383,9 @@ formatfloat(PyObject *v, struct unicode_format_arg_t *arg,
|
|||
prec = 6;
|
||||
|
||||
if (arg->flags & F_ALT)
|
||||
dtoa_flags = Py_DTSF_ALT;
|
||||
else
|
||||
dtoa_flags = 0;
|
||||
dtoa_flags |= Py_DTSF_ALT;
|
||||
if (arg->flags & F_NO_NEG_0)
|
||||
dtoa_flags |= Py_DTSF_NO_NEG_0;
|
||||
p = PyOS_double_to_string(x, arg->ch, prec, dtoa_flags, NULL);
|
||||
if (p == NULL)
|
||||
return -1;
|
||||
|
|
|
@ -310,6 +310,7 @@ simple_format_arg_parse(PyObject *fmt, Py_ssize_t *ppos,
|
|||
case ' ': *flags |= F_BLANK; continue;
|
||||
case '#': *flags |= F_ALT; continue;
|
||||
case '0': *flags |= F_ZERO; continue;
|
||||
case 'z': *flags |= F_NO_NEG_0; continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -130,6 +130,7 @@ typedef struct {
|
|||
Py_UCS4 fill_char;
|
||||
Py_UCS4 align;
|
||||
int alternate;
|
||||
int no_neg_0;
|
||||
Py_UCS4 sign;
|
||||
Py_ssize_t width;
|
||||
enum LocaleType thousands_separators;
|
||||
|
@ -166,6 +167,7 @@ parse_internal_render_format_spec(PyObject *obj,
|
|||
format->fill_char = ' ';
|
||||
format->align = default_align;
|
||||
format->alternate = 0;
|
||||
format->no_neg_0 = 0;
|
||||
format->sign = '\0';
|
||||
format->width = -1;
|
||||
format->thousands_separators = LT_NO_LOCALE;
|
||||
|
@ -193,6 +195,13 @@ parse_internal_render_format_spec(PyObject *obj,
|
|||
++pos;
|
||||
}
|
||||
|
||||
/* If the next character is z, request coercion of negative 0.
|
||||
Applies only to floats. */
|
||||
if (end-pos >= 1 && READ_spec(pos) == 'z') {
|
||||
format->no_neg_0 = 1;
|
||||
++pos;
|
||||
}
|
||||
|
||||
/* If the next character is #, we're in alternate mode. This only
|
||||
applies to integers. */
|
||||
if (end-pos >= 1 && READ_spec(pos) == '#') {
|
||||
|
@ -779,6 +788,14 @@ format_string_internal(PyObject *value, const InternalFormatSpec *format,
|
|||
goto done;
|
||||
}
|
||||
|
||||
/* negative 0 coercion is not allowed on strings */
|
||||
if (format->no_neg_0) {
|
||||
PyErr_SetString(PyExc_ValueError,
|
||||
"Negative zero coercion (z) not allowed in string format "
|
||||
"specifier");
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* alternate is not allowed on strings */
|
||||
if (format->alternate) {
|
||||
PyErr_SetString(PyExc_ValueError,
|
||||
|
@ -872,6 +889,13 @@ format_long_internal(PyObject *value, const InternalFormatSpec *format,
|
|||
"Precision not allowed in integer format specifier");
|
||||
goto done;
|
||||
}
|
||||
/* no negative zero coercion on integers */
|
||||
if (format->no_neg_0) {
|
||||
PyErr_SetString(PyExc_ValueError,
|
||||
"Negative zero coercion (z) not allowed in integer"
|
||||
" format specifier");
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* special case for character formatting */
|
||||
if (format->type == 'c') {
|
||||
|
@ -1049,6 +1073,8 @@ format_float_internal(PyObject *value,
|
|||
|
||||
if (format->alternate)
|
||||
flags |= Py_DTSF_ALT;
|
||||
if (format->no_neg_0)
|
||||
flags |= Py_DTSF_NO_NEG_0;
|
||||
|
||||
if (type == '\0') {
|
||||
/* Omitted type specifier. Behaves in the same way as repr(x)
|
||||
|
@ -1238,6 +1264,8 @@ format_complex_internal(PyObject *value,
|
|||
|
||||
if (format->alternate)
|
||||
flags |= Py_DTSF_ALT;
|
||||
if (format->no_neg_0)
|
||||
flags |= Py_DTSF_NO_NEG_0;
|
||||
|
||||
if (type == '\0') {
|
||||
/* Omitted type specifier. Should be like str(self). */
|
||||
|
|
|
@ -916,6 +916,18 @@ char * PyOS_double_to_string(double val,
|
|||
(flags & Py_DTSF_ALT ? "#" : ""), precision,
|
||||
format_code);
|
||||
_PyOS_ascii_formatd(buf, bufsize, format, val, precision);
|
||||
|
||||
if (flags & Py_DTSF_NO_NEG_0 && buf[0] == '-') {
|
||||
char *buf2 = buf + 1;
|
||||
while (*buf2 == '0' || *buf2 == '.') {
|
||||
++buf2;
|
||||
}
|
||||
if (*buf2 == 0 || *buf2 == 'e') {
|
||||
size_t len = buf2 - buf + strlen(buf2);
|
||||
assert(buf[len] == 0);
|
||||
memmove(buf, buf+1, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Add sign when requested. It's convenient (esp. when formatting
|
||||
|
@ -995,8 +1007,8 @@ static char *
|
|||
format_float_short(double d, char format_code,
|
||||
int mode, int precision,
|
||||
int always_add_sign, int add_dot_0_if_integer,
|
||||
int use_alt_formatting, const char * const *float_strings,
|
||||
int *type)
|
||||
int use_alt_formatting, int no_negative_zero,
|
||||
const char * const *float_strings, int *type)
|
||||
{
|
||||
char *buf = NULL;
|
||||
char *p = NULL;
|
||||
|
@ -1022,6 +1034,11 @@ format_float_short(double d, char format_code,
|
|||
assert(digits_end != NULL && digits_end >= digits);
|
||||
digits_len = digits_end - digits;
|
||||
|
||||
if (no_negative_zero && sign == 1 &&
|
||||
(digits_len == 0 || (digits_len == 1 && digits[0] == '0'))) {
|
||||
sign = 0;
|
||||
}
|
||||
|
||||
if (digits_len && !Py_ISDIGIT(digits[0])) {
|
||||
/* Infinities and nans here; adapt Gay's output,
|
||||
so convert Infinity to inf and NaN to nan, and
|
||||
|
@ -1301,6 +1318,7 @@ char * PyOS_double_to_string(double val,
|
|||
flags & Py_DTSF_SIGN,
|
||||
flags & Py_DTSF_ADD_DOT_0,
|
||||
flags & Py_DTSF_ALT,
|
||||
flags & Py_DTSF_NO_NEG_0,
|
||||
float_strings, type);
|
||||
}
|
||||
#endif // _PY_SHORT_FLOAT_REPR == 1
|
||||
|
|
Loading…
Reference in New Issue