RGS
RGS

Reputation: 981

Float to Fraction conversion : recognizing cyclic decimals

I am currently implementing a Fraction class for training my OOP skills, and I have got to a problem... In my class, you are able to do math operations between:

And I want to add both:

Since when I work with integers, what I do is to make them a Fraction with denominator 1, I want to, when operating with float, creating a Fraction that represents that given float. And that is where I have my problem.

Firstly, the minimum code required to understand my Fraction class:

class Fraction(object):
    def __init__(self,num,den=1,reduce=True):
        # only accept integers or convertable strings
        if not(type(num) == int and type(den) == int):
            if type(num) == str:
                try:
                    num = int(num)
                except ValueError:
                    raise RuntimeError("You can only pass to the numerator and \
denominator integers or integer convertable strings!")
            else:
                raise RuntimeError("You can only pass to the numerator and \
denominator integers or integer convertable strings!")
            if type(den) == str:
                try:
                    den = int(den)
                except ValueError:
                    raise RuntimeError("You can only pass to the numerator and \
denominator integers or integer convertable strings!")
            else:
                raise RuntimeError("You can only pass to the numerator and \
denominator integers or integer convertable strings!")
        # don't accept fractions with denominator 0
        if den == 0:
            raise ZeroDivisionError("The denominator must not be 0")
        # if both num and den are negative, flip both
        if num < 0 and den < 0:
            num = abs(num)
            den = abs(num)
        # if only the den is negative, change the "-" to the numerator
        elif den < 0:
            num *= -1
            den = abs(den)
        self.num = num
        self.den = den
        # the self.auto is a variable that will tell us if we are supposed to
        #automatically reduce the Fraction to its lower terms. when doing some
        #maths, if either one of the fractions has self.auto==False, the result
        #will also have self.auto==False
        self.auto = reduce
        if self.auto:
            self.reduce()

    def float_to_fraction(f):
        '''should not be called by an instance of a Fraction, since it does not\
accept, purposedly, the "self" argument. Instead, call it as\
Fraction.float_to_fraction to create a new Fraction with a given float'''
        # Start by making the number a string
        f = str(f)
        exp = ""
        # If the number has an exponent (is multiplied by 10 to the power of sth
        #store it for later.
        if "e" in f:
            # Get the "e+N" or "e-N"
            exp = f[f.index("e"):]
            # Slice the exponent from the string
            f = f[:f.index("e")]
        # Start the numerator and the denominator
        num = "0"
        den = "1"
        # Variable to check if we passed a ".", marking the decimal part of a
        #number
        decimal = False
        for char in f:
            if char != ".":
                # Add the current char to the numerator
                num += char
                if decimal:
                    # If we are to the right of the ".", also add a 0 to the
                    #denominator to keep proportion
                    den += "0"
                # Slice parsed character
                f = f[1:]
            if char == ".":
                # Tell the function we are now going to the decimal part of the
                #float.
                decimal = True
                # Slice the "."
                f = f[1:]
        # Parse the exponent, if there is one
        if exp != "":
            # If it is a negative exponent, we should make the denominator bigger
            if exp[1] == "-":
                # Add as many 0s to the den as the absolute value of what is to
                #the right of the "-" sign. e.g.: if exp = "e-12", add 12 zeros
                den += "0"*int(exp[2:])
            # Same stuff as above, but in the numerator
            if exp[1] == "+":
                num += "0"*int(exp[2:])
        # Last, return the Fraction object with the parsed num and den!
        return Fraction(int(num),int(den))

My float_to_fraction() function converts, 100% accurately, a given float to a Fraction. But as I remember from my math classes a cyclic decimal with a n-digit long cycle, like 0.123123123123... or 0.(123) can be written in the form of a fraction with numerator = cycle and denominator = (as many 9s as the length of the cycle):

123/999 = 0.(123) 3/9 (=1/3) = 0.(3); 142857/999999 (=1/7) = 0.(142857) etc, etc...

But with this implementation, if I pass to the float_to_fraction() an argument like 1/3, it will parse "0.3333333333333333" which is finite, returning this Fraction: 3333333333333333/10000000000000000. It IS accurate, since I passed to the function a finite number! How can I implement, in this function, a way of recognizing cyclic decimals, so I can return, instead of a Fraction with a denominator = 10^n, a denominator with loads of 9s.

Upvotes: 0

Views: 360

Answers (1)

Lutz Lehmann
Lutz Lehmann

Reputation: 26040

The best way to convert a decimal expression into an approximating rational is via the continued fraction expansion.

For x=0.123123123123 this results in

r=x, 
a[0]=floor(r)=0, r=r-a[0], r=1/r=8.121951219520,
a[1]=floor(r)=8, r=r-a[1], r=1/r=8.199999999453,
a[2]=floor(r)=8, r=r-a[2], r=1/r=5.000000013653,
a[3]=floor(r)=5, r=r-a[3], r=1/r=73243975.48780,

And at this point r-a[3]<1e-5 the iteration stops. The rational approximation found is

x=1/(8+1/(8+1/5))=1/(8+5/41)=41/333 (=123/999)

The intermediate convergents are

1/8=0.125,      x- 1/8   = -0.001876876877,
1/(8+1/8)=8/65, x- 8/65  =  0.0000462000460,
41/333,         x-41/333 = -1.231231231231e-13.

Upvotes: 0

Related Questions