Created
November 8, 2020 23:52
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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