cs95
cs95

Reputation: 402962

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.

Upvotes: 6

Views: 2013

Answers (5)

The_Wonbats
The_Wonbats

Reputation: 1

can't you just subtract the integer?

number = float(input("Please type in a number: "))
i = int(number)
d = number - i
integer = (round(i,2))
decimal = (round(d,2))

print(integer)
print(decimal)

Upvotes: -1

Leon
Leon

Reputation: 32514

Here is a solution without string manipulation (frac_digits is the count of decimal digits that you can guarantee the fractional part of your numbers will fit into):

>>> def integer_and_fraction(x, frac_digits=3):
...     i = int(x)
...     c = 10**frac_digits
...     f = round(x*c-i*c)/c
...     return (i, f)
... 
>>> integer_and_fraction(1.2)
(1, 0.2)
>>> integer_and_fraction(1.2, 1)
(1, 0.2)
>>> integer_and_fraction(1.2, 2)
(1, 0.2)
>>> integer_and_fraction(1.2, 5)
(1, 0.2)
>>> 

Upvotes: 1

Raymond Hettinger
Raymond Hettinger

Reputation: 226624

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

Upvotes: 7

brian
brian

Reputation: 163

So I just did the following in a python terminal and it seemed to work properly...

x=1.2
s=str(x).split('.')
i=int(s[0])
d=int(s[1])/10

Upvotes: 0

silassales
silassales

Reputation: 36

You could try converting 1.2 to string, splitting on the '.' and then converting the two strings ("1" and "2") back to the format you want.

Additionally padding the second portion with a '0.' will give you a nice format.

Upvotes: 0

Related Questions