Python decimal.Decimal producing result in scientific notation
Asked Answered
S

3

2

I'm dividing a very long into much smaller number. Both are of type decimal.Decimal().

The result is coming out in scientific notation. How do I stop this? I need to print the number in full.

>>> decimal.getcontext().prec
50
>>> val
Decimal('1000000000000000000000000')
>>> units
Decimal('1500000000')
>>> units / val
Decimal('1.5E-15')
Scherzo answered 7/6, 2021 at 23:19 Comment(1)
Use formattingMonteith
S
2

Render the decimal into a formatted string with a float type-indicator {:,f}, and it will display just the right number of digits to express the whole number, regardless of whether it is a very large integer or a very large decimal.

>>> val
Decimal('1000000000000000000000000')

>>> units
Decimal('1500000000')

>>> "{:,f}".format(units / val)
'0.0000000000000015'

# very large decimal integer, formatted as float-type string, appears without any decimal places at all when it has none! Nice!

>>> "{:,f}".format(units * val)
 '1,500,000,000,000,000,000,000,000,000,000,000'

You don't need to specify the decimal places. It will display only as many as required to express the number, omitting that trail of useless zeros that appear after the final decimal digit when the decimal is shorter than a fixed format width. And you don't get any decimal places if the number has no fraction part.

Very large numbers are therefore accommodated without having to second guess how large they will be. And you don't have to second guess whether they will be have decimal places either.

Any specified thousands separator {:,f} will likewise only have effect if it turns out that the number is a large integer instead of a long decimal.

Proviso

Decimal(), however, has this idea of significant places, by which it will add trailing zeros if it thinks you want them.

The idea is that it intelligently handles situations where you might be dealing with currency digits such as £ 10.15. To use the example from the documentation:

>>> decimal.Decimal('1.30') +  decimal.Decimal('1.20')
Decimal('2.50')

It makes no difference if you format the Decimal() - you still get the trailing zero if the Decimal() deems it to be significant:

>>> "{:,f}".format( decimal.Decimal('1.30') +  decimal.Decimal('1.20'))
'2.50'

The same thing happens (perhaps for some good reason?) when you treat thousands and fractions together:

>>> decimal.Decimal(2500) * decimal.Decimal('0.001')
Decimal('2.500')

Remove significant trailing zeros with the Decimal().normalize() method:

>>> (2500 * decimal.Decimal('0.001')).normalize()
Decimal('2.5')
Scherzo answered 8/6, 2021 at 9:1 Comment(0)
O
2

The precision is kept internally - you just have to explicitly call for the number of decimal places you want at the point you are exporting your decimal value to a string.

So, if you are going a print, or inserting the value in an HTML template, the first step is to use the string format method (or f-strings), to ensure the number is encompassed:

In [29]: print(f"{units/val:.50f}")                                                                                                                    
0.00000000000000150000000000000000000000000000000000

Unfortunatelly, the string-format minilanguage has no way to eliminate by itself the redundant zeroes on the right hand side. (the left side can be padded with "0", " ", custom characters, whatever one want, but all the precision after the decimal separator is converted to trailing 0s).

Since finding the least significant non-zero digit is complicated - otherwiser we could use a parameter extracted from the number instead of the "50" for precision in the format expression, the simpler thing is to remove those zeros after formatting take place, with the string .rstrip method:


In [30]: print(f"{units/val:.50f}".rstrip("0"))                                                                                                        
0.0000000000000015

In short: this seems to be the only way to go: in all interface points, where the number is leaving the core to an output where it is representd as a string, you format it with an excess of precision with the fixed point notation, and strip out the tailing zeros with f-string:

return template.render(number=f"{number:.50f}".rstrip("0"), ...)
Oxblood answered 7/6, 2021 at 23:55 Comment(1)
Oh yes, of course. And best of all - you don't actually need to specify the decimal places at all: print("{:,f}".format(units / val) '0.0000000000000015' That way you don't get a trail of useless zeros after the final decimal digit when the decimal is shorter than your format width - and very large numbers are therefore accommodated without having to second guess how large they will be. Thank you for correcting my late night fog.Scherzo
S
2

Render the decimal into a formatted string with a float type-indicator {:,f}, and it will display just the right number of digits to express the whole number, regardless of whether it is a very large integer or a very large decimal.

>>> val
Decimal('1000000000000000000000000')

>>> units
Decimal('1500000000')

>>> "{:,f}".format(units / val)
'0.0000000000000015'

# very large decimal integer, formatted as float-type string, appears without any decimal places at all when it has none! Nice!

>>> "{:,f}".format(units * val)
 '1,500,000,000,000,000,000,000,000,000,000,000'

You don't need to specify the decimal places. It will display only as many as required to express the number, omitting that trail of useless zeros that appear after the final decimal digit when the decimal is shorter than a fixed format width. And you don't get any decimal places if the number has no fraction part.

Very large numbers are therefore accommodated without having to second guess how large they will be. And you don't have to second guess whether they will be have decimal places either.

Any specified thousands separator {:,f} will likewise only have effect if it turns out that the number is a large integer instead of a long decimal.

Proviso

Decimal(), however, has this idea of significant places, by which it will add trailing zeros if it thinks you want them.

The idea is that it intelligently handles situations where you might be dealing with currency digits such as £ 10.15. To use the example from the documentation:

>>> decimal.Decimal('1.30') +  decimal.Decimal('1.20')
Decimal('2.50')

It makes no difference if you format the Decimal() - you still get the trailing zero if the Decimal() deems it to be significant:

>>> "{:,f}".format( decimal.Decimal('1.30') +  decimal.Decimal('1.20'))
'2.50'

The same thing happens (perhaps for some good reason?) when you treat thousands and fractions together:

>>> decimal.Decimal(2500) * decimal.Decimal('0.001')
Decimal('2.500')

Remove significant trailing zeros with the Decimal().normalize() method:

>>> (2500 * decimal.Decimal('0.001')).normalize()
Decimal('2.5')
Scherzo answered 8/6, 2021 at 9:1 Comment(0)
D
0

I have written a function to format such numbers without the usage of scientific notation.

Here it is:

from decimal import Decimal


def numfmt(d: Decimal) -> str:
    sign, digits, exponent = d.as_tuple()
    assert exponent <= 0
    sign_str = '-' if sign else ''
    integer_len = len(digits) + exponent
    integer: tuple[int, ...] = digits[:integer_len] or (0, )
    fraction: tuple[int, ...] = digits[exponent:]
    integer_str = ''.join(map(str, integer))
    res = f"{sign_str}{integer_str}"
    if fraction:
        fraction_str = ''.join(map(str, fraction))
        fraction_str = fraction_str.zfill(abs(exponent))
        fraction_str = fraction_str.rstrip('0')
        if fraction_str:
            res = f"{res}.{fraction_str}"
    return res


if __name__ == "__main__":
    v = Decimal('1.0000000000000000000000000234') * (Decimal('10') ** -150)
    f = numfmt(v)
    v1 = Decimal(f)
    print(f)
    assert v == v1
    
    # Output: 0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000023


Dominic answered 15/4 at 10:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.