Skip to content

Instantly share code, notes, and snippets.

@OrangeChannel
Created November 8, 2020 23:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save OrangeChannel/b2563c0bd266700f91927c3fbc386e5b to your computer and use it in GitHub Desktop.
Save OrangeChannel/b2563c0bd266700f91927c3fbc386e5b to your computer and use it in GitHub Desktop.
Function that extends the standard library Fraction constructor behavior to accept non-rational values.
import math
import fractions
import numbers
def Frac(
numerator: numbers.Real,
denominator: numbers.Real = 1,
frac_acc: int = 100,
places: int = 9,
):
"""Extends fractions.Fraction to accept *close* to rational instances for both params.
Useful for when a function or formula returning a float might actually be able to return a rational number,
but the possibility is lost due to the limits of binary representation.
Also useful for needing to reduce ratios between two decimal numbers that do not result in an integer
(i.e. .105/.03)
NOTE: ``fractions.Fraction(0.6/0.3)`` already will return the expected ``Fraction(2, 1)``,
but the same can not be said for other decimal ratios.
If able to be cast directly into a fraction, will skip over the `frac_acc` and `places` parameters.
NOTE: This only covers cases when both the numerator and denominator are rational instances.
This means that even though ``fractions.Fraction(3.14)`` is valid, ``Frac(3.14)`` will not return the same
value (see examples).
If able to be cast directly into a `Fraction`, will skip over the `frac_acc` and `places` parameters:
>>> Frac(16, 9) # Fraction(16, 9)
>>> Frac(123456789, 234567890, frac_acc=10, places=5) # Fraction(123456789, 234567890)
>>> Frac(6.0000000004) # Fraction(6, 1)
>>> Frac(5.9999999994) # Fraction(6, 1)
The `places` param can be used to more aggresively round.
>>> Frac(4.9999) # Fraction(49999, 1000)
>>> Frac(4.9999, places=3) # Fraction(5, 1)
>>> Frac(1.08, .04) # Fraction(27, 1)
>>> Frac(3, 0.054) # Fraction(500, 9)
>>> Frac(7.571428571428571, 1.99999) # Fraction(5300000, 1399993)
>>> Frac(7.571428571428571, 1.99999, places=4) # Fraction(53, 14)
For simple psuedo-rational numbers that can't be represented rationally directly due to binary limitations,
this function returns the value that could be achieved normally with ``fractions.Fraction().limit_denominator()``:
>>> fractions.Fraction(3.14) # Fraction(7070651414971679, 2251799813685248)
>>> fractions.Fraction(3.14).limit_denominator(100) # Fraction(157, 50)
>>> Frac(3.14) # Fraction(157, 50)
:param frac_acc: The largest denominator compared against when trying to reduce.
Default of 100 will recognize common prime denominator fractions like x/7, x/13, x/97.
:param places: The limit of decimal places to attempt to rationalize with when rounding.
Can be lowered to more aggresively round (i.e. Frac(4.9999, places=3)).
"""
if all(isinstance(i, numbers.Rational) for i in [numerator, denominator]):
return fractions.Fraction(numerator, denominator)
def round_ext(x: numbers.Number):
if isinstance(x, numbers.Rational):
return x
integer_part = math.floor(x)
dec_part = x - integer_part
if math.isclose(dec_part, 0, rel_tol=10 ** -places):
return integer_part
elif math.isclose(dec_part, 1, rel_tol=10 ** -places):
return integer_part + 1
else:
test_frac = fractions.Fraction(
round(dec_part * 10 ** places), int(10 ** places)
)
if math.isclose(dec_part, test_frac.limit_denominator(frac_acc)):
return integer_part + test_frac.limit_denominator(frac_acc)
else:
return integer_part + test_frac
return fractions.Fraction(round_ext(numerator), round_ext(denominator))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment