Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

bpo-45995: add "z" format specifer to coerce negative 0 to zero #30049

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Apr 11, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a633e88
bpo-45995: add "z" format specifer to coerce negative 0 to zero
belm0 Dec 7, 2021
636da05
formatting
belm0 Dec 11, 2021
3f4085d
implementation for Decimal
belm0 Dec 13, 2021
68a049e
📜🤖 Added by blurb_it.
blurb-it[bot] Dec 14, 2021
ad32be5
consistent flag names
belm0 Dec 14, 2021
3dcaf5f
add test case for integer value with z option
belm0 Feb 3, 2022
6b9ab3b
reference pending PEP
belm0 Feb 4, 2022
e243568
Apply some formatting and doc suggestions
belm0 Mar 16, 2022
be4fda2
revise "z" option description
belm0 Mar 19, 2022
043d76a
add test cases for explicit sign option
belm0 Mar 19, 2022
104e023
revise tests for format options expected to fail on floats
belm0 Mar 19, 2022
61c64df
"float presentation" -> "floating-point presentation"
belm0 Mar 19, 2022
76d61ae
news file terminating newline
belm0 Mar 19, 2022
33fe72c
add test coverage for Decimal bugs
belm0 Mar 19, 2022
9393136
Decimal: handle 'z' fill character correctly
belm0 Mar 21, 2022
f88f7fc
Decimal: const qualifier on fmt variable
belm0 Mar 21, 2022
20c9cf1
fix rounding of 'e', 'g', and '%' presentation types for Decimal
belm0 Mar 23, 2022
bf1a891
fix Decimal directed rounding
belm0 Mar 23, 2022
3f5b392
consistency among tests
belm0 Mar 23, 2022
8d7a745
fix stack-use-after-scope sanitizer error
belm0 Mar 23, 2022
2a24e61
clarify Decimal strategy
belm0 Mar 23, 2022
0cbff6a
fix Decimal format parsing
belm0 Apr 6, 2022
8e7b51c
fix Decimal when no precision is specified
belm0 Apr 7, 2022
418ab76
fix comment typo
belm0 Apr 7, 2022
3ee6f6b
add attribution to news blurb
belm0 Apr 11, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
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
  • Loading branch information
belm0 committed Mar 19, 2022
commit a633e8870ff2952d1727b6d18352be8baaa0c86a
10 changes: 9 additions & 1 deletion 10 Doc/library/string.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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: "+" | "-" | " "
Expand Down Expand Up @@ -380,6 +380,14 @@ following:
+---------+----------------------------------------------------------+


.. index:: single: z; in string formatting

The ``'z'`` option causes negative zero to be coerced to zero. This
belm0 marked this conversation as resolved.
Show resolved Hide resolved
option is only valid for float and complex types.
belm0 marked this conversation as resolved.
Show resolved Hide resolved

.. versionchanged:: 3.11
Added the ``'z'`` option.

.. index:: single: # (hash); in string formatting

The ``'#'`` option causes the "alternate form" to be used for the
Expand Down
2 changes: 2 additions & 0 deletions 2 Include/internal/pycore_format.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions 1 Include/pystrtod.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion 8 Lib/pydoc_data/topics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ::= <any character>\n'
' align ::= "<" | ">" | "=" | "^"\n'
' sign ::= "+" | "-" | " "\n'
Expand Down Expand Up @@ -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'
belm0 marked this conversation as resolved.
Show resolved Hide resolved
'\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 '
Expand Down
2 changes: 1 addition & 1 deletion 2 Lib/test/test_float.py
Original file line number Diff line number Diff line change
Expand Up @@ -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%':
belm0 marked this conversation as resolved.
Show resolved Hide resolved
self.assertRaises(ValueError, format, 0.0, format_spec)
self.assertRaises(ValueError, format, 1.0, format_spec)
self.assertRaises(ValueError, format, -1.0, format_spec)
Expand Down
57 changes: 57 additions & 0 deletions 57 Lib/test/test_format.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from decimal import Decimal
from test.support import verbose, TestFailed
import locale
import sys
Expand Down Expand Up @@ -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")
belm0 marked this conversation as resolved.
Show resolved Hide resolved

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")

belm0 marked this conversation as resolved.
Show resolved Hide resolved

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()
2 changes: 1 addition & 1 deletion 2 Lib/test/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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%':
belm0 marked this conversation as resolved.
Show resolved Hide resolved
self.assertRaises(ValueError, format, 0.0, format_spec)
self.assertRaises(ValueError, format, 1.0, format_spec)
self.assertRaises(ValueError, format, -1.0, format_spec)
Expand Down
9 changes: 7 additions & 2 deletions 9 Objects/bytesobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand All @@ -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;
belm0 marked this conversation as resolved.
Show resolved Hide resolved
p = PyOS_double_to_string(x, type, prec, dtoa_flags, NULL);

if (p == NULL)
return NULL;
Expand Down Expand Up @@ -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;
}
Expand Down
8 changes: 4 additions & 4 deletions 8 Objects/unicodeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions 1 Python/ast_opt.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
28 changes: 28 additions & 0 deletions 28 Python/formatter_unicode.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) == '#') {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 */
belm0 marked this conversation as resolved.
Show resolved Hide resolved
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') {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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). */
Expand Down
22 changes: 20 additions & 2 deletions 22 Python/pystrtod.c
Original file line number Diff line number Diff line change
Expand Up @@ -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] == '-') {
belm0 marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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
Morty Proxy This is a proxified and sanitized view of the page, visit original site.