gh-87790: support thousands separators for formatting fractional part… · python/cpython@f39a07b · GitHub
Skip to content

Commit f39a07b

Browse files
authored
gh-87790: support thousands separators for formatting fractional part of floats (#125304)
```pycon >>> f"{123_456.123_456:_._f}" # Whole and fractional '123_456.123_456' >>> f"{123_456.123_456:_f}" # Integer component only '123_456.123456' >>> f"{123_456.123_456:._f}" # Fractional component only '123456.123_456' >>> f"{123_456.123_456:.4_f}" # with precision '123456.1_235' ```
1 parent fa6a814 commit f39a07b

9 files changed

Lines changed: 218 additions & 45 deletions

File tree

Doc/library/string.rst

Lines changed: 23 additions & 3 deletions

Doc/whatsnew/3.14.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,11 @@ Other language changes
336336
making it a :term:`generic type`.
337337
(Contributed by Brian Schubert in :gh:`126012`.)
338338

339+
* Support underscore and comma as thousands separators in the fractional part
340+
for floating-point presentation types of the new-style string formatting
341+
(with :func:`format` or :ref:`f-strings`).
342+
(Contrubuted by Sergey B Kirpichev in :gh:`87790`.)
343+
339344
* ``\B`` in :mod:`regular expression <re>` now matches empty input string.
340345
Now it is always the opposite of ``\b``.
341346
(Contributed by Serhiy Storchaka in :gh:`124130`.)

Include/internal/pycore_unicodeobject.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,8 @@ extern Py_ssize_t _PyUnicode_InsertThousandsGrouping(
246246
Py_ssize_t min_width,
247247
const char *grouping,
248248
PyObject *thousands_sep,
249-
Py_UCS4 *maxchar);
249+
Py_UCS4 *maxchar,
250+
int forward);
250251

251252
/* --- Misc functions ----------------------------------------------------- */
252253

Lib/test/test_float.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,28 @@ def test_format(self):
754754
self.assertEqual(format(INF, 'f'), 'inf')
755755
self.assertEqual(format(INF, 'F'), 'INF')
756756

757+
# thousands separators
758+
x = 123_456.123_456
759+
self.assertEqual(format(x, '_f'), '123_456.123456')
760+
self.assertEqual(format(x, ',f'), '123,456.123456')
761+
self.assertEqual(format(x, '._f'), '123456.123_456')
762+
self.assertEqual(format(x, '.,f'), '123456.123,456')
763+
self.assertEqual(format(x, '_._f'), '123_456.123_456')
764+
self.assertEqual(format(x, ',.,f'), '123,456.123,456')
765+
self.assertEqual(format(x, '.10_f'), '123456.123_456_000_0')
766+
self.assertEqual(format(x, '.10,f'), '123456.123,456,000,0')
767+
self.assertEqual(format(x, '>21._f'), ' 123456.123_456')
768+
self.assertEqual(format(x, '<21._f'), '123456.123_456 ')
769+
self.assertEqual(format(x, '+.11_e'), '+1.234_561_234_56e+05')
770+
self.assertEqual(format(x, '+.11,e'), '+1.234,561,234,56e+05')
771+
772+
self.assertRaises(ValueError, format, x, '._6f')
773+
self.assertRaises(ValueError, format, x, '.,_f')
774+
self.assertRaises(ValueError, format, x, '.6,_f')
775+
self.assertRaises(ValueError, format, x, '.6_,f')
776+
self.assertRaises(ValueError, format, x, '.6_n')
777+
self.assertRaises(ValueError, format, x, '.6,n')
778+
757779
@support.requires_IEEE_754
758780
def test_format_testfile(self):
759781
with open(format_testfile, encoding="utf-8") as testfile:

Lib/test/test_format.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,11 +515,15 @@ def test_with_a_commas_and_an_underscore_in_format_specifier(self):
515515
error_msg = re.escape("Cannot specify both ',' and '_'.")
516516
with self.assertRaisesRegex(ValueError, error_msg):
517517
'{:,_}'.format(1)
518+
with self.assertRaisesRegex(ValueError, error_msg):
519+
'{:.,_f}'.format(1.1)
518520

519521
def test_with_an_underscore_and_a_comma_in_format_specifier(self):
520522
error_msg = re.escape("Cannot specify both ',' and '_'.")
521523
with self.assertRaisesRegex(ValueError, error_msg):
522524
'{:_,}'.format(1)
525+
with self.assertRaisesRegex(ValueError, error_msg):
526+
'{:._,f}'.format(1.1)
523527

524528
def test_better_error_message_format(self):
525529
# https://bugs.python.org/issue20524
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Support underscore and comma as thousands separators in the fractional part for
2+
floating-point presentation types of the new-style string formatting (with
3+
:func:`format` or :ref:`f-strings`). Patch by Sergey B Kirpichev.

Objects/stringlib/localeutil.h

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ InsertThousandsGrouping_fill(_PyUnicodeWriter *writer, Py_ssize_t *buffer_pos,
4747
PyObject *digits, Py_ssize_t *digits_pos,
4848
Py_ssize_t n_chars, Py_ssize_t n_zeros,
4949
PyObject *thousands_sep, Py_ssize_t thousands_sep_len,
50-
Py_UCS4 *maxchar)
50+
Py_UCS4 *maxchar, int forward)
5151
{
5252
if (!writer) {
5353
/* if maxchar > 127, maxchar is already set */
@@ -59,24 +59,39 @@ InsertThousandsGrouping_fill(_PyUnicodeWriter *writer, Py_ssize_t *buffer_pos,
5959
}
6060

6161
if (thousands_sep) {
62-
*buffer_pos -= thousands_sep_len;
63-
62+
if (!forward) {
63+
*buffer_pos -= thousands_sep_len;
64+
}
6465
/* Copy the thousands_sep chars into the buffer. */
6566
_PyUnicode_FastCopyCharacters(writer->buffer, *buffer_pos,
6667
thousands_sep, 0,
6768
thousands_sep_len);
69+
if (forward) {
70+
*buffer_pos += thousands_sep_len;
71+
}
6872
}
6973

70-
*buffer_pos -= n_chars;
71-
*digits_pos -= n_chars;
74+
if (!forward) {
75+
*buffer_pos -= n_chars;
76+
*digits_pos -= n_chars;
77+
}
7278
_PyUnicode_FastCopyCharacters(writer->buffer, *buffer_pos,
7379
digits, *digits_pos,
7480
n_chars);
81+
if (forward) {
82+
*buffer_pos += n_chars;
83+
*digits_pos += n_chars;
84+
}
7585

7686
if (n_zeros) {
77-
*buffer_pos -= n_zeros;
87+
if (!forward) {
88+
*buffer_pos -= n_zeros;
89+
}
7890
int kind = PyUnicode_KIND(writer->buffer);
7991
void *data = PyUnicode_DATA(writer->buffer);
8092
unicode_fill(kind, data, '0', *buffer_pos, n_zeros);
93+
if (forward) {
94+
*buffer_pos += n_zeros;
95+
}
8196
}
8297
}

Objects/unicodeobject.c

Lines changed: 7 additions & 6 deletions

0 commit comments

Comments
 (0)