foxmath 0.1.2__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: foxmath
3
- Version: 0.1.2
3
+ Version: 0.3.0
4
4
  Summary: Minimalist CLI for math & cryptography exploration 🦊
5
5
  Author-email: Abhrankan Chakrabarti <abhrankan@gmail.com>
6
6
  License: MIT
@@ -27,11 +27,13 @@ Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
27
27
 
28
28
  ## Features
29
29
  - Legendre symbol
30
- - Catalan-inspired π approximation
30
+ - Euler/Hermann Machin-like π approximation
31
31
  - Elliptic curve point addition (over finite fields)
32
- - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd)
32
+ - Chinese Remainder Theorem solver (handles non-coprime moduli)
33
+ - Continued fraction expansion and convergents
34
+ - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd, foxmath-crt, foxmath-cf)
33
35
  - JSON output mode
34
- - Coming soon: CRT, continued fractions, challenge mode, more curves
36
+ - Coming soon: challenge mode, more curves
35
37
 
36
38
  ## Installation
37
39
 
@@ -49,24 +51,61 @@ pip install -e .
49
51
  ## Usage Examples
50
52
 
51
53
  ```bash
52
- # Basic
53
54
  foxmath pi --terms 200
55
+ ```
56
+ ```
57
+ π ≈ 3.1415926535897932384626433832795028841971693993752 (200 terms)
58
+ ```
59
+
60
+ ```bash
54
61
  foxmath legendre 5 17
62
+ ```
63
+ ```
64
+ Legendre (5/17) = -1
65
+ ```
55
66
 
67
+ ```bash
56
68
  # Symlinks (optional)
57
69
  foxmath-ecadd --x1 1 --y1 2 --x2 3 --y2 4 --p 17
70
+ ```
71
+ ```
72
+ Result point: (14, 2)
73
+ ```
58
74
 
59
- # JSON output
60
- foxmath pi --terms 100 --json
75
+ ```bash
76
+ # Chinese Remainder Theorem
77
+ foxmath crt --r 2 3 2 --m 3 5 7
78
+ ```
79
+ ```
80
+ x ≡ 23 (mod 105)
81
+ ```
82
+
83
+ ```bash
84
+ # Continued fraction expansion + convergents
85
+ foxmath cf --num 355 --den 113
86
+ ```
87
+ ```
88
+ [3; 7, 16]
89
+ 3/1
90
+ 22/7
91
+ 355/113
61
92
  ```
62
93
 
63
- ### Example Output
94
+ ```bash
95
+ # JSON output
96
+ foxmath pi --terms 100 --json
64
97
  ```
65
- π ≈ 3.1415926535897932384626433832795028841971693993751 (200 terms)
98
+ ```json
99
+ {
100
+ "tool": "foxmath",
101
+ "command": "pi",
102
+ "terms": 100,
103
+ "approx": "3.1415926535897932384626433832795028841971693993752"
104
+ }
66
105
  ```
67
106
 
68
107
  ## Why FoxMath?
69
- Because math and cryptography are more fun when you can play with them instantly in the terminal — with the same reliability as FoxPipe and fox-vault.
108
+ Because math and cryptography are more fun when you can play with them instantly in the terminal — with the same reliability as [FoxPipe](https://github.com/foxhackerzdevs/FoxPipe) and [fox-vault](https://github.com/foxhackerzdevs/fox-vault).
70
109
 
71
110
  **Simple. Practical. Reliable.**
72
111
 
@@ -8,11 +8,13 @@ Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
8
8
 
9
9
  ## Features
10
10
  - Legendre symbol
11
- - Catalan-inspired π approximation
11
+ - Euler/Hermann Machin-like π approximation
12
12
  - Elliptic curve point addition (over finite fields)
13
- - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd)
13
+ - Chinese Remainder Theorem solver (handles non-coprime moduli)
14
+ - Continued fraction expansion and convergents
15
+ - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd, foxmath-crt, foxmath-cf)
14
16
  - JSON output mode
15
- - Coming soon: CRT, continued fractions, challenge mode, more curves
17
+ - Coming soon: challenge mode, more curves
16
18
 
17
19
  ## Installation
18
20
 
@@ -30,24 +32,61 @@ pip install -e .
30
32
  ## Usage Examples
31
33
 
32
34
  ```bash
33
- # Basic
34
35
  foxmath pi --terms 200
36
+ ```
37
+ ```
38
+ π ≈ 3.1415926535897932384626433832795028841971693993752 (200 terms)
39
+ ```
40
+
41
+ ```bash
35
42
  foxmath legendre 5 17
43
+ ```
44
+ ```
45
+ Legendre (5/17) = -1
46
+ ```
36
47
 
48
+ ```bash
37
49
  # Symlinks (optional)
38
50
  foxmath-ecadd --x1 1 --y1 2 --x2 3 --y2 4 --p 17
51
+ ```
52
+ ```
53
+ Result point: (14, 2)
54
+ ```
39
55
 
40
- # JSON output
41
- foxmath pi --terms 100 --json
56
+ ```bash
57
+ # Chinese Remainder Theorem
58
+ foxmath crt --r 2 3 2 --m 3 5 7
59
+ ```
60
+ ```
61
+ x ≡ 23 (mod 105)
62
+ ```
63
+
64
+ ```bash
65
+ # Continued fraction expansion + convergents
66
+ foxmath cf --num 355 --den 113
67
+ ```
68
+ ```
69
+ [3; 7, 16]
70
+ 3/1
71
+ 22/7
72
+ 355/113
42
73
  ```
43
74
 
44
- ### Example Output
75
+ ```bash
76
+ # JSON output
77
+ foxmath pi --terms 100 --json
45
78
  ```
46
- π ≈ 3.1415926535897932384626433832795028841971693993751 (200 terms)
79
+ ```json
80
+ {
81
+ "tool": "foxmath",
82
+ "command": "pi",
83
+ "terms": 100,
84
+ "approx": "3.1415926535897932384626433832795028841971693993752"
85
+ }
47
86
  ```
48
87
 
49
88
  ## Why FoxMath?
50
- Because math and cryptography are more fun when you can play with them instantly in the terminal — with the same reliability as FoxPipe and fox-vault.
89
+ Because math and cryptography are more fun when you can play with them instantly in the terminal — with the same reliability as [FoxPipe](https://github.com/foxhackerzdevs/FoxPipe) and [fox-vault](https://github.com/foxhackerzdevs/fox-vault).
51
90
 
52
91
  **Simple. Practical. Reliable.**
53
92
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "foxmath"
7
- version = "0.1.2"
7
+ version = "0.3.0"
8
8
  description = "Minimalist CLI for math & cryptography exploration 🦊"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -31,3 +31,5 @@ foxmath = "foxmath.foxmath:main"
31
31
  foxmath-legendre = "foxmath.foxmath:main"
32
32
  foxmath-pi = "foxmath.foxmath:main"
33
33
  foxmath-ecadd = "foxmath.foxmath:main"
34
+ foxmath-crt = "foxmath.foxmath:main"
35
+ foxmath-cf = "foxmath.foxmath:main"
@@ -27,7 +27,7 @@ def _arctan_series(inv_n: Decimal, terms: int) -> Decimal:
27
27
  total += sign * inv_n**(2 * k + 1) / (2 * k + 1)
28
28
  return total
29
29
 
30
- def catalan_pi_approx(terms: int = 100) -> Decimal:
30
+ def euler_hermann_pi_approx(terms: int = 100) -> Decimal:
31
31
  """
32
32
  Fast-converging π approximation via the Euler/Hermann Machin-like
33
33
  identity: pi/4 = arctan(1/2) + arctan(1/3).
@@ -54,6 +54,80 @@ def ec_point_add(x1, y1, x2, y2, a, p):
54
54
  y3 = (lam * (x1 - x3) - y1) % p
55
55
  return x3, y3
56
56
 
57
+ def _extended_gcd(a: int, b: int):
58
+ """Returns (g, x, y) such that a*x + b*y == g == gcd(a, b)."""
59
+ old_r, r = a, b
60
+ old_s, s = 1, 0
61
+ old_t, t = 0, 1
62
+ while r != 0:
63
+ q = old_r // r
64
+ old_r, r = r, old_r - q * r
65
+ old_s, s = s, old_s - q * s
66
+ old_t, t = t, old_t - q * t
67
+ return old_r, old_s, old_t
68
+
69
+ def _combine_crt(r1: int, m1: int, r2: int, m2: int):
70
+ """Combine x≡r1 (mod m1) and x≡r2 (mod m2) into a single x≡r (mod lcm(m1,m2))."""
71
+ g, p, _ = _extended_gcd(m1, m2)
72
+ if (r2 - r1) % g != 0:
73
+ raise ValueError(
74
+ f"No solution exists: moduli {m1} and {m2} are inconsistent "
75
+ f"for remainders {r1} and {r2}."
76
+ )
77
+ lcm = m1 // g * m2
78
+ x = (r1 + (r2 - r1) // g * p % (m2 // g) * m1) % lcm
79
+ return x, lcm
80
+
81
+ def crt_solve(remainders, moduli):
82
+ """
83
+ Chinese Remainder Theorem, generalized to non-pairwise-coprime moduli.
84
+ Returns (x, m) such that x is the unique solution mod m = lcm(moduli),
85
+ or raises ValueError if the system is inconsistent.
86
+ """
87
+ if len(remainders) != len(moduli):
88
+ raise ValueError("remainders and moduli must have the same length")
89
+ if not remainders:
90
+ raise ValueError("need at least one congruence")
91
+ if any(m <= 0 for m in moduli):
92
+ raise ValueError("all moduli must be positive")
93
+
94
+ r, m = remainders[0] % moduli[0], moduli[0]
95
+ for ri, mi in zip(remainders[1:], moduli[1:]):
96
+ r, m = _combine_crt(r, m, ri % mi, mi)
97
+ return r, m
98
+
99
+ def continued_fraction(num: int, den: int, max_terms: int = 100):
100
+ """
101
+ Continued fraction expansion [a0; a1, a2, ...] of num/den via the
102
+ Euclidean algorithm. Terminates naturally for any rational input.
103
+ """
104
+ if den == 0:
105
+ raise ValueError("denominator cannot be zero")
106
+ cf = []
107
+ n, d = num, den
108
+ while d != 0 and len(cf) < max_terms:
109
+ a = n // d
110
+ cf.append(a)
111
+ n, d = d, n - a * d
112
+ return cf
113
+
114
+ def cf_convergents(cf):
115
+ """
116
+ Convergents (p_n, q_n) of a continued fraction, via the standard
117
+ recurrence h_n = a_n*h_(n-1) + h_(n-2), same for k_n, with
118
+ h_(-2)=0, h_(-1)=1, k_(-2)=1, k_(-1)=0.
119
+ """
120
+ convergents = []
121
+ h2, h1 = 0, 1
122
+ k2, k1 = 1, 0
123
+ for a in cf:
124
+ h = a * h1 + h2
125
+ k = a * k1 + k2
126
+ convergents.append((h, k))
127
+ h2, h1 = h1, h
128
+ k2, k1 = k1, k
129
+ return convergents
130
+
57
131
  # ------------------- CLI -------------------
58
132
 
59
133
  def get_command():
@@ -92,7 +166,7 @@ def main():
92
166
  leg.add_argument("p", type=int, help="Odd prime p")
93
167
 
94
168
  # Pi
95
- pi_cmd = subparsers.add_parser("pi", help="Catalan-inspired π approximation")
169
+ pi_cmd = subparsers.add_parser("pi", help="Euler/Hermann Machin-like π approximation")
96
170
  pi_cmd.add_argument("--terms", type=int, default=100)
97
171
 
98
172
  # Elliptic curve add
@@ -104,6 +178,17 @@ def main():
104
178
  ec.add_argument("--a", type=int, default=-3, help="Curve parameter a")
105
179
  ec.add_argument("--p", type=int, required=True, help="Prime modulus")
106
180
 
181
+ # Chinese Remainder Theorem
182
+ crt = subparsers.add_parser("crt", help="Solve a system of congruences x = r (mod m)")
183
+ crt.add_argument("--r", type=int, nargs="+", required=True, help="Remainders, e.g. --r 2 3 2")
184
+ crt.add_argument("--m", type=int, nargs="+", required=True, help="Moduli, e.g. --m 3 5 7")
185
+
186
+ # Continued fractions
187
+ cf_cmd = subparsers.add_parser("cf", help="Continued fraction expansion and convergents")
188
+ cf_cmd.add_argument("--num", type=int, required=True, help="Numerator")
189
+ cf_cmd.add_argument("--den", type=int, required=True, help="Denominator")
190
+ cf_cmd.add_argument("--terms", type=int, default=100, help="Max terms to expand")
191
+
107
192
  args = parser.parse_args(argv)
108
193
 
109
194
  result = {"tool": "foxmath"}
@@ -116,7 +201,7 @@ def main():
116
201
  print(f"Legendre ({args.a}/{args.p}) = {ls}")
117
202
 
118
203
  elif cmd == "pi" or args.command == "pi":
119
- approx = catalan_pi_approx(args.terms)
204
+ approx = euler_hermann_pi_approx(args.terms)
120
205
  result.update({"command": "pi", "terms": args.terms, "approx": str(approx)})
121
206
  if not json_output:
122
207
  print(f"π ≈ {approx} ({args.terms} terms)")
@@ -127,6 +212,24 @@ def main():
127
212
  if not json_output:
128
213
  print(f"Result point: ({x3}, {y3})")
129
214
 
215
+ elif cmd == "crt" or args.command == "crt":
216
+ x, m = crt_solve(args.r, args.m)
217
+ result.update({"command": "crt", "r": args.r, "m": args.m, "x": x, "mod": m})
218
+ if not json_output:
219
+ print(f"x ≡ {x} (mod {m})")
220
+
221
+ elif cmd == "cf" or args.command == "cf":
222
+ cf = continued_fraction(args.num, args.den, args.terms)
223
+ convs = cf_convergents(cf)
224
+ result.update({
225
+ "command": "cf", "num": args.num, "den": args.den,
226
+ "cf": cf, "convergents": convs,
227
+ })
228
+ if not json_output:
229
+ print(f"[{cf[0]}; {', '.join(str(a) for a in cf[1:])}]")
230
+ for p, q in convs:
231
+ print(f" {p}/{q}")
232
+
130
233
  except Exception as e:
131
234
  print(f"Error: {e}", file=sys.stderr)
132
235
  sys.exit(1)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: foxmath
3
- Version: 0.1.2
3
+ Version: 0.3.0
4
4
  Summary: Minimalist CLI for math & cryptography exploration 🦊
5
5
  Author-email: Abhrankan Chakrabarti <abhrankan@gmail.com>
6
6
  License: MIT
@@ -27,11 +27,13 @@ Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
27
27
 
28
28
  ## Features
29
29
  - Legendre symbol
30
- - Catalan-inspired π approximation
30
+ - Euler/Hermann Machin-like π approximation
31
31
  - Elliptic curve point addition (over finite fields)
32
- - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd)
32
+ - Chinese Remainder Theorem solver (handles non-coprime moduli)
33
+ - Continued fraction expansion and convergents
34
+ - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd, foxmath-crt, foxmath-cf)
33
35
  - JSON output mode
34
- - Coming soon: CRT, continued fractions, challenge mode, more curves
36
+ - Coming soon: challenge mode, more curves
35
37
 
36
38
  ## Installation
37
39
 
@@ -49,24 +51,61 @@ pip install -e .
49
51
  ## Usage Examples
50
52
 
51
53
  ```bash
52
- # Basic
53
54
  foxmath pi --terms 200
55
+ ```
56
+ ```
57
+ π ≈ 3.1415926535897932384626433832795028841971693993752 (200 terms)
58
+ ```
59
+
60
+ ```bash
54
61
  foxmath legendre 5 17
62
+ ```
63
+ ```
64
+ Legendre (5/17) = -1
65
+ ```
55
66
 
67
+ ```bash
56
68
  # Symlinks (optional)
57
69
  foxmath-ecadd --x1 1 --y1 2 --x2 3 --y2 4 --p 17
70
+ ```
71
+ ```
72
+ Result point: (14, 2)
73
+ ```
58
74
 
59
- # JSON output
60
- foxmath pi --terms 100 --json
75
+ ```bash
76
+ # Chinese Remainder Theorem
77
+ foxmath crt --r 2 3 2 --m 3 5 7
78
+ ```
79
+ ```
80
+ x ≡ 23 (mod 105)
81
+ ```
82
+
83
+ ```bash
84
+ # Continued fraction expansion + convergents
85
+ foxmath cf --num 355 --den 113
86
+ ```
87
+ ```
88
+ [3; 7, 16]
89
+ 3/1
90
+ 22/7
91
+ 355/113
61
92
  ```
62
93
 
63
- ### Example Output
94
+ ```bash
95
+ # JSON output
96
+ foxmath pi --terms 100 --json
64
97
  ```
65
- π ≈ 3.1415926535897932384626433832795028841971693993751 (200 terms)
98
+ ```json
99
+ {
100
+ "tool": "foxmath",
101
+ "command": "pi",
102
+ "terms": 100,
103
+ "approx": "3.1415926535897932384626433832795028841971693993752"
104
+ }
66
105
  ```
67
106
 
68
107
  ## Why FoxMath?
69
- Because math and cryptography are more fun when you can play with them instantly in the terminal — with the same reliability as FoxPipe and fox-vault.
108
+ Because math and cryptography are more fun when you can play with them instantly in the terminal — with the same reliability as [FoxPipe](https://github.com/foxhackerzdevs/FoxPipe) and [fox-vault](https://github.com/foxhackerzdevs/fox-vault).
70
109
 
71
110
  **Simple. Practical. Reliable.**
72
111
 
@@ -1,5 +1,7 @@
1
1
  [console_scripts]
2
2
  foxmath = foxmath.foxmath:main
3
+ foxmath-cf = foxmath.foxmath:main
4
+ foxmath-crt = foxmath.foxmath:main
3
5
  foxmath-ecadd = foxmath.foxmath:main
4
6
  foxmath-legendre = foxmath.foxmath:main
5
7
  foxmath-pi = foxmath.foxmath:main
@@ -79,6 +79,18 @@ class TestSymlinkInvocation(unittest.TestCase):
79
79
  self.assertEqual(code, 0)
80
80
  self.assertIn("Result point:", out)
81
81
 
82
+ def test_crt_symlink(self):
83
+ script = self._make_symlink_script("foxmath-crt")
84
+ code, out, err = run("--r", "2", "3", "2", "--m", "3", "5", "7", script=script)
85
+ self.assertEqual(code, 0)
86
+ self.assertIn("x ≡ 23 (mod 105)", out)
87
+
88
+ def test_cf_symlink(self):
89
+ script = self._make_symlink_script("foxmath-cf")
90
+ code, out, err = run("--num", "355", "--den", "113", script=script)
91
+ self.assertEqual(code, 0)
92
+ self.assertIn("[3; 7, 16]", out)
93
+
82
94
  def test_symlink_with_json(self):
83
95
  script = self._make_symlink_script("foxmath-legendre")
84
96
  code, out, err = run("2", "7", "--json", script=script)
@@ -98,6 +110,43 @@ class TestNormalUsage(unittest.TestCase):
98
110
  self.assertEqual(code, 1)
99
111
  self.assertIn("point at infinity", err)
100
112
 
113
+ def test_crt(self):
114
+ code, out, err = run("crt", "--r", "2", "3", "2", "--m", "3", "5", "7")
115
+ self.assertEqual(code, 0)
116
+ self.assertIn("x ≡ 23 (mod 105)", out)
117
+
118
+ def test_crt_json_is_pure_json(self):
119
+ code, out, err = run("crt", "--r", "2", "3", "2", "--m", "3", "5", "7", "--json")
120
+ self.assertEqual(code, 0)
121
+ import json
122
+ parsed = json.loads(out)
123
+ self.assertEqual(parsed["x"], 23)
124
+ self.assertEqual(parsed["mod"], 105)
125
+
126
+ def test_crt_inconsistent_system_errors(self):
127
+ code, out, err = run("crt", "--r", "1", "2", "--m", "4", "6")
128
+ self.assertEqual(code, 1)
129
+ self.assertIn("inconsistent", err)
130
+
131
+ def test_cf(self):
132
+ code, out, err = run("cf", "--num", "355", "--den", "113")
133
+ self.assertEqual(code, 0)
134
+ self.assertIn("[3; 7, 16]", out)
135
+ self.assertIn("355/113", out)
136
+
137
+ def test_cf_json_is_pure_json(self):
138
+ code, out, err = run("cf", "--num", "355", "--den", "113", "--json")
139
+ self.assertEqual(code, 0)
140
+ import json
141
+ parsed = json.loads(out)
142
+ self.assertEqual(parsed["cf"], [3, 7, 16])
143
+ self.assertEqual(parsed["convergents"][-1], [355, 113])
144
+
145
+ def test_cf_zero_denominator_errors(self):
146
+ code, out, err = run("cf", "--num", "5", "--den", "0")
147
+ self.assertEqual(code, 1)
148
+ self.assertIn("Error", err)
149
+
101
150
 
102
151
  if __name__ == "__main__":
103
152
  unittest.main()
@@ -12,7 +12,10 @@ from decimal import Decimal
12
12
 
13
13
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")))
14
14
 
15
- from foxmath.foxmath import legendre_symbol, catalan_pi_approx, ec_point_add
15
+ from foxmath.foxmath import (
16
+ legendre_symbol, euler_hermann_pi_approx, ec_point_add, crt_solve,
17
+ continued_fraction, cf_convergents,
18
+ )
16
19
 
17
20
  # Reference pi digits (mpmath, 250 dps of working precision), "3" + fractional digits.
18
21
  PI_REFERENCE = Decimal(
@@ -47,16 +50,16 @@ class TestLegendreSymbol(unittest.TestCase):
47
50
  legendre_symbol(3, 1)
48
51
 
49
52
 
50
- class TestCatalanPiApprox(unittest.TestCase):
53
+ class TestEulerHermannPiApprox(unittest.TestCase):
51
54
  def test_converges_to_pi(self):
52
55
  # Regression check: this used to converge to 16*arctan(1/2) =~ 7.418,
53
56
  # not pi, due to a double-counted scaling factor.
54
- approx = catalan_pi_approx(100)
57
+ approx = euler_hermann_pi_approx(100)
55
58
  self.assertLess(abs(approx - PI_REFERENCE), Decimal("1e-40"))
56
59
 
57
60
  def test_more_terms_is_more_accurate(self):
58
- err_10 = abs(catalan_pi_approx(10) - PI_REFERENCE)
59
- err_50 = abs(catalan_pi_approx(50) - PI_REFERENCE)
61
+ err_10 = abs(euler_hermann_pi_approx(10) - PI_REFERENCE)
62
+ err_50 = abs(euler_hermann_pi_approx(50) - PI_REFERENCE)
60
63
  self.assertLess(err_50, err_10)
61
64
 
62
65
 
@@ -82,5 +85,72 @@ class TestECPointAdd(unittest.TestCase):
82
85
  ec_point_add(3, 6, 3, self.P - 6, self.A, self.P)
83
86
 
84
87
 
88
+ class TestCRT(unittest.TestCase):
89
+ def test_classic_coprime_case(self):
90
+ x, m = crt_solve([2, 3, 2], [3, 5, 7])
91
+ self.assertEqual((x, m), (23, 105))
92
+ self.assertEqual(x % 3, 2)
93
+ self.assertEqual(x % 5, 3)
94
+ self.assertEqual(x % 7, 2)
95
+
96
+ def test_non_coprime_consistent(self):
97
+ x, m = crt_solve([2, 2], [4, 6])
98
+ self.assertEqual((x, m), (2, 12))
99
+
100
+ def test_non_coprime_inconsistent_raises(self):
101
+ with self.assertRaises(ValueError):
102
+ crt_solve([1, 2], [4, 6])
103
+
104
+ def test_single_congruence(self):
105
+ self.assertEqual(crt_solve([5], [11]), (5, 11))
106
+
107
+ def test_mismatched_lengths_raises(self):
108
+ with self.assertRaises(ValueError):
109
+ crt_solve([2, 3], [3, 5, 7])
110
+
111
+ def test_empty_raises(self):
112
+ with self.assertRaises(ValueError):
113
+ crt_solve([], [])
114
+
115
+ def test_negative_remainder_normalized(self):
116
+ x, m = crt_solve([-1, 3], [5, 7])
117
+ self.assertEqual(x % 5, 4) # -1 mod 5 == 4
118
+ self.assertEqual(x % 7, 3)
119
+
120
+
121
+ class TestContinuedFractions(unittest.TestCase):
122
+ def test_classic_pi_approximation(self):
123
+ cf = continued_fraction(355, 113)
124
+ self.assertEqual(cf, [3, 7, 16])
125
+ convs = cf_convergents(cf)
126
+ self.assertEqual(convs, [(3, 1), (22, 7), (355, 113)])
127
+
128
+ def test_convergents_reduce_to_final_fraction(self):
129
+ cf = continued_fraction(19, 7)
130
+ convs = cf_convergents(cf)
131
+ self.assertEqual(convs[-1], (19, 7))
132
+
133
+ def test_negative_numerator(self):
134
+ cf = continued_fraction(-19, 7)
135
+ convs = cf_convergents(cf)
136
+ self.assertEqual(convs[-1], (-19, 7))
137
+
138
+ def test_zero_denominator_raises(self):
139
+ with self.assertRaises(ValueError):
140
+ continued_fraction(5, 0)
141
+
142
+ def test_randomized_against_fractions_module(self):
143
+ import random
144
+ from fractions import Fraction
145
+ random.seed(42)
146
+ for _ in range(200):
147
+ n = random.randint(-10000, 10000)
148
+ d = random.randint(1, 10000)
149
+ cf = continued_fraction(n, d)
150
+ convs = cf_convergents(cf)
151
+ expected = Fraction(n, d)
152
+ self.assertEqual(convs[-1], (expected.numerator, expected.denominator))
153
+
154
+
85
155
  if __name__ == "__main__":
86
156
  unittest.main()
File without changes
File without changes
File without changes