Skip to content

Commit

Permalink
Release 0.11.22 (#55)
Browse files Browse the repository at this point in the history
* docs: some fixes to docstrings in `lib` + tweak citation file

* chore: minor tweaks to `lib` docstrings + `__all__`

* refactor: `ContinuedFraction.__init__` logic now uses structural pattern matching (`match`-`case` statement) + update docstrings and tests
  • Loading branch information
sr-murthy authored Mar 19, 2024
1 parent 8dbaa8e commit eebb273
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 49 deletions.
5 changes: 2 additions & 3 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,5 @@ keywords:
- rational approximation
- mediants
license: MPL-2.0
commit: 3d4fc41
version: 0.11.21
date-released: '2024-03-18'
version: 0.11.22
date-released: '2024-03-19'
2 changes: 1 addition & 1 deletion docs/sources/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ The CI/CD pipelines are defined in the `CI YML <.github/workflows/ci.yml>`_, and
Versioning and Releases
=======================

The `PyPI package <https://pypi.org/project/continuedfractions/>`_ is currently at version ``0.11.21`` - `semantic versioning <https://semver.org/>`_ is used.
The `PyPI package <https://pypi.org/project/continuedfractions/>`_ is currently at version ``0.11.22`` - `semantic versioning <https://semver.org/>`_ is used.

There is currently no dedicated pipeline for releases - both `GitHub releases <https://github.com/sr-murthy/continuedfractions/releases>`_ and `PyPI packages <https://pypi.org/project/continuedfractions>`_ are published manually, but both have the same version tag.

Expand Down
53 changes: 32 additions & 21 deletions src/continuedfractions/continuedfraction.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ class ContinuedFraction(Fraction):
# Slots - ATM only ``_elements`` to store the continued fraction elements sequence
__slots__ = ['_elements',]

# To support pattern matching of instances in ``match`` statements
__match_args__ = ('numerator', 'denominator')

# Class attribute to store an error message for input errors
__valid_inputs_msg__ = (
"Only single integers, non-nan floats, numeric strings, \n"
Expand Down Expand Up @@ -421,24 +424,34 @@ def __init__(self, *args: int | float | str | Fraction | ContinuedFraction | De
"""
super().__init__()

if len(args) == 1 and isinstance(args[0], ContinuedFraction):
self._elements: Final[tuple[int]] = args[0].elements
if len(args) == 1 and isinstance(args[0], int):
self._elements: Final[tuple[int]] = tuple(continued_fraction_rational(Fraction(args[0])))
elif len(args) == 1 and isinstance(args[0], float):
self._elements: Final[tuple[int]] = tuple(continued_fraction_real(args[0]))
elif len(args) == 1 and isinstance(args[0], str) and _RATIONAL_FORMAT.match(args[0]) and '/' in args[0]:
self._elements: Final[tuple[int]] = tuple(continued_fraction_rational(Fraction(*self.as_integer_ratio())))
elif len(args) == 1 and isinstance(args[0], str) and _RATIONAL_FORMAT.match(args[0]) and '/' not in args[0]:
self._elements: Final[tuple[int]] = tuple(continued_fraction_real(args[0]))
elif len(args) == 1 and (isinstance(args[0], Fraction) or isinstance(args[0], Decimal)):
self._elements: Final[tuple[int]] = tuple(continued_fraction_rational(Fraction(*args[0].as_integer_ratio())))
elif len(args) == 2 and set(map(type, args)) == set([int]):
self._elements: Final[tuple[int]] = tuple(continued_fraction_rational(Fraction(args[0], args[1])))
elif len(args) == 2 and set(map(type, args)).issubset([int, Fraction, ContinuedFraction]):
self._elements: Final[tuple[int]] = tuple(continued_fraction_rational(Fraction(*self.as_integer_ratio())))
else: # pragma: no cover
raise ValueError(self.__class__.__valid_inputs_msg__)
match args:
# -- case of a single ``ContinuedFraction`` --
case (ContinuedFraction(),):
self._elements: Final[tuple[int]] = args[0].elements
# -- case of a single ``int`` --
case (int(),):
self._elements: Final[tuple[int]] = tuple(continued_fraction_rational(Fraction(args[0])))
# -- case of a single ``float`` --
case (float(),):
self._elements: Final[tuple[int]] = tuple(continued_fraction_real(args[0]))
# -- case of a single (signed or unsigned) numeric string matching --
# -- ``fractions._RATIONAL_FORMAT`` --
case (str(),):
self._elements: Final[tuple[int]] = tuple(continued_fraction_real(args[0]))
# -- case of a single ``fractions.Fraction`` or ``decimal.Decimal``
case (Fraction(),) | (Decimal(),):
self._elements: Final[tuple[int]] = tuple(continued_fraction_rational(Fraction(*args[0].as_integer_ratio())))
# -- case of a pair of ``int``s
case (int(), int()):
self._elements: Final[tuple[int]] = tuple(continued_fraction_rational(Fraction(args[0], args[1])))
# -- case of a pairwise combination of ``int``, --
# -- ``fractions.Fraction`` or ``ContinuedFraction`` instances --
case (int() | Fraction() | ContinuedFraction(), int() | Fraction() | ContinuedFraction()):
self._elements: Final[tuple[int]] = tuple(continued_fraction_rational(Fraction(*self.as_integer_ratio())))
# -- any other case - these cases would have been excluded by --
# ``validate`` but just to be sure --
case _: # pragma: no cover
raise ValueError(self.__class__.__valid_inputs_msg__)

def __add__(self, other: int | float | Fraction | ContinuedFraction, /) -> ContinuedFraction:
return self.__class__(super().__add__(other))
Expand Down Expand Up @@ -837,9 +850,7 @@ def remainders(self) -> MappingProxyType[int, ContinuedFraction]:

@functools.cache
def mediant(self, other: Fraction, /, *, dir="right", k: int = 1) -> ContinuedFraction:
"""
Returns the ``k``-th left- or right-mediant of the rational number
represented by the continued fraction, and another rational number.
"""Returns the ``k``-th left- or right-mediant of the continued fraction with another rational number.
The "direction" of the mediant is specified with ``dir``, and can only
be one of ``"left"`` or ``"right"``.
Expand Down
67 changes: 44 additions & 23 deletions src/continuedfractions/lib.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
__all__ = [
'continued_fraction_real',
'continued_fraction_rational',
'fraction_from_elements',
'convergent',
'fraction_from_elements',
'mediant',
]


# -- IMPORTS --

# -- Standard libraries --
import re

from collections import deque
from decimal import Decimal
from itertools import accumulate
from fractions import Fraction
from fractions import Fraction, _RATIONAL_FORMAT
from typing import Generator

# -- 3rd party libraries --
Expand Down Expand Up @@ -104,23 +106,22 @@ def continued_fraction_rational(r: Fraction, /) -> Generator[int, None, None]:


def continued_fraction_real(x: int | float | str, /) -> Generator[int, None, None]:
"""Generates elements/coefficients of the finite, simple continued fraction of the given "real" number.
"""Generates elements/coefficients of the (finite) simple continued fraction of the given "real" number.
Generates the (integer) elements of a "simple" continued fraction
Generates the (integer) elements of a (finite) "simple" continued fraction
representation of ``x``, which can be either an integer, float or an
equivalent string representation, except for nans and non-numeric strings.
As floats are finite precision representations of real numbers, if ``x``
is a float representing a real number with a fractional part containing
an infinite periodic sequence of digits, or is an irrational number, the
continued fraction representation of ``x``, as given by the elements
generated by the function, will necessarily be finite, but not necessarily
unique.
continued fraction that is generated for ``x`` will be approximate and not
necessarily unique.
No attempt is made to raise exceptions or errors directly - if ``x`` is not
an ``int`` or ``float``, or is a ``nan`` or a non-numeric string, either
the ``decimal.Decimal`` conversion or the call to
``continued_fraction_rational`` will trigger upstream error(s).
To get the most accurate and precise continued fraction representation use
a ``decimal.Decimal`` input, with the context precision set to the maximum
level allowed by available memory and any other environment/system
limitations.
Parameters
----------
Expand All @@ -129,6 +130,12 @@ def continued_fraction_real(x: int | float | str, /) -> Generator[int, None, Non
or ``float``, or an equivalent string representation of an ``int`` or
``float``, except for nans (``float("nan")``) and non-numeric strings.
Raises
------
ValueError:
If ``x`` is not a numeric string - ``fractions._RATIONAL_FORMAT`` is
used to check this.
Yields
------
int
Expand All @@ -148,12 +155,15 @@ def continued_fraction_real(x: int | float | str, /) -> Generator[int, None, Non
>>> list(continued_fraction_real(1/1j))
Traceback (most recent call last):
...
decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]
ValueError: "-1j" is not a valid numeric string for a continued fraction representation
>>> list(continued_fraction_real('-1/3'))
>>> list(continued_fraction_real("not a numeric string"))
Traceback (most recent call last):
...
decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]
ValueError: "not a numeric string" is not a valid numeric string for a continued fraction representation
>>> list(continued_fraction_real('-1/3'))
[-1, 1, 2]
>>> list(continued_fraction_real(-649/200))
[-4, 1, 3, 12, 4]
Expand All @@ -174,14 +184,25 @@ def continued_fraction_real(x: int | float | str, /) -> Generator[int, None, Non
But actual fractions where the numerator and denominator are ``int`` or
``float``, e.g. ``-1/4`` or ``5.6/2``, can be processed successfully.
"""
num, denum = Decimal(str(x)).as_integer_ratio()
xstr = str(x)

if not re.match(_RATIONAL_FORMAT, xstr):
raise ValueError(
f'"{x}" is not a valid numeric string for a continued '
'fraction representation'
)

num, denum = (
Fraction(xstr).as_integer_ratio() if '/' in xstr
else Decimal(xstr).as_integer_ratio()
)

for elem in continued_fraction_rational(Fraction(num, denum)):
yield elem


def fraction_from_elements(*elements: int) -> Fraction:
"""Returns a rational number from a sequence of elements of simple continued fraction.
"""Returns the rational number represented by a simple continued fraction from a sequence of its elements.
Returns a ``fractions.Fraction`` object representing the rational number
represented by the simple continued fraction as given by the sequence
Expand Down Expand Up @@ -237,7 +258,7 @@ def fraction_from_elements(*elements: int) -> Fraction:


def convergent(*elements: int, k: int = 1) -> Fraction:
"""Returns the ``k``-th convergent of a simple continued fraction as represented by a sequence of its elements.
"""Returns the ``k``-th convergent of a simple continued fraction given a sequence of its elements.
Returns a ``fractions.Fraction`` object representing the ``k``-th
convergent of a (finite) continued fraction given by an ordered sequence of
Expand All @@ -256,10 +277,10 @@ def convergent(*elements: int, k: int = 1) -> Fraction:
Parameters
----------
*elements : int
*elements : `int`
A variable-length sequence of integer elements of a continued fraction.
k : int, default=1
k : `int`, default=1
The order of the convergent.
Returns
Expand Down Expand Up @@ -342,17 +363,17 @@ def mediant(r: Fraction, s: Fraction, /, *, dir='right', k: int = 1) -> Fraction
Parameters
----------
r : fractions.Fraction
r : `fractions.Fraction`
The first rational number.
s : fractions.Fraction
s : `fractions.Fraction`
The second rational number.
dir : str, default='right'
dir : `str`, default='right'
The "direction" of the mediant - `'left'` or `'right'`, as defined
above.
k : int, default=1
k : `int`, default=1
The order of the mediant, as defined above.
Returns
Expand Down
2 changes: 1 addition & 1 deletion src/continuedfractions/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.11.21"
__version__ = "0.11.22"
12 changes: 12 additions & 0 deletions tests/units/test_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ def test_continued_fraction_rational__valid_integers__correct_elements_generated

class TestContinuedFractionReal:

@pytest.mark.parametrize(
"x",
[
(1/1j,),
("not a numeric string"),
("1a23"),
]
)
def test_continued_fraction_real__invalid_inputs__value_error_raised(self, x):
with pytest.raises(ValueError):
list(continued_fraction_real(x))

@pytest.mark.parametrize(
"x, elements",
[
Expand Down

0 comments on commit eebb273

Please sign in to comment.