0

I have checked dozens of other questions on stackoverflow about rounding floats in Python. I have learned a lot about how computers store these numbers, which is the root cause of the issue. However, I have not found a solution for my situation.

Imagine the following situation:

amount = 0.053
withdraw = 0.00547849

print(amount - withdraw)

>>> 0.047521509999999996

Here, I would actually like to receive 0.04752151, so a more rounded version. However, I do not know the number of decimals these numbers should be rounded upon.

If I knew the number of decimals, I could do the following:

num_of_decimals = 8
round((amount - withdraw),num_of_decimals)
>>> 0.04752151

However, I do not know this parameter num_of_decimals. It could be 8,5,10,2,..

Any advice?

6
  • 1
    You might find success with docs.python.org/3/library/decimal.html Commented Aug 21, 2022 at 15:33
  • 6
    Don't use floats for money. Commented Aug 21, 2022 at 15:33
  • Use Decimal: from decimal import Decimal; amount = Decimal(0.053) Commented Aug 21, 2022 at 15:53
  • if you know how floats are stored, you know that's not the correct thing to store the information you want to store Commented Aug 21, 2022 at 16:24
  • 1
    But as is, the question cannot be properly answered without that sort of information. Commented Aug 23, 2022 at 9:00

3 Answers 3

2

When decimals are important, then Python offers a decimal module that works best with correct rounding.

from decimal import Decimal

amount = Decimal(0.053)
withdraw = Decimal(0.00547849)

print(amount - withdraw)

# 0.04752150999999999857886789911

num_of_decimals = 8
print(round(amount - withdraw,num_of_decimals))
# 0.04752151

Update: Get number of decimal

num_of_decimals = (amount - withdraw).as_tuple().exponent * -1

#or find
num_of_decimals = str(amount - withdraw)[::-1].find('.')

Instead of round

from decimal import getcontext

...

getcontext().prec = num_of_decimals

print(amount - withdraw)
# 0.047521510

See more decimal documentation

Sign up to request clarification or add additional context in comments.

4 Comments

Hi, I will take a look at the decimal module. But, the problems still remains, how do I know the num_of_decimals value. Because I do not know that up front, all the numbers in my data could have different lenghts. Some could be 0.01233333334 or 0.0120001, etc.
just use Decimal with string input, to have exact value, skipping floats entirely
Let me rephrase it a bit. This num_of_decimals is just for explanation purposes, it is not something I know. So in the data i have, i only have a list of the amounts and withdraws, where each of them have different number of decimals. So I have to round the amount-withdraw to a decimal number n, which is unknown. But i think that the answer of @rolf_of_saxony works!
Updated to get decimal len
2

Use decimal with string inputs:

from decimal import Decimal

amount = Decimal("0.053")
withdraw = Decimal("0.00547849")

print(amount - withdraw)

# 0.04752151

Avoid creating floats entirely, and avoid the rounding issues.

If you end up with numbers small enough, and the default representation is now something like 1.2345765645033E-8, you can always get back to decimal notation in your representation:

>>> withdraw = Decimal("0.000000012345765645033")
>>> '{0:f}'.format(withdraw)
'0.000000012345765645033'

Comments

0

Concentrating purely on the number of decimal places, something simple, like converting the number to a string might help.

>>> x = 0.00547849
>>> len(str(x).split('.')[1])
8
>>> x = 103.323
>>> len(str(x).split('.')[1])
3

It appears that I've offended the gods by offering a dirty but practical solution but here goes nothing, digging myself deeper.

If the number drops into scientific notation via the str function you won't get an accurate answer. Picking the bones out of '23456e-05' isn't simple. Sadly, the decimal solution also falls at this hurdle, once the number becomes small enough e.g.

>>> withdraw = decimal.Decimal('0.000000123456')
>>> withdraw
Decimal('1.23456E-7')

Also it's not clear how you'd go about getting the string for input into the decimal function, because if you pump in a float, the result can be bonkers, not to put too fine a point on it.

>>> withdraw = decimal.Decimal(0.000000123456)
>>> withdraw
Decimal('1.23455999999999990576323642514633416311653490993194282054901123046875E-7')

Perhaps I'm just displaying my ignorance.

So roll the dice, as to your preferred solution, being aware of the caveats.
It's a bit of a minefield.

Last attempt at making this even more hacky, whilst covering the bases, with a default mantissa length, in case it all goes horribly wrong:

>>> def mantissa(n):
...     res_type = "Ok"
...     try:
...         x = str(n).split('.')[1]
...         if x.isnumeric():
...             res = len(x)
...         else:
...             res_type = "Scientific notation "+x
...             res = 4
...     except Exception as e:
...         res_type = "Scientific notation "+str(n)
...         res = 4
...     return res, res_type
... 
>>> print(mantissa(0.11))
(2, 'Ok')
>>> print(mantissa(0.0001))
(4, 'Ok')
>>> print(mantissa(0.0001234567890))
(12, 'Ok')
>>> print(mantissa(0.0001234567890123456789))
(20, 'Ok')
>>> print(mantissa(0.00001))
(4, 'Scientific notation 1e-05')
>>> print(mantissa(0.0000123456789))
(4, 'Scientific notation 23456789e-05')
>>> print(mantissa(0.0010123456789))
(13, 'Ok')

9 Comments

OP specifically said "I do not know this parameter num_of_decimals", otherwise they would use the built-in round rather than using strings for that purpose
hey this indeed works! It is kind of a different approach but I tried it with a few examples and it does the trick!
Yes, but if I have that piece of information I can round the float on that, giving me the correct answer.
Note that this gives subtly wrong results in cases like x = 0.0000123456. In that case there are ten digits following the decimal point, but len(str(x).split(".")[1]) gives 9. And for x = 0.00001 we get an IndexError.
"Sadly, the decimal solution also falls at this hurdle, once the number becomes small enough". No. Only the default representation may look confusing because it is displayed in exponent notation. '{0:f}'.format(withdraw) gives you the full representation will all the correct zeros.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.