From 9cac4515afa64379ed20ea683ced40d9c028fd4d Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 7 Apr 2025 09:54:06 +0300 Subject: [PATCH 1/4] gh-87790: support thousands separators for formatting fractional part of Decimal's --- Lib/_pydecimal.py | 14 +++++++++++++- Lib/test/test_decimal.py | 12 ++++++++++++ .../2025-04-07-09-53-54.gh-issue-87790.6nj3zQ.rst | 2 ++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2025-04-07-09-53-54.gh-issue-87790.6nj3zQ.rst diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index ec036199331396..4b41826146e7ca 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -6099,7 +6099,11 @@ def _convert_for_comparison(self, other, equality_op=False): (?P0)? (?P(?!0)\d+)? (?P,)? -(?:\.(?P0|(?!0)\d+))? +(?:\. + (?=\d|[,_]) # lookahead for digit or separator + (?P0|(?!0)\d+)? + (?P[,_])? +)? (?P[eEfFgGn%])? \Z """, re.VERBOSE|re.DOTALL) @@ -6192,6 +6196,9 @@ def _parse_format_specifier(format_spec, _localeconv=None): format_dict['grouping'] = [3, 0] format_dict['decimal_point'] = '.' + if format_dict['frac_separators'] is None: + format_dict['frac_separators'] = '' + return format_dict def _format_align(sign, body, spec): @@ -6311,6 +6318,11 @@ def _format_number(is_negative, intpart, fracpart, exp, spec): sign = _format_sign(is_negative, spec) + frac_sep = spec['frac_separators'] + if fracpart and frac_sep: + fracpart = frac_sep.join(fracpart[pos:pos + 3] + for pos in range(0, len(fracpart), 3)) + if fracpart or spec['alt']: fracpart = spec['decimal_point'] + fracpart diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index d2327d247fa498..0feb320a7fb777 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -1082,6 +1082,15 @@ def test_formatting(self): (',%', '123.456789', '12,345.6789%'), (',e', '123456', '1.23456e+5'), (',E', '123456', '1.23456E+5'), + # and now for something completely different... + ('.,', '1.23456789', '1.234,567,89'), + ('._', '1.23456789', '1.234_567_89'), + ('.6_f', '1.23456789', '1.234_568'), + (',._%', '123.456789', '12,345.678_9%'), + (',._e', '123456', '1.234_56e+5'), + (',.4_e', '123456', '1.234_6e+5'), + (',.3_e', '123456', '1.235e+5'), + (',._E', '123456', '1.234_56E+5'), # negative zero: default behavior ('.1f', '-0', '-0.0'), @@ -1155,6 +1164,9 @@ def test_formatting(self): # bytes format argument self.assertRaises(TypeError, Decimal(1).__format__, b'-020') + # precision or fractional part separator should follow after dot + self.assertRaises(ValueError, format, Decimal(1), '.f') + def test_negative_zero_format_directed_rounding(self): with self.decimal.localcontext() as ctx: ctx.rounding = ROUND_CEILING diff --git a/Misc/NEWS.d/next/Library/2025-04-07-09-53-54.gh-issue-87790.6nj3zQ.rst b/Misc/NEWS.d/next/Library/2025-04-07-09-53-54.gh-issue-87790.6nj3zQ.rst new file mode 100644 index 00000000000000..cf80c71271bbd1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-07-09-53-54.gh-issue-87790.6nj3zQ.rst @@ -0,0 +1,2 @@ +Support underscore and comma as thousands separators in the fractional part +for :class:`~decimal.Decimal`'s formatting. Patch by Sergey B Kirpichev. From 387090a78ece102a7009a793f97da3051e7fe5c8 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 3 Jul 2025 15:10:12 +0300 Subject: [PATCH 2/4] Update Lib/_pydecimal.py Co-authored-by: Serhiy Storchaka --- Lib/_pydecimal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index 58e8b109afdb40..9b8e42a2342536 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -6123,7 +6123,7 @@ def _convert_for_comparison(self, other, equality_op=False): (?P\d+)? (?P[,_])? (?:\. - (?=\d|[,_]) # lookahead for digit or separator + (?=[\d,_]) # lookahead for digit or separator (?P\d+)? (?P[,_])? )? From 51592d4b66a36bbdc546dffd21f6378c2afc9b84 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 3 Jul 2025 15:39:43 +0300 Subject: [PATCH 3/4] address review: test error for _6f --- Lib/test/test_decimal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index c7694172852675..2db54553647404 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -1173,6 +1173,7 @@ def test_formatting(self): # precision or fractional part separator should follow after dot self.assertRaises(ValueError, format, Decimal(1), '.f') + self.assertRaises(ValueError, format, Decimal(1), '._6f') def test_negative_zero_format_directed_rounding(self): with self.decimal.localcontext() as ctx: From 0353e990b27cc07f168f2171ec4e17713c59d002 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 3 Jul 2025 16:25:24 +0300 Subject: [PATCH 4/4] address review: fix .6_f test (enlarge integer part) --- Lib/test/test_decimal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 2db54553647404..08a8f4c3b36bd6 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -1092,7 +1092,7 @@ def test_formatting(self): # and now for something completely different... ('.,', '1.23456789', '1.234,567,89'), ('._', '1.23456789', '1.234_567_89'), - ('.6_f', '1.23456789', '1.234_568'), + ('.6_f', '12345.23456789', '12345.234_568'), (',._%', '123.456789', '12,345.678_9%'), (',._e', '123456', '1.234_56e+5'), (',.4_e', '123456', '1.234_6e+5'),