From 9cac4515afa64379ed20ea683ced40d9c028fd4d Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 7 Apr 2025 09:54:06 +0300 Subject: [PATCH] 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.