Skip to content

Commit dddcad0

Browse files
committed
Changes for release 0.10.0
1 parent 1f1a4df commit dddcad0

File tree

7 files changed

+120
-111
lines changed

7 files changed

+120
-111
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,4 @@ cython_debug/
163163
# PDM
164164
.pdm.python
165165
pdm.lock.dev
166+
review.txt

README.md

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ True
4949

5050
## Installation & Dependencies
5151

52-
The package does not use any 3rd party (production) dependencies, only Python standard libraries, and is supported on Python versions `3.10`-`3.12`. It is CI-tested on Ubuntu Linux (22.04.3 LTS), Mac OS (12.7.3) and Windows (Windows Server 2022), but should also install on any other platform supporting these Python versions.
52+
The package uses only Python standard libraries, and is supported on Python versions `3.10`-`3.12`. It is CI-tested on Ubuntu Linux (22.04.3 LTS), Mac OS (12.7.3) and Windows (Windows Server 2022), but should also install on any other platform supporting these Python versions.
5353

5454
The simplest way of installing it is a standard `pip`/`pip3` install:
5555

@@ -63,11 +63,15 @@ For contributors there are development requirements which are specified in the [
6363

6464
[Continued fractions](https://en.wikipedia.org/wiki/Continued_fraction) are beautiful and interesting mathematical objects, with many connections in [number theory](https://en.wikipedia.org/wiki/Number_theory) and also very useful practical applications, including the [rational approximation of real numbers](https://en.wikipedia.org/wiki/Continued_fraction#Best_rational_approximations).
6565

66-
The `continuedfractions` package is designed to make it easy to construct (finite) continued fractions as Python objects, and explore their key properties, such as elements/coefficients, convergents, segments, remainders, and others. They have been implemented as instances of the standard library [`fractions.Fraction`](https://docs.python.org/3/library/fractions.html#fractions.Fraction) class, of which they are automatically instances, and are thus fully operable as rational numbers.
66+
The `continuedfractions` package is designed to:
67+
68+
* make it easy to construct (finite) continued fractions as Python objects
69+
* explore their key properties, such as elements/coefficients, convergents, segments, remainders, and others
70+
* operate on them as rationals and instances of the standard library [`fractions.Fraction`](https://docs.python.org/3/library/fractions.html#fractions.Fraction) class
6771

6872
### Package Structure
6973

70-
The `continuedfractions` package consists of two libraries:
74+
The `continuedfractions` package consists of two core libraries:
7175

7276
* [`continuedfractions.lib`](https://github.com/sr-murthy/continuedfractions/blob/main/src/continuedfractions/lib.py) - this contains the core functionality of (1) generating continued fraction representations (as ordered element sequences) of any valid Python number, given as an integer, non-nan `float`, valid numeric string, a `fractions.Fraction` or `decimal.Decimal` object, or as a pair of integers and/or `fractions.Fraction` objects; and conversely (2) reconstructing rational fractions from continued fraction representations (again, given as ordered element sequences).
7377

@@ -147,12 +151,25 @@ The **order** of a continued fraction is defined to be number of its elements **
147151
3
148152
```
149153

154+
The elements and order of `ContinuedFraction` objects are well behaved with respect to all rational operations supported by `fractions.Fraction`:
155+
156+
```python
157+
>>> ContinuedFraction(415, 93).elements
158+
(4, 2, 6, 7)
159+
>>> ContinuedFraction(649, 200) + ContinuedFraction(415, 93)
160+
ContinuedFraction(143357, 18600)
161+
>>> (ContinuedFraction(649, 200) + ContinuedFraction(415, 93)).elements
162+
(7, 1, 2, 2, 2, 1, 1, 11, 1, 2, 12)
163+
>>> (ContinuedFraction(649, 200) + ContinuedFraction(415, 93)).order
164+
10
165+
```
166+
150167
#### Convergents and Rational Approximations
151168

152169
For an integer $k >= 0$ the $k$-th **convergent** $C_k$ of a continued fraction $[a_0; a_1,\ldots]$ of a real number $x$ is defined to be the rational number and finite continued fraction represented by $[a_0; a_1,\ldots,a_k]$, formed from the first $k + 1$ elements of the original.
153170

154171
$$
155-
C_k = a_0 + \frac{1}{a_1 + \frac{1}{\ddots \frac{1}{a_{k-1} + \frac{1}{a_k}}}}
172+
C_k = a_0 + \frac{1}{a_1 + \frac{1}{a_2 \ddots \frac{1}{a_{k-1} + \frac{1}{a_k}}}}
156173
$$
157174

158175
Each convergent $C_k$ represents a rational approximation $\frac{p_k}{q_k}$ of $x$, and we can define an error term $\epsilon_k = x - C_k = x - \frac{p_k}{q_k}$. If we assume $x > 0$ then the convergents form a strictly increasing sequence of rational numbers converging to $x$ as $n \longrightarrow \infty$. So, formally:
@@ -254,7 +271,7 @@ $$
254271
\frac{a + nc}{b + nd}
255272
$$
256273

257-
For $k = 1$ the left- and right-mediants are identical, but as $k \longrightarrow \infty$ they separate into two distinct strictly monotonic sequences with opposite limits: the left-mediants form a strictly decreasing sequence lower-bounded by $\frac{a}{b}$, thus converging to $\frac{a}{b}$:
274+
For $k = 1$ the left- and right-mediants are identical, but as $k \longrightarrow \infty$ they separate into two distinct, strictly monotonic, sequences converging to opposite limits[^3]: the left-mediants form a strictly decreasing sequence lower-bounded by $\frac{a}{b}$, converging to $\frac{a}{b}$:
258275

259276
$$
260277
\frac{a}{b} < \cdots < \frac{3a + c}{3b + d} < \frac{2a + c}{2b + d} < \frac{a + c}{b + d} < \frac{c}{d}
@@ -264,7 +281,7 @@ $$
264281
\lim_{k \to \infty} \frac{ka + c}{kb + d} = \frac{a}{b}
265282
$$
266283

267-
while the right-mediants form a strictly increasing sequence upper-bounded by $\frac{c}{d}$, thus converging to $\frac{c}{d}$:
284+
while the right-mediants form a strictly increasing sequence upper-bounded by $\frac{c}{d}$, converging to $\frac{c}{d}$:
268285

269286
$$
270287
\frac{a}{b} < \frac{a + c}{b + d} < \frac{a + 2c}{b + 2d} < \frac{a + 3c}{b + 3d} < \cdots < \frac{c}{d}
@@ -274,7 +291,7 @@ $$
274291
\lim_{k \to \infty} \frac{a + kc}{b + kd} = \frac{c}{d}
275292
$$
276293

277-
We can illustrate this using `ContinuedFraction.mediant` method using the `dir` option to set the "direction" of the mediant - either `'left'` or `'right` (default) - and using `k` to set the mediant order, which defaults to `k=1`.
294+
We can illustrate this using the `ContinuedFraction.mediant` method using the `dir` option to set the "direction" of the mediant - either `'left'` or `'right` (default) - and using `k` to set the mediant order, which defaults to `k=1`.
278295
279296
```python
280297
# Right mediants
@@ -285,7 +302,7 @@ ContinuedFraction(4, 7)
285302
>>> c1.mediant(c2).as_float()
286303
0.5714285714285714
287304
>>> c1.mediant(c2, k=10)
288-
0.5961538461538461
305+
ContinuedFraction(31, 52)
289306
>>> c1.mediant(c2, k=100)
290307
0.599601593625498
291308
>>> c1.mediant(c2, k=10 ** 6)
@@ -310,7 +327,7 @@ ContinuedFraction(1000003, 2000005)
310327
0.500000249999375
311328
```
312329

313-
Mediants have other [interesting connections](https://en.wikipedia.org/wiki/Stern%E2%80%93Brocot_tree), including to sequences and orderings of rational numbers.
330+
Mediants have very interesting connections to other areas of number, including sequences and orderings of rational numbers - for example, see the [Stern-Brocot tree](https://en.wikipedia.org/wiki/Stern%E2%80%93Brocot_tree).
314331

315332
### Constructing Continued Fractions from Element Sequences
316333

@@ -340,7 +357,18 @@ ContinuedFraction(-200, 649)
340357

341358
### Continued Fractions with Negative Terms
342359

343-
Continued fractions representations with negative terms are valid, provided we use the [Euclidean integer division algorithm](https://en.wikipedia.org/wiki/Continued_fraction#Calculating_continued_fraction_representations) to calculate the successive quotients and remainders in each step. For example, $\frac{-415}{93} = \frac{-5 \times 93 + 50}{93}$ has the continued fraction representation $[-5; 1, 1, 6, 7]$. Compare this with $[4; 2, 6, 7]$, which is the continued fraction representation of $\frac{415}{93}$.
360+
Continued fractions representations with negative terms are valid, provided we use the [Euclidean integer division algorithm](https://en.wikipedia.org/wiki/Continued_fraction#Calculating_continued_fraction_representations) to calculate the successive quotients and remainders in each step. For example, $\frac{-415}{93} = \frac{-5 \times 93 + 50}{93}$ has the continued fraction representation $[-5; 1, 1, 6, 7]$:
361+
362+
$$
363+
-\frac{415}{93} = -5 + \frac{1}{1 + \frac{1}{1 + \frac{1}{6 + \frac{1}{7}}}}
364+
$$
365+
366+
Compare this with $[4; 2, 6, 7]$, which is the continued fraction representation of $\frac{415}{93}$:
367+
368+
$$
369+
\frac{415}{93} = 4 + \frac{1}{2 + \frac{1}{6 + \frac{1}{7}}}
370+
$$
371+
344372

345373
`ContinuedFraction` objects for negative numbers are constructed in the same way as with positive numbers, subject to the validation rules described above. And to avoid zero division problems if a fraction has a negative denominator the minus sign is "transferred" to the numerator. A few examples are given below.
346374

@@ -489,7 +517,7 @@ The CI/CD pipelines are defined in the [CI YML](.github/workflows/ci.yml), and p
489517

490518
### Versioning & Package Publishing
491519

492-
The package is currently at version `0.0.9` ([semantic versioning](https://semver.org/) is used), and packages are published manually to [PyPI](https://pypi.org/project/continuedfractions/) using [twine](https://twine.readthedocs.io/en/stable/). There is currently no CI release pipeline - this will be added later.
520+
The package is currently at version `0.10.0` ([semantic versioning](https://semver.org/) is used), and packages are published manually to [PyPI](https://pypi.org/project/continuedfractions/) using [twine](https://twine.readthedocs.io/en/stable/). There is currently no CI release pipeline - this will be added later.
493521

494522
## License
495523

@@ -509,7 +537,9 @@ The project is [licensed](LICENSE) under the [Mozilla Public License 2.0](https:
509537

510538
[6] Python 3.12.2 Docs. "decimal - Decimal fixed point and floating point arithmetic." https://docs.python.org/3/library/decimal.html. Accessed 21 February 2024.
511539

512-
[7] Wikipedia. "Continued Fraction". https://en.wikipedia.org/wiki/Continued_fraction. Accessed 19 February 2024.
540+
[7] Wikipedia. "Stern-Brocot Tree". https://en.wikipedia.org/wiki/Stern%E2%80%93Brocot_tree. Accessed 23 February 2024.
541+
542+
[8] Wikipedia. "Continued Fraction". https://en.wikipedia.org/wiki/Continued_fraction. Accessed 19 February 2024.
513543

514544
[^1]: Due to the nature of [binary floating point arithmetic](https://docs.python.org/3/tutorial/floatingpoint.html) it is not always possible to exactly represent a given [real number](https://en.wikipedia.org/wiki/Real_number). For the same reason, the continued fraction representations produced by the package will necessarily be [finite](https://en.wikipedia.org/wiki/Continued_fraction#Finite_continued_fractions).
515545

src/continuedfractions/continuedfraction.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,12 @@ class ContinuedFraction(Fraction):
124124
"denominator, respectively, of a rational fraction, are valid."
125125
)
126126

127+
def __str__(self) -> str:
128+
return (
129+
f"{super().__str__()}: [{self.elements[0]}" +
130+
(f";{','.join(map(str, self.elements[1:]))}]" if self.order > 0 else ";]")
131+
)
132+
127133
@classmethod
128134
def validate(cls, *args: int | float | str | Fraction | Decimal, **kwargs: Any) -> None:
129135
"""
@@ -365,19 +371,19 @@ def __init__(self, *args: int | float | str | Fraction | Decimal, **kwargs: Any
365371
super().__init__()
366372

367373
if len(args) == 1 and isinstance(args[0], int):
368-
self._elements = tuple(continued_fraction_rational(args[0], 1))
374+
self._elements = tuple(continued_fraction_rational(Fraction(args[0])))
369375
elif len(args) == 1 and isinstance(args[0], float):
370376
self._elements = tuple(continued_fraction_real(args[0]))
371377
elif len(args) == 1 and isinstance(args[0], str) and _RATIONAL_FORMAT.match(args[0]) and '/' in args[0]:
372-
self._elements = tuple(continued_fraction_rational(*self.as_integer_ratio()))
378+
self._elements = tuple(continued_fraction_rational(Fraction(*self.as_integer_ratio())))
373379
elif len(args) == 1 and isinstance(args[0], str) and _RATIONAL_FORMAT.match(args[0]) and '/' not in args[0]:
374380
self._elements = tuple(continued_fraction_real(args[0]))
375381
elif len(args) == 1 and (isinstance(args[0], Fraction) or isinstance(args[0], Decimal)):
376-
self._elements = tuple(continued_fraction_rational(*args[0].as_integer_ratio()))
382+
self._elements = tuple(continued_fraction_rational(Fraction(*args[0].as_integer_ratio())))
377383
elif len(args) == 2 and set(map(type, args)) == set([int]):
378-
self._elements = tuple(continued_fraction_rational(args[0], args[1]))
384+
self._elements = tuple(continued_fraction_rational(Fraction(args[0], args[1])))
379385
elif len(args) == 2 and set(map(type, args)).issubset([int, Fraction]):
380-
self._elements = tuple(continued_fraction_rational(*self.as_integer_ratio()))
386+
self._elements = tuple(continued_fraction_rational(Fraction(*self.as_integer_ratio())))
381387
else: # pragma: no cover
382388
raise ValueError(self.__class__.__valid_inputs_msg__)
383389

src/continuedfractions/lib.py

Lines changed: 11 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@
1919
# -- Internal libraries --
2020

2121

22-
def continued_fraction_rational(x: int, y: int, /) -> Generator[int, None, None]:
22+
def continued_fraction_rational(r: Fraction, /) -> Generator[int, None, None]:
2323
"""
2424
Generates the (integer) elements (also called coefficient or terms) of a
2525
unique, finite, "simple" continued fraction representation of the
26-
rational fraction `x/y` with numerator `x` and non-zero denominator `y`.
26+
rational fraction `x/y` with numerator `x` and non-zero denominator `y`,
27+
given as a `fractions.Fraction` object.
2728
2829
The number of elements generated minus 1 is called the order of the
2930
continued fraction, and as the function applies only to rational fractions
@@ -43,50 +44,34 @@ def continued_fraction_rational(x: int, y: int, /) -> Generator[int, None, None]
4344
4445
Parameters
4546
----------
46-
x : int
47-
Numerator of the rational fraction.
48-
49-
y : int
50-
Denominator of the rational fraction.
47+
r : fractions.Fraction
48+
The rational number to represented as a continued fraction.
5149
5250
Yields
5351
------
5452
int
5553
Elements of a unique, finite "simple" continued fraction representation
5654
of the given rational fraction `x/y` (where `y` is non-zero).
5755
58-
Raises
59-
------
60-
ValueError
61-
If `x` or `y` are not integers.
62-
63-
ZeroDivisionError
64-
If `y`, the denominator, is zero.
65-
6656
Examples
6757
--------
6858
A few examples are given below of how this function can be used.
6959
70-
>>> for e in continued_fraction_rational(649, 200):
60+
>>> for e in continued_fraction_rational(Fraction(649, 200)):
7161
... print(e)
7262
...
7363
3
7464
4
7565
12
7666
4
7767
78-
>>> list(continued_fraction_rational(415, 93))
68+
>>> list(continued_fraction_rational(Fraction(415, 93)))
7969
[4, 2, 6, 7]
8070
81-
>> list(continued_fraction_rational(2.5, 3))
82-
Traceback (most recent call last):
83-
...
84-
ValueError: `x` and `y` must be integers
85-
86-
>>> list(continued_fraction_rational(-649, 200))
71+
>>> list(continued_fraction_rational(Fraction(-649, 200)))
8772
[-4, 1, 3, 12, 4]
8873
89-
>>> list(continued_fraction_rational(123235, 334505))
74+
>>> list(continued_fraction_rational(Fraction(123235, 334505)))
9075
[0, 2, 1, 2, 1, 1, 250, 1, 13]
9176
9277
Notes
@@ -104,16 +89,7 @@ def continued_fraction_rational(x: int, y: int, /) -> Generator[int, None, None]
10489
have the numerator `1`, and it is the simple version that is generated by
10590
the function.
10691
"""
107-
if not (isinstance(x, int) and isinstance(y, int)):
108-
raise ValueError("`x` and `y` must be integers")
109-
110-
if y == 0:
111-
raise ZeroDivisionError("`y` must be non-zero")
112-
113-
num, denom = x, y
114-
115-
if denom < 0:
116-
num, denom = -num, -denom
92+
num, denom = r.as_integer_ratio()
11793

11894
quo, rem = divmod(num, denom)
11995
yield quo
@@ -196,7 +172,7 @@ def continued_fraction_real(x: int | float | str, /) -> Generator[int, None, Non
196172
"""
197173
num, denum = Decimal(str(x)).as_integer_ratio()
198174

199-
for elem in continued_fraction_rational(num, denum):
175+
for elem in continued_fraction_rational(Fraction(num, denum)):
200176
yield elem
201177

202178

src/continuedfractions/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.0.9"
1+
__version__ = "0.10.0"

0 commit comments

Comments
 (0)