summaryrefslogtreecommitdiffstats
path: root/Lib/decimal.py
diff options
context:
space:
mode:
authorMark Dickinson <dickinsm@gmail.com>2008-02-29 02:16:37 (GMT)
committerMark Dickinson <dickinsm@gmail.com>2008-02-29 02:16:37 (GMT)
commit1ddf1d8482f4fd8ae034bfd0221696ee2068c144 (patch)
tree4be97e288b09a332816fa0e6b93c80034bf20a7f /Lib/decimal.py
parentb4cbc98c3929b05501e74236e744fced9e3d4a35 (diff)
downloadcpython-1ddf1d8482f4fd8ae034bfd0221696ee2068c144.zip
cpython-1ddf1d8482f4fd8ae034bfd0221696ee2068c144.tar.gz
cpython-1ddf1d8482f4fd8ae034bfd0221696ee2068c144.tar.bz2
Add __format__ method to Decimal, to support PEP 3101
Diffstat (limited to 'Lib/decimal.py')
-rw-r--r--Lib/decimal.py240
1 files changed, 240 insertions, 0 deletions
diff --git a/Lib/decimal.py b/Lib/decimal.py
index 8fcb093..d8abf55 100644
--- a/Lib/decimal.py
+++ b/Lib/decimal.py
@@ -2380,6 +2380,29 @@ class Decimal(object):
coeff = str(int(coeff)+1)
return _dec_from_triple(self._sign, coeff, exp)
+ def _round(self, places, rounding):
+ """Round a nonzero, nonspecial Decimal to a fixed number of
+ significant figures, using the given rounding mode.
+
+ Infinities, NaNs and zeros are returned unaltered.
+
+ This operation is quiet: it raises no flags, and uses no
+ information from the context.
+
+ """
+ if places <= 0:
+ raise ValueError("argument should be at least 1 in _round")
+ if self._is_special or not self:
+ return Decimal(self)
+ ans = self._rescale(self.adjusted()+1-places, rounding)
+ # it can happen that the rescale alters the adjusted exponent;
+ # for example when rounding 99.97 to 3 significant figures.
+ # When this happens we end up with an extra 0 at the end of
+ # the number; a second rescale fixes this.
+ if ans.adjusted() != self.adjusted():
+ ans = ans._rescale(ans.adjusted()+1-places, rounding)
+ return ans
+
def to_integral_exact(self, rounding=None, context=None):
"""Rounds to a nearby integer.
@@ -3431,6 +3454,95 @@ class Decimal(object):
return self # My components are also immutable
return self.__class__(str(self))
+ # PEP 3101 support. See also _parse_format_specifier and _format_align
+ 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. 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.
+ """
+
+ # Note: PEP 3101 says that if the type is not present then
+ # there should be at least one digit after the decimal point.
+ # We take the liberty of ignoring this requirement for
+ # Decimal---it's presumably there to make sure that
+ # format(float, '') behaves similarly to str(float).
+ if context is None:
+ context = getcontext()
+
+ spec = _parse_format_specifier(specifier)
+
+ # special values don't care about the type or precision...
+ if self._is_special:
+ return _format_align(str(self), spec)
+
+ # a type of None defaults to 'g' or 'G', depending on context
+ # if type is '%', adjust exponent of self accordingly
+ if spec['type'] is None:
+ spec['type'] = ['g', 'G'][context.capitals]
+ elif spec['type'] == '%':
+ self = _dec_from_triple(self._sign, self._int, self._exp+2)
+
+ # 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':
+ if len(self._int) > precision:
+ self = self._round(precision, rounding)
+ elif spec['type'] in 'fF%':
+ self = self._rescale(-precision, rounding)
+ # special case: zeros with a positive exponent can't be
+ # represented in fixed point; rescale them to 0e0.
+ elif not self and self._exp > 0 and spec['type'] in 'fF%':
+ self = self._rescale(0, rounding)
+
+ # figure out placement of the decimal point
+ leftdigits = self._exp + len(self._int)
+ if spec['type'] in 'fF%':
+ dotplace = leftdigits
+ elif spec['type'] in 'eE':
+ if not self and precision is not None:
+ dotplace = 1 - precision
+ else:
+ dotplace = 1
+ elif spec['type'] in 'gG':
+ 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))
+ else:
+ num = self._int[:dotplace] + '.' + self._int[dotplace:]
+
+ # ...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']]
+ num = num + "{0}{1:+}".format(echar, leftdigits-dotplace)
+ elif spec['type'] == '%':
+ num = num + '%'
+
+ # add sign
+ 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
@@ -5250,8 +5362,136 @@ _parser = re.compile(r""" # A numeric string consists of:
_all_zeros = re.compile('0*$').match
_exact_half = re.compile('50*$').match
+
+##### 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__
+#
+# A format specifier for Decimal looks like:
+#
+# [[fill]align][sign][0][minimumwidth][.precision][type]
+#
+
+_parse_format_specifier_regex = re.compile(r"""\A
+(?:
+ (?P<fill>.)?
+ (?P<align>[<>=^])
+)?
+(?P<sign>[-+ ])?
+(?P<zeropad>0)?
+(?P<minimumwidth>(?!0)\d+)?
+(?:\.(?P<precision>0|(?!0)\d+))?
+(?P<type>[eEfFgG%])?
+\Z
+""", re.VERBOSE)
+
del re
+def _parse_format_specifier(format_spec):
+ """Parse and validate a format specifier.
+
+ Turns 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 'eEfFgG%', or None
+ unicode: either True or False (always True for Python 3.x)
+
+ """
+ m = _parse_format_specifier_regex.match(format_spec)
+ if m is None:
+ raise ValueError("Invalid format specifier: " + 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 conflicts with '0'"
+ " in format specifier: " + format_spec)
+ if align is not None and align != '=':
+ raise ValueError("Alignment conflicts with '0' in "
+ "format specifier: " + format_spec)
+ 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; precision remains None if not given
+ format_dict['minimumwidth'] = int(format_dict['minimumwidth'] or '0')
+ if format_dict['precision'] is not None:
+ format_dict['precision'] = int(format_dict['precision'])
+
+ # if format type is 'g' or 'G' then a precision of 0 makes little
+ # sense; convert it to 1. Same if format type is unspecified.
+ if format_dict['precision'] == 0:
+ if format_dict['type'] in 'gG' or format_dict['type'] is None:
+ format_dict['precision'] = 1
+
+ # 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 the sign; we only examine the first character, so if
+ # body has leading whitespace the results may be surprising.
+ 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) ################################