Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to reliably separate decimal and floating parts from a number?

This is not a duplicate of this, I'll explain here.

Consider x = 1.2. I'd like to separate it out into 1 and 0.2. I've tried all these methods as outlined in the linked question:

In [370]: x = 1.2

In [371]: divmod(x, 1)
Out[371]: (1.0, 0.19999999999999996)

In [372]: math.modf(x)
Out[372]: (0.19999999999999996, 1.0)

In [373]: x - int(x)
Out[373]: 0.19999999999999996

In [374]: x - int(str(x).split('.')[0])
Out[374]: 0.19999999999999996

Nothing I try gives me exactly 1 and 0.2.

Is there any way to reliably convert a floating number to its decimal and floating point equivalents that is not hindered by the limitation of floating point representation?

I understand this might be due to the limitation of how the number is itself stored, so I'm open to any suggestion (like a package or otherwise) that overcomes this.

Edit: Would prefer a way that didn't involve string manipulation, if possible.

like image 337
cs95 Avatar asked Jul 19 '17 15:07

cs95


1 Answers

Solution

It may seem like a hack, but you could separate the string form (actually repr) and convert it back to ints and floats:

In [1]: x = 1.2

In [2]: s = repr(x)

In [3]: p, q = s.split('.')

In [4]: int(p)
Out[4]: 1

In [5]: float('.' + q)
Out[5]: 0.2

How it works

The reason for approaching it this way is that the internal algorithm for displaying 1.2 is very sophisticated (a fast variant of David Gay's algorithm). It works hard to show the shortest of the possible representations of numbers that cannot be represented exactly. By splitting the repr form, you're taking advantage of that algorithm.

Internally, the value entered as 1.2 is stored as the binary fraction, 5404319552844595 / 4503599627370496 which is actually equal to 1.1999999999999999555910790149937383830547332763671875. The Gay algorithm is used to display this as the string 1.2. The split then reliably extracts the integer portion.

In [6]: from decimal import Decimal

In [7]: Decimal(1.2)
Out[7]: Decimal('1.1999999999999999555910790149937383830547332763671875')

In [8]: (1.2).as_integer_ratio()
Out[8]: (5404319552844595, 4503599627370496)

Rationale and problem analysis

As stated, your problem roughly translates to "I want to split the integral and fractional parts of the number as it appears visually rather that according to how it is actually stored".

Framed that way, it is clear that the solution involves parsing how it is displayed visually. While it make feel like a hack, this is the most direct way to take advantage of the very sophisticated display algorithms and actually match what you see.

This way may the only reliable way to match what you see unless you manually reproduce the internal display algorithms.

Failure of alternatives

If you want to stay in realm of integers, you could try rounding and subtraction but that would give you an unexpected value for the floating point portion:

In [9]: round(x)
Out[9]: 1.0

In [10]: x - round(x)
Out[10]: 0.19999999999999996
like image 143
Raymond Hettinger Avatar answered Nov 08 '22 21:11

Raymond Hettinger