Index: Lib/decimal.py =================================================================== --- Lib/decimal.py (revision 61050) +++ Lib/decimal.py (working copy) @@ -3431,6 +3431,143 @@ return self # My components are also immutable return self.__class__(str(self)) + # PEP 3101 support; see also the parse_format_specifier and + # format_pad functions. + def __format__(self, specifier, context=None): + """Format a Decimal class according to the given specifier. + + The specifier should be a standard format specifier, with the + form described in PEP 3101. The floating-point types 'e', + 'E', 'f', 'F', 'g', 'G', '%' and '' are supported. + + At this time the 'n' format specifier type (which is supposed + to use the current locale) is not supported. + """ + + spec = parse_format_specifier(specifier) + if self._is_special: + return format_align(str(self), spec) + + # use rounding mode from the current context, if necessary + if context is None: + context = getcontext() + rounding = context.rounding + + precision = spec['precision'] + if spec['type'] in 'eE': + # type 'e': + # if precision is None, produce a string s + # such that Decimal(s) reproduces self exactly, including + # the exponent. + # + # if precision is not None, rescale, and pad with zeros + # if necessary, so that there are exactly 'precision' + # digits after the decimal point. For zeros, we make + # sure that the adjusted exponent of the zero is unchanged. + if precision is not None and not self.is_zero(): + adjusted = self.adjusted() + self = self._rescale(self.adjusted() - precision, + rounding = rounding) + if self._exp != self.adjusted() - precision: + self = self._rescale(self.adjusted() - precision, + rounding = rounding) + + if len(self._int) > 1: + num = self._int[0] + '.' + self._int[1:] + num = num + spec['type'] + '%+d' % self.adjusted() + elif self.is_zero() and precision is not None: + num = '0.' + '0'*precision + num = num + spec['type'] + '%+d' % (self.adjusted()+precision) + else: + num = self._int + num = num + spec['type'] + '%+d' % self.adjusted() + + elif spec['type'] in 'fF%': + # type 'f': + # if precision is None, don't do any rounding; + # pad self._int with as many zeros as necessary to get + # us up to the decimal point. + # + # if precision is not None, rescale so that the exponent + # of self is -precision. We do this even for zeros. (In + # effect, we do a quantize, but without the possible + # exceptions that can arise.) + if precision is not None: + if spec['type'] == '%': + precision += 2 + self = self._rescale(-precision, rounding = rounding) + + dotplace = len(self._int) + self._exp + if spec['type'] == '%': + dotplace += 2 + + if dotplace <= 0: + num = '0' + '.' + '0'*(-dotplace) + self._int + elif dotplace >= len(self._int): + if self.is_zero(): + num = '0' + else: + num = self._int + '0'*(dotplace - len(self._int)) + else: + num = self._int[:dotplace] + '.' + self._int[dotplace:] + + if spec['type'] == '%': + num = num + '%' + + elif spec['type'] in 'gG' or spec['type'] is '': + # type 'g': + # if precision is None we follow exactly the same rules + # as for __str__, except that we use the format type + # instead of the context when deciding whether to use + # 'e' or 'E'. + # + # if precision is not None, first make sure that we have + # at most p+1 significant digits, by doing a rescale if + # necessary (but don't pad with zeros if we have fewer + # than p+1 significant digits). If self is zero then + # we don't change its exponent. + + if precision is not None: + # round only if necessary + if len(self._int) > precision+1: + self = self.rescale(self.adjusted() - precision, + rounding = rounding) + + leftdigits = self._exp + len(self._int) + if self._exp <= 0 and leftdigits > -6: + dotplace = leftdigits + else: + dotplace = 1 + + if dotplace <= 0: + num = '0.' + '0'*(-dotplace) + self._int + elif dotplace >= len(self._int): + num = self._int + '0'*(dotplace-len(self._int)) + else: + num = self._int[:dotplace] + '.' + self._int[dotplace:] + + if leftdigits != dotplace: + # add exponent + if spec['type'] == 'g': + num = num + ('e%+d' % self.adjusted()) + elif spec['type'] == 'G': + num = num + ('E%+d' % self.adjusted()) + elif spec['type'] == '' and '.' not in num: + # if there's no exponent and no decimal point, add a + # .0 + if '.' not in num: + num = num + '.0' + + + else: + raise ValueError("Format specification type '%s' " + "unsupported by Decimal" % spec['type']) + + if self._sign == 1: + num = '-' + num + return format_align(num, spec) + + def _dec_from_triple(sign, coefficient, exponent, special=False): """Create a decimal instance directly, without any validation, normalization (e.g. removal of leading zeros) or argument @@ -5253,7 +5390,131 @@ _exact_half = re.compile('50*$').match del re +##### PEP3101 support functions ############################################## +# The functions parse_format_specifier and format_align have little to do +# with the Decimal class, and could potentially be reused for other pure +# Python numeric classes that want to implement __format__ +import re +_parse_format_specifier_regex = re.compile(r"""\A +((?P.)?(?P[<>=^]))? +(?P[-+ ])? +(?P0)? +(?P(?!0)\d+)? +(?:\.(?P\d*))? # allows precision of '01'; should this be illegal? +(?P[eEfFgGn%bcdoxX]?) +\Z +""", re.VERBOSE) +del re + +def parse_format_specifier(format_spec): + """Turn a standard numeric format specifier into a dict, with the + following entries: + + fill: fill character to pad field to minimum width + align: alignment type, either '<', '>', '=' or '^' + sign: either '+', '-' or ' ' + minimumwidth: nonnegative integer giving minimum width + precision: nonnegative integer giving precision, or None + type: one of the characters 'eEfFgGn%bcdoxX', or '' + unicode: either True or False (for Python 3.0 this shold always be True) + + """ + m = _parse_format_specifier_regex.match(format_spec) + if m is None: + raise ValueError("Invalid format specifier: %s" % format_spec) + + # get the dictionary + format_dict = m.groupdict() + + # defaults for fill and alignment + fill = format_dict['fill'] + align = format_dict['align'] + if format_dict.pop('zeropad') is not None: + # in the face of conflict, refuse the temptation to guess + if fill is not None and fill != '0': + raise ValueError("Fill character of %s conflicts with '0'" + " in format specifier." % fill) + if align is not None and align != '=': + raise ValueError("Alignment of %s conflicts with '0' in " + "format specifier." % align) + fill = '0' + align = '=' + format_dict['fill'] = fill or ' ' + format_dict['align'] = align or '<' + + if format_dict['sign'] is None: + format_dict['sign'] = '-' + + # turn minimumwidth and precision entries into integers; + # minimumwidth defaults to 0. + format_dict['minimumwidth'] = int(format_dict['minimumwidth'] or '0') + + # precision remains None if not given + prec = format_dict['precision'] + if prec is not None: + if prec == '': + raise ValueError("Format specifier has missing precision: " + "%s." % format_spec) + # the next line probably isn't worth bothering with + if prec.startswith('0') and len(prec) != 1: + raise ValueError("Nonzero precision should not have leading zero.") + format_dict['precision'] = int(prec) + + # record whether return type should be str or unicode + format_dict['unicode'] = isinstance(format_spec, unicode) + + return format_dict + +def format_align(body, spec_dict): + """Given an unpadded, non-aligned numeric string, add padding and + aligment to conform with the given format specifier dictionary (as + output from parse_format_specifier). + + It's assumed that if body is negative then it starts with '-'. + Any leading sign ('-' or '+') is stripped from the body before + applying the alignment and padding rules, and replaced in the + appropriate position. + + """ + + # figure out what sign to use; note that we only look at the first + # character to figure out the sign. Something like ' -2.3' may + # give unexpected results. + if len(body) > 0 and body[0] in '-+': + sign = body[0] + body = body[1:] + else: + sign = '' + + if sign != '-': + if spec_dict['sign'] in ' +': + sign = spec_dict['sign'] + else: + sign = '' + + # how much extra space do we have to play with? + minimumwidth = spec_dict['minimumwidth'] + fill = spec_dict['fill'] + padding = fill*(max(minimumwidth - (len(sign+body)), 0)) + + align = spec_dict['align'] + if align == '<': + result = padding + sign + body + elif align == '>': + result = sign + body + padding + elif align == '=': + result = sign + padding + body + else: #align == '^' + half = len(padding)//2 + result = padding[:half] + sign + body + padding[half:] + + # make sure that result is unicode if necessary + if spec_dict['unicode']: + result = unicode(result) + + return result + ##### Useful Constants (internal use only) ################################ # Reusable defaults Index: Lib/test/test_decimal.py =================================================================== --- Lib/test/test_decimal.py (revision 61050) +++ Lib/test/test_decimal.py (working copy) @@ -615,6 +615,75 @@ self.assertEqual(eval('Decimal(10)' + sym + 'E()'), '10' + rop + 'str') +class DecimalFormatTest(unittest.TestCase): + '''Unit tests for the format function.''' + + def test_formatting(self): + # triples giving a format, a Decimal, and the expected result + test_values = [ + ('e', '0E-15', '0e-15'), + ('e', '2.3E-15', '2.3e-15'), + ('e', '2.30E+2', '2.30e+2'), # preserve significant zeros + ('e', '2.30000E-15', '2.30000e-15'), + ('e', '1.23456789123456789e40', '1.23456789123456789e+40'), + ('e', '1.5', '1.5e+0'), + ('e', '0.15', '1.5e-1'), + ('e', '0.015', '1.5e-2'), + ('e', '0.0000000000015', '1.5e-12'), + ('e', '15.0', '1.50e+1'), + ('e', '-15', '-1.5e+1'), + ('e', '0', '0e+0'), + ('e', '0E1', '0e+1'), + ('e', '0.0', '0e-1'), + ('e', '0.00', '0e-2'), + ('.6e', '0E-15', '0.000000e-9'), + ('.6e', '0', '0.000000e+6'), + ('.6e', '9.999999', '9.999999e+0'), + ('.6e', '9.9999999', '1.000000e+1'), + ('.6e', '-1.23e5', '-1.230000e+5'), + ('.6e', '1.23456789e-3', '1.234568e-3'), + + + ('f', '0', '0'), + ('f', '0.0', '0.0'), + ('f', '0E-2', '0.00'), + ('f', '0.00E-8', '0.0000000000'), + ('f', '0E1', '0'), + ('f', '3.2E1', '32'), + ('f', '3.2E2', '320'), + ('f', '3.20E2', '320'), + ('f', '3.200E2', '320.0'), + ('f', '3.2E-6', '0.0000032'), + + ('.6f', '0E-15', '0.000000'), # loses exponent info + + ('g', '0', '0'), + ('g', '0.0', '0.0'), + ('g', '0E1', '0e+1'), + ('G', '0E1', '0E+1'), + ('g', '0E-5', '0.00000'), + ('g', '0E-6', '0.000000'), + ('g', '0E-7', '0e-7'), + ('g', '-0E2', '-0e+2'), + + ('%', '0E1', '0%'), + ('%', '0E0', '0%'), + ('%', '0E-1', '0%'), + ('%', '0E-2', '0%'), + ('%', '0E-3', '0.0%'), + ('%', '0E-4', '0.00%'), + + ('.3%', '0', '0.000%'), # all zeros treated equally + ('.3%', '0E10', '0.000%'), + ('.3%', '0E-10', '0.000%'), + ('.3%', '2.34', '234.000%'), + ('.3%', '1.234567', '123.457%'), + ('.0%', '1.23', '123%'), + + ] + for fmt, d, result in test_values: + self.assertEqual(format(Decimal(d), fmt), result) + class DecimalArithmeticOperatorsTest(unittest.TestCase): '''Unit tests for all arithmetic operators, binary and unary.''' @@ -1363,6 +1432,7 @@ DecimalExplicitConstructionTest, DecimalImplicitConstructionTest, DecimalArithmeticOperatorsTest, + DecimalFormatTest, DecimalUseOfContextTest, DecimalUsabilityTest, DecimalPythonAPItests,