David Wolever
David Wolever

Reputation: 154682

Get precise decimal string representation of Python Decimal?

If I've got a Python Decimal, how can I reliably get the precise decimal string (ie, not scientific notation) representation of the number without trailing zeros?

For example, if I have:

>>> d = Decimal('1e-14')

I would like:

>>> get_decimal_string(d)
'0.00000000000001'

However:

  1. The Decimal class doesn't have any to_decimal_string method, or even any to_radix_string(radix) (cf: https://docs.python.org/3/library/decimal.html#decimal.Context.to_eng_string)
  2. The %f formatter either defaults to rounding to 6 decimal places - '%f' %(d, ) ==> '0.000000' - or requires a precise number of decimal places.
  3. The {:f}.format(...) formatter appears to work - '{:f}'.format(d) ==> '0.00000000000001' - however I'm reluctant to trust that, as this actually runs counter to the documentation, which says "'f' … Displays the number as a fixed-point number. The default precision is 6"
  4. Decimal.__repr__ and Decimal.__str__ sometimes return scientific notation: repr(d) ==> "Decimal('1E-14')"

So, is there any way to get a decimal string from a Python Decimal? Or do I need to roll my own using Decimal.as_tuple()?

Upvotes: 8

Views: 1127

Answers (2)

Carl Walsh
Carl Walsh

Reputation: 7009

@Julian's answer gives great details for Python 2's decimal.py:Decimal.__format__.

The answer in Python 3 is more complicated, because decimal.py is just a wrapper for the native module:

from _decimal import *

In the native module, __format__ is implemented by _decimal.c:dec_format which will call into _decimal/libmpdec/io.c:mpd_parse_fmt_str to parse the format string. If that succeeds, it calls into io.c:mpd_qformat_spec which handles the e formatting:

            case 'e':
                if (mpd_iszero(dec)) {
                    dplace = 1-spec->prec;
                }
                else {
                    _mpd_round(&tmp, dec, spec->prec+1, ctx,
                               &workstatus);
                    dec = &tmp;
                }
                break;

If mpd_qformat_spec failed, then instead pydec_format is called, which calls into _pydecimal.py:Decimal.__format__ with the same logic as Python 2.

Upvotes: 0

Julian
Julian

Reputation: 1143

Short answer:

>>> d
Decimal('1E-14')
>>> '{:f}'.format(d)
'0.00000000000001'

Long answer:

As Brandon Rhodes pointed out PEP 3101 (which is the string format PEP) states:

The syntax for format specifiers is open-ended, since a class can override the standard format specifiers. In such cases, the str.format() method merely passes all of the characters between the first colon and the matching brace to the relevant underlying formatting method.

And thus, the Decimal.__format__ method is what python's string format will utilize to generate the str representation of the Decimal value. Basically Decimal overrides the formatting to be "smart" but will default to whatever values the format string sets (ie {:.4f} will truncate the decimal to 4 places).

Here's why you can trust it (snippet from decimal.py:Decimal.__format__):

def __format__(self, specifier, context=None, _localeconv=None):
    #
    # ...implementation snipped.
    #

    # figure out placement of the decimal point
    leftdigits = self._exp + len(self._int)
    if spec['type'] in 'eE':
        if not self and precision is not None:
            dotplace = 1 - precision
        else:
            dotplace = 1
    elif spec['type'] in 'fF%':
        dotplace = leftdigits
    elif spec['type'] in 'gG':
        if self._exp <= 0 and leftdigits > -6:
            dotplace = leftdigits
        else:
            dotplace = 1

    # find digits before and after decimal point, and get exponent
    if dotplace < 0:
        intpart = '0'
        fracpart = '0'*(-dotplace) + self._int
    elif dotplace > len(self._int):
        intpart = self._int + '0'*(dotplace-len(self._int))
        fracpart = ''
    else:
        intpart = self._int[:dotplace] or '0'
        fracpart = self._int[dotplace:]
    exp = leftdigits-dotplace

    # done with the decimal-specific stuff;  hand over the rest
    # of the formatting to the _format_number function
    return _format_number(self._sign, intpart, fracpart, exp, spec)

Long story short, the Decimal.__format__ method will calculate the necessary padding to represent the number before and after the decimal based upon exponentiation provided from Decimal._exp (in your example, 14 significant digits).

>>> d._exp
-14

Upvotes: 6

Related Questions