views:

197

answers:

6

I am writing a function to extract decimals from a number. Ignore the exception and its syntax, I am working on 2.5.2 (default Leopard version). My function does not yet handle 0's. My issue is, the function produces random errors with certain numbers, and I don't understand the reason. I will post an error readout after the code.


Function:

def extractDecimals(num):
    try:
     if(num > int(num)):
      decimals = num - int(num)
      while(decimals > int(decimals)):
       print 'decimal: ' + str(decimals)
       print 'int: ' + str(int(decimals))
       decimals *= 10
      decimals = int(decimals)
      return decimals
     else:
      raise DecimalError(num)
    except DecimalError, e:
     e.printErrorMessage()


Exception Class:

class DecimalError(Exception):
    def __init__(self, value):
     self.value = value

    def printErrorMessage(self):
     print 'The number, ' + str(self.value) + ', is not a decimal.'


Here is error output when I input the number 1.988:
decimal: 0.988
int: 0
decimal: 9.88
int: 9
decimal: 98.8
int: 98
decimal: 988.0
int: 987
decimal: 9880.0
int: 9879
decimal: 98800.0
int: 98799
decimal: 988000.0
int: 987999
decimal: 9880000.0
int: 9879999
decimal: 98800000.0
int: 98799999
decimal: 988000000.0
int: 987999999
decimal: 9880000000.0
int: 9879999999
decimal: 98800000000.0
int: 98799999999
decimal: 988000000000.0
int: 987999999999
decimal: 9.88e+12
int: 9879999999999
decimal: 9.88e+13
int: 98799999999999
decimal: 9.88e+14
int: 987999999999999
9879999999999998



I do not know why this error is popping up. Hopefully you guys can help me out.

+5  A: 

The problem is that (binary) floating point numbers aren't precisely representable as decimals. See http://stackoverflow.com/questions/1089018/why-cant-decimal-numbers-be-represented-exactly-in-binary for more information.

Ned Batchelder
Thanks for all of the help, everyone. There are definitely better ways that I could have gone about doing this program, as you guys pointed out, but I am less interested in the result and more interested in what I learn along the way. That's part of the reason why I don't use all of the built in capabilities of Python. Thanks for the answers.
dbmikus
+1  A: 

If your're just trying to split the number in to two parts:

n = map(int, str(num).split('.'))

Edit: Here are some more with timing.

import timeit

num = 3239.2929

def extract_a():
    return map(int, str(num).split('.'))

def extract_b():
    n = str(num).partition('.')
    integer = int(n[0])
    fractional = int(n[2]) if n[2] else 0
    return integer, fractional

def extract_c():
    i = int(num)
    f = int(str(num - i)[2:] or 0)
    return i, f

def extract_d():
    return int(num), num % 1

print 'extract_a time: %s' % timeit.timeit(extract_a)
print 'extract_b time: %s' % timeit.timeit(extract_b)
print 'extract_c time: %s' % timeit.timeit(extract_c)
print 'extract_d time: %s' % timeit.timeit(extract_d)

Output:

extract_a time: 5.98865621203
extract_b time: 5.20949334571
extract_c time: 4.01362424812
extract_d time: 0.753302766373
chris
+1  A: 

As Ned Batchelder said, not all decimals are exactly representable as floats. A float is represented by a certain number of binary digits which are used to approximate the decimal as closely as possible. You can never assume a float is exactly equal to a decimal.

In [49]: num
Out[49]: 1.988

In [50]: decimals=num - int(num)

In [51]: decimals
Out[51]: 0.98799999999999999

In [52]: print decimals   # Notice that print rounds the result, masking the inaccuracy.
0.988

See http://en.wikipedia.org/wiki/Floating%5Fpoint for more info on the binary representation of floats.

There are other ways to achieve you goal. Here is one way, using string operations:

def extractDecimals(num):
    try:
        numstr=str(num)
        return int(numstr[numstr.find('.')+1:])
    except ValueError, e:
        print 'The number, %s is not a decimal.'%num
unutbu
A: 

As others have said in their answers, arithmetic with floats doesn't always result in what you expect due to rounding errors. In this case, perhaps converting the float into a string and back is better?

In [1]: num = 1.988

In [2]: num_str = str(num)

In [3]: decimal = num_str.split('.')[1]

In [4]: decimal = int(decimal)

In [5]: decimal
Out[5]: 988
Mike Mazur
+1  A: 

As others have already pointed out, the issue you are seeing is due to the inexact representation of floating point numbers

Try your program with Python's Decimal

from decimal import Decimal
extractDecimals(Decimal("0.988"))
gnibbler
+1  A: 

As has already been said, floating point numbers are not exactly equal to decimals. You can see this by using the modulus operator like so:

>>> 0.988 % 1
0.98799999999999999
>>> 9.88 % 1
0.88000000000000078
>>> 98.8 % 1
0.79999999999999716

This gives the remainder of division by 1, or the decimal.

Sean