Source code for maelzel.rational

"""
Rational numbers with float-like repr

    >>> from fractions import Fraction
    >>> from maelzel.rational import Rat
    >>> import math
    >>> pifraction = Fraction.from_float(math.pi)
    >>> pifraction
    Fraction(884279719003555, 281474976710656)
    >>> pirational = Rat.from_float(math.pi)
    >>> pirational
    3.1415927
    >>> pirational == pifraction
    True

To check types, always check against the abstract class. This makes it easy to
use classes like Rat or quicktions.Fraction:

    >>> import numbers
    >>> isinstance(pirational, numbers.Rational)
    True
    >>> isinstance(pirational, Fraction)
    False

"""
from __future__ import annotations
from numbers import Rational
from typing import Tuple, Any

try:
    from quicktions import Fraction as _F
except ImportError:
    from fractions import Fraction as _F


def fractionToDecimal(numerator: int, denominator: int) -> str:
    """
    Converts a fraction to a decimal number with repeating period

    Args:
        numerator: the numerator of the fraction
        denominator: the denominator of the fraction

    Returns:
        the string representation of the resulting decimal. Any repeating
        period will be prefixed with '('

    Example
    ~~~~~~~

        >>> from emlib.mathlib import *
        >>> fraction_to_decimal(1, 3)
        '0.(3'
        >>> fraction_to_decimal(1, 7)
        '0.(142857'
        >>> fraction_to_decimal(100, 7)
        '14.(285714'
        >>> fraction_to_decimal(355, 113)
        '3.(1415929203539823008849557522123893805309734513274336283185840707964601769911504424778761061946902654867256637168'
    """
    result = [str(numerator//denominator) + "."]
    subresults = [numerator % denominator]
    numerator %= denominator
    while numerator != 0:
        numerator *= 10
        result_digit, numerator = divmod(numerator, denominator)
        result.append(str(result_digit))
        if numerator not in subresults:
            subresults.append(numerator)
        else:
            result.insert(subresults.index(numerator) + 1, "(")
            break
    return "".join(result)


[docs] class Rat(_F): """ Drop-in replacement to fractions.Fraction with float-like repr A rational number used to avoid rounding errors. A :class:`maelzel.rational.Rat` is a drop-in replacement for :class:`fractions.Fraction` with float-like repr. It can be used whenever a Fraction is used to avoid rounding errors, but its ``repr`` resembles that of a float If the package `quicktions` is installed (a fast implementation of Fraction in cython), it is used as a base class of Rat. For that, to test if a Rat is Fraction-like, avoid using isinstance(x, Fraction) but use:: >>> from numbers import Rational >>> from maelzel.rational import Rat >>> x = Rat(1, 3) >>> isinstance(x, Rational) True The same is valid when using type annotations:: import numbers def square(a: numbers.Rational) -> numbers.Rational: return a*a For all other aspects the documentation for python ``fractions.Fraction`` is valid for this implementation: https://docs.python.org/3/library/fractions.html """ _reprWithFraction = False "If True, add the fraction to the repr" _reprElipsisMaxDenominator = 9999 "A fraction with a denom. higher than this adds a ... to its float repr " _reprMaxDenominator = 99999999 "A fraction with a denom. higher than this is shown as ~num/den, when num/den is rounded" _reprShowRepeatingPeriod = False "If True, show the repeating period, if any" def __repr__(self): if self.denominator == 1: return str(self.numerator) if self.denominator > self._reprElipsisMaxDenominator: floatpart = f"{float(self):.8g}…" elif self._reprShowRepeatingPeriod: floatpart = fractionToDecimal(self.numerator, self.denominator) if self.denominator != 1: i, rest = floatpart.split(".") if len(rest) > 8: rest = rest[:8] + '…' floatpart = f'{i}.{rest}' else: floatpart = f"{float(self):.8g}" if not self._reprWithFraction: return floatpart if self.denominator > self._reprMaxDenominator: f = self.limit_denominator(self._reprMaxDenominator) return f'{floatpart} (~{f.numerator}/{f.denominator})' else: return f'{floatpart} ({self.numerator}/{self.denominator})' def __floordiv__(self, other: Any) -> int: r = _F.__floordiv__(self, other) return Rat(r.numerator, r.denominator) if isinstance(r, _F) else r def __format__(self, format_spec) -> str: if not format_spec: return self.__repr__() return float(self).__format__(format_spec) def __add__(self, other) -> Rat: r = _F.__add__(self, other) return Rat(r.numerator, r.denominator) if isinstance(r, _F) else r def __radd__(self, other) -> Rat: r = _F.__radd__(self, other) return Rat(r.numerator, r.denominator) if isinstance(r, _F) else r def __round__(self, ndigits: int=None) -> Rat: if ndigits is None: floor, remainder = divmod(self.numerator, self.denominator) if remainder*2<self.denominator: return floor elif remainder*2>self.denominator: return floor+1 # Deal with the half case: elif floor%2 == 0: return floor else: return floor+1 shift = 10**abs(ndigits) if ndigits > 0: return Rat(round(self*shift), shift) else: return Rat(round(self/shift)*shift) def __sub__(self, other) -> Rat: r = _F.__sub__(self, other) return Rat(r.numerator, r.denominator) if isinstance(r, _F) else r def __rsub__(self, other) -> Rat: r = _F.__rsub__(self, other) return Rat(r.numerator, r.denominator) if isinstance(r, _F) else r def __mul__(self, other) -> Rat: r = _F.__mul__(self, other) return Rat(r.numerator, r.denominator) if isinstance(r, _F) else r def __divmod__(self, other) -> Tuple[int, Rat]: a, b = _F.__divmod__(self, other) return (a, Rat(b)) def __float__(self) -> float: return self.numerator/self.denominator def __abs__(self) -> Rat: return Rat(abs(self.numerator), self.denominator) def __mod__(self, other) -> Rat: r = _F.__mod__(self, other) return Rat(r.numerator, r.denominator) if isinstance(r, _F) else r def __neg__(self) -> Rat: r = _F.__neg__(self) return Rat(r.numerator, r.denominator) def __pow__(self, other) -> Rat: r = _F.__pow__(self, other) return Rat(r.numerator, r.denominator) if isinstance(r, _F) else r def __rfloordiv__(self, other) -> Rat: r = _F.__rfloordiv__(self, other) return Rat(r.numerator, r.denominator) def __truediv__(self, other) -> Rat: r = _F.__truediv__(self, other) return Rat(r.numerator, r.denominator) if isinstance(r, _F) else r def __pos__(self) -> Rat: return Rat(_F.__pos__(self)) def __rmod__(self, other) -> Rat: r = _F.__rmod__(self, other) return Rat(r.numerator, r.denominator) if isinstance(r, _F) else r def __rmul__(self, other) -> Rat: r = _F.__rmul__(self, other) return Rat(r.numerator, r.denominator) if isinstance(r, _F) else r def __rpow__(self, other) -> Rat: r = _F.__rpow__(self, other) return Rat(r.numerator, r.denominator) if isinstance(r, _F) else r def __rtruediv__(self, other) -> Rat: r = _F.__rtruediv__(self, other) return Rat(r.numerator, r.denominator) if isinstance(r, _F) else r
[docs] @classmethod def from_float(cls, x: float) -> Rat: return cls(*x.as_integer_ratio())
[docs] def limit_denominator(self, max_denominator=1000000) -> Rat: r = _F.limit_denominator(self, max_denominator) return Rat(r.numerator, r.denominator)
def asRat(x) -> Rat: if isinstance(x, Rat): return x elif isinstance(x, Rational): return Rat(x.numerator, x.denominator) else: return Rat(x)