10

I want to write a function in Python 3 that converts fractions given as numerator and denominator into their string representation as decimal number, but with repeating decimal places in brackets.

An example:

  • convert(1, 4) should output "0.25"
  • convert(1, 3) should output "0.(3)" instead of "0.3333333333"
  • convert(7, 11) should output "0.(63)" instead of "0.6363636364"
  • convert(29. 12) should output "2.41(6)" instead of "2.4166666667"

My current code is at the end of the question, but it fails if there are non-repeating and repeating decimal places. Here's an example run including the debug output (commented print calls):

----> 29 / 12
5
appended 4
2
appended 1
8
index 2 ['29', 2, 8] result ['2.', '4', '(', '1']
repeating 8
['2.', '4', '(', '1', ')']

What am I doing wrong here?


My code:

def convert(numerator, denominator):
    #print("---->", numerator, "/", denominator)
    result = [str(numerator//denominator) + "."]
    subresults = [str(numerator)]
    numerator %= denominator
    while numerator != 0:
        #print(numerator)
        numerator *= 10
        result_digit, numerator = divmod(numerator, denominator)
        if numerator not in subresults:
            subresults.append(numerator)
            result.append(str(result_digit))
            #print("appended", result_digit)
        else:
            result.insert(subresults.index(numerator), "(")
            #print("index", subresults.index(numerator), subresults, "result", result)
            result.append(")")
            #print("repeating", numerator)
            break
    #print(result)
    return "".join(result)

4 Answers 4

4

Your code just needed some minor changes (see the comments below):

def convert(numerator, denominator):
    #print("---->", numerator, "/", denominator)
    result = [str(numerator//denominator) + "."]
    subresults = [numerator % denominator]          ### changed ###
    numerator %= denominator
    while numerator != 0:
        #print(numerator)
        numerator *= 10
        result_digit, numerator = divmod(numerator, denominator)
        result.append(str(result_digit))             ### moved before if-statement

        if numerator not in subresults:
            subresults.append(numerator)
            #print("appended", result_digit)

        else:
            result.insert(subresults.index(numerator) + 1, "(")   ### added '+ 1'
            #print("index", subresults.index(numerator), subresults, "result", result)
            result.append(")")
            #print("repeating", numerator)
            break
    #print(result)
    return "".join(result)
3

I believe what is wrong is that you should only be checking if the number of decimal places previously seen is the number of the length of the cycle and it was seen just previous to this length.

I think the best way to do this would be to use some good ol' math.

Lets try and devise a way to find the decimal representation of fractions and how to know when there will be repeating decimals.

The best way to know if a fraction will terminate (or repeat) is to look at the factorization (hard problem) of the denominator.

There are many ways to find the factorization, but what we really want to know is, does this number have a prime factor other than 2 or 5. Why? Well a decimal expansion is just some number a / 10 * b. maybe 1/2 = .5 = 5/10. 1/20 = .05 = 5/100. etc.

So the factors of 10 are 2 and 5, so we want to find out if it has any other factors other than 2 and 5. Perfect, thats easy, just keep dividing by 2 until it is not divisible by 2 anymore, than do the same with 5. Or the other way around.

First we may want to find out if it is divisible by 2 or 5 before we start doing some serious work.

def div_by_a_or_b( a, b, number):
    return not ( number % a ) or not ( number % b )

Then we divide out all the fives then all the twos and check if the number is 1

def powers_of_only_2_or_5(number):
    numbers_to_check = [ 2, 5 ]
    for n in numbers_to_check:
        while not number % n: # while it is still divisible by n
            number = number // n # divide it by n
    return number == 1 # if it is 1 then it was only divisble by the numbers in numbers_to_check

I made this a bit more polymorphic so you can change this around if you want to change the base. (all you need is the factors of that base, for instance in base 14 you check 2 and 7 instead of 2 and 5)

Now all thats left to do is find out what we do in the case of non-terminating/repeating fractions.

Now this is super number theory filled, so i'll leave you with the algorithm and let you decide if you want to find out more on mathforum.org or wolfram alpha

Now that we can easily tell if a fraction will terminate and if not, what will be the length of its cycle of repeating digits. Now all thats left to do is find the cycle or how many digits it will start in.

In my search for an efficient algorithm, I found this post on https://softwareengineering.stackexchange.com/ which should be helpful.

some great insight - "When a rational number m/n with (m,n)=1 is expanded, the period begins after s terms and has length t, where s and t are the smallest numbers satisfying

10^s=10^(s+t) (mod n). "

So all we need to do is find s and t:

def length_of_cycle(denominator):
    mods = {}
    for i in range(denominator):
        key = 10**i % denominator
        if key in mods:
            return [ mods[key], i ]
        else:
            mods[ key ] = i

Let's generate the numbers of the expansion

def expasionGenerator( numerator, denominator ):
    while numerator:
        yield numerator // denominator
        numerator = ( numerator % denominator ) * 10

Now be careful about using this as it will create an infinite loop in a repeating expansion (as it should ).

Now I think we have all the tools in place to write our function:

def the_expansion( numerator, denominator ):   
# will return a list of two elements, the first is the expansion 
# the second is the repeating digits afterwards
# the first element's first 
    integer_part = [ numerator // denominator ]
    numerator %= denominator
    if div_by_a_or_b( 2, 5, denominator ) and powers_of_only_2_or_5( denominator ):
        return [ integer_part, [ n for n in expasionGenerator( numerator, denominator ) ][1:], [0] ]
    # if it is not, then it is repeating
    from itertools import islice
    length_of_cycle = cycleLength( denominator )
    generator = expasionGenerator( numerator*10, denominator ) 
    # multiply by 10 since we want to skip the parts before the decimal place
    list_of_expansion = [ n for n in islice(generator, length_of_cycle[0]) ] 
    list_of_repeating = [ n for n in islice(generator, length_of_cycle[1]) ]
    return [ integer_part, list_of_expansion, list_of_repeating ] 

Now all thats left is to print it, that shouldn't be too bad. I am just going to build a function first that takes a list of numbers to a string:

def listOfNumbersToString(the_list):
    string = ""
    for n in the_list:
        string += str(n)
    return string

Then create the convert function:

def convert(numerator, denominator):
    expansion = the_expansion(numerator,denominator)
    expansion = [ listOfNumbersToString(ex) for ex in expansion ]
    return expansion[0] + "." + expansion[1] + "(" + expansion[2] + ")"

interesting read on the topic at http://thestarman.pcministry.com/ and a similar-ish question on stackoverflow

1

This doesn't answer your actual question ("why isn't my code working?") but maybe it will be useful to you anyway. A few months ago I wrote some code to do the same thing you're trying to do now. Here it is.

import itertools

#finds the first number in the sequence (9, 99, 999, 9999, ...) that is divisible by x.
def first_divisible_repunit(x):
    assert x%2 != 0 and x%5 != 0
    for i in itertools.count(1):
        repunit = int("9"*i)
        if repunit % x == 0:
            return repunit

#return information about the decimal representation of a rational number.
def form(numerator, denominator):    
    shift = 0
    for x in (10,2,5):
        while denominator % x == 0:
            denominator //= x
            numerator *= (10//x)
            shift += 1
    base = numerator // denominator
    numerator = numerator % denominator
    repunit = first_divisible_repunit(denominator)
    repeat_part = numerator * (repunit // denominator)
    repeat_size = len(str(repunit))
    decimal_part = base % (10**shift)
    integer_part = base // (10**shift)
    return integer_part, decimal_part, shift, repeat_part, repeat_size

def printable_form(n,d):
    integer_part, decimal_part, shift, repeat_part, repeat_size = form(n,d)
    s = str(integer_part)
    if not (decimal_part or repeat_part):
        return s
    s = s + "."
    if decimal_part or shift:
        s = s + "{:0{}}".format(decimal_part, shift)
    if repeat_part:
        s = s + "({:0{}})".format(repeat_part, repeat_size)
    return s

test_cases = [
    (1,4),
    (1,3),
    (7,11),
    (29, 12),
    (1, 9),
    (2, 3),
    (9, 11),
    (7, 12),
    (1, 81),
    (22, 7),
    (11, 23),
    (1,97),
    (5,6),
]

for n,d in test_cases:
    print("{} / {} == {}".format(n, d, printable_form(n,d)))

Result:

1 / 4 == 0.25
1 / 3 == 0.(3)
7 / 11 == 0.(63)
29 / 12 == 2.41(6)
1 / 9 == 0.(1)
2 / 3 == 0.(6)
9 / 11 == 0.(81)
7 / 12 == 0.58(3)
1 / 81 == 0.(012345679)
22 / 7 == 3.(142857)
11 / 23 == 0.(4782608695652173913043)
1 / 97 == 0.(0103092783505154639175257
73195876288659793814432989690721649484
536082474226804123711340206185567)
5 / 6 == 0.8(3)

I forget exactly how it works... I think I was trying to reverse-engineer the process for finding the fraction form of a number, given its repeating decimal, which is much easier than the other way around. For example:

x = 3.(142857)
1000000*x = 3142857.(142857)
999999*x = 1000000*x - x 
999999*x = 3142857.(142857) - 3.(142857)
999999*x = 3142854
x = 3142854 / 999999
x = 22 / 7

In theory, you can use the same approach going from fraction to decimal. The primary obstacle is that it's not completely trivial to turn an arbitrary fraction into something of the form "(some number) / (some amount of nines)". If your original denominator is divisible by 2 or 5, it can't evenly divide any 9-repunit. So a lot of form's work is about removing factors that would otherwise make it impossible to divide by 999...9.

4
  • Check your program for test_cases = [(3,12)] Commented Aug 10, 2018 at 15:05
  • Let's see... It gives 0.25 as expected when I run it in Python 2.7. In 3.X, I get 0.0.25.0. That's a problem. I'll see if I can make a version-agnostic approach.
    – Kevin
    Commented Aug 10, 2018 at 15:09
  • Only what you need to do is change / to // in lines 16 and 17 :) Commented Aug 10, 2018 at 15:11
  • Yep, agreed. The fact that I used // elsewhere suggests that I had tried from the outset to make it Python 3 compatible. Strange that I didn't apply it everywhere.
    – Kevin
    Commented Aug 10, 2018 at 15:12
0

The main idea is to find out the decimal place. In order word, where to put a decimal '.'

When a number is divided by 2 or 5, there is no recurring decimal. 1/2 = 0.5, 1/5 = 0.2. Only those are not 2 or not 5. eg. 3, 7, 11. How about 6? In fact, 6 is 2x3 where recurring decimal occurs due to the factor of 3. 1/6 = 1/2 - 1/3 = non recurring part + recurring part.

Take an other example 1/56. 56=8x7=2^3x7. Note that 1/56 = 1/7 - 1/8 = 1/7 - 1/2^3. There are 2 parts. The front part is 1/7 which is recurring 0.(142857), while the latter part 1/2^3 = 0.125 not recurring. However, 1/56 = 0.017(857142). 1/7 has recurring just after the '.' The recurring part for 1/56 is 3 decimal place later. This is because 0.125 has 3 decimal place and make it not recurring until 3 decimal place later. When we know where the recurring part starts, it is not hard to use long division to find out where the recurring's last digit.

Similar case for 5. Any fraction can have form like = a/2^m + b/5^n + recurring part. The recurring part is pushed rightward either by a/2^m or b/5^n. This is not hard to find out which ones push harder. Then we know where the recurring part starts.

For finding recurring decimal, we use long division. Since long divison will get the remainder, multiply the remainder by 10 and then use as a new nomerator and divide again. This process goes on and on. If the digit appear again. This is the end of the recurring.

Not the answer you're looking for? Browse other questions tagged or ask your own question.