masts 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: masts
3
+ Version: 0.1.0
4
+ Summary: Step-by-step solutions for math expressions and equations (1 + 2 x 3 -> ['1 + 6', '7']), plus text-to-symbol helpers (6^6 -> 6⁶).
5
+ Author: PipoAndDodo
6
+ License-Expression: MIT
7
+ Keywords: math,steps,solver,equation,calculator,superscript,education
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Education
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Education
18
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Dynamic: license-file
24
+
25
+ # math-step-to-step
26
+
27
+ Giải biểu thức / phương trình **từng bước** và trả về danh sách các bước.
28
+ Không phụ thuộc thư viện ngoài, dùng phân số chính xác (`fractions.Fraction`) nên không bị sai số.
29
+
30
+ *Step-by-step solver for math expressions and equations. Pure Python, zero dependencies, exact arithmetic.*
31
+
32
+ ```bash
33
+ pip install math-step-to-step
34
+ ```
35
+
36
+ ## Dùng nhanh
37
+
38
+ ```python
39
+ from math_step_to_step import steps, solve, evaluate, prettify
40
+
41
+ steps("1 + 2 x 3") # ['1 + 6', '7']
42
+ steps("(1 + 2) x 3 + 4 x 5") # ['3 × 3 + 4 × 5', '9 + 20', '29']
43
+ steps("2 + 3 x (4 - 1)^2") # ['2 + 3 × 3²', '2 + 3 × 9', '2 + 27', '29']
44
+ steps("1/2 + 1/3") # ['1/2 + 1/3', '5/6']
45
+
46
+ # tìm x
47
+ steps("2x + 3 = 7") # ['2x = 7 - 3', '2x = 4', 'x = 4 ÷ 2', 'x = 2']
48
+ steps("2(x + 3) = 10") # ['2x + 6 = 10', '2x = 10 - 6', '2x = 4', 'x = 4 ÷ 2', 'x = 2']
49
+ steps("x^2 - 3x + 2 = 0") # a, b, c -> Δ -> x₁, x₂ (từng bước)
50
+
51
+ # kết quả có cấu trúc
52
+ sol = solve("x^2 - 3x + 2 = 0")
53
+ sol.kind, sol.values # ('roots', (2, 1))
54
+
55
+ evaluate("2 + 3 x 4") # 14 (nhanh, không tạo chuỗi bước)
56
+
57
+ # đổi chuỗi thành ký hiệu
58
+ prettify("6^6") # '6⁶'
59
+ prettify("x^2 + y^(n+1) <= sqrt(2)*pi") # 'x² + yⁿ⁺¹ ≤ √2×π'
60
+ ```
61
+
62
+ ## Cú pháp nhập
63
+
64
+ | Viết | Ý nghĩa |
65
+ |---|---|
66
+ | `+ - * x × · / ÷ :` | cộng, trừ, nhân, chia |
67
+ | `x` giữa hai toán hạng, vd `2 x 3` | **phép nhân**; còn `2x`, `x + 1`, `x²` thì `x` là **biến** |
68
+ | `^`, `**`, `²` `³` | lũy thừa (số mũ nguyên) |
69
+ | `sqrt(…)`, `√9` | căn bậc hai |
70
+ | `( )`, `[ ]`, `{ }` | ngoặc |
71
+ | `2x`, `3(x+1)`, `(a)(b)` | nhân ngầm |
72
+ | `1,5` hoặc `1.5` | số thập phân |
73
+ | `=` | phương trình (1 biến, bậc 1 hoặc 2) |
74
+
75
+ ## Tùy chọn của `steps()` / `solve()`
76
+
77
+ | Tham số | Mặc định | Ý nghĩa |
78
+ |---|---|---|
79
+ | `include_original` | `False` | thêm biểu thức gốc làm bước đầu |
80
+ | `detailed` | `False` | mỗi bước chỉ làm 1 phép (`1+2+3` → `3 + 3` → `6`) thay vì gộp |
81
+ | `pretty` | `True` | dùng `× ÷ ² √`; `False` ra ASCII (`* / ^ sqrt()`), có thể đưa lại vào thư viện |
82
+ | `style` | `"auto"` | `"fraction"`, `"decimal"` hoặc `"auto"` (thập phân nếu đầu vào có số thập phân) |
83
+ | `lang` | `"vi"` | ngôn ngữ thông báo: `"vi"` / `"en"` |
84
+
85
+ Cách chọn bước: ngoặc trước → lũy thừa/căn → nhân chia → cộng trừ; các phép độc lập được làm **cùng một bước**
86
+ và chuỗi cộng/nhân liên tiếp được gộp để lời giải ngắn nhất. Với phương trình: rút gọn → chuyển vế → chia hệ số;
87
+ bậc 2 dùng Δ.
88
+
89
+ ## Ký hiệu
90
+
91
+ ```python
92
+ from math_step_to_step import superscript, subscript, prettify
93
+ superscript("123") # '¹²³'
94
+ subscript("12") # '₁₂'
95
+ prettify("H_2O, 6^6, x^(n+1), 1/2, a<=b, a!=b, +-3, alpha")
96
+ # 'H₂O, 6⁶, xⁿ⁺¹, ½, a≤b, a≠b, ±3, α'
97
+ ```
98
+
99
+ ## Giới hạn (cố ý, để ít lỗi)
100
+ Một biến; phương trình bậc ≤ 2; số mũ nguyên; không có số phức; không rút gọn căn phức tạp
101
+ (căn không chính phương được rút gọn dạng `2 × √3` và cho giá trị gần đúng). Đầu vào sai/không hỗ trợ
102
+ luôn ném `MathStepError` (kế thừa `ValueError`) với thông báo rõ ràng.
103
+
104
+ ## Dòng lệnh
105
+
106
+ ```bash
107
+ math-step-to-step "(1 + 2) x 3 + 4 x 5"
108
+ python -m math_step_to_step "2x + 3 = 7" --en
109
+ ```
110
+
111
+ MIT License.
@@ -0,0 +1,19 @@
1
+ masts-0.1.0.dist-info/licenses/LICENSE,sha256=HuddgMduGRH3Rmjsz_kIc1AXIsYnUX0OaWcZmBo2P7E,1060
2
+ math_step_to_step/__init__.py,sha256=qALbk7msTQSaF8vzIls-2hENsO4LoWl06HdXOg-E9gI,384
3
+ math_step_to_step/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
4
+ math_step_to_step/cli.py,sha256=b9y7uaibfXj0cJlYPckDCryd1Z35KhwQwDHaiW4uIrA,1472
5
+ math_step_to_step/context.py,sha256=_B18eX3AxXF2y_nqdtlgAf8bNw3AA_2iyGiL61ifPjU,834
6
+ math_step_to_step/engine.py,sha256=a5nBfypqPw6vBbTdob0fJfDEXDgTpfO0aEu0mZpsZuY,9425
7
+ math_step_to_step/errors.py,sha256=VR1TnwJvQWLx6zesagS1_OoHXqGvFGpIeCANOz6cEC0,3276
8
+ math_step_to_step/fmt.py,sha256=QTgwXSWHYd27yiRDDo6kUKXaMVq3YyOwygslVHI0Exs,4434
9
+ math_step_to_step/nodes.py,sha256=bWmML_X38s5OEscRIb8_QX9eE-Fg87tPXZXnj7kK4sg,2663
10
+ math_step_to_step/parser.py,sha256=0CS0wkRO63qr2ovxnQhSYFq5pehVFX_ddowcJ6fetds,6636
11
+ math_step_to_step/poly.py,sha256=Z8AXkyDqqh_mJi7qJ0zi_7elI8QtX27yt3CXGJB8WcM,2409
12
+ math_step_to_step/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ math_step_to_step/solver.py,sha256=rIJi6bkrFXujkfkoQaVv6FNx_9mgss9Xf2ZknQklmfo,7772
14
+ math_step_to_step/symbols.py,sha256=WvJ4oZFylSZ8KMACXzhqAFVn58TbAie6BhZM37IysXg,4163
15
+ masts-0.1.0.dist-info/METADATA,sha256=ZbPzGmkk5ZG1PbIYqxcuOLKYphEPqItUIr62e3X5RNo,4647
16
+ masts-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
17
+ masts-0.1.0.dist-info/entry_points.txt,sha256=OjnpkOR84iqxnnr11TOACnCYqpEt8HCeaKBbH7ZgttA,65
18
+ masts-0.1.0.dist-info/top_level.txt,sha256=JQYQ70VdSlkzoDDn-LNDL6n02UW0h2pxA5frTikwMpw,18
19
+ masts-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ math-step-to-step = math_step_to_step.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VVT
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ math_step_to_step
@@ -0,0 +1,10 @@
1
+ """math-step-to-step: step-by-step solutions for math expressions and equations."""
2
+ from .errors import MathStepError
3
+ from .solver import Solution, evaluate, solve, steps
4
+ from .symbols import prettify, subscript, superscript
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = [
8
+ "steps", "solve", "evaluate", "Solution", "MathStepError",
9
+ "superscript", "subscript", "prettify", "__version__",
10
+ ]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,41 @@
1
+ """Command line: math-step-to-step "1 + 2 x 3"."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import sys
6
+
7
+ from . import __version__
8
+ from .errors import MathStepError
9
+ from .solver import solve
10
+
11
+
12
+ def main(argv=None):
13
+ ap = argparse.ArgumentParser(prog="math-step-to-step", description=__doc__)
14
+ ap.add_argument("expression", nargs="*", help="expression; omit to read lines from stdin")
15
+ ap.add_argument("-d", "--detailed", action="store_true", help="one operation per step")
16
+ ap.add_argument("--ascii", action="store_true", help="plain ASCII output")
17
+ ap.add_argument("--en", action="store_true", help="English messages")
18
+ ap.add_argument("--style", choices=["auto", "fraction", "decimal"], default="auto")
19
+ ap.add_argument("-V", "--version", action="version", version=__version__)
20
+ a = ap.parse_args(argv)
21
+
22
+ lines = [" ".join(a.expression)] if a.expression else (l.strip() for l in sys.stdin)
23
+ code = 0
24
+ for line in lines:
25
+ if not line:
26
+ continue
27
+ try:
28
+ sol = solve(line, include_original=True, detailed=a.detailed,
29
+ pretty=not a.ascii, style=a.style, lang="en" if a.en else "vi")
30
+ except MathStepError as e:
31
+ print(f"Error: {e}", file=sys.stderr)
32
+ code = 1
33
+ continue
34
+ print(sol.steps[0])
35
+ for s in sol.steps[1:]:
36
+ print(" →", s)
37
+ return code
38
+
39
+
40
+ if __name__ == "__main__":
41
+ raise SystemExit(main())
@@ -0,0 +1,23 @@
1
+ """Options shared by the parser, engine and formatter."""
2
+ from __future__ import annotations
3
+
4
+ from .errors import LANGS
5
+
6
+ MAX_LEN = 10_000 # max characters of input
7
+ MAX_BITS = 14_000 # max size of any intermediate number (~4200 digits)
8
+ MAX_DEG = 60 # max polynomial degree
9
+
10
+
11
+ class Ctx:
12
+ __slots__ = ("pretty", "detailed", "lang", "style", "decimal")
13
+
14
+ def __init__(self, pretty=True, detailed=False, lang="vi", style="auto"):
15
+ if lang not in LANGS:
16
+ raise ValueError(f"lang must be one of {LANGS}")
17
+ if style not in ("auto", "fraction", "decimal"):
18
+ raise ValueError("style must be 'auto', 'fraction' or 'decimal'")
19
+ self.pretty = bool(pretty)
20
+ self.detailed = bool(detailed)
21
+ self.lang = lang
22
+ self.style = style
23
+ self.decimal = style == "decimal"
@@ -0,0 +1,318 @@
1
+ """Step-by-step arithmetic engine.
2
+
3
+ Each iteration finds every sub-expression that can be evaluated next and
4
+ evaluates them together, giving one displayed step:
5
+
6
+ * phase A - nodes whose operands are all numbers (e.g. ``2 x 3``, ``(1 + 2)``);
7
+ anything inside parentheses goes first;
8
+ * phase B - only if phase A found nothing: merge the plain numbers of a sum or
9
+ product that also holds non-numeric parts (``2 + 3 + x`` -> ``5 + x``).
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from decimal import Decimal, InvalidOperation, localcontext
14
+ from fractions import Fraction
15
+ from math import isqrt
16
+
17
+ from .context import MAX_BITS
18
+ from .errors import error
19
+ from .nodes import Eq, Neg, Num, Pow, Prod, Sqrt, Sum, kids, make_neg, make_prod, make_sum
20
+
21
+ ZERO = Fraction(0)
22
+ ONE = Fraction(1)
23
+ _RAD_LIMIT = 20_000
24
+
25
+
26
+ def chk(v, c):
27
+ if v.numerator.bit_length() > MAX_BITS or v.denominator.bit_length() > MAX_BITS:
28
+ raise error(c.lang, "too_big")
29
+ return v
30
+
31
+
32
+ def power(b, e, c):
33
+ if e.denominator != 1:
34
+ raise error(c.lang, "int_exp")
35
+ k = e.numerator
36
+ if b == 0:
37
+ if k == 0:
38
+ raise error(c.lang, "undefined")
39
+ if k < 0:
40
+ raise error(c.lang, "div_zero")
41
+ return ZERO
42
+ if b != 1 and b != -1:
43
+ bits = max(b.numerator.bit_length(), b.denominator.bit_length())
44
+ if abs(k) * (bits - 1) > MAX_BITS:
45
+ raise error(c.lang, "too_big")
46
+ return chk(b ** k, c)
47
+
48
+
49
+ def _exact_sqrt(v):
50
+ n, d = v.numerator, v.denominator
51
+ rn, rd = isqrt(n), isqrt(d)
52
+ if rn * rn == n and rd * rd == d:
53
+ return Fraction(rn, rd)
54
+ return None
55
+
56
+
57
+ def _radical(n):
58
+ """n = k*k*m with m as small as cheaply possible."""
59
+ k, m, p = 1, n, 2
60
+ while p * p <= m and p <= _RAD_LIMIT:
61
+ pp = p * p
62
+ while m % pp == 0:
63
+ m //= pp
64
+ k *= p
65
+ p += 1 if p == 2 else 2
66
+ return k, m
67
+
68
+
69
+ def _mul_div(acc, op, v, c):
70
+ if op == "*":
71
+ return chk(acc * v, c)
72
+ if v == 0:
73
+ raise error(c.lang, "div_zero")
74
+ return chk(acc / v, c)
75
+
76
+
77
+ # ---------------------------------------------------------------- reductions
78
+ def _red_sum(n, c):
79
+ terms = n.terms
80
+ nums = [i for i, (_, x) in enumerate(terms) if type(x) is Num]
81
+ if len(nums) == len(terms):
82
+ if c.detailed and len(terms) > 2:
83
+ (s0, a), (s1, b) = terms[0], terms[1]
84
+ tot = chk(s0 * a.v + s1 * b.v, c)
85
+ return make_sum(((1, Num(tot)),) + terms[2:], n.d)
86
+ tot = ZERO
87
+ for s, x in terms:
88
+ tot += x.v if s > 0 else -x.v
89
+ return Num(chk(tot, c))
90
+ if len(nums) >= 2:
91
+ idx = nums[:2] if c.detailed else nums
92
+ tot = chk(sum((terms[i][0] * terms[i][1].v for i in idx), ZERO), c)
93
+ first, drop = idx[0], set(idx[1:])
94
+ new = []
95
+ for i, (s, x) in enumerate(terms):
96
+ if i in drop:
97
+ continue
98
+ if i == first:
99
+ if tot != 0:
100
+ new.append((1, Num(tot)))
101
+ else:
102
+ new.append((s, x))
103
+ return make_sum(new, n.d)
104
+ if len(nums) == 1 and terms[nums[0]][1].v == 0:
105
+ return make_sum([t for i, t in enumerate(terms) if i != nums[0]], n.d)
106
+ return None
107
+
108
+
109
+ def _red_prod(n, c):
110
+ facs = n.facs
111
+ if all(type(x) is Num for _, x in facs):
112
+ if c.detailed and len(facs) > 2:
113
+ v = _mul_div(facs[0][1].v, facs[1][0], facs[1][1].v, c)
114
+ return make_prod([("*", Num(v))] + list(facs[2:]), n.d)
115
+ acc = facs[0][1].v
116
+ for op, x in facs[1:]:
117
+ acc = _mul_div(acc, op, x.v, c)
118
+ return Num(acc)
119
+ nums = [i for i, (_, x) in enumerate(facs) if type(x) is Num]
120
+ if len(nums) >= 2: # merge the numeric factors: 2 x sqrt(2) / 2 -> sqrt(2)
121
+ idx = nums[:2] if c.detailed else nums
122
+ m = ONE
123
+ for i in idx:
124
+ m = _mul_div(m, facs[i][0], facs[i][1].v, c)
125
+ if m == 0:
126
+ return Num(ZERO)
127
+ first, drop = idx[0], set(idx[1:])
128
+ new, neg = [], False
129
+ for i, (op, x) in enumerate(facs):
130
+ if i in drop:
131
+ continue
132
+ if i == first:
133
+ if m == 1:
134
+ continue
135
+ if m == -1:
136
+ neg = True
137
+ continue
138
+ if op == "/" and (1 / m).denominator == 1:
139
+ new.append(("/", Num(1 / m)))
140
+ else:
141
+ new.append(("*", Num(m)))
142
+ else:
143
+ new.append((op, x))
144
+ if new[0][0] == "/":
145
+ new.insert(0, ("*", Num(ONE)))
146
+ res = make_prod(new, n.d)
147
+ return make_neg(res, n.d) if neg else res
148
+ if len(nums) == 1 and all(op == "*" for op, _ in facs):
149
+ v = facs[nums[0]][1].v
150
+ if v == 0:
151
+ return Num(ZERO)
152
+ if v == 1:
153
+ return make_prod([f for i, f in enumerate(facs) if i != nums[0]], n.d)
154
+ return None
155
+
156
+
157
+ def _red_sqrt(n, c):
158
+ v = n.arg.v
159
+ if v < 0:
160
+ raise error(c.lang, "sqrt_neg")
161
+ r = _exact_sqrt(v)
162
+ if r is not None:
163
+ return Num(r)
164
+ if v.denominator == 1:
165
+ k, m = _radical(v.numerator)
166
+ if k > 1:
167
+ return Prod((("*", Num(k)), ("*", Sqrt(Num(m), n.d))), n.d)
168
+ return None
169
+
170
+
171
+ def _reduce(n, c):
172
+ t = type(n)
173
+ if t is Sum:
174
+ return _red_sum(n, c)
175
+ if t is Prod:
176
+ return _red_prod(n, c)
177
+ if t is Pow:
178
+ return Num(power(n.base.v, n.exp.v, c))
179
+ return _red_sqrt(n, c)
180
+
181
+
182
+ # ----------------------------------------------------------------- traversal
183
+ def _full(n, out, c):
184
+ t = type(n)
185
+ if t is Num:
186
+ return
187
+ if t is Sum:
188
+ if all(type(x) is Num for _, x in n.terms):
189
+ out.append(n)
190
+ return
191
+ elif t is Prod:
192
+ if all(type(x) is Num for _, x in n.facs):
193
+ out.append(n)
194
+ return
195
+ elif t is Pow:
196
+ if type(n.base) is Num and type(n.exp) is Num:
197
+ out.append(n)
198
+ return
199
+ elif t is Sqrt:
200
+ if type(n.arg) is Num:
201
+ if _red_sqrt(n, c) is not None:
202
+ out.append(n)
203
+ return
204
+ for k in kids(n):
205
+ _full(k, out, c)
206
+
207
+
208
+ def _partial(n, out, c):
209
+ t = type(n)
210
+ found = False
211
+ for k in kids(n):
212
+ if _partial(k, out, c):
213
+ found = True
214
+ if found:
215
+ return True
216
+ if (t is Sum and _red_sum(n, c) is not None) or (t is Prod and _red_prod(n, c) is not None):
217
+ out.append(n)
218
+ return True
219
+ return False
220
+
221
+
222
+ def _rebuild(n, picks, c):
223
+ if id(n) in picks:
224
+ return _reduce(n, c)
225
+ t = type(n)
226
+ if t is Sum:
227
+ new, changed = [], False
228
+ for s, x in n.terms:
229
+ y = _rebuild(x, picks, c)
230
+ changed = changed or y is not x
231
+ new.append((s, y))
232
+ return make_sum(new, n.d) if changed else n
233
+ if t is Prod:
234
+ new, changed = [], False
235
+ for op, x in n.facs:
236
+ y = _rebuild(x, picks, c)
237
+ if y is not x:
238
+ changed = True
239
+ if type(y) is Prod and op == "*": # (a x b) x c -> a x b x c
240
+ new.extend(y.facs)
241
+ continue
242
+ new.append((op, y))
243
+ return make_prod(new, n.d) if changed else n
244
+ if t is Pow:
245
+ b, e = _rebuild(n.base, picks, c), _rebuild(n.exp, picks, c)
246
+ return n if b is n.base and e is n.exp else Pow(b, e, n.d)
247
+ if t is Sqrt:
248
+ a = _rebuild(n.arg, picks, c)
249
+ return n if a is n.arg else Sqrt(a, n.d)
250
+ if t is Neg:
251
+ a = _rebuild(n.arg, picks, c)
252
+ return n if a is n.arg else make_neg(a, n.d)
253
+ if t is Eq:
254
+ l, r = _rebuild(n.lhs, picks, c), _rebuild(n.rhs, picks, c)
255
+ return n if l is n.lhs and r is n.rhs else Eq(l, r)
256
+ return n
257
+
258
+
259
+ def run(tree, c, emit):
260
+ """Reduce `tree` step by step; call emit(new_tree) after each step."""
261
+ for _ in range(1_000_000):
262
+ cands = []
263
+ _full(tree, cands, c)
264
+ if not cands:
265
+ _partial(tree, cands, c)
266
+ if not cands:
267
+ return tree
268
+ if any(x.d > 0 for x in cands): # parentheses first
269
+ cands = [x for x in cands if x.d > 0]
270
+ tree = _rebuild(tree, {id(x) for x in cands}, c)
271
+ emit(tree)
272
+ raise error(c.lang, "too_big")
273
+
274
+
275
+ # ------------------------------------------------------------ approximations
276
+ _Q = Decimal(1).scaleb(-10)
277
+
278
+
279
+ def _ap(n):
280
+ t = type(n)
281
+ if t is Num:
282
+ return Decimal(n.v.numerator) / Decimal(n.v.denominator)
283
+ if t is Sum:
284
+ tot = Decimal(0)
285
+ for s, x in n.terms:
286
+ tot += _ap(x) if s > 0 else -_ap(x)
287
+ return tot
288
+ if t is Prod:
289
+ acc = _ap(n.facs[0][1])
290
+ for op, x in n.facs[1:]:
291
+ acc = acc * _ap(x) if op == "*" else acc / _ap(x)
292
+ return acc
293
+ if t is Pow:
294
+ return _ap(n.base) ** _ap(n.exp)
295
+ if t is Sqrt:
296
+ return _ap(n.arg).sqrt()
297
+ if t is Neg:
298
+ return -_ap(n.arg)
299
+ raise TypeError(t)
300
+
301
+
302
+ def approx(n, c):
303
+ try:
304
+ with localcontext() as ctx:
305
+ ctx.prec = 40
306
+ return _ap(n)
307
+ except ArithmeticError:
308
+ raise error(c.lang, "unsupported", what="approximation") from None
309
+
310
+
311
+ def dec_str(d):
312
+ try:
313
+ s = format(d.quantize(_Q), "f")
314
+ except InvalidOperation:
315
+ return format(d, ".15g")
316
+ if "." in s:
317
+ s = s.rstrip("0").rstrip(".")
318
+ return "0" if s in ("", "-0") else s
@@ -0,0 +1,72 @@
1
+ """Exception type and localized messages."""
2
+ from __future__ import annotations
3
+
4
+
5
+ class MathStepError(ValueError):
6
+ """Raised when an expression is invalid or not supported."""
7
+
8
+
9
+ _MESSAGES = {
10
+ "vi": {
11
+ "empty": "Biểu thức rỗng.",
12
+ "too_long": "Biểu thức quá dài (tối đa {n} ký tự).",
13
+ "bad_char": "Ký tự không hợp lệ {c!r} ở vị trí {pos}.",
14
+ "unexpected": "Không hiểu {tok!r} ở vị trí {pos}.",
15
+ "unexpected_end": "Biểu thức bị thiếu ở cuối.",
16
+ "missing_rparen": "Thiếu dấu đóng ngoặc.",
17
+ "multi_eq": "Chỉ được có một dấu '='.",
18
+ "div_zero": "Không thể chia cho 0.",
19
+ "undefined": "0^0 không xác định.",
20
+ "too_big": "Số quá lớn để tính.",
21
+ "sqrt_neg": "Không thể lấy căn bậc hai của số âm.",
22
+ "int_exp": "Chỉ hỗ trợ số mũ nguyên.",
23
+ "multi_var": "Chỉ hỗ trợ một biến, nhưng tìm thấy: {names}.",
24
+ "unsupported": "Chưa hỗ trợ dạng biểu thức này ({what}).",
25
+ "degree": "Chỉ giải được phương trình bậc 1 và bậc 2.",
26
+ "nested": "Biểu thức lồng nhau quá sâu.",
27
+ "has_var": "Biểu thức chứa biến nên không tính ra một số được.",
28
+ "no_solution": "Phương trình vô nghiệm.",
29
+ "infinite": "Phương trình có vô số nghiệm.",
30
+ "delta_neg": "Δ < 0 ⇒ phương trình vô nghiệm (trong tập số thực).",
31
+ "delta_zero": "Δ = 0 ⇒ phương trình có nghiệm kép.",
32
+ "delta_pos": "Δ > 0 ⇒ phương trình có hai nghiệm phân biệt.",
33
+ "true": "Đúng.",
34
+ "false": "Sai.",
35
+ },
36
+ "en": {
37
+ "empty": "Empty expression.",
38
+ "too_long": "Expression too long (max {n} characters).",
39
+ "bad_char": "Invalid character {c!r} at position {pos}.",
40
+ "unexpected": "Unexpected {tok!r} at position {pos}.",
41
+ "unexpected_end": "Unexpected end of expression.",
42
+ "missing_rparen": "Missing closing parenthesis.",
43
+ "multi_eq": "Only one '=' is allowed.",
44
+ "div_zero": "Division by zero.",
45
+ "undefined": "0^0 is undefined.",
46
+ "too_big": "Number too large to compute.",
47
+ "sqrt_neg": "Square root of a negative number.",
48
+ "int_exp": "Only integer exponents are supported.",
49
+ "multi_var": "Only one variable is supported, found: {names}.",
50
+ "unsupported": "Unsupported expression ({what}).",
51
+ "degree": "Only linear and quadratic equations can be solved.",
52
+ "nested": "Expression is nested too deeply.",
53
+ "has_var": "Expression contains a variable and has no numeric value.",
54
+ "no_solution": "No solution.",
55
+ "infinite": "Infinitely many solutions.",
56
+ "delta_neg": "Δ < 0 ⇒ no real solutions.",
57
+ "delta_zero": "Δ = 0 ⇒ one repeated root.",
58
+ "delta_pos": "Δ > 0 ⇒ two distinct roots.",
59
+ "true": "True.",
60
+ "false": "False.",
61
+ },
62
+ }
63
+
64
+ LANGS = tuple(_MESSAGES)
65
+
66
+
67
+ def msg(lang: str, key: str, **kw) -> str:
68
+ return _MESSAGES[lang][key].format(**kw)
69
+
70
+
71
+ def error(lang: str, key: str, **kw) -> MathStepError:
72
+ return MathStepError(msg(lang, key, **kw))