5

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.

Raymond Hettinger
  • 216,523
  • 63
  • 388
  • 485
cs95
  • 379,657
  • 97
  • 704
  • 746
  • 4
    But `1.2` cannot be represented correctly. It is simply formatting that writes it like `1.2`. In fact the number is `1.19999....`. – Willem Van Onsem Jul 19 '17 at 15:12
  • 1
    @WillemVanOnsem I'm open to anything that allows this to become possible... Maybe a new python package :) – cs95 Jul 19 '17 at 15:13
  • Well there are of course packages that allow to perform calculations using the decimal number format, or that work lazily and thus can evaluate with arbitrary precision, etc. But python floats work according to IEEE-574 specs. And `0.2` cannot be represented exactly in such format. – Willem Van Onsem Jul 19 '17 at 15:15
  • @WillemVanOnsem Yes... open to suggestions! – cs95 Jul 19 '17 at 15:18
  • Do you have any guarantees about the count of digits in the fractional part of your numbers? – Leon Jul 19 '17 at 15:39
  • @Leon Nah dude, I'm looking for a general solution, – cs95 Jul 19 '17 at 16:00
  • 1
    @cᴏʟᴅsᴘᴇᴇᴅ Then the problem is ill-defined. Do you still want to be able to exactly separate the fractional part from this number: 1.23456789 ? Note that the larger the integer part of your number the more inaccuracy is present in its fractional part. – Leon Jul 19 '17 at 16:09
  • @Leon you have a point there. I suppose anything to an accuracy of 2 digits is fine. – cs95 Jul 19 '17 at 16:21
  • @cᴏʟᴅsᴘᴇᴇᴅ Then [my answer](https://stackoverflow.com/a/45195413/6394138) should work for you. – Leon Jul 19 '17 at 16:23

4 Answers4

7

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
Raymond Hettinger
  • 216,523
  • 63
  • 388
  • 485
  • I knew something could be done with strings. But like you said, it's hacky, and dirty. I'll see if there's a method that doesn't involve string manipulation, otherwise I suppose this would be considered the only way. – cs95 Jul 19 '17 at 15:23
  • You have me there. Although, to give you a bit of context, I wanted to extend my answer to [this](https://stackoverflow.com/questions/45164645/how-to-separate-a-number-odd-or-even-into-two-integer-parts/45164689#45164689) question. Tried to replicate with floats and hit upon this. Wanted to see if there was a simple python solution before implementing it in JS. – cs95 Jul 19 '17 at 15:35
  • @Coldspeed For JS, you can still do a string conversion to a fixed number of decimal places and take advantage of its internal rounding. It won't be as perfect as the Python solution but should be pretty reliable overall. Use ``toFixed(2)``. – Raymond Hettinger Jul 19 '17 at 15:44
  • @Coldspeed I moved the earlier comment into the "rationale" section of the answer. You had asked for "reliable" way. I think this may be the *only* reliable way that exactly reproduces what you see visually (unless you painfully implement the Gay algorithm or equivalent manually). If you agree, please mark this answer as accepted. – Raymond Hettinger Jul 19 '17 at 15:47
  • @RaymondHettinger I provided a [working alternative](https://stackoverflow.com/a/45195413/6394138) – Leon Jul 19 '17 at 15:54
  • I appreciate the thought and effort put into this answer. Thank you. Let me wait a bit more to see if there's any other ways. – cs95 Jul 19 '17 at 16:02
1

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)
>>> 
Leon
  • 31,443
  • 4
  • 72
  • 97
  • Are you sure this works for all cases? I suspect there are still fractional components that will have rounded to an integer power of ten but will still display oddly. – Raymond Hettinger Jul 19 '17 at 19:03
  • @RaymondHettinger For a correctly set `frac_digits` setting my method is identical to yours, with the difference that the computation is done without involving strings. You separate the fractional part as a string, I do it numerically (but require a hint about the count of digits in the fractional part) – Leon Jul 20 '17 at 06:29
  • In my opinion, the biggest downside of this approach, generally speaking, is knowing (or perhaps having to determine) the value of `frac_digits`. This is easy in OP's case where an explicit value decimal was assigned, but in the real world that's probably not going to be how it would be used. – martineau Jun 03 '18 at 20:33
0

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.

silassales
  • 36
  • 8
0

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
brian
  • 155
  • 1
  • 7
  • I now realize that dividing by 10 will only work when there is one digit behind the decimal so you could either divide by 10^(length of d) or pad the string with a '0.' at the front then convert to an int – brian Jul 19 '17 at 15:22