Separation of Concerns
Your function is doing too much. It is:
- Converting the float to a fraction
- Converting the fraction to a string of digits
- Printing the string of digits
You should separate these.
Float to Fraction
First, let's move the float-to-fraction code into its own function.
from typing import Tuple
def float_to_fraction(value: float) -> Tuple[int, int]:
"""
Convert a floating point number in the range [0, 1) into a fraction.
>>> float_to_fraction(0.625)
(5, 8)
>>> float_to_fraction(0.1)
(3602879701896397, 36028797018963968)
"""
if not (0 <= value < 1):
raise ValueError("Value out of range (0 <= value < 1)")
numerator, denominator = 0, 1
while value:
value *= 2
numerator *= 2
denominator *= 2
if value >= 1:
value -= 1
numerator += 1
return numerator, denominator
Here, I've added:
- type hints, describing the input and output types for the function,
- a
"""docstring""" describing how to use the function,
- including an example formatted for use with the
doctest module,
- input range validation, since your expectation is for the range to be within a narrow range of
float values.
I've also changed num to numerator, since it would be easy to misinterpret the abbreviation to mean number. Similarity, den became denominator and x became value.
Fraction to String
Again, converting a numerator/denominator fraction into a series of digits is logical unit of code, which could be reused elsewhere, so I've made it into a function:
def fraction_to_string(numerator: int, denominator: int) -> str:
"""
Convert a proper fraction into a corresponding series of digits.
>>> fraction_to_string(5, 8)
'0.625'
"""
if not (0 <= numerator < denominator):
raise ValueError("Improper or negative fraction given")
if denominator & (denominator - 1) != 0:
raise ValueError("Denominator must be a power of 2")
digits = '0.'
while numerator:
numerator *= 10
digits += str(numerator // denominator)
numerator %= denominator
return digits
Note: str(0.0) returns '0.0' but your code (and my duplication of it here) returns '0.'.
Your code originally guaranteed the denominator was a power of 2. With this new function, a caller could ask for fraction_to_digits(1, 3) which would be an infinitely long string of digits, so I've added a condition restricting the denominator to a power of 2.
Float to String
Now we can compose these two functions, as well as recreate the original function's functionality:
def float_to_string(value: float) -> str:
numerator, denominator = float_to_fraction(value)
return fraction_to_string(numerator, denominator)
def print_float(x):
print(float_to_string(x))
if __name__ == '__main__':
import doctest
doctest.testmod(verbose=True)
from random import random
for _ in range(3):
x = random()
print(f'{x}:')
print(('%.2000f' % x).rstrip('0'))
print_float(x)
print()
Additionally, I've added a "main guard" (always a good idea), and I've added a call to doctest.testmod() so the tests embedded in the """docstrings""" are executed.
Notes
The function float_to_fraction(value) may be replaced by the built-in function value.as_integer_ratio().
>>> value = 0.625
>>> value.as_integer_ratio()
(5, 8)