Index: Lib/decimal.py =================================================================== --- Lib/decimal.py (revision 70244) +++ Lib/decimal.py (working copy) @@ -138,7 +138,15 @@ import math as _math import numbers as _numbers +# The locale module is only needed for the 'n' format specifier. The +# rest of this module functions quite happily without it, so we don't +# care too much if locale isn't present. try: + import locale as _locale +except ImportError: + pass + +try: from collections import namedtuple as _namedtuple DecimalTuple = _namedtuple('DecimalTuple', 'sign digits exponent') except ImportError: @@ -3505,17 +3513,15 @@ return self.__class__(str(self)) # PEP 3101 support. See also _parse_format_specifier and _format_align - def __format__(self, specifier, context=None): + # _localeconv keyword argument is provided for ease of testing. + def __format__(self, specifier, context=None, _localeconv=None): """Format a Decimal instance according to the given specifier. The specifier should be a standard format specifier, with the form described in PEP 3101. Formatting types 'e', 'E', 'f', - 'F', 'g', 'G', and '%' are supported. If the formatting type - is omitted it defaults to 'g' or 'G', depending on the value - of context.capitals. - - At this time the 'n' format specifier type (which is supposed - to use the current locale) is not supported. + 'F', 'g', 'G', 'n' and '%' are supported. If the formatting + type is omitted it defaults to 'g' or 'G', depending on the + value of context.capitals. """ # Note: PEP 3101 says that if the type is not present then @@ -3539,13 +3545,16 @@ elif spec['type'] == '%': self = _dec_from_triple(self._sign, self._int, self._exp+2) + if spec['type'] == 'n' and _localeconv is None: + _localeconv = _locale.localeconv() + # round if necessary, taking rounding mode from the context rounding = context.rounding precision = spec['precision'] if precision is not None: if spec['type'] in 'eE': self = self._round(precision+1, rounding) - elif spec['type'] in 'gG': + elif spec['type'] in 'gGn': if len(self._int) > precision: self = self._round(precision, rounding) elif spec['type'] in 'fF%': @@ -3564,25 +3573,43 @@ dotplace = 1 - precision else: dotplace = 1 - elif spec['type'] in 'gG': + elif spec['type'] in 'gGn': if self._exp <= 0 and leftdigits > -6: dotplace = leftdigits else: dotplace = 1 # figure out main part of numeric string... - if dotplace <= 0: - num = '0.' + '0'*(-dotplace) + self._int - elif dotplace >= len(self._int): - # make sure we're not padding a '0' with extra zeros on the right - assert dotplace==len(self._int) or self._int != '0' - num = self._int + '0'*(dotplace-len(self._int)) + if dotplace < 0: + intpart = '0' + fracpart = '0'*(-dotplace) + self._int + elif dotplace > len(self._int): + # make sure we're not padding a '0' + assert self._int != '0' + intpart = self._int + '0'*(dotplace-len(self._int)) + fracpart = '' else: - num = self._int[:dotplace] + '.' + self._int[dotplace:] + intpart = self._int[:dotplace] or '0' + fracpart = self._int[dotplace:] + # for 'n' specifier, put separators into intpart + if spec['type'] == 'n': + intpart = _insert_separators(intpart, _localeconv) + + # add decimal point if necessary + if fracpart: + if spec['type'] == 'n': + decimal_point = _localeconv['decimal_point'] + else: + decimal_point = '.' + num = intpart + decimal_point + fracpart + else: + num = intpart + # ...then the trailing exponent, or trailing '%' if leftdigits != dotplace or spec['type'] in 'eE': - echar = {'E': 'E', 'e': 'e', 'G': 'E', 'g': 'e'}[spec['type']] + echar = {'E': 'E', 'e': 'e', 'G': 'E', 'g': 'e', + 'n': 'e'}[spec['type']] num = num + "{0}{1:+}".format(echar, leftdigits-dotplace) elif spec['type'] == '%': num = num + '%' @@ -5453,7 +5480,7 @@ (?P0)? (?P(?!0)\d+)? (?:\.(?P0|(?!0)\d+))? -(?P[eEfFgG%])? +(?P[eEfFgGn%])? \Z """, re.VERBOSE) @@ -5564,6 +5591,47 @@ return result +def _group_lengths(grouping): + """Convert a localeconv-style grouping into a (possibly infinite) + iterable of integers representing group lengths. + + """ + # The result from localeconv()['grouping'], and the input to this + # function, should be a list of integers in one of the + # following three forms: + # + # (1) an empty list, or + # (2) list of positive integers + [locale.CHAR_MAX], or + # (3) nonempty list of positive integers + [0] + + from itertools import chain, repeat + if not grouping: + return [] + elif grouping[-1] == _locale.CHAR_MAX: + return grouping[:-1] + elif grouping[-1] == 0 and len(grouping) >= 2: + return chain(grouping[:-1], repeat(grouping[-2])) + else: + raise ValueError('unrecognised format for grouping') + +def _insert_separators(digits, _localeconv): + """Insert thousands separators into a digit string.""" + + if not digits: + raise ValueError("digits should be nonempty") + + groups = [] + for l in _group_lengths(_localeconv['grouping']): + if l <= 0: + raise ValueError("group length should be positive") + groups.append(digits[-l:]) + digits = digits[:-l] + if not digits: + break + else: + groups.append(digits) + return _localeconv['thousands_sep'].join(reversed(groups)) + ##### Useful Constants (internal use only) ################################ # Reusable defaults Index: Lib/test/test_decimal.py =================================================================== --- Lib/test/test_decimal.py (revision 70244) +++ Lib/test/test_decimal.py (working copy) @@ -708,6 +708,57 @@ for fmt, d, result in test_values: self.assertEqual(format(Decimal(d), fmt), result) + def test_n_format(self): + try: + from locale import CHAR_MAX + except ImportError: + return + + # Set up some localeconv-like dictionaries + en_US = { + 'decimal_point' : '.', + 'grouping' : [3, 3, 0], + 'thousands_sep': ',' + } + + fr_FR = { + 'decimal_point' : ',', + 'grouping' : [CHAR_MAX], + 'thousands_sep' : '' + } + + ru_RU = { + 'decimal_point' : ',', + 'grouping' : [3, 3, 0], + 'thousands_sep' : ' ' + } + + crazy = { + 'decimal_point' : '&', + 'grouping' : [1, 4, 2, CHAR_MAX], + 'thousands_sep' : '-' + } + + + def get_fmt(x, locale, fmt='n'): + return Decimal.__format__(Decimal(x), fmt, _localeconv=locale) + + self.assertEqual(get_fmt(Decimal('12.7'), en_US), '12.7') + self.assertEqual(get_fmt(Decimal('12.7'), fr_FR), '12,7') + self.assertEqual(get_fmt(Decimal('12.7'), ru_RU), '12,7') + self.assertEqual(get_fmt(Decimal('12.7'), crazy), '1-2&7') + + self.assertEqual(get_fmt(123456789, en_US), '123,456,789') + self.assertEqual(get_fmt(123456789, fr_FR), '123456789') + self.assertEqual(get_fmt(123456789, ru_RU), '123 456 789') + self.assertEqual(get_fmt(1234567890123, crazy), '123456-78-9012-3') + + self.assertEqual(get_fmt(123456789, en_US, '.6n'), '1.23457e+8') + self.assertEqual(get_fmt(123456789, fr_FR, '.6n'), '1,23457e+8') + self.assertEqual(get_fmt(123456789, ru_RU, '.6n'), '1,23457e+8') + self.assertEqual(get_fmt(123456789, crazy, '.6n'), '1&23457e+8') + + class DecimalArithmeticOperatorsTest(unittest.TestCase): '''Unit tests for all arithmetic operators, binary and unary.'''