Source code for continuedfractions.continuedfraction

from __future__ import annotations


__all__ = [
    'ContinuedFraction',
]


# -- IMPORTS --

# -- Standard libraries --
import collections
import decimal
import math
import statistics
import typing

from decimal import Decimal
from fractions import Fraction

# -- 3rd party libraries --

# -- Internal libraries --
from continuedfractions.lib import (
    continued_fraction_rational,
    convergent,
    convergents,
    fraction_from_coefficients,
    left_mediant,
    mediant,
    remainder,
    remainders,
    right_mediant,
)


[docs] class ContinuedFraction(Fraction): """An object-oriented representation of a (finite) simple continued fraction. An implementation of simple continued fractions as Python objects and instances of the standard library :py:class:`fractions.Fraction` class, with various properties for the continued fraction, including its coefficients, the order, convergents, and remainders. The term "simple continued fraction" denotes a specific type of continued fraction where the fractional terms only have numerators of :math:`1`. Arguments can be any which are valid for creating objects of the :py:class:`fractions.Fraction` superclass. For clarification, valid arguments can be one of the following: * a single instance of :py:class:`numbers.Rational`, including :py:class:`int`, :py:class:`fractions.Fraction` or :py:class:`ContinuedFraction`, named or unnamed * a pair of :py:class:`numbers.Rational` instances, including :py:class:`int`, :py:class:`fractions.Fraction` and :py:class:`ContinuedFraction`, named or unnamed * a single :py:class:`float` or :py:class:`decimal.Decimal` value that is not a special value such as :py:data:`math.nan`, ``float('inf')``, or ``Decimal('infinity')`` * a single numeric valid string (:py:class:`str`) - validity is determined in the superclass by the :py:data:`fractions._RATIONAL_FORMAT` test """ @property def coefficients(self) -> typing.Generator[int, None, None]: """:py:class:`typing.Generator`: A generator of the (ordered) sequence of coefficients of the continued fraction. Examples -------- >>> cf = ContinuedFraction('.12345') >>> cf ContinuedFraction(2469, 20000) >>> tuple(cf.coefficients) (0, 8, 9, 1, 21, 1, 1, 5) """ yield from continued_fraction_rational(self) @property def order(self) -> int: """:py:class:`int`: The order of the continued fraction, which is the number of its coefficients minus :math:`1`. Examples -------- >>> cf = ContinuedFraction('.12345') >>> cf ContinuedFraction(2469, 20000) >>> tuple(cf.coefficients) (0, 8, 9, 1, 21, 1, 1, 5) >>> cf.order 7 """ return sum(1 for coeff in self.coefficients) - 1 @property def counter(self) -> collections.Counter: """:py:class:`collections.Counter` : A counter for the coefficients. Examples -------- >>> cf = ContinuedFraction(928374923, 8249234) >>> cf.counter Counter({1: 6, 2: 3, 24: 2, 112: 1, 5: 1, 3: 1}) """ return collections.Counter(self.coefficients) @property def khinchin_mean(self) -> decimal.Decimal | None: """:py:class:`decimal.Decimal` or :py:data:`None`: The Khinchin mean of the continued fraction, which is defined as the geometric mean of all the tail coefficients. We define the Khinchin mean :math:`K_n` of a (simple) continued fraction :math:`[a_0; a_1, a_2, \\ldots, a_n]` as: .. math:: K_n := \\sqrt[n]{a_1a_2 \\cdots a_n} = \\left( a_1a_2 \\cdots a_n \\right)^{\\frac{1}{n}}, \\hskip{3em} n \\geq 1 This property is intended to make it easier to study the limit of :math:`K_n` as :math:`n \\to \\infty`. In the special case of integers or fractions representing integers, whose continued fraction representations consist of only a single element, a null value is returned. Examples -------- Note that the default :py:mod:`decimal` context precision of :math:`28` is used in these examples. >>> tuple(ContinuedFraction(649, 200).coefficients) (3, 4, 12, 4) >>> ContinuedFraction(649, 200).khinchin_mean Decimal('5.76899828122963409526846589869819581508636474609375') >>> tuple(ContinuedFraction(415, 93).coefficients) (4, 2, 6, 7) >>> ContinuedFraction(415, 93).khinchin_mean Decimal('4.37951913988788898990378584130667150020599365234375') >>> tuple((ContinuedFraction(649, 200) + ContinuedFraction(415, 93)).coefficients) (7, 1, 2, 2, 2, 1, 1, 11, 1, 2, 12) >>> (ContinuedFraction(649, 200) + ContinuedFraction(415, 93)).khinchin_mean Decimal('2.15015313349074244086978069390170276165008544921875') >>> ContinuedFraction(5000).khinchin_mean """ coeffs = tuple(self.coefficients) if self.order == 1: return Decimal(coeffs[-1]) elif self.order > 1: return Decimal(statistics.geometric_mean(coeffs[1:]))
[docs] @classmethod def from_coefficients(cls, *coeffs: int) -> ContinuedFraction: """Returns a :py:class:`ContinuedFraction` instance from a sequence of (integer) coefficients of a (finite) simple continued fraction. Invalid coefficients will trigger a :py:class:`ValueError`. Parameters ---------- *coeffs : int An ordered sequence of integer coefficients of a (finite) simple continued fraction. Returns ------- ContinuedFraction A new and fully initialised instance of :py:class:`ContinuedFraction` with the given sequence of coefficients. Raises ------ ValueError If the given coefficients include non-integers, or where the tail segment contains non-positive integers. Examples -------- Constructing a continued fraction for the rational :math:`\\frac{649}{200}` using the sequence of coefficients :math:`3, 4, 12, 4`. >>> c1 = ContinuedFraction.from_coefficients(3, 4, 12, 4) >>> c1 ContinuedFraction(649, 200) Constructing the continued fraction of the (multiplicative) inverse :math:`\\frac{200}{649}` using the sequence of coefficients :math:`0, 3, 4, 12, 4`. >>> c2 = ContinuedFraction.from_coefficients(0, 3, 4, 12, 4) >>> c2 ContinuedFraction(200, 649) Validation of coefficients. >>> ContinuedFraction.from_coefficients('0', 1) Traceback (most recent call last): ... ValueError: Continued fraction coefficients must be integers, and all coefficients from the 1st onwards must be positive. >>> ContinuedFraction.from_coefficients(0, 1, 2.5) Traceback (most recent call last): ... ValueError: Continued fraction coefficients must be integers, and all coefficients from the 1st onwards must be positive. >>> ContinuedFraction.from_coefficients(1, 0) Traceback (most recent call last): ... ValueError: Continued fraction coefficients must be integers, and all coefficients from the 1st onwards must be positive. >>> ContinuedFraction.from_coefficients(1, -1) Traceback (most recent call last): ... ValueError: Continued fraction coefficients must be integers, and all coefficients from the 1st onwards must be positive. """ # Create a new ``ContinuedFraction`` instance from the given coefficients if any(not isinstance(coeff, int) or (coeff <= 0 and i > 0) for i, coeff in enumerate(coeffs)): raise ValueError( "Continued fraction coefficients must be integers, and all " "coefficients from the 1st onwards must be positive." ) # A step to ensure uniqueness of the simple form of the continued # fraction - if the last element is ``1`` it can be "removed" by # adding it to the second last element, thereby shortening the # sequence by one element. The resulting simple continued # fraction becomes unique for the number that is represented. if len(coeffs) > 1 and coeffs[-1] == 1: coeffs = coeffs[:-2] + (coeffs[-2] + 1,) # Call the superclass constructor with the ``fractions.Fraction`` # instance returned by ``lib.fraction_from_coefficients`` - this will # be the highest-order convergent of the simple continued # fraction represented by the given sequence of coefficients self = cls(fraction_from_coefficients(*coeffs)) return self
[docs] def extend(self, *new_coeffs: int) -> None: """Performs an in-place tail extension of the current sequence of coefficients. Raises a :py:class:`ValueError` if the given sequence of values is empty, or includes non-integers or non-positive integers. .. note:: As this method performs an in-place modification of the existing/ current instance the object ID remains the same. Parameters ---------- new_coeffs : int An (ordered) sequence of new (positive) integer coefficients by which the tail of the existing sequence of coefficients is to be extended. Raises ------ ValueError If the given sequence is empty or includes non-integers or non- positive integers. Examples -------- >>> cf = ContinuedFraction('3.245') >>> cf ContinuedFraction(649, 200) >>> tuple(cf.coefficients) (3, 4, 12, 4) >>> cf.order 3 >>> tuple(cf.convergents) ((0, ContinuedFraction(3, 1)), (1, ContinuedFraction(13, 4)), (2, ContinuedFraction(159, 49)), (3, ContinuedFraction(649, 200))) >>> tuple(cf.remainders) ((3, ContinuedFraction(4, 1)), (2, ContinuedFraction(49, 4)), (1, ContinuedFraction(200, 49)), (0, ContinuedFraction(649, 200))) >>> cf.extend(5, 2) >>> cf ContinuedFraction(7457, 2298) >>> tuple(cf.coefficients) (3, 4, 12, 4, 5, 2) >>> cf.order 5 >>> tuple(cf.convergents) ((0, ContinuedFraction(3, 1)), (1, ContinuedFraction(13, 4)), (2, ContinuedFraction(159, 49)), (3, ContinuedFraction(649, 200)), (4, ContinuedFraction(3404, 1049)), (5, ContinuedFraction(7457, 2298))) >>> tuple(cf.remainders) ((5, ContinuedFraction(2, 1)), (4, ContinuedFraction(11, 2)), (3, ContinuedFraction(46, 11)), (2, ContinuedFraction(563, 46)), (1, ContinuedFraction(2298, 563)), (0, ContinuedFraction(7457, 2298))) >>> cf = ContinuedFraction(649, 200) >>> cf.extend(0, 1) Traceback (most recent call last): ... ValueError: The coefficients to be added to the tail must be positive integers. >>> cf.extend(1, -1) Traceback (most recent call last): ... ValueError: The coefficients to be added to the tail must be positive integers. """ if not new_coeffs or any(not isinstance(e, int) or e < 1 for e in new_coeffs): raise ValueError( "The coefficients to be added to the tail must be positive " "integers." ) coeffs = tuple(self.coefficients) + new_coeffs # A step to ensure uniqueness of the simple form of the continued # fraction - if the last of the new coefficients is ``1`` it can be # "absorbed" by adding it to the second last coefficient, thereby # shortening the sequence by one. The resulting simple continued # fraction becomes unique for the number that is represented. if len(coeffs) > 1 and coeffs[-1] == 1: coeffs = coeffs[:-2] + (coeffs[-2] + 1,) fraction = fraction_from_coefficients(*coeffs) self._numerator, self._denominator = fraction.as_integer_ratio()
[docs] def truncate(self, *tail_coeffs: int) -> None: """Performs an in-place truncation of a contiguous trailing segment of the coefficients in the tail. .. note:: As this method performs an in-place modification of the existing/ current instance the object ID remains the same. Parameters ---------- tail_coeffs : int An (ordered) sequence of (positive) integers which form a contiguous trailing segment of the current sequence of coefficients and is to be truncated. Raises ------ ValueError If the tail coefficients provided are not positive integers, or do not form a contiguous trailing segment, or their number exceeds the length of the existing tail, i.e. the order of the continued fraction represented by the instance. Examples -------- >>> cf = ContinuedFraction('3.245') >>> cf ContinuedFraction(649, 200) >>> tuple(cf.coefficients) (3, 4, 12, 4) >>> cf.order 3 >>> cf.counter Counter({4: 2, 3: 1, 12: 1}) >>> tuple(cf.convergents) ((0, ContinuedFraction(3, 1)), (1, ContinuedFraction(13, 4)), (2, ContinuedFraction(159, 49)), (3, ContinuedFraction(649, 200))) >>> tuple(cf.remainders) ((3, ContinuedFraction(4, 1)), (2, ContinuedFraction(49, 4)), (1, ContinuedFraction(200, 49)), (0, ContinuedFraction(649, 200))) >>> cf.truncate(12, 4) >>> cf ContinuedFraction(13, 4) >>> tuple(cf.coefficients) (3, 4) >>> cf.order 1 >>> tuple(cf.convergents) ((0, ContinuedFraction(3, 1)), (1, ContinuedFraction(13, 4))) >>> tuple(cf.remainders) ((1, ContinuedFraction(4, 1)), (0, ContinuedFraction(13, 4))) >>> cf = ContinuedFraction(649, 200) >>> cf.truncate(4, 12) Traceback (most recent call last): ... ValueError: The coefficients to be truncated from the tail must consist of positive integers and form a contiguous trailing segment of the tail. >>> cf.truncate(3, 4, 12, 4) Traceback (most recent call last): ... ValueError: The coefficients to be truncated from the tail must consist of positive integers and form a contiguous trailing segment of the tail. """ coeffs = tuple(self.coefficients) order = len(coeffs[1:]) truncation_length = len(tail_coeffs) if ( not tail_coeffs or any(not isinstance(coeff, int) or coeff < 1 for coeff in tail_coeffs) or truncation_length > order or coeffs[order + 1 - truncation_length:] != tail_coeffs ): raise ValueError( "The coefficients to be truncated from the tail must consist " "of positive integers and form a contiguous trailing segment " "of the tail." ) coeffs = coeffs[:order + 1 - truncation_length] # A step to ensure uniqueness of the simple form of the continued # fraction - if the last coefficient is ``1`` it can be "removed" by # adding it to the second last coefficient, thereby shortening the # sequence by one. The resulting simple continued fraction becomes # unique for the number that is represented. if len(coeffs) > 1 and coeffs[-1] == 1: coeffs = coeffs[:-2] + (coeffs[-2] + 1,) fraction = fraction_from_coefficients(*coeffs) self._numerator, self._denominator = fraction.as_integer_ratio()
[docs] def as_decimal(self) -> Decimal: """Returns the :py:class:`decimal.Decimal` value of the continued fraction. Returns ------- decimal.Decimal The :py:class:`decimal.Decimal` representation of the continued fraction. Examples -------- Note that the default :py:mod:`decimal` context precision of :math:`28` is used in these examples. >>> import math >>> math.pi 3.141592653589793 Now construct a :py:class:`ContinuedFraction` instance from it, and check the :py:class:`float` value. >>> cf = ContinuedFraction(math.pi) >>> cf ContinuedFraction(884279719003555, 281474976710656) >>> cf.as_decimal() Decimal('3.141592653589793115997963469') """ return Decimal(self.numerator) / Decimal(self.denominator)
[docs] def convergent(self, k: int, /) -> ContinuedFraction: """Returns the :math:`k`-th (simple) convergent of the continued fraction. Given a (simple) continued fraction :math:`[a_0;a_1,a_2,\\ldots]` the :math:`k`-th convergent is defined as: .. math:: C_k = a_0 + \\cfrac{1}{a_1 + \\cfrac{1}{a_2 \\ddots \\cfrac{1}{a_{k-1} + \\cfrac{1}{a_k}}}} The result is a :py:class:`~continuedfractions.continuedfraction.ContinuedFraction` instance. If the continued fraction is of order :math:`n` then it has exactly :math:`n + 1` convergents :math:`C_0,C_1,C_2,\\ldots,C_n` where the :math:`k`-th convergent :math:`C_k = \\frac{p_k}{q_k}` is given by the recurrence relation: .. math:: \\begin{align} p_k &= a_kp_{k - 1} + p_{k - 2} \\\\ q_k &= a_kq_{k - 1} + q_{k - 2}, \\hskip{3em} k \\geq 3 \\end{align} where :math:`p_0 = a_0`, :math:`q_0 = 1`, :math:`p_1 = p_1p_0 + 1`, and :math:`q_1 = p_1`. Parameters ---------- k : int The index of the convergent, as described above. Returns ------- ContinuedFraction A new :py:class:`ContinuedFraction` instance representing the :math:`k`-th (simple) convergent of the original continued fraction, as described above. Examples -------- >>> cf = ContinuedFraction('.12345') >>> cf ContinuedFraction(2469, 20000) >>> cf.convergent(0) ContinuedFraction(0, 1) >>> cf.convergent(2) ContinuedFraction(9, 73) >>> cf.convergent(6) ContinuedFraction(448, 3629) >>> cf.convergent(7) ContinuedFraction(2469, 20000) """ return self.__class__(convergent(k, *self.coefficients))
@property def convergents(self) -> typing.Generator[tuple[int, ContinuedFraction], None, None]: """Generates an enumerated sequence of all convergents of the continued fraction. The convergents are generated as tuples of :py:class:`int` and :py:class:`~continuedfraction.continuedfraction.ContinuedFraction` instances, where the integers represent the indices of the convergents. If :math:`n` is the order of the continued fraction then :math:`n + 1` convergents :math:`C_0, C_1, \\ldots, C_n` are generated in that order. Yields ------ tuple A tuple of convergent index (:py:class:`int`) and convergents (:py:class:`~continuedfractions.continuedfraction.ContinuedFraction`) of the continued fraction. Examples -------- >>> cf = ContinuedFraction('3.245') >>> tuple(cf.convergents) ((0, ContinuedFraction(3, 1)), (1, ContinuedFraction(13, 4)), (2, ContinuedFraction(159, 49)), (3, ContinuedFraction(649, 200))) """ yield from enumerate(map(self.__class__, convergents(*self.coefficients))) @property def even_order_convergents(self) -> typing.Generator[tuple[int, ContinuedFraction], None, None]: """Generates an enumerated sequence of all even-order convergents of the continued fraction. The convergents are generated as tuples of :py:class:`int` and :py:class:`~continuedfraction.continuedfraction.ContinuedFraction` instances, where the integers represent the indices of the convergents. If :math:`n` is the order of the continued fraction then only the even- indexed convergents :math:`C_0, C_2, C_4, \\ldots` are generated. Yields ------ tuple A tuple of convergent index (:py:class:`int`) and convergents (:py:class:`~continuedfractions.continuedfraction.ContinuedFraction`) of the continued fraction. Examples -------- >>> tuple(ContinuedFraction('3.245').even_order_convergents) ((0, ContinuedFraction(3, 1)), (2, ContinuedFraction(159, 49))) """ yield from (t for t in self.convergents if not t[0] % 2) @property def odd_order_convergents(self) -> typing.Generator[tuple[int, ContinuedFraction], None, None]: """Generates an enumerated sequence of all odd-order convergents of the continued fraction. The convergents are generated as tuples of :py:class:`int` and :py:class:`~continuedfraction.continuedfraction.ContinuedFraction` instances, where the integers represent the indices of the convergents. If :math:`n` is the order of the continued fraction then only the odd- indexed convergents :math:`C_1, C_3, C_5, \\ldots` are generated. Yields ------ tuple A tuple of convergent index (:py:class:`int`) and convergents (:py:class:`~continuedfractions.continuedfraction.ContinuedFraction`) of the continued fraction. Examples -------- >>> tuple(ContinuedFraction('3.245').odd_order_convergents) ((1, ContinuedFraction(13, 4)), (3, ContinuedFraction(649, 200))) """ yield from (t for t in self.convergents if t[0] % 2)
[docs] def semiconvergent(self, k: int, m: int, /) -> ContinuedFraction: """Returns the :math:`m`-th semiconvergent of two consecutive convergents :math:`p_{k - 1}` and :math:`p_k` of the continued fraction. The integer :math:`k` must be positive and determine two consecutive convergents :math:`p_{k - 1}` and :math:`p_k` of a (finite, simple) continued fraction. The integer :math:`m` can be any positive integer. Parameters ---------- k : int The integer :math:`k` determining two consecutive convergents :math:`p_{k - 1}` and :math:`p_k` of a (finite, simple) continued fraction :math:`[a_0; a_1, \\ldots, a_{k}, a_{k + 1}, \\ldots, a_n]`. m : int The index of the semiconvergent of the convergents :math:`p_{k - 1}` and :math:`p_k`. Returns ------- ContinuedFraction The :math:`m`-th semiconvergent of the convergents :math:`p_{k - 1}` and :math:`p_k`. Raises ------ ValueError If :math:`k` or :math:`m` are not positive integers, or :math:`k` is an integer that does **not** satisfy :math:`1 \\leq k \\leq n` where :math:`n` is the order of the (finite, simple) continued fraction of which :math:`p_{k - 1}` and :math:`p_k` are convergents. Examples -------- >>> cf = ContinuedFraction(-415, 93) >>> tuple(cf.coefficients) (-5, 1, 1, 6, 7) >>> tuple(cf.convergents) ((0, ContinuedFraction(-5, 1)), (1, ContinuedFraction(-4, 1)), (2, ContinuedFraction(-9, 2)), (3, ContinuedFraction(-58, 13)), (4, ContinuedFraction(-415, 93))) >>> cf.semiconvergent(3, 1) ContinuedFraction(-67, 15) >>> cf.semiconvergent(3, 2) ContinuedFraction(-125, 28) >>> cf.semiconvergent(3, 3) ContinuedFraction(-183, 41) >>> cf.semiconvergent(3, 4) ContinuedFraction(-241, 54) >>> cf.semiconvergent(3, 5) ContinuedFraction(-299, 67) >>> cf.semiconvergent(3, 6) ContinuedFraction(-357, 80) >>> cf.semiconvergent(3, 7) ContinuedFraction(-415, 93) """ if not isinstance(k, int) or k not in range(1, self.order + 1) or not isinstance(m, int) or m < 1: raise ValueError( "`k` and `m` must be positive integers and `k` must be an " "integer in the range `1..n` where `n` is the order of the " "continued fraction" ) return self.convergent(k - 1).right_mediant(self.convergent(k), k=m)
[docs] def remainder(self, k: int, /) -> ContinuedFraction: """Returns the :math:`k`-th remainder of the continued fraction. Given a (simple) continued fraction :math:`[a_0;a_1,a_2,\\ldots]` the :math:`k`-th remainder :math:`R_k` is the (simple) continued fraction :math:`[a_k; a_{k + 1}, a_{k + 2}, \\ldots]`: .. math:: R_k = a_k + \\cfrac{1}{a_{k + 1} + \\cfrac{1}{a_{k + 2} \\ddots }} where :math:`R_0` is just the original continued fraction, i.e. :math:`R_0 = [a_0; a_1, a_2, \\ldots]`. The result is a :py:class:`~continuedfractions.continuedfraction.ContinuedFraction` instance. Parameters ---------- k : int The index of the remainder, as described above. Returns ------- ContinuedFraction A new :py:class:`ContinuedFraction` instance representing the :math:`k`-th remainder of the original continued fraction, as described above. Examples -------- >>> cf = ContinuedFraction('.12345') >>> cf ContinuedFraction(2469, 20000) >>> cf.remainder(0) ContinuedFraction(2469, 20000) >>> cf.remainder(2) ContinuedFraction(2469, 248) >>> cf.remainder(6) ContinuedFraction(6, 5) >>> cf.remainder(7) ContinuedFraction(5, 1) """ return self.__class__(remainder(k, *self.coefficients))
@property def remainders(self) -> typing.Generator[tuple[int, ContinuedFraction], None, None]: """Generates an enumerated sequence of all remainders of the continued fraction in descending order of index. If :math:`n` is the order of the continued fraction then there are :math:`n + 1` remainders :math:`R_0, R_1, \\ldots, R_n`, and the method generates these in reverse order :math:`R_0, R_1, \\ldots, R_n`. The remainders are generated as tuples of :py:class:`int` and :py:class:`~continuedfraction.continuedfraction.ContinuedFraction` instances, where the integers represent the indexes of the remainders. Yields ------ tuple A tuple of remainder indices (:py:class:`int`) and remainders (:py:class:`~continuedfractions.continuedfraction.ContinuedFraction`) of the continued fraction. Examples -------- >>> tuple(ContinuedFraction('3.245').remainders) ((3, ContinuedFraction(4, 1)), (2, ContinuedFraction(49, 4)), (1, ContinuedFraction(200, 49)), (0, ContinuedFraction(649, 200))) >>> tuple(ContinuedFraction(-415, 93).remainders) ((4, ContinuedFraction(7, 1)), (3, ContinuedFraction(43, 7)), (2, ContinuedFraction(50, 43)), (1, ContinuedFraction(93, 50)), (0, ContinuedFraction(-415, 93))) """ coeffs = tuple(self.coefficients) order = len(coeffs[1:]) yield from zip( reversed(range(order + 1)), map(self.__class__, remainders(*coeffs)) )
[docs] def left_mediant(self, other: Fraction, /, *, k: int = 1) -> ContinuedFraction: """Returns the :math:`k`-th left-mediant of the continued fraction with another rational number. For a positive integer :math:`k`, the :math:`k`-th left-mediant of two rational numbers :math:`r = \\frac{a}{b}` and :math:`s = \\frac{c}{d}`, where :math:`b, d, b + d \\neq 0`, is defined as: .. math:: \\frac{ka + c}{kb + d}, \\hskip{3em} k \\geq 1 Parameters ---------- other : fractions.Fraction, ContinuedFraction The second fraction to use to calculate the :math:`k`-th mediant with the first. k : int, default=1 The order of the mediant, as defined above. Returns ------- ContinuedFraction The :math:`k`-th left-mediant of the original fraction and the second fraction, as a :py:class:`ContinuedFraction` instance. Examples -------- >>> c1 = ContinuedFraction('1/2') >>> c2 = ContinuedFraction(3, 5) >>> c1, c2 (ContinuedFraction(1, 2), ContinuedFraction(3, 5)) >>> c1.left_mediant(c2) ContinuedFraction(4, 7) >>> c1.left_mediant(c2, k=2) ContinuedFraction(5, 9) >>> c1.left_mediant(c2, k=3) ContinuedFraction(6, 11) >>> c1.left_mediant(c2, k=100) ContinuedFraction(103, 205) >>> assert c1.left_mediant(c2, k=2) < c1.right_mediant(c2, k=2) >>> assert c1.left_mediant(c2, k=3) < c1.right_mediant(c2, k=3) >>> assert c1.left_mediant(c2, k=100) < c1.right_mediant(c2, k=100) """ return self.__class__(left_mediant(self, other, k=k))
[docs] def right_mediant(self, other: Fraction, /, *, k: int = 1) -> ContinuedFraction: """Returns the :math:`k`-th right-mediant of the continued fraction with another rational number. For a positive integer :math:`k`, the :math:`k`-th right-mediant of two rational numbers :math:`r = \\frac{a}{b}` and :math:`s = \\frac{c}{d}`, where :math:`b, d, b + d \\neq 0`, is defined as: .. math:: \\frac{a + kc}{b + kd}, \\hskip{3em} k \\geq 1 Parameters ---------- other : fractions.Fraction, ContinuedFraction The second fraction to use to calculate the :math:`k`-th mediant with the first. k : int, default=1 The order of the mediant, as defined above. Returns ------- ContinuedFraction The :math:`k`-th right-mediant of the original fraction and the second fraction, as a :py:class:`ContinuedFraction` instance. Examples -------- >>> c1 = ContinuedFraction('1/2') >>> c2 = ContinuedFraction(3, 5) >>> c1, c2 (ContinuedFraction(1, 2), ContinuedFraction(3, 5)) >>> c1.right_mediant(c2) ContinuedFraction(4, 7) >>> c1.right_mediant(c2, k=2) ContinuedFraction(7, 12) >>> c1.right_mediant(c2, k=3) ContinuedFraction(10, 17) >>> c1.right_mediant(c2, k=100) ContinuedFraction(301, 502) >>> assert c1.left_mediant(c2, k=2) < c1.right_mediant(c2, k=2) >>> assert c1.left_mediant(c2, k=3) < c1.right_mediant(c2, k=3) >>> assert c1.left_mediant(c2, k=100) < c1.right_mediant(c2, k=100) """ return self.__class__(right_mediant(self, other, k=k))
[docs] def mediant(self, other: Fraction, /) -> ContinuedFraction: """Returns the simple mediant of the continued fraction with another continued fraction. The simple mediant of two rational numbers :math:`r = \\frac{a}{b}` and :math:`s = \\frac{c}{d}`, where :math:`b, d, b + d \\neq 0`, is defined as: .. math:: \\frac{a + c}{b + d} The resulting value :math:`\\frac{a + c}{b + d}` is the same as the 1st order left- or right-mediant of :math:`r = \\frac{a}{b}` and :math:`s = \\frac{c}{d}`. So this method would produce the same result as the :py:meth:`~continuedfractions.continuedfraction.ContinuedFraction.left_mediant` or :py:meth:`~continuedfractions.continuedfraction.ContinuedFraction.right_mediant` methods where the order :math:`k` is set to :math:`1`. Parameters ---------- other : fractions.Fraction, ContinuedFraction The other continued fraction. Returns ------- ContinuedFraction The simple mediant of the original fraction and the other continued fraction. Examples -------- >>> ContinuedFraction(1, 2).mediant(ContinuedFraction(3, 5)) ContinuedFraction(4, 7) >>> assert ContinuedFraction(1, 2).mediant(ContinuedFraction(3, 5)) == ContinuedFraction(1, 2).left_mediant(ContinuedFraction(3, 5), k=1) >>> assert ContinuedFraction(1, 2).mediant(ContinuedFraction(3, 5)) == ContinuedFraction(1, 2).right_mediant(ContinuedFraction(3, 5), k=1) """ return self.__class__(mediant(self, other))
@property def height(self) -> int: """:py:class:`int` : The height of this rational number. Returns ------- int The height of the fraction as a rational number represented by homogeneous coordinates in projective space :math:`\\mathbb{P}^1(\\mathbb{Q})`. Examples -------- >>> ContinuedFraction(0, 1).height 1 >>> ContinuedFraction(-1, 2).height 2 >>> ContinuedFraction(3, -2).height 3 """ return max(abs(self).as_integer_ratio()) @property def log_height(self) -> Decimal: """:py:class:`decimal.Decimal` : The (natural) logarithm of the height of this rational number as defined above. Returns ------- decimal.Decimal The (natural) logarithm of the height of this fraction as a rational number as defined above. Examples -------- >>> ContinuedFraction(0, 1).log_height Decimal('0') >>> ContinuedFraction(-1, 2).log_height Decimal('0.69314718055994528622676398299518041312694549560546875') >>> ContinuedFraction(3, -2).log_height Decimal('1.0986122886681097821082175869378261268138885498046875') """ return Decimal(math.log(self.height)) def __add__(self, other, /): return self.__class__(super().__add__(other)) def __radd__(self, other, /): return self.__class__(super().__radd__(other)) def __sub__(self, other, /): return self.__class__(super().__sub__(other)) def __rsub__(self, other, /): return self.__class__(super().__rsub__(other)) def __mul__(self, other, /): return self.__class__(super().__mul__(other)) def __rmul__(self, other, /): return self.__class__(super().__rmul__(other)) def __truediv__(self, other, /): return self.__class__(super().__truediv__(other)) def __rtruediv__(self, other, /): return self.__class__(super().__rtruediv__(other)) def __floordiv__(self, other, /): return self.__class__(super().__floordiv__(other)) def __rfloordiv__(self, other, /): return self.__class__(super().__rfloordiv__(other)) def __divmod__(self, other, /): quo, rem = super().__divmod__(other) return self.__class__(quo), self.__class__(rem) def __rdivmod__(self, other, /): quo, rem = super().__rdivmod__(other) return self.__class__(quo), self.__class__(rem) def __pow__(self, other, /) -> ContinuedFraction: return self.__class__(super().__pow__(other)) def __rpow__(self, other, /): return self.__class__(Fraction(other).__rpow__(self)) def __pos__(self) -> ContinuedFraction: return self.__class__(super().__pos__())
[docs] def __neg__(self) -> ContinuedFraction: """ The negation algorithm for a finite simple continued fraction. The basic algorithm can be described as follows: if :math:`[a_0; a_1,\\ldots, a_n]` is the simple continued fraction of a **positive** rational number :math:`\\frac{a}{b}` of finite order :math:`n` then :math:`-\\frac{a}{b}` has the simple continued fraction: .. math:: \\begin{cases} [-a_0;] \\hskip{3em} & n = 0 \\\\ [-(a_0 + 1); 2] \\hskip{3em} & n = 1 \\text{ and } a_1 = 2 \\\\ [-(a_0 + 1); a_2 + 1, a_3,\\ldots, a_n] \\hskip{3em} & n \\geq 2 \\text{ and } a_1 = 1 \\\\ [-(a_0 + 1); 1, a_1 - 1, a_2, \\ldots,a_n] \\hskip{3em} & n \\geq 2 \\text{ and } a_1 \\geq 2 \\end{cases} In applying this algorithm there is an assumption that the last coefficient :math:`a_n > 1`, as any simple continued fraction of type :math:`[a_0; a_1,\\ldots, a_{n} = 1]` can be reduced to the simple continued fraction :math:`[a_0; a_1,\\ldots, a_{n - 1} + 1]`. """ coeffs = tuple(self.coefficients) n = len(coeffs) if n == 1: # Case (1) of the algorithm neg_coeffs = (-coeffs[0],) elif n == 2 and coeffs[1] == 2: # Case (2) of the algorithm neg_coeffs = (-(coeffs[0] + 1), 2) elif n > 1 and coeffs[1] == 1: # Case (3) of the algorithm neg_coeffs = (-(coeffs[0] + 1), coeffs[2] + 1, *coeffs[3:]) else: # Case (4) of the algorithm neg_coeffs = (-(coeffs[0] + 1), 1, coeffs[1] - 1, *coeffs[2:]) neg_fraction = convergent(len(neg_coeffs) - 1, *neg_coeffs) neg_self = self.__class__(1) neg_self._numerator, neg_self._denominator = neg_fraction.as_integer_ratio() return neg_self
def __abs__(self) -> ContinuedFraction: return self.__class__(super().__abs__())
if __name__ == "__main__": # pragma: no cover # Doctest the module from the project root using # # PYTHONPATH="src" python3 -m doctest -v src/continuedfractions/continuedfraction.py # # NOTE: the doctest examples using ``decimal.Decimal`` values are based on # a context precision of 28 digits. decimal.getcontext().prec = 28 import doctest doctest.testmod()