2

Consider the following Python3 snippet: (Python 3.7.7 on mac os catalina)

>>> from decimal import Decimal as d
>>> zero = d('0')
>>> one = d('1')
>>> for q in range(10):
...   one.quantize(d('10') ** -q)
... 
Decimal('1')
Decimal('1.0')
Decimal('1.00')
Decimal('1.000')
Decimal('1.0000')
Decimal('1.00000')
Decimal('1.000000')
Decimal('1.0000000')
Decimal('1.00000000')
Decimal('1.000000000')

>>> for q in range(10):
...   zero.quantize(d('10') ** -q)
... 
Decimal('0')
Decimal('0.0')
Decimal('0.00')
Decimal('0.000')
Decimal('0.0000')
Decimal('0.00000')
Decimal('0.000000')
Decimal('0E-7')
Decimal('0E-8')
Decimal('0E-9')

Why does quantize with zero change to E notation at this point? Why is it inconsistent with other numbers? And how can I control it?

Note: I get exactly the same inconsistency if I use use the built-in round function instead of quantize, which leads me to guess that round calls quantize when it gets a Decimal.

Since I want strings with trailing zeros, the best workaround I can think of is to write my own _round() function:

def _round(s, n):
    if decimal.Decimal(s).is_zero():
        return '0.' + '0' * n
    return decimal.Decimal(s).quantize(decimal.Decimal(10) ** -n)

but that seems a bit lame. And anyway I'd like to understand why Decimal.quantize behaves like this.

Thruston
  • 1,467
  • 17
  • 23
  • 1
    I don't think this is only limited to `quantize` method, rather it's how `__str__` method is defined. You can check the source code [here](https://github.com/python/cpython/blob/master/Lib/_pydecimal.py). – ywbaek May 10 '20 at 02:36
  • Don't confuse a `Decimal` object with its string representation. This question has nothing particularly to do with `quantize` or `round`. If you simply enter `Decimal('0.0000000')` at a prompt, you'll see the same output from the REPL: `Decimal('0E-7')`. You want to look into string formatting. – Mark Dickinson May 10 '20 at 11:54
  • @ywbaek thanks - I shall look at the string formatting. And perhaps update the question to ask how to control that ... – Thruston May 10 '20 at 12:18
  • `format(my_decimal_object, 'f')` may be all you need (following the `quantize` option). Or perhaps use an explicit precision, e.g., `format(my_decimal_object, '.9f')` , and then there's no need for `round` or `quantize`. – Mark Dickinson May 10 '20 at 12:36
  • @markdickinson yes - that was the conclusion I came to. The inconsistency (apparently) arises from the attempt to get rid of leading zeros in Decimal.__new__() where is says `self._int = str(int(intpart+fracpart))` -- I think this would be better written as `self._int = str(int(intpart))+fracpart` but there may be some other implications of this change that I cannot see. – Thruston May 10 '20 at 13:04
  • No, the code is right. While trailing zeros are significant for `Decimal`, leading zeros are not: there's no difference between `Decimal("000E-6")` and `Decimal("0E-6")`. None of this is accidental: there's a carefully designed [specification](http://speleotrove.com/decimal/decarith.html) that the `decimal` module follows. – Mark Dickinson May 10 '20 at 13:08
  • Apologies for the extended comment discussion, @MarkDickinson, but when both intpart and fracpart are all zeros (as in my case) then the code as is, destroys the trailing zeros. Which it should not do? – Thruston May 10 '20 at 13:13
  • It destroy _leading_ zeros, not _trailing_ zeros - it keeps track of the exponent. The code is correct (and very well tested :-) ). – Mark Dickinson May 10 '20 at 13:18

1 Answers1

1

Answering my own question, leaving aside why Python3 Decimal has this behaviour, the way to control the format is not to mess with quantize at all. It is much simpler and more consistent to use format, like so: (with the same definitions of zero and one as Decimals)

>>> for q in range(10):
...   '{:.{n}f}'.format(zero, n=q)
... 
'0'
'0.0'
'0.00'
'0.000'
'0.0000'
'0.00000'
'0.000000'
'0.0000000'
'0.00000000'
'0.000000000'
>>> for q in range(10):
...   '{:.{n}f}'.format(one, n=q)
... 
'1'
'1.0'
'1.00'
'1.000'
'1.0000'
'1.00000'
'1.000000'
'1.0000000'
'1.00000000'
'1.000000000'

With thanks to the commentary for useful pointers.

Thruston
  • 1,467
  • 17
  • 23