[Python-checkins] bpo-45995: add "z" format specifer to coerce negative 0 to zero (GH-30049)

mdickinson webhook-mailer at python.org
Mon Apr 11 10:34:35 EDT 2022


https://github.com/python/cpython/commit/b0b836b20cb56c225874a4a39ef895f89ab2970f
commit: b0b836b20cb56c225874a4a39ef895f89ab2970f
branch: main
author: John Belmonte <john at neggie.net>
committer: mdickinson <dickinsm at gmail.com>
date: 2022-04-11T15:34:18+01:00
summary:

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 at gmail.com>

files:
A Misc/NEWS.d/next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst
M Doc/library/string.rst
M Include/internal/pycore_format.h
M Include/pystrtod.h
M Lib/_pydecimal.py
M Lib/pydoc_data/topics.py
M Lib/test/test_decimal.py
M Lib/test/test_float.py
M Lib/test/test_format.py
M Lib/test/test_types.py
M Modules/_decimal/_decimal.c
M Objects/bytesobject.c
M Objects/unicodeobject.c
M Python/ast_opt.c
M Python/formatter_unicode.c
M Python/pystrtod.c

diff --git a/Doc/library/string.rst b/Doc/library/string.rst
index 78bd167bcf579..35e9bc116803f 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: <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
diff --git a/Include/internal/pycore_format.h b/Include/internal/pycore_format.h
index 1b8d57539ca50..1899609e77ef2 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 c1e84de6fe542..fa056d17b6395 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/_pydecimal.py b/Lib/_pydecimal.py
index f6d9ddf42e473..89646fa714c54 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['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 @@ 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<align>[<>=^])
 )?
 (?P<sign>[-+ ])?
+(?P<no_neg_0>z)?
 (?P<alt>\#)?
 (?P<zeropad>0)?
 (?P<minimumwidth>(?!0)\d+)?
diff --git a/Lib/pydoc_data/topics.py b/Lib/pydoc_data/topics.py
index 9b684c6dfc296..f3c79cab220a1 100644
--- a/Lib/pydoc_data/topics.py
+++ b/Lib/pydoc_data/topics.py
@@ -6119,7 +6119,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            ::= <any character>\n'
                   '   align           ::= "<" | ">" | "=" | "^"\n'
                   '   sign            ::= "+" | "-" | " "\n'
@@ -6221,6 +6221,15 @@
                   '   '
                   '+-----------+------------------------------------------------------------+\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 '
diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py
index 0e7491ecdd45c..5e77e3c56cbbc 100644
--- a/Lib/test/test_decimal.py
+++ b/Lib/test/test_decimal.py
@@ -1072,6 +1072,57 @@ 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.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 @@ 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')
+
+    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_float.py b/Lib/test/test_float.py
index 9cf223f892678..d8c0fe1854eba 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 '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')
diff --git a/Lib/test/test_format.py b/Lib/test/test_format.py
index 16d29d1ea3d94..69b0d5f1c5a51 100644
--- a/Lib/test/test_format.py
+++ b/Lib/test/test_format.py
@@ -546,6 +546,80 @@ 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
+
+        ## 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()
diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py
index f8b239117f513..42fd4f56235fa 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 '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')
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 0000000000000..dd42bc092c280
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-12-14-13-15-41.bpo-45995.Am9pNL.rst
@@ -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.
diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c
index 35a115676a71b..4637b8b34c4ce 100644
--- a/Modules/_decimal/_decimal.c
+++ b/Modules/_decimal/_decimal.c
@@ -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 @@ 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;
 }
diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c
index 78c42c2c54b5f..d0124c050d1e1 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,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;
             }
diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c
index 1b2f33212a779..c665f577abaa4 100644
--- a/Objects/unicodeobject.c
+++ b/Objects/unicodeobject.c
@@ -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;
diff --git a/Python/ast_opt.c b/Python/ast_opt.c
index 77ed29d0cdddd..b1d807bcf10ae 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 a1e50e20c9d8c..04d37c0be28cd 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 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). */
diff --git a/Python/pystrtod.c b/Python/pystrtod.c
index 1b27f0a3ad36a..d77b846f0403f 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



More information about the Python-checkins mailing list