1

I know this has been asked numerous times and I've come across many blogs and SO answers but this one's making me pull my hair out. I just want to multiply a two decimal number by 100 so I get rid of its decimals:

>>> 4321.90 * 100
432189.99999999994
>>> Decimal(4321.90) * Decimal(100)
Decimal('432189.9999999999636202119291')

I'm scared to use rounding for such seemingly trivial operation. Would it be safe? What if the precision problem plays tricks on me and the result is close to xxx.5? Can that happen? I do understand the problem at the binary level, but I come from C# and I don't have that problem with .Net's decimal type:

decimal x = 4321.90m;
decimal y = 100m;
Console.WriteLine(x * y);
432190,00

I thought Python's decimal module was supposed to fix that. I'm about to convert the initial value to string and do the math with string manipulations, and I feel bad about it...

Jerther
  • 5,558
  • 8
  • 40
  • 59
  • 3
    The literal `4321.90` is a binary float no matter what context it appears in. So you're not passing the decimal value you _think_ you're passing to the `Decimal()` constructor. Spell it `Decimal("4321.90")` instead. – Tim Peters Jun 28 '17 at 21:00
  • if it always is a two decimal place number convert it to string and replace the dot '.' with nothing. – Ma0 Jun 28 '17 at 21:00
  • @Ev.Kounis this is string manipulation, and I'd rather not do this. – Jerther Jun 29 '17 at 12:05
  • The question which this one is the duplicate of, answers the underlying problem I'm facing here indeed. However, this question is what made me realize there is actually a difference between Decimal(4321.90) and Decimal('4321.90'), which then leads to said question. – Jerther Jun 29 '17 at 12:20

3 Answers3

4

The main reason it fails with Python is because 4321.90 is interpreted as float (you lose precision at that point) and then casted to Decimal at runtime. With C# 4321.90m is interpreted as decimal to begin with. Python simply doesn't support decimals as a built-in structure.

But there's an easy way to fix that with Python. Simply use strings:

>>> Decimal('4321.90') * Decimal('100')
Decimal('432190.00')
freakish
  • 54,167
  • 9
  • 132
  • 169
  • So in all cases, the initial value should be a string, then passed to Decimal(), whether it comes from a user input, a database query or is hard coded. Right? (btw I like your answer. Clear and concise) – Jerther Jun 29 '17 at 12:09
  • 1
    @Jerther Exactly, to safely (or rather precisely) initialize `Decimal` you need strings. – freakish Jun 29 '17 at 12:15
  • There's also Decimal(repr(4321.90)) that does the trick pretty well. https://stackoverflow.com/a/18886013/2498426 – Jerther Aug 22 '17 at 15:54
  • 1
    @Jerther No, it does not: `repr(11.000000000000001)` is `'11.000000000000002'`. It suffers from the same problem. The float inside `repr` is parsed and then rounded to the nearest floating point number. Then repr is called on that number which at that point is different from the original one. – freakish Aug 23 '17 at 08:08
  • right, sorry. And from what I gather, str() has yet another behavior. – Jerther Aug 24 '17 at 16:00
2

I'm about to convert the initial value to string

Yes! (but don't do it by calling str - use a string literal)

and do the math with string manipulations

No!

When hardcoding a decimal value into your source code, you should initialize it from a string literal, not a float literal. With 4321.90, floating-point rounding has already occurred, and building a Decimal won't undo that. With "4321.90", Decimal has the original text you wrote available to perform an exact initialization:

Decimal('4321.90')
user2357112
  • 260,549
  • 28
  • 431
  • 505
1

Floating point inaccuracy again.

Decimal(number) doesn't change a thing: the value is modified before it hits Decimal.

You can avoid that by passing strings to Decimal, though:

Decimal("4321.90") * Decimal("100")

result:

Decimal('432190.00')

(so Decimal handles the floating point numbers without using the floating point registers & operations at all)

Jean-François Fabre
  • 137,073
  • 23
  • 153
  • 219