From a633e8870ff2952d1727b6d18352be8baaa0c86a Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Tue, 7 Dec 2021 21:29:31 +0900 Subject: [PATCH 01/25] bpo-45995: add "z" format specifer to coerce negative 0 to zero This covers str.format() and f-strings. Old-style string interpolation is not supported. TODO: Decimal support --- Doc/library/string.rst | 10 +++++- Include/internal/pycore_format.h | 2 ++ Include/pystrtod.h | 1 + Lib/pydoc_data/topics.py | 8 ++++- Lib/test/test_float.py | 2 +- Lib/test/test_format.py | 57 ++++++++++++++++++++++++++++++++ Lib/test/test_types.py | 2 +- Objects/bytesobject.c | 9 +++-- Objects/unicodeobject.c | 8 ++--- Python/ast_opt.c | 1 + Python/formatter_unicode.c | 28 ++++++++++++++++ Python/pystrtod.c | 22 ++++++++++-- 12 files changed, 138 insertions(+), 12 deletions(-) diff --git a/Doc/library/string.rst b/Doc/library/string.rst index 78bd167bcf579b..28db5595553222 100644 --- a/Doc/library/string.rst +++ b/Doc/library/string.rst @@ -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: align: "<" | ">" | "=" | "^" sign: "+" | "-" | " " @@ -380,6 +380,14 @@ following: +---------+----------------------------------------------------------+ +.. index:: single: z; in string formatting + +The ``'z'`` option causes negative zero to be coerced to zero. This +option is only valid for float and complex types. + +.. versionchanged:: 3.11 + Added the ``'z'`` option. + .. index:: single: # (hash); in string formatting The ``'#'`` option causes the "alternate form" to be used for the diff --git a/Include/internal/pycore_format.h b/Include/internal/pycore_format.h index 1b8d57539ca505..1899609e77ef20 100644 --- a/Include/internal/pycore_format.h +++ b/Include/internal/pycore_format.h @@ -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 } diff --git a/Include/pystrtod.h b/Include/pystrtod.h index c1e84de6fe5428..fa056d17b6395f 100644 --- a/Include/pystrtod.h +++ b/Include/pystrtod.h @@ -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 diff --git a/Lib/pydoc_data/topics.py b/Lib/pydoc_data/topics.py index 433c905096e9eb..994c81564e0d1a 100644 --- a/Lib/pydoc_data/topics.py +++ b/Lib/pydoc_data/topics.py @@ -6140,7 +6140,7 @@ '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 ::= \n' ' align ::= "<" | ">" | "=" | "^"\n' ' sign ::= "+" | "-" | " "\n' @@ -6242,6 +6242,12 @@ ' ' '+-----------+------------------------------------------------------------+\n' '\n' + 'The "\'z\'" option causes negative zero to be coerced to ' + 'zero. This\n' + 'option is only valid for float and complex types.\n' + '\n' + 'Changed in version 3.11: Added the "\'z\'" option.\n' + '\n' 'The "\'#\'" option causes the “alternate form” to be used ' 'for the\n' 'conversion. The alternate form is defined differently for ' diff --git a/Lib/test/test_float.py b/Lib/test/test_float.py index 9cf223f8926786..266482e81f4654 100644 --- a/Lib/test/test_float.py +++ b/Lib/test/test_float.py @@ -705,7 +705,7 @@ def test_format(self): # 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%': + if not format_spec in 'eEfFgGnz%': self.assertRaises(ValueError, format, 0.0, format_spec) self.assertRaises(ValueError, format, 1.0, format_spec) self.assertRaises(ValueError, format, -1.0, format_spec) diff --git a/Lib/test/test_format.py b/Lib/test/test_format.py index 16d29d1ea3d944..cf208cd8d17b6a 100644 --- a/Lib/test/test_format.py +++ b/Lib/test/test_format.py @@ -1,3 +1,4 @@ +from decimal import Decimal from test.support import verbose, TestFailed import locale import sys @@ -546,6 +547,62 @@ def test_unicode_in_error_message(self): 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 + self.assertEqual(f"{Decimal('-0'):.1f}", "-0.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"{-.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"{-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.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") + + + 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 + + error_msg = re.escape("Negative zero coercion (z) not allowed") + with self.assertRaisesRegex(ValueError, error_msg): + f"{0:zd}" # can't apply to int + 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() diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index f8b239117f513f..a09a3da20ab6b6 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -528,7 +528,7 @@ def test(f, format_spec, result): # 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%': + if not format_spec in 'eEfFgGnz%': self.assertRaises(ValueError, format, 0.0, format_spec) self.assertRaises(ValueError, format, 1.0, format_spec) self.assertRaises(ValueError, format, -1.0, format_spec) diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c index fd1c58c2f233eb..86494cadbab9c0 100644 --- a/Objects/bytesobject.c +++ b/Objects/bytesobject.c @@ -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,11 @@ 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 +710,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; } diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c index 5dfe6e1e93f9f7..f5cd81ec7924ee 100644 --- a/Objects/unicodeobject.c +++ b/Objects/unicodeobject.c @@ -14374,7 +14374,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()) @@ -14385,9 +14385,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; diff --git a/Python/ast_opt.c b/Python/ast_opt.c index 77ed29d0cdddd8..b1d807bcf10ae1 100644 --- a/Python/ast_opt.c +++ b/Python/ast_opt.c @@ -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; } diff --git a/Python/formatter_unicode.c b/Python/formatter_unicode.c index a1e50e20c9d8c6..bd87a50ba68fce 100644 --- a/Python/formatter_unicode.c +++ b/Python/formatter_unicode.c @@ -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 negatize 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). */ diff --git a/Python/pystrtod.c b/Python/pystrtod.c index 1b27f0a3ad36ad..d77b846f0403f0 100644 --- a/Python/pystrtod.c +++ b/Python/pystrtod.c @@ -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 From 636da05ea4dee13bba3ba35478227caf16cffee2 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Sat, 11 Dec 2021 23:07:29 +0900 Subject: [PATCH 02/25] formatting --- Lib/test/test_format.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_format.py b/Lib/test/test_format.py index cf208cd8d17b6a..6f52b3633d2ca3 100644 --- a/Lib/test/test_format.py +++ b/Lib/test/test_format.py @@ -591,13 +591,13 @@ def test_negative_zero(self): 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 + f"{0:z+f}" # 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 + f"{0:zd}" # can't apply to int with self.assertRaisesRegex(ValueError, error_msg): - f"{'x':zs}" # can't apply to string + f"{'x':zs}" # can't apply to string error_msg = re.escape("unsupported format character 'z'") with self.assertRaisesRegex(ValueError, error_msg): From 3f4085d88b0b8e073ea83b41fc050aa028701f9f Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Mon, 13 Dec 2021 23:34:25 +0900 Subject: [PATCH 03/25] implementation for Decimal --- Lib/_pydecimal.py | 9 +++++++-- Lib/test/test_decimal.py | 33 +++++++++++++++++++++++++++++++++ Lib/test/test_format.py | 2 -- Modules/_decimal/_decimal.c | 36 ++++++++++++++++++++++++++++++++---- 4 files changed, 72 insertions(+), 8 deletions(-) diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index f6d9ddf42e4734..6cf7cc9991d83f 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -3795,6 +3795,10 @@ def __format__(self, specifier, context=None, _localeconv=None): # 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['coerce_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 @@ def __format__(self, specifier, context=None, _localeconv=None): # 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 @@ def _convert_for_comparison(self, other, equality_op=False): # # 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 @@ def _convert_for_comparison(self, other, equality_op=False): (?P[<>=^]) )? (?P[-+ ])? +(?Pz)? (?P\#)? (?P0)? (?P(?!0)\d+)? diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index b68cfbef23f16d..2961ddd4c5692e 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -1071,6 +1071,39 @@ def test_formatting(self): (',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.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'), + # issue 6850 ('a=-7.0', '0.12345', 'aaaa0.1'), diff --git a/Lib/test/test_format.py b/Lib/test/test_format.py index 6f52b3633d2ca3..42b8ccbd34ec9a 100644 --- a/Lib/test/test_format.py +++ b/Lib/test/test_format.py @@ -1,4 +1,3 @@ -from decimal import Decimal from test.support import verbose, TestFailed import locale import sys @@ -552,7 +551,6 @@ def test_negative_zero(self): 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 - self.assertEqual(f"{Decimal('-0'):.1f}", "-0.0") ## z sign option self.assertEqual(f"{0.:z.1f}", "0.0") diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index 35a115676a71be..cbb468ca9ee383 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -3221,9 +3221,11 @@ dec_format(PyObject *dec, PyObject *args) PyObject *context; mpd_spec_t spec; char *fmt; + char *fmt_copy = NULL; char *decstring = NULL; uint32_t status = 0; int replace_fillchar = 0; + int coerce_neg_0 = 0; Py_ssize_t size; @@ -3241,11 +3243,24 @@ dec_format(PyObject *dec, PyObject *args) /* 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] = '_'; + } + char *z_start = strchr(fmt, 'z'); + if (z_start != NULL) { + coerce_neg_0 = 1; + size_t z_index = z_start - fmt; + if (fmt_copy == NULL) { + fmt = fmt_copy = dec_strdup(fmt, size); + if (fmt_copy == NULL) { + return NULL; + } + } + memmove(fmt_copy + z_index, fmt_copy + z_index + 1, size - z_index); + size -= 1; } } else { @@ -3311,6 +3326,19 @@ dec_format(PyObject *dec, PyObject *args) } } + if (coerce_neg_0 && mpd_isnegative(MPD(dec)) && !mpd_isspecial(MPD(dec))) { + /* round into a temporary and clear sign if result is zero */ + mpd_uint_t dt[MPD_MINALLOC_MAX]; + mpd_t tmp = {MPD_STATIC|MPD_STATIC_DATA,0,0,0,MPD_MINALLOC_MAX,dt}; + mpd_qrescale(&tmp, MPD(dec), -spec.prec, CTX(context), &status); + if (status & MPD_Errors) { + PyErr_SetString(PyExc_ValueError, "unexpected error when rounding"); + goto finish; + } + if (mpd_iszero(&tmp)) { + mpd_set_positive(MPD(dec)); + } + } decstring = mpd_qformat_spec(MPD(dec), &spec, CTX(context), &status); if (decstring == NULL) { @@ -3335,7 +3363,7 @@ dec_format(PyObject *dec, PyObject *args) 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; } From 68a049ebe3469b96860214d9c38fad4b43fa294b Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Tue, 14 Dec 2021 13:15:43 +0000 Subject: [PATCH 04/25] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst diff --git a/Misc/NEWS.d/next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst b/Misc/NEWS.d/next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst new file mode 100644 index 00000000000000..7beb4bc1d909b6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst @@ -0,0 +1,3 @@ +Add "z" option to the string formatting specification, which coerces negative +zero floating-point values to positive zero after rounding to the format precision. +Covers ``float`` and ``Decimal`` formatting via :func:`str.format` and f-strings. \ No newline at end of file From ad32be5e37685388b6f487e353242b9fa593e843 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Tue, 14 Dec 2021 22:21:39 +0900 Subject: [PATCH 05/25] consistent flag names --- Lib/_pydecimal.py | 4 ++-- Modules/_decimal/_decimal.c | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index 6cf7cc9991d83f..89646fa714c542 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -3795,7 +3795,7 @@ def __format__(self, specifier, context=None, _localeconv=None): # 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['coerce_neg_0'] and self._sign: + if not self and spec['no_neg_0'] and self._sign: adjusted_sign = 0 else: adjusted_sign = self._sign @@ -6155,7 +6155,7 @@ def _convert_for_comparison(self, other, equality_op=False): (?P[<>=^]) )? (?P[-+ ])? -(?Pz)? +(?Pz)? (?P\#)? (?P0)? (?P(?!0)\d+)? diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index cbb468ca9ee383..7f09e50992b9bd 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -3225,7 +3225,7 @@ dec_format(PyObject *dec, PyObject *args) char *decstring = NULL; uint32_t status = 0; int replace_fillchar = 0; - int coerce_neg_0 = 0; + int no_neg_0 = 0; Py_ssize_t size; @@ -3251,7 +3251,7 @@ dec_format(PyObject *dec, PyObject *args) } char *z_start = strchr(fmt, 'z'); if (z_start != NULL) { - coerce_neg_0 = 1; + no_neg_0 = 1; size_t z_index = z_start - fmt; if (fmt_copy == NULL) { fmt = fmt_copy = dec_strdup(fmt, size); @@ -3326,7 +3326,7 @@ dec_format(PyObject *dec, PyObject *args) } } - if (coerce_neg_0 && mpd_isnegative(MPD(dec)) && !mpd_isspecial(MPD(dec))) { + if (no_neg_0 && mpd_isnegative(MPD(dec)) && !mpd_isspecial(MPD(dec))) { /* round into a temporary and clear sign if result is zero */ mpd_uint_t dt[MPD_MINALLOC_MAX]; mpd_t tmp = {MPD_STATIC|MPD_STATIC_DATA,0,0,0,MPD_MINALLOC_MAX,dt}; From 3dcaf5f3169550bbfcec82e20533a8908c337b17 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Fri, 4 Feb 2022 08:55:36 +0900 Subject: [PATCH 06/25] add test case for integer value with z option --- Lib/test/test_format.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_format.py b/Lib/test/test_format.py index 42b8ccbd34ec9a..cd4ede52af8d9f 100644 --- a/Lib/test/test_format.py +++ b/Lib/test/test_format.py @@ -558,6 +558,7 @@ def test_negative_zero(self): 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") From 6b9ab3b10acd11fe0ed8a72e1c10fd8720371b8e Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Fri, 4 Feb 2022 11:45:31 +0900 Subject: [PATCH 07/25] reference pending PEP --- Doc/library/string.rst | 2 +- Lib/pydoc_data/topics.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Doc/library/string.rst b/Doc/library/string.rst index 28db5595553222..46ff19ebf6ae78 100644 --- a/Doc/library/string.rst +++ b/Doc/library/string.rst @@ -386,7 +386,7 @@ The ``'z'`` option causes negative zero to be coerced to zero. This option is only valid for float and complex types. .. versionchanged:: 3.11 - Added the ``'z'`` option. + Added the ``'z'`` option (see also :pep:`682`). .. index:: single: # (hash); in string formatting diff --git a/Lib/pydoc_data/topics.py b/Lib/pydoc_data/topics.py index 994c81564e0d1a..b9f1ecd6392e74 100644 --- a/Lib/pydoc_data/topics.py +++ b/Lib/pydoc_data/topics.py @@ -6246,7 +6246,8 @@ 'zero. This\n' 'option is only valid for float and complex types.\n' '\n' - 'Changed in version 3.11: Added the "\'z\'" option.\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' From e24356807a772697b864fbdb0ad4c3552ef28e4d Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Thu, 17 Mar 2022 08:46:20 +0900 Subject: [PATCH 08/25] Apply some formatting and doc suggestions Co-authored-by: Mark Dickinson --- Doc/library/string.rst | 2 +- Lib/test/test_format.py | 1 - Objects/bytesobject.c | 6 ++++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Doc/library/string.rst b/Doc/library/string.rst index 46ff19ebf6ae78..013ab9d9e7413d 100644 --- a/Doc/library/string.rst +++ b/Doc/library/string.rst @@ -383,7 +383,7 @@ following: .. index:: single: z; in string formatting The ``'z'`` option causes negative zero to be coerced to zero. This -option is only valid for float and complex types. +option is only valid for float presentation types. .. versionchanged:: 3.11 Added the ``'z'`` option (see also :pep:`682`). diff --git a/Lib/test/test_format.py b/Lib/test/test_format.py index cd4ede52af8d9f..f0ef0479d0a697 100644 --- a/Lib/test/test_format.py +++ b/Lib/test/test_format.py @@ -586,7 +586,6 @@ def test_negative_zero(self): self.assertEqual(f"{.01j:z.1f}", "0.0+0.0j") self.assertEqual(f"{-.01j:z.1f}", "0.0+0.0j") - def test_specifier_z_error(self): error_msg = re.compile("Invalid format specifier '.*z.*'") with self.assertRaisesRegex(ValueError, error_msg): diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c index 86494cadbab9c0..526d723a9fa0b1 100644 --- a/Objects/bytesobject.c +++ b/Objects/bytesobject.c @@ -427,10 +427,12 @@ formatfloat(PyObject *v, int flags, int prec, int type, if (prec < 0) prec = 6; - if (flags & F_ALT) + if (flags & F_ALT) { dtoa_flags |= Py_DTSF_ALT; - if (flags & F_NO_NEG_0) + } + 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) From be4fda24fa878cff534cb06cca07afc906a5074b Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Sat, 19 Mar 2022 13:31:51 +0900 Subject: [PATCH 09/25] revise "z" option description --- Doc/library/string.rst | 5 +++-- Lib/pydoc_data/topics.py | 8 +++++--- .../next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst | 6 +++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Doc/library/string.rst b/Doc/library/string.rst index 013ab9d9e7413d..572eec9f148c91 100644 --- a/Doc/library/string.rst +++ b/Doc/library/string.rst @@ -382,8 +382,9 @@ following: .. index:: single: z; in string formatting -The ``'z'`` option causes negative zero to be coerced to zero. This -option is only valid for float presentation types. +The ``'z'`` option coerces negative zero floating-point values to positive +zero after rounding to the format precision. This option is only valid for +float presentation types. .. versionchanged:: 3.11 Added the ``'z'`` option (see also :pep:`682`). diff --git a/Lib/pydoc_data/topics.py b/Lib/pydoc_data/topics.py index b9f1ecd6392e74..870205771507cb 100644 --- a/Lib/pydoc_data/topics.py +++ b/Lib/pydoc_data/topics.py @@ -6242,9 +6242,11 @@ ' ' '+-----------+------------------------------------------------------------+\n' '\n' - 'The "\'z\'" option causes negative zero to be coerced to ' - 'zero. This\n' - 'option is only valid for float and complex types.\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' + 'float presentation types.\n' '\n' 'Changed in version 3.11: Added the "\'z\'" option (see also ' '**PEP 682**).\n' diff --git a/Misc/NEWS.d/next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst b/Misc/NEWS.d/next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst index 7beb4bc1d909b6..82063960b155e8 100644 --- a/Misc/NEWS.d/next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst +++ b/Misc/NEWS.d/next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst @@ -1,3 +1,3 @@ -Add "z" option to the string formatting specification, which coerces negative -zero floating-point values to positive zero after rounding to the format precision. -Covers ``float`` and ``Decimal`` formatting via :func:`str.format` and f-strings. \ No newline at end of file +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. \ No newline at end of file From 043d76a93f59a6fa8f45da7551f316b9f2018400 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Sat, 19 Mar 2022 14:02:01 +0900 Subject: [PATCH 10/25] add test cases for explicit sign option --- Lib/test/test_decimal.py | 7 +++++++ Lib/test/test_format.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 2961ddd4c5692e..13f638f52660a9 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -1104,6 +1104,13 @@ def test_formatting(self): ('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'), + # issue 6850 ('a=-7.0', '0.12345', 'aaaa0.1'), diff --git a/Lib/test/test_format.py b/Lib/test/test_format.py index f0ef0479d0a697..c2dd8df23beae4 100644 --- a/Lib/test/test_format.py +++ b/Lib/test/test_format.py @@ -581,6 +581,13 @@ def test_negative_zero(self): 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") From 104e023cc5442d6339bd79141f764a65580a44b5 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Sat, 19 Mar 2022 14:11:45 +0900 Subject: [PATCH 11/25] revise tests for format options expected to fail on floats --- Lib/test/test_float.py | 22 ++++++++++------------ Lib/test/test_types.py | 22 ++++++++++------------ 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/Lib/test/test_float.py b/Lib/test/test_float.py index 266482e81f4654..d8c0fe1854eba5 100644 --- a/Lib/test/test_float.py +++ b/Lib/test/test_float.py @@ -701,18 +701,16 @@ def test_format(self): # 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 'eEfFgGnz%': - 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') diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index a09a3da20ab6b6..42fd4f56235fab 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -524,18 +524,16 @@ def test(f, format_spec, result): 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 'eEfFgGnz%': - 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') From 61c64df7b0ac401aefd78e912af9890f7f60faa0 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Sat, 19 Mar 2022 14:52:45 +0900 Subject: [PATCH 12/25] "float presentation" -> "floating-point presentation" --- Doc/library/string.rst | 2 +- Lib/pydoc_data/topics.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/string.rst b/Doc/library/string.rst index 572eec9f148c91..35e9bc116803ff 100644 --- a/Doc/library/string.rst +++ b/Doc/library/string.rst @@ -384,7 +384,7 @@ following: The ``'z'`` option coerces negative zero floating-point values to positive zero after rounding to the format precision. This option is only valid for -float presentation types. +floating-point presentation types. .. versionchanged:: 3.11 Added the ``'z'`` option (see also :pep:`682`). diff --git a/Lib/pydoc_data/topics.py b/Lib/pydoc_data/topics.py index 870205771507cb..0141347a355894 100644 --- a/Lib/pydoc_data/topics.py +++ b/Lib/pydoc_data/topics.py @@ -6246,7 +6246,7 @@ 'values to positive\n' 'zero after rounding to the format precision. This option ' 'is only valid for\n' - 'float presentation types.\n' + 'floating-point presentation types.\n' '\n' 'Changed in version 3.11: Added the "\'z\'" option (see also ' '**PEP 682**).\n' From 76d61aee5f6b793784851d4a72759bc09ac8be30 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Sat, 19 Mar 2022 15:08:05 +0900 Subject: [PATCH 13/25] news file terminating newline --- .../next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst b/Misc/NEWS.d/next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst index 82063960b155e8..92ff0b29aeb26e 100644 --- a/Misc/NEWS.d/next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst +++ b/Misc/NEWS.d/next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst @@ -1,3 +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. \ No newline at end of file +precision. From 33fe72c71bbe8906bcdbb2d71aacf1a9ff3cc6a8 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Sat, 19 Mar 2022 18:53:37 +0900 Subject: [PATCH 14/25] add test coverage for Decimal bugs * 'z' character fill * non-fixed exponent * directed rounding --- Lib/test/test_decimal.py | 12 ++++++++++++ Lib/test/test_format.py | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 13f638f52660a9..32bc5f4bee397a 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -1093,6 +1093,9 @@ def test_formatting(self): ('z.1E', '0.', '0.0E+1'), ('z.1E', '-0.', '0.0E+1'), + ('z.2e', '-0.001', '-1.00e-3'), # non-fixed exponent, FIXME + ('z.2%', '-0.001', '-0.10%'), # FIXME + ('z.1f', '-00000.000001', '0.0'), ('z.1f', '-00000.', '0.0'), ('z.1f', '-.0000000000', '0.0'), @@ -1111,6 +1114,9 @@ def test_formatting(self): ('+z.0f', '-1.', '-1'), ('-z.0f', '-1.', '-1'), + ('z>6.1f', '-0.', 'zz-0.0'), # FIXME + ('z>z6.1f', '-0.', 'zzz0.0'), # FIXME + # issue 6850 ('a=-7.0', '0.12345', 'aaaa0.1'), @@ -1125,6 +1131,12 @@ def test_formatting(self): # 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') # FIXME + def test_n_format(self): Decimal = self.decimal.Decimal diff --git a/Lib/test/test_format.py b/Lib/test/test_format.py index c2dd8df23beae4..ffd610829b7479 100644 --- a/Lib/test/test_format.py +++ b/Lib/test/test_format.py @@ -570,6 +570,9 @@ def test_negative_zero(self): 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") # non-fixed exponent + 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") @@ -593,6 +596,9 @@ def test_negative_zero(self): 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") # fill with 'z' still works + self.assertEqual(f"{-0.:z>z6.1f}", "zzz0.0") + def test_specifier_z_error(self): error_msg = re.compile("Invalid format specifier '.*z.*'") with self.assertRaisesRegex(ValueError, error_msg): From 93931365f26aaf942fdd3ceb81cab63a7f523feb Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Mon, 21 Mar 2022 16:09:33 +0900 Subject: [PATCH 15/25] Decimal: handle 'z' fill character correctly --- Lib/test/test_decimal.py | 5 +++-- Lib/test/test_format.py | 3 ++- Modules/_decimal/_decimal.c | 9 ++++++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 32bc5f4bee397a..50bb1535763121 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -1114,8 +1114,9 @@ def test_formatting(self): ('+z.0f', '-1.', '-1'), ('-z.0f', '-1.', '-1'), - ('z>6.1f', '-0.', 'zz-0.0'), # FIXME - ('z>z6.1f', '-0.', 'zzz0.0'), # FIXME + ('z>6.1f', '-0.', 'zz-0.0'), + ('z>z6.1f', '-0.', 'zzz0.0'), + ('x>z6.1f', '-0.', 'xxx0.0'), # issue 6850 ('a=-7.0', '0.12345', 'aaaa0.1'), diff --git a/Lib/test/test_format.py b/Lib/test/test_format.py index ffd610829b7479..bacd14ca3e9659 100644 --- a/Lib/test/test_format.py +++ b/Lib/test/test_format.py @@ -596,8 +596,9 @@ def test_negative_zero(self): 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") # fill with 'z' still works + 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") def test_specifier_z_error(self): error_msg = re.compile("Invalid format specifier '.*z.*'") diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index 7f09e50992b9bd..3c1be63f13b7ff 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -3249,7 +3249,12 @@ dec_format(PyObject *dec, PyObject *args) } fmt_copy[0] = '_'; } - char *z_start = strchr(fmt, 'z'); + /* Strip 'z' option, which isn't understood by mpd_parse_fmt_str(). + * First, skip [[fill]align], since 'fill' itself may be 'z'. + * NOTE: fmt is always null terminated by PyUnicode_AsUTF8AndSize() */ + char *fmt_offset = size >= 2 && strchr("<>=^", fmt[1]) != NULL ? + fmt + 2 : fmt; + char *z_start = strchr(fmt_offset, 'z'); if (z_start != NULL) { no_neg_0 = 1; size_t z_index = z_start - fmt; @@ -3259,6 +3264,8 @@ dec_format(PyObject *dec, PyObject *args) 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; } From f88f7fcaa2426aaf4d8fd8f903e1100fa1a4e00a Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Mon, 21 Mar 2022 17:09:27 +0900 Subject: [PATCH 16/25] Decimal: const qualifier on fmt variable --- Modules/_decimal/_decimal.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index 3c1be63f13b7ff..ddb44a461c08a4 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -3220,7 +3220,7 @@ 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; @@ -3252,8 +3252,8 @@ dec_format(PyObject *dec, PyObject *args) /* Strip 'z' option, which isn't understood by mpd_parse_fmt_str(). * First, skip [[fill]align], since 'fill' itself may be 'z'. * NOTE: fmt is always null terminated by PyUnicode_AsUTF8AndSize() */ - char *fmt_offset = size >= 2 && strchr("<>=^", fmt[1]) != NULL ? - fmt + 2 : fmt; + char const *fmt_offset = size >= 2 && strchr("<>=^", fmt[1]) != NULL ? + fmt + 2 : fmt; char *z_start = strchr(fmt_offset, 'z'); if (z_start != NULL) { no_neg_0 = 1; From 20c9cf1d7d52ef9f8adfaf4bf37630fbefae2f39 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Wed, 23 Mar 2022 17:17:52 +0900 Subject: [PATCH 17/25] fix rounding of 'e', 'g', and '%' presentation types for Decimal --- Lib/test/test_decimal.py | 5 ++-- Modules/_decimal/_decimal.c | 46 ++++++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 50bb1535763121..c49c945c589800 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -1093,8 +1093,9 @@ def test_formatting(self): ('z.1E', '0.', '0.0E+1'), ('z.1E', '-0.', '0.0E+1'), - ('z.2e', '-0.001', '-1.00e-3'), # non-fixed exponent, FIXME - ('z.2%', '-0.001', '-0.10%'), # FIXME + ('z.2e', '-0.001', '-1.00e-3'), # tests for mishandled rounding + ('z.2g', '-0.001', '-0.001'), + ('z.2%', '-0.001', '-0.10%'), ('z.1f', '-00000.000001', '0.0'), ('z.1f', '-00000.', '0.0'), diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index ddb44a461c08a4..2792bc74ad96c7 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -3183,6 +3183,27 @@ 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); + } +} + static int dict_get_item_string(PyObject *dict, const char *key, PyObject **valueobj, const char **valuestr) { @@ -3337,12 +3358,35 @@ dec_format(PyObject *dec, PyObject *args) /* round into a temporary and clear sign if result is zero */ mpd_uint_t dt[MPD_MINALLOC_MAX]; mpd_t tmp = {MPD_STATIC|MPD_STATIC_DATA,0,0,0,MPD_MINALLOC_MAX,dt}; - mpd_qrescale(&tmp, MPD(dec), -spec.prec, CTX(context), &status); + mpd_ssize_t prec; + mpd_qcopy(&tmp, MPD(dec), &status); + /* mirror rounding of mpd_qformat_spec() */ + 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)) { + /* TODO: format rounded tmp itself (addresses directed rounding) */ mpd_set_positive(MPD(dec)); } } From bf1a891d8d5b30202674c4b2f6e67c091313d912 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Wed, 23 Mar 2022 20:16:35 +0900 Subject: [PATCH 18/25] fix Decimal directed rounding --- Lib/test/test_decimal.py | 2 +- Modules/_decimal/_decimal.c | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index c49c945c589800..96172273829e40 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -1137,7 +1137,7 @@ 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') # FIXME + '0.00') def test_n_format(self): Decimal = self.decimal.Decimal diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index 2792bc74ad96c7..2bc94e29120e9e 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -3248,6 +3248,7 @@ dec_format(PyObject *dec, PyObject *args) int replace_fillchar = 0; int no_neg_0 = 0; Py_ssize_t size; + mpd_t *mpd = MPD(dec); CURRENT_CONTEXT(context); @@ -3354,13 +3355,16 @@ dec_format(PyObject *dec, PyObject *args) } } - if (no_neg_0 && mpd_isnegative(MPD(dec)) && !mpd_isspecial(MPD(dec))) { - /* round into a temporary and clear sign if result is zero */ + 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 this pre-rounded value. + (The format will then do no additional rounding, which is + significant for directed rounding cases like ROUND_CEILING.) */ mpd_uint_t dt[MPD_MINALLOC_MAX]; mpd_t tmp = {MPD_STATIC|MPD_STATIC_DATA,0,0,0,MPD_MINALLOC_MAX,dt}; mpd_ssize_t prec; - mpd_qcopy(&tmp, MPD(dec), &status); - /* mirror rounding of mpd_qformat_spec() */ + mpd_qcopy(&tmp, mpd, &status); switch (spec.type) { case 'f': mpd_qrescale(&tmp, &tmp, -spec.prec, CTX(context), &status); @@ -3386,12 +3390,12 @@ dec_format(PyObject *dec, PyObject *args) goto finish; } if (mpd_iszero(&tmp)) { - /* TODO: format rounded tmp itself (addresses directed rounding) */ - mpd_set_positive(MPD(dec)); + 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(); From 3f5b3928c32e092b7447bdaa01c9e92cbfcf6b99 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Wed, 23 Mar 2022 20:38:05 +0900 Subject: [PATCH 19/25] consistency among tests --- Lib/test/test_format.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_format.py b/Lib/test/test_format.py index bacd14ca3e9659..7a33dd62e450d5 100644 --- a/Lib/test/test_format.py +++ b/Lib/test/test_format.py @@ -570,7 +570,9 @@ def test_negative_zero(self): 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") # non-fixed exponent + 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") @@ -607,7 +609,7 @@ def test_specifier_z_error(self): error_msg = re.escape("Negative zero coercion (z) not allowed") with self.assertRaisesRegex(ValueError, error_msg): - f"{0:zd}" # can't apply to int + f"{0:zd}" # can't apply to int presentation type with self.assertRaisesRegex(ValueError, error_msg): f"{'x':zs}" # can't apply to string From 8d7a7454c8d8b72fa981bdb5564505bfc619ac9f Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Wed, 23 Mar 2022 20:57:26 +0900 Subject: [PATCH 20/25] fix stack-use-after-scope sanitizer error --- Modules/_decimal/_decimal.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index 2bc94e29120e9e..bcd6e5bcc03191 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -3249,6 +3249,8 @@ dec_format(PyObject *dec, PyObject *args) 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); @@ -3361,8 +3363,6 @@ dec_format(PyObject *dec, PyObject *args) If so, clear the sign and format this pre-rounded value. (The format will then do no additional rounding, which is significant for directed rounding cases like ROUND_CEILING.) */ - mpd_uint_t dt[MPD_MINALLOC_MAX]; - mpd_t tmp = {MPD_STATIC|MPD_STATIC_DATA,0,0,0,MPD_MINALLOC_MAX,dt}; mpd_ssize_t prec; mpd_qcopy(&tmp, mpd, &status); switch (spec.type) { From 2a24e610d1384608b43050121d22b412cc9f8355 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Wed, 23 Mar 2022 21:35:30 +0900 Subject: [PATCH 21/25] clarify Decimal strategy --- Modules/_decimal/_decimal.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index bcd6e5bcc03191..06fd3b453640d2 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -3360,9 +3360,7 @@ 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 this pre-rounded value. - (The format will then do no additional rounding, which is - significant for directed rounding cases like ROUND_CEILING.) */ + If so, clear the sign and format the resulting positive zero. */ mpd_ssize_t prec; mpd_qcopy(&tmp, mpd, &status); switch (spec.type) { From 0cbff6ac9853cbbe57b2dd7057dde30add4a1de4 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Wed, 6 Apr 2022 22:03:20 +0900 Subject: [PATCH 22/25] fix Decimal format parsing --- Lib/test/test_decimal.py | 4 ++++ Lib/test/test_format.py | 3 +++ Modules/_decimal/_decimal.c | 47 ++++++++++++++++++++++++++++++------- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 96172273829e40..3b6c91b823f9b5 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -1118,6 +1118,7 @@ def test_formatting(self): ('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'), @@ -1139,6 +1140,9 @@ def test_negative_zero_format_directed_rounding(self): 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 diff --git a/Lib/test/test_format.py b/Lib/test/test_format.py index 7a33dd62e450d5..69b0d5f1c5a515 100644 --- a/Lib/test/test_format.py +++ b/Lib/test/test_format.py @@ -601,11 +601,14 @@ def test_negative_zero(self): 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): diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index 06fd3b453640d2..9c206968bbcea0 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -3184,8 +3184,9 @@ dotsep_as_utf8(const char *s) } /* 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) +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; @@ -3204,6 +3205,34 @@ static void _mpd_round(mpd_t *result, const mpd_t *a, mpd_ssize_t prec, } } +/* 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) { @@ -3259,10 +3288,13 @@ 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(). */ @@ -3274,14 +3306,11 @@ dec_format(PyObject *dec, PyObject *args) fmt_copy[0] = '_'; } /* Strip 'z' option, which isn't understood by mpd_parse_fmt_str(). - * First, skip [[fill]align], since 'fill' itself may be 'z'. * NOTE: fmt is always null terminated by PyUnicode_AsUTF8AndSize() */ - char const *fmt_offset = size >= 2 && strchr("<>=^", fmt[1]) != NULL ? - fmt + 2 : fmt; - char *z_start = strchr(fmt_offset, 'z'); - if (z_start != NULL) { + char const *z_position = format_spec_z_search(fmt, size); + if (z_position != NULL) { no_neg_0 = 1; - size_t z_index = z_start - fmt; + size_t z_index = z_position - fmt; if (fmt_copy == NULL) { fmt = fmt_copy = dec_strdup(fmt, size); if (fmt_copy == NULL) { From 8e7b51c011dbdd0275bd0ba5b08b14ac57c53363 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Thu, 7 Apr 2022 21:18:01 +0900 Subject: [PATCH 23/25] fix Decimal when no precision is specified --- Lib/test/test_decimal.py | 2 ++ Modules/_decimal/_decimal.c | 40 +++++++++++++++++++------------------ 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 3b6c91b823f9b5..c0892a765852f4 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -1097,6 +1097,8 @@ def test_formatting(self): ('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'), diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index 9c206968bbcea0..4637b8b34c4cec 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -3392,25 +3392,27 @@ dec_format(PyObject *dec, PyObject *args) If so, clear the sign and format the resulting positive zero. */ mpd_ssize_t prec; mpd_qcopy(&tmp, mpd, &status); - 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 (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"); From 418ab7605e48071443e8c2deeefb96ffe4e40801 Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Thu, 7 Apr 2022 21:20:32 +0900 Subject: [PATCH 24/25] fix comment typo Co-authored-by: Mark Dickinson --- Python/formatter_unicode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/formatter_unicode.c b/Python/formatter_unicode.c index bd87a50ba68fce..04d37c0be28cdd 100644 --- a/Python/formatter_unicode.c +++ b/Python/formatter_unicode.c @@ -889,7 +889,7 @@ format_long_internal(PyObject *value, const InternalFormatSpec *format, "Precision not allowed in integer format specifier"); goto done; } - /* no negatize zero coercion on integers */ + /* no negative zero coercion on integers */ if (format->no_neg_0) { PyErr_SetString(PyExc_ValueError, "Negative zero coercion (z) not allowed in integer" From 3ee6f6b5113d2dd7183f4a1b9d9638c97b2204cd Mon Sep 17 00:00:00 2001 From: John Belmonte Date: Mon, 11 Apr 2022 20:50:53 +0900 Subject: [PATCH 25/25] add attribution to news blurb --- .../next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst b/Misc/NEWS.d/next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst index 92ff0b29aeb26e..dd42bc092c2801 100644 --- a/Misc/NEWS.d/next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst +++ b/Misc/NEWS.d/next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst @@ -1,3 +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. +precision. Contributed by John Belmonte.