Skip to content

Commit 9a7c11a

Browse files
author
Łukasz Stelmach
committed
bpo-35638: Introduce fixed point locale aware format type
Add 'm' format to present floating point numbers with an arbitrary number of decimal places and locale correct decimal point.
1 parent f8b5344 commit 9a7c11a

10 files changed

Lines changed: 42 additions & 14 deletions

File tree

‎Doc/library/string.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,10 @@ The available presentation types for floating point and decimal values are:
522522
| | ``'E'`` if the number gets too large. The |
523523
| | representations of infinity and NaN are uppercased, too. |
524524
+---------+----------------------------------------------------------+
525+
| ``'m'`` | Number. This is the same as ``'f'``, except that it uses |
526+
| | the current locale setting to insert the appropriate |
527+
| | number separator characters. |
528+
+---------+----------------------------------------------------------+
525529
| ``'n'`` | Number. This is the same as ``'g'``, except that it uses |
526530
| | the current locale setting to insert the appropriate |
527531
| | number separator characters. |

‎Lib/_pydecimal.py‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3746,7 +3746,7 @@ def __format__(self, specifier, context=None, _localeconv=None):
37463746
37473747
The specifier should be a standard format specifier, with the
37483748
form described in PEP 3101. Formatting types 'e', 'E', 'f',
3749-
'F', 'g', 'G', 'n' and '%' are supported. If the formatting
3749+
'F', 'g', 'G', 'm', 'n' and '%' are supported. If the formatting
37503750
type is omitted it defaults to 'g' or 'G', depending on the
37513751
value of context.capitals.
37523752
"""
@@ -6154,7 +6154,7 @@ def _convert_for_comparison(self, other, equality_op=False):
61546154
(?P<minimumwidth>(?!0)\d+)?
61556155
(?P<thousands_sep>,)?
61566156
(?:\.(?P<precision>0|(?!0)\d+))?
6157-
(?P<type>[eEfFgGn%])?
6157+
(?P<type>[eEfFgGmn%])?
61586158
\Z
61596159
""", re.VERBOSE|re.DOTALL)
61606160

@@ -6229,14 +6229,15 @@ def _parse_format_specifier(format_spec, _localeconv=None):
62296229

62306230
# determine thousands separator, grouping, and decimal separator, and
62316231
# add appropriate entries to format_dict
6232-
if format_dict['type'] == 'n':
6232+
if format_dict['type'] in ('m', 'n'):
62336233
# apart from separators, 'n' behaves just like 'g'
6234-
format_dict['type'] = 'g'
6234+
# and 'm' like 'f'
6235+
format_dict['type'] = 'g' if format_dict['type'] == 'n' else 'f'
62356236
if _localeconv is None:
62366237
_localeconv = _locale.localeconv()
62376238
if format_dict['thousands_sep'] is not None:
62386239
raise ValueError("Explicit thousands separator conflicts with "
6239-
"'n' type in format specifier: " + format_spec)
6240+
"'m' or 'n' type in format specifier: " + format_spec)
62406241
format_dict['thousands_sep'] = _localeconv['thousands_sep']
62416242
format_dict['grouping'] = _localeconv['grouping']
62426243
format_dict['decimal_point'] = _localeconv['decimal_point']

‎Lib/test/test_decimal.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,7 @@ def test_formatting(self):
974974
('g', '0E-7', '0e-7'),
975975
('g', '-0E2', '-0e+2'),
976976
('.0g', '3.14159265', '3'), # 0 sig fig -> 1 sig fig
977+
('.0m', '3.14159265', '3'), # same for 'm'
977978
('.0n', '3.14159265', '3'), # same for 'n'
978979
('.1g', '3.14159265', '3'),
979980
('.2g', '3.14159265', '3.1'),

‎Lib/test/test_float.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,7 @@ def test_format(self):
666666
# in particular int specifiers
667667
for format_spec in ([chr(x) for x in range(ord('a'), ord('z')+1)] +
668668
[chr(x) for x in range(ord('A'), ord('Z')+1)]):
669-
if not format_spec in 'eEfFgGn%':
669+
if not format_spec in 'eEfFgGmn%':
670670
self.assertRaises(ValueError, format, 0.0, format_spec)
671671
self.assertRaises(ValueError, format, 1.0, format_spec)
672672
self.assertRaises(ValueError, format, -1.0, format_spec)

‎Lib/test/test_format.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,11 @@ def test_locale(self):
436436
self.assertIn(sep, text)
437437
self.assertIn(point, text)
438438
self.assertEqual(text.replace(sep, ''), '1234' + point + '5')
439+
440+
text = format(1234.5, ".3m")
441+
self.assertIn(sep, text)
442+
self.assertIn(point, text)
443+
self.assertEqual(text.replace(sep, ''), '1234' + point + '500')
439444
finally:
440445
locale.setlocale(locale.LC_ALL, oldloc)
441446

‎Lib/test/test_types.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,7 @@ def test(f, format_spec, result):
511511
# in particular int specifiers
512512
for format_spec in ([chr(x) for x in range(ord('a'), ord('z')+1)] +
513513
[chr(x) for x in range(ord('A'), ord('Z')+1)]):
514-
if not format_spec in 'eEfFgGn%':
514+
if not format_spec in 'eEfFgGmn%':
515515
self.assertRaises(ValueError, format, 0.0, format_spec)
516516
self.assertRaises(ValueError, format, 1.0, format_spec)
517517
self.assertRaises(ValueError, format, -1.0, format_spec)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Introduce "m" formatter for float, complex and decimal numbers. It behaves
2+
like "f" formatter but uses separators from locale data.

‎Modules/_decimal/libmpdec/io.c‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -864,15 +864,20 @@ mpd_parse_fmt_str(mpd_spec_t *spec, const char *fmt, int caps)
864864
*cp == 'G' || *cp == 'g' || *cp == '%') {
865865
spec->type = *cp++;
866866
}
867-
else if (*cp == 'N' || *cp == 'n') {
867+
else if (*cp == 'N' || *cp == 'n' ||
868+
*cp == 'M' || *cp == 'm') {
868869
/* locale specific conversion */
869870
struct lconv *lc;
870871
/* separator has already been specified */
871872
if (*spec->sep) {
872873
return 0;
873874
}
874875
spec->type = *cp++;
875-
spec->type = (spec->type == 'N') ? 'G' : 'g';
876+
if (spec->type == 'N' || spec->type == 'n') {
877+
spec->type = (spec->type == 'N') ? 'G' : 'g';
878+
} else {
879+
spec->type = (spec->type == 'M') ? 'F' : 'f';
880+
}
876881
lc = localeconv();
877882
spec->dot = lc->decimal_point;
878883
spec->sep = lc->thousands_sep;

‎Modules/_decimal/tests/formathelper.py‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ def rand_fillchar():
235235

236236
# Generate random format strings
237237
# [[fill]align][sign][#][0][width][.precision][type]
238-
def rand_format(fill, typespec='EeGgFfn%'):
238+
def rand_format(fill, typespec='EeGgFfmn%'):
239239
active = sorted(random.sample(range(7), random.randrange(8)))
240240
have_align = 0
241241
s = ''
@@ -277,7 +277,7 @@ def all_format_sep():
277277
type = random.choice(('', 'E', 'e', 'G', 'g', 'F', 'f', '%'))
278278
yield ''.join((fill, align, sign, zeropad, width, ',', prec, type))
279279

280-
# Partially brute force all possible format strings with an 'n' specifier.
280+
# Partially brute force all possible format strings with an 'n' and 'm' specifiers.
281281
# [[fill]align][sign][#][0][width][,][.precision][type]
282282
def all_format_loc():
283283
for align in ('', '<', '>', '=', '^'):
@@ -288,7 +288,8 @@ def all_format_loc():
288288
if align != '': zeropad = ''
289289
for width in ['']+[str(y) for y in range(1, 20)]+['101']:
290290
for prec in ['']+['.'+str(y) for y in range(1, 20)]:
291-
yield ''.join((fill, align, sign, zeropad, width, prec, 'n'))
291+
for type in ('n', 'm'):
292+
yield ''.join((fill, align, sign, zeropad, width, prec, type))
292293

293294
# Generate random format strings with a unicode fill character
294295
# [[fill]align][sign][#][0][width][,][.precision][type]
@@ -338,5 +339,5 @@ def rand_locale():
338339
elif elem == 4: # prec
339340
s += '.'
340341
s += str(random.randrange(100))
341-
s += 'n'
342+
s += random.choice('mn')
342343
return s

‎Python/formatter_unicode.c‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1060,6 +1060,9 @@ format_float_internal(PyObject *value,
10601060
/* 'n' is the same as 'g', except for the locale used to
10611061
format the result. We take care of that later. */
10621062
type = 'g';
1063+
else if (type == 'm')
1064+
/* 'm' is for 'n', what 'f' is for 'g'. */
1065+
type = 'f';
10631066

10641067
val = PyFloat_AsDouble(value);
10651068
if (val == -1.0 && PyErr_Occurred())
@@ -1096,6 +1099,7 @@ format_float_internal(PyObject *value,
10961099
if (format->sign != '+' && format->sign != ' '
10971100
&& format->width == -1
10981101
&& format->type != 'n'
1102+
&& format->type != 'm'
10991103
&& !format->thousands_separators)
11001104
{
11011105
/* Fast path */
@@ -1125,7 +1129,7 @@ format_float_internal(PyObject *value,
11251129
parse_number(unicode_tmp, index, index + n_digits, &n_remainder, &has_decimal);
11261130

11271131
/* Determine the grouping, separator, and decimal point, if any. */
1128-
if (get_locale_info(format->type == 'n' ? LT_CURRENT_LOCALE :
1132+
if (get_locale_info(format->type == 'n' || format->type == 'm' ? LT_CURRENT_LOCALE :
11291133
format->thousands_separators,
11301134
&locale) == -1)
11311135
goto done;
@@ -1250,6 +1254,9 @@ format_complex_internal(PyObject *value,
12501254
/* 'n' is the same as 'g', except for the locale used to
12511255
format the result. We take care of that later. */
12521256
type = 'g';
1257+
else if (type == 'm')
1258+
/* 'm' is for 'n', what 'f' is for 'g'. */
1259+
type = 'f';
12531260

12541261
if (precision < 0)
12551262
precision = default_precision;
@@ -1542,6 +1549,7 @@ _PyFloat_FormatAdvancedWriter(_PyUnicodeWriter *writer,
15421549
case 'F':
15431550
case 'g':
15441551
case 'G':
1552+
case 'm':
15451553
case 'n':
15461554
case '%':
15471555
/* no conversion, already a float. do the formatting */
@@ -1581,6 +1589,7 @@ _PyComplex_FormatAdvancedWriter(_PyUnicodeWriter *writer,
15811589
case 'F':
15821590
case 'g':
15831591
case 'G':
1592+
case 'm':
15841593
case 'n':
15851594
/* no conversion, already a complex. do the formatting */
15861595
return format_complex_internal(obj, &format, writer);

0 commit comments

Comments
 (0)