Repository: sympy/sympy

Issue:
Let `continued_fraction_convergents` accept periodic continued fraction
The motivation is to use `continued_fraction_convergents` to find fundamental solutions to [Pell's equation](https://en.wikipedia.org/wiki/Pell%27s_equation). (Edit: I am aware that the Diophantine solver can be used if I just want a solution. In my use case I only want solutions below a certain bound. Using continued fractions allows me to cut the search if no solution is found within the bound, while the Diophantine solver may take too long if the fundamental solution is very large.)

`continued_fraction_convergents` only accepts iterables of integers and does not accept the usual representation of periodic continued fraction. The documentation suggests using `continued_fraction_iterator`, but it is implemented naively and is not efficient for quadratic irrationals.  So to efficiently enumerate convergents of quadratic irrationals, I have to create an efficient iterator from the result of `continued_fraction` myself, like the following:
```py
from sympy.ntheory.continued_fraction import continued_fraction
import itertools

# Only works for rationals and quadratic irrationals
def fast_continued_fraction_iterator(x):
    cf = continued_fraction(x)
    if isinstance(cf[-1], list):
        return itertools.chain(cf[:-1], itertools.cycle(cf[-1]))
    else:
        return cf
```
I would like to add a code path in `continued_fraction_convergents` to check if the input looks like a periodic continued fraction representation, and convert it to an iterator like above.

Discussion:

Patch:
diff --git a/.mailmap b/.mailmap
index 9f6ea36991..cc0769a17b 100644
--- a/.mailmap
+++ b/.mailmap
@@ -837,6 +837,7 @@ Leonid Blouvshtein <leonidbl91@gmail.com>
 Leonid Kovalev <leonidvkovalev@gmail.com> <leonid.traveling@gmail.com>
 Leonid Kovalev <leonidvkovalev@gmail.com> <normalhuman@users.noreply.github.com>
 Lev Chelyadinov <leva181777@gmail.com>
+Liwei Cai <cai.lw123@gmail.com>
 Ljubiša Moćić <3rdslasher@gmail.com>
 Lokesh Sharma <lokeshhsharma@gmail.com> <your_email@youremail.com>
 Longqi Wang <iqgnol@gmail.com>
diff --git a/sympy/ntheory/continued_fraction.py b/sympy/ntheory/continued_fraction.py
index 579ac3d4cd..62f8e2d729 100644
--- a/sympy/ntheory/continued_fraction.py
+++ b/sympy/ntheory/continued_fraction.py
@@ -1,4 +1,5 @@
 from __future__ import annotations
+import itertools
 from sympy.core.exprtools import factor_terms
 from sympy.core.numbers import Integer, Rational
 from sympy.core.singleton import S
@@ -304,11 +305,15 @@ def continued_fraction_convergents(cf):
     """
     Return an iterator over the convergents of a continued fraction (cf).
 
-    The parameter should be an iterable returning successive
-    partial quotients of the continued fraction, such as might be
-    returned by continued_fraction_iterator.  In computing the
-    convergents, the continued fraction need not be strictly in
-    canonical form (all integers, all but the first positive).
+    The parameter should be in either of the following to forms:
+    - A list of partial quotients, possibly with the last element being a list
+    of repeating partial quotients, such as might be returned by
+    continued_fraction and continued_fraction_periodic.
+    - An iterable returning successive partial quotients of the continued
+    fraction, such as might be returned by continued_fraction_iterator.
+
+    In computing the convergents, the continued fraction need not be strictly
+    in canonical form (all integers, all but the first positive).
     Rational and negative elements may be present in the expansion.
 
     Examples
@@ -336,12 +341,25 @@ def continued_fraction_convergents(cf):
     104348/33215
     208341/66317
 
+    >>> it = continued_fraction_convergents([1, [1, 2]])  # sqrt(3)
+    >>> for n in range(7):
+    ...     print(next(it))
+    1
+    2
+    5/3
+    7/4
+    19/11
+    26/15
+    71/41
+
     See Also
     ========
 
-    continued_fraction_iterator
+    continued_fraction_iterator, continued_fraction, continued_fraction_periodic
 
     """
+    if isinstance(cf, list) and isinstance(cf[-1], list):
+        cf = itertools.chain(cf[:-1], itertools.cycle(cf[-1]))
     p_2, q_2 = S.Zero, S.One
     p_1, q_1 = S.One, S.Zero
     for a in cf:


Test patch:
diff --git a/sympy/ntheory/tests/test_continued_fraction.py b/sympy/ntheory/tests/test_continued_fraction.py
index babab948f1..8ca6088507 100644
--- a/sympy/ntheory/tests/test_continued_fraction.py
+++ b/sympy/ntheory/tests/test_continued_fraction.py
@@ -1,3 +1,4 @@
+import itertools
 from sympy.core import GoldenRatio as phi
 from sympy.core.numbers import (Rational, pi)
 from sympy.core.singleton import S
@@ -45,15 +46,10 @@ def test_continued_fraction():
     assert cf_p(6589, 2569) == [2, 1, 1, 3, 2, 1, 3, 1, 23]
 
     def take(iterator, n=7):
-        res = []
-        for i, t in enumerate(cf_i(iterator)):
-            if i >= n:
-                break
-            res.append(t)
-        return res
+        return list(itertools.islice(iterator, n))
 
-    assert take(phi) == [1, 1, 1, 1, 1, 1, 1]
-    assert take(pi) == [3, 7, 15, 1, 292, 1, 1]
+    assert take(cf_i(phi)) == [1, 1, 1, 1, 1, 1, 1]
+    assert take(cf_i(pi)) == [3, 7, 15, 1, 292, 1, 1]
 
     assert list(cf_i(Rational(17, 12))) == [1, 2, 2, 2]
     assert list(cf_i(Rational(-17, 12))) == [-2, 1, 1, 2, 2]
@@ -63,6 +59,14 @@ def take(iterator, n=7):
     assert list(cf_c([1, 1, 1, 1, 1, 1, 1])) == [S.One, S(2), Rational(3, 2), Rational(5, 3),
                                                  Rational(8, 5), Rational(13, 8), Rational(21, 13)]
     assert list(cf_c([1, 6, Rational(-1, 2), 4])) == [S.One, Rational(7, 6), Rational(5, 4), Rational(3, 2)]
+    assert take(cf_c([[1]])) == [S.One, S(2), Rational(3, 2), Rational(5, 3), Rational(8, 5),
+                                 Rational(13, 8), Rational(21, 13)]
+    assert take(cf_c([1, [1, 2]])) == [S.One, S(2), Rational(5, 3), Rational(7, 4), Rational(19, 11),
+                                    Rational(26, 15), Rational(71, 41)]
+
+    cf_iter_e = (2 if i == 1 else i // 3 * 2 if i % 3 == 0 else 1 for i in itertools.count(1))
+    assert take(cf_c(cf_iter_e)) == [S(2), S(3), Rational(8, 3), Rational(11, 4), Rational(19, 7),
+                                     Rational(87, 32), Rational(106, 39)]
 
     assert cf_r([1, 6, 1, 8]) == Rational(71, 62)
     assert cf_r([3]) == S(3)