foxmath 0.2.0__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.2.0
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
@@ -30,9 +30,10 @@ Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
30
30
  - Euler/Hermann Machin-like π approximation
31
31
  - Elliptic curve point addition (over finite fields)
32
32
  - Chinese Remainder Theorem solver (handles non-coprime moduli)
33
- - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd, foxmath-crt)
33
+ - Continued fraction expansion and convergents
34
+ - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd, foxmath-crt, foxmath-cf)
34
35
  - JSON output mode
35
- - Coming soon: continued fractions, challenge mode, more curves
36
+ - Coming soon: challenge mode, more curves
36
37
 
37
38
  ## Installation
38
39
 
@@ -79,6 +80,17 @@ foxmath crt --r 2 3 2 --m 3 5 7
79
80
  x ≡ 23 (mod 105)
80
81
  ```
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
92
+ ```
93
+
82
94
  ```bash
83
95
  # JSON output
84
96
  foxmath pi --terms 100 --json
@@ -11,9 +11,10 @@ Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
11
11
  - Euler/Hermann Machin-like π approximation
12
12
  - Elliptic curve point addition (over finite fields)
13
13
  - Chinese Remainder Theorem solver (handles non-coprime moduli)
14
- - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd, foxmath-crt)
14
+ - Continued fraction expansion and convergents
15
+ - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd, foxmath-crt, foxmath-cf)
15
16
  - JSON output mode
16
- - Coming soon: continued fractions, challenge mode, more curves
17
+ - Coming soon: challenge mode, more curves
17
18
 
18
19
  ## Installation
19
20
 
@@ -60,6 +61,17 @@ foxmath crt --r 2 3 2 --m 3 5 7
60
61
  x ≡ 23 (mod 105)
61
62
  ```
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
73
+ ```
74
+
63
75
  ```bash
64
76
  # JSON output
65
77
  foxmath pi --terms 100 --json
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "foxmath"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Minimalist CLI for math & cryptography exploration 🦊"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -32,3 +32,4 @@ foxmath-legendre = "foxmath.foxmath:main"
32
32
  foxmath-pi = "foxmath.foxmath:main"
33
33
  foxmath-ecadd = "foxmath.foxmath:main"
34
34
  foxmath-crt = "foxmath.foxmath:main"
35
+ foxmath-cf = "foxmath.foxmath:main"
@@ -96,6 +96,38 @@ def crt_solve(remainders, moduli):
96
96
  r, m = _combine_crt(r, m, ri % mi, mi)
97
97
  return r, m
98
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
+
99
131
  # ------------------- CLI -------------------
100
132
 
101
133
  def get_command():
@@ -151,6 +183,12 @@ def main():
151
183
  crt.add_argument("--r", type=int, nargs="+", required=True, help="Remainders, e.g. --r 2 3 2")
152
184
  crt.add_argument("--m", type=int, nargs="+", required=True, help="Moduli, e.g. --m 3 5 7")
153
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
+
154
192
  args = parser.parse_args(argv)
155
193
 
156
194
  result = {"tool": "foxmath"}
@@ -180,6 +218,18 @@ def main():
180
218
  if not json_output:
181
219
  print(f"x ≡ {x} (mod {m})")
182
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
+
183
233
  except Exception as e:
184
234
  print(f"Error: {e}", file=sys.stderr)
185
235
  sys.exit(1)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: foxmath
3
- Version: 0.2.0
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
@@ -30,9 +30,10 @@ Built with curiosity — for students, tinkerers, and crypto/math enthusiasts.
30
30
  - Euler/Hermann Machin-like π approximation
31
31
  - Elliptic curve point addition (over finite fields)
32
32
  - Chinese Remainder Theorem solver (handles non-coprime moduli)
33
- - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd, foxmath-crt)
33
+ - Continued fraction expansion and convergents
34
+ - Symlink-friendly (foxmath-legendre, foxmath-pi, foxmath-ecadd, foxmath-crt, foxmath-cf)
34
35
  - JSON output mode
35
- - Coming soon: continued fractions, challenge mode, more curves
36
+ - Coming soon: challenge mode, more curves
36
37
 
37
38
  ## Installation
38
39
 
@@ -79,6 +80,17 @@ foxmath crt --r 2 3 2 --m 3 5 7
79
80
  x ≡ 23 (mod 105)
80
81
  ```
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
92
+ ```
93
+
82
94
  ```bash
83
95
  # JSON output
84
96
  foxmath pi --terms 100 --json
@@ -1,5 +1,6 @@
1
1
  [console_scripts]
2
2
  foxmath = foxmath.foxmath:main
3
+ foxmath-cf = foxmath.foxmath:main
3
4
  foxmath-crt = foxmath.foxmath:main
4
5
  foxmath-ecadd = foxmath.foxmath:main
5
6
  foxmath-legendre = foxmath.foxmath:main
@@ -85,6 +85,12 @@ class TestSymlinkInvocation(unittest.TestCase):
85
85
  self.assertEqual(code, 0)
86
86
  self.assertIn("x ≡ 23 (mod 105)", out)
87
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
+
88
94
  def test_symlink_with_json(self):
89
95
  script = self._make_symlink_script("foxmath-legendre")
90
96
  code, out, err = run("2", "7", "--json", script=script)
@@ -122,6 +128,25 @@ class TestNormalUsage(unittest.TestCase):
122
128
  self.assertEqual(code, 1)
123
129
  self.assertIn("inconsistent", err)
124
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
+
125
150
 
126
151
  if __name__ == "__main__":
127
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, euler_hermann_pi_approx, ec_point_add, crt_solve
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(
@@ -115,5 +118,39 @@ class TestCRT(unittest.TestCase):
115
118
  self.assertEqual(x % 7, 3)
116
119
 
117
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
+
118
155
  if __name__ == "__main__":
119
156
  unittest.main()
File without changes
File without changes
File without changes