math-step-to-step 0.2.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.
- math_step_to_step/__init__.py +10 -0
- math_step_to_step/__main__.py +3 -0
- math_step_to_step/cli.py +41 -0
- math_step_to_step/context.py +23 -0
- math_step_to_step/engine.py +413 -0
- math_step_to_step/errors.py +74 -0
- math_step_to_step/fmt.py +183 -0
- math_step_to_step/fonts/LICENSE_STIX +124 -0
- math_step_to_step/fonts/STIXGeneral.ttf +0 -0
- math_step_to_step/fonts/STIXGeneralItalic.ttf +0 -0
- math_step_to_step/mathlabel.py +127 -0
- math_step_to_step/mathrender.py +876 -0
- math_step_to_step/nodes.py +156 -0
- math_step_to_step/parser.py +257 -0
- math_step_to_step/poly.py +91 -0
- math_step_to_step/py.typed +0 -0
- math_step_to_step/solver.py +241 -0
- math_step_to_step/symbols.py +166 -0
- math_step_to_step-0.2.0.dist-info/METADATA +165 -0
- math_step_to_step-0.2.0.dist-info/RECORD +24 -0
- math_step_to_step-0.2.0.dist-info/WHEEL +5 -0
- math_step_to_step-0.2.0.dist-info/entry_points.txt +2 -0
- math_step_to_step-0.2.0.dist-info/licenses/LICENSE +21 -0
- math_step_to_step-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -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.2.0"
|
|
7
|
+
__all__ = [
|
|
8
|
+
"steps", "solve", "evaluate", "Solution", "MathStepError",
|
|
9
|
+
"superscript", "subscript", "prettify", "__version__",
|
|
10
|
+
]
|
math_step_to_step/cli.py
ADDED
|
@@ -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,413 @@
|
|
|
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, Pi, Pow, Prod, Root, Sqrt, Sum, kids, make_neg,
|
|
20
|
+
make_prod, make_sum)
|
|
21
|
+
|
|
22
|
+
ZERO = Fraction(0)
|
|
23
|
+
ONE = Fraction(1)
|
|
24
|
+
PI = Fraction("3.14159265359") # the value used for pi
|
|
25
|
+
_RAD_LIMIT = 20_000
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def chk(v, c):
|
|
29
|
+
if v.numerator.bit_length() > MAX_BITS or v.denominator.bit_length() > MAX_BITS:
|
|
30
|
+
raise error(c.lang, "too_big")
|
|
31
|
+
return v
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def power(b, e, c):
|
|
35
|
+
if e.denominator != 1:
|
|
36
|
+
raise error(c.lang, "int_exp")
|
|
37
|
+
k = e.numerator
|
|
38
|
+
if b == 0:
|
|
39
|
+
if k == 0:
|
|
40
|
+
raise error(c.lang, "undefined")
|
|
41
|
+
if k < 0:
|
|
42
|
+
raise error(c.lang, "div_zero")
|
|
43
|
+
return ZERO
|
|
44
|
+
if b != 1 and b != -1:
|
|
45
|
+
bits = max(b.numerator.bit_length(), b.denominator.bit_length())
|
|
46
|
+
if abs(k) * (bits - 1) > MAX_BITS:
|
|
47
|
+
raise error(c.lang, "too_big")
|
|
48
|
+
return chk(b ** k, c)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _exact_sqrt(v):
|
|
52
|
+
n, d = v.numerator, v.denominator
|
|
53
|
+
rn, rd = isqrt(n), isqrt(d)
|
|
54
|
+
if rn * rn == n and rd * rd == d:
|
|
55
|
+
return Fraction(rn, rd)
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _radical(n):
|
|
60
|
+
"""n = k*k*m with m as small as cheaply possible."""
|
|
61
|
+
k, m, p = 1, n, 2
|
|
62
|
+
while p * p <= m and p <= _RAD_LIMIT:
|
|
63
|
+
pp = p * p
|
|
64
|
+
while m % pp == 0:
|
|
65
|
+
m //= pp
|
|
66
|
+
k *= p
|
|
67
|
+
p += 1 if p == 2 else 2
|
|
68
|
+
return k, m
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _iroot(n, k):
|
|
72
|
+
"""floor(n ** (1/k)) for integers n >= 0, k >= 1."""
|
|
73
|
+
if n < 2:
|
|
74
|
+
return n
|
|
75
|
+
if k >= n.bit_length():
|
|
76
|
+
return 1
|
|
77
|
+
x = 1 << ((n.bit_length() + k - 1) // k)
|
|
78
|
+
while True:
|
|
79
|
+
y = ((k - 1) * x + n // x ** (k - 1)) // k
|
|
80
|
+
if y >= x:
|
|
81
|
+
return x
|
|
82
|
+
x = y
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _radical_k(n, k):
|
|
86
|
+
"""n = c**k * m with m as small as cheaply possible."""
|
|
87
|
+
if n < 2 or k >= n.bit_length():
|
|
88
|
+
return 1, n
|
|
89
|
+
cf, m, p = 1, n, 2
|
|
90
|
+
while p <= _RAD_LIMIT:
|
|
91
|
+
pk = p ** k
|
|
92
|
+
if pk > m:
|
|
93
|
+
break
|
|
94
|
+
while m % pk == 0:
|
|
95
|
+
m //= pk
|
|
96
|
+
cf *= p
|
|
97
|
+
p += 1 if p == 2 else 2
|
|
98
|
+
return cf, m
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _mul_div(acc, op, v, c):
|
|
102
|
+
if op == "*":
|
|
103
|
+
return chk(acc * v, c)
|
|
104
|
+
if v == 0:
|
|
105
|
+
raise error(c.lang, "div_zero")
|
|
106
|
+
return chk(acc / v, c)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------- reductions
|
|
110
|
+
def _red_sum(n, c):
|
|
111
|
+
terms = n.terms
|
|
112
|
+
nums = [i for i, (_, x) in enumerate(terms) if type(x) is Num]
|
|
113
|
+
if len(nums) == len(terms):
|
|
114
|
+
if c.detailed and len(terms) > 2:
|
|
115
|
+
(s0, a), (s1, b) = terms[0], terms[1]
|
|
116
|
+
tot = chk(s0 * a.v + s1 * b.v, c)
|
|
117
|
+
return make_sum(((1, Num(tot)),) + terms[2:], n.d)
|
|
118
|
+
tot = ZERO
|
|
119
|
+
for s, x in terms:
|
|
120
|
+
tot += x.v if s > 0 else -x.v
|
|
121
|
+
return Num(chk(tot, c))
|
|
122
|
+
if len(nums) >= 2:
|
|
123
|
+
idx = nums[:2] if c.detailed else nums
|
|
124
|
+
tot = chk(sum((terms[i][0] * terms[i][1].v for i in idx), ZERO), c)
|
|
125
|
+
first, drop = idx[0], set(idx[1:])
|
|
126
|
+
new = []
|
|
127
|
+
for i, (s, x) in enumerate(terms):
|
|
128
|
+
if i in drop:
|
|
129
|
+
continue
|
|
130
|
+
if i == first:
|
|
131
|
+
if tot != 0:
|
|
132
|
+
new.append((1, Num(tot)))
|
|
133
|
+
else:
|
|
134
|
+
new.append((s, x))
|
|
135
|
+
return make_sum(new, n.d)
|
|
136
|
+
if len(nums) == 1 and terms[nums[0]][1].v == 0:
|
|
137
|
+
return make_sum([t for i, t in enumerate(terms) if i != nums[0]], n.d)
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _red_prod(n, c):
|
|
142
|
+
facs = n.facs
|
|
143
|
+
if all(type(x) is Num for _, x in facs):
|
|
144
|
+
if c.detailed and len(facs) > 2:
|
|
145
|
+
v = _mul_div(facs[0][1].v, facs[1][0], facs[1][1].v, c)
|
|
146
|
+
return make_prod([("*", Num(v))] + list(facs[2:]), n.d)
|
|
147
|
+
acc = facs[0][1].v
|
|
148
|
+
for op, x in facs[1:]:
|
|
149
|
+
acc = _mul_div(acc, op, x.v, c)
|
|
150
|
+
return Num(acc)
|
|
151
|
+
nums = [i for i, (_, x) in enumerate(facs) if type(x) is Num]
|
|
152
|
+
if len(nums) >= 2: # merge the numeric factors: 2 x sqrt(2) / 2 -> sqrt(2)
|
|
153
|
+
idx = nums[:2] if c.detailed else nums
|
|
154
|
+
m = ONE
|
|
155
|
+
for i in idx:
|
|
156
|
+
m = _mul_div(m, facs[i][0], facs[i][1].v, c)
|
|
157
|
+
if m == 0:
|
|
158
|
+
return Num(ZERO)
|
|
159
|
+
first, drop = idx[0], set(idx[1:])
|
|
160
|
+
new, neg = [], False
|
|
161
|
+
for i, (op, x) in enumerate(facs):
|
|
162
|
+
if i in drop:
|
|
163
|
+
continue
|
|
164
|
+
if i == first:
|
|
165
|
+
if m == 1:
|
|
166
|
+
continue
|
|
167
|
+
if m == -1:
|
|
168
|
+
neg = True
|
|
169
|
+
continue
|
|
170
|
+
if op == "/" and (1 / m).denominator == 1:
|
|
171
|
+
new.append(("/", Num(1 / m)))
|
|
172
|
+
else:
|
|
173
|
+
new.append(("*", Num(m)))
|
|
174
|
+
else:
|
|
175
|
+
new.append((op, x))
|
|
176
|
+
if new[0][0] == "/":
|
|
177
|
+
new.insert(0, ("*", Num(ONE)))
|
|
178
|
+
res = make_prod(new, n.d)
|
|
179
|
+
return make_neg(res, n.d) if neg else res
|
|
180
|
+
if len(nums) == 1 and all(op == "*" for op, _ in facs):
|
|
181
|
+
v = facs[nums[0]][1].v
|
|
182
|
+
if v == 0:
|
|
183
|
+
return Num(ZERO)
|
|
184
|
+
if v == 1:
|
|
185
|
+
return make_prod([f for i, f in enumerate(facs) if i != nums[0]], n.d)
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _red_sqrt(n, c):
|
|
190
|
+
v = n.arg.v
|
|
191
|
+
if v < 0:
|
|
192
|
+
raise error(c.lang, "sqrt_neg")
|
|
193
|
+
r = _exact_sqrt(v)
|
|
194
|
+
if r is not None:
|
|
195
|
+
return Num(r)
|
|
196
|
+
if v.denominator == 1:
|
|
197
|
+
k, m = _radical(v.numerator)
|
|
198
|
+
if k > 1:
|
|
199
|
+
return Prod((("*", Num(k)), ("*", Sqrt(Num(m), n.d))), n.d)
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _red_root(n, c):
|
|
204
|
+
iv = n.idx.v
|
|
205
|
+
if iv.denominator != 1 or iv < 1:
|
|
206
|
+
raise error(c.lang, "bad_index")
|
|
207
|
+
k = iv.numerator
|
|
208
|
+
v = n.arg.v
|
|
209
|
+
if k == 1:
|
|
210
|
+
return Num(v)
|
|
211
|
+
sign = 1
|
|
212
|
+
if v < 0:
|
|
213
|
+
if k % 2 == 0:
|
|
214
|
+
raise error(c.lang, "sqrt_neg")
|
|
215
|
+
sign, v = -1, -v
|
|
216
|
+
num, den = v.numerator, v.denominator
|
|
217
|
+
rn, rd = _iroot(num, k), _iroot(den, k)
|
|
218
|
+
if rn ** k == num and rd ** k == den:
|
|
219
|
+
return Num(sign * Fraction(rn, rd))
|
|
220
|
+
if den == 1:
|
|
221
|
+
cf, m = _radical_k(num, k)
|
|
222
|
+
if cf > 1:
|
|
223
|
+
return Prod((("*", Num(sign * cf)), ("*", Root(Num(k), Num(m), n.d))), n.d)
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _reduce(n, c):
|
|
228
|
+
t = type(n)
|
|
229
|
+
if t is Pi:
|
|
230
|
+
return Num(PI)
|
|
231
|
+
if t is Root:
|
|
232
|
+
return _red_root(n, c)
|
|
233
|
+
if t is Sum:
|
|
234
|
+
return _red_sum(n, c)
|
|
235
|
+
if t is Prod:
|
|
236
|
+
return _red_prod(n, c)
|
|
237
|
+
if t is Pow:
|
|
238
|
+
return Num(power(n.base.v, n.exp.v, c))
|
|
239
|
+
return _red_sqrt(n, c)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ----------------------------------------------------------------- traversal
|
|
243
|
+
def _full(n, out, c):
|
|
244
|
+
t = type(n)
|
|
245
|
+
if t is Num:
|
|
246
|
+
return
|
|
247
|
+
if t is Sum:
|
|
248
|
+
if all(type(x) is Num for _, x in n.terms):
|
|
249
|
+
out.append(n)
|
|
250
|
+
return
|
|
251
|
+
elif t is Prod:
|
|
252
|
+
if all(type(x) is Num for _, x in n.facs):
|
|
253
|
+
out.append(n)
|
|
254
|
+
return
|
|
255
|
+
elif t is Pow:
|
|
256
|
+
if type(n.base) is Num and type(n.exp) is Num:
|
|
257
|
+
out.append(n)
|
|
258
|
+
return
|
|
259
|
+
elif t is Sqrt:
|
|
260
|
+
if type(n.arg) is Num:
|
|
261
|
+
if _red_sqrt(n, c) is not None:
|
|
262
|
+
out.append(n)
|
|
263
|
+
return
|
|
264
|
+
elif t is Root:
|
|
265
|
+
if type(n.idx) is Num and type(n.arg) is Num:
|
|
266
|
+
if _red_root(n, c) is not None:
|
|
267
|
+
out.append(n)
|
|
268
|
+
return
|
|
269
|
+
for k in kids(n):
|
|
270
|
+
_full(k, out, c)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _partial(n, out, c):
|
|
274
|
+
t = type(n)
|
|
275
|
+
found = False
|
|
276
|
+
for k in kids(n):
|
|
277
|
+
if _partial(k, out, c):
|
|
278
|
+
found = True
|
|
279
|
+
if found:
|
|
280
|
+
return True
|
|
281
|
+
if (t is Sum and _red_sum(n, c) is not None) or (t is Prod and _red_prod(n, c) is not None):
|
|
282
|
+
out.append(n)
|
|
283
|
+
return True
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _rebuild(n, picks, c):
|
|
288
|
+
if id(n) in picks:
|
|
289
|
+
return _reduce(n, c)
|
|
290
|
+
t = type(n)
|
|
291
|
+
if t is Sum:
|
|
292
|
+
new, changed = [], False
|
|
293
|
+
for s, x in n.terms:
|
|
294
|
+
y = _rebuild(x, picks, c)
|
|
295
|
+
changed = changed or y is not x
|
|
296
|
+
new.append((s, y))
|
|
297
|
+
return make_sum(new, n.d) if changed else n
|
|
298
|
+
if t is Prod:
|
|
299
|
+
new, changed = [], False
|
|
300
|
+
for op, x in n.facs:
|
|
301
|
+
y = _rebuild(x, picks, c)
|
|
302
|
+
if y is not x:
|
|
303
|
+
changed = True
|
|
304
|
+
if type(y) is Prod and op == "*": # (a x b) x c -> a x b x c
|
|
305
|
+
new.extend(y.facs)
|
|
306
|
+
continue
|
|
307
|
+
new.append((op, y))
|
|
308
|
+
if not changed:
|
|
309
|
+
return n
|
|
310
|
+
res = make_prod(new, n.d)
|
|
311
|
+
if n.frac and type(res) is Prod and len(res.facs) == 2 and res.facs[1][0] == "/":
|
|
312
|
+
res.frac = True
|
|
313
|
+
return res
|
|
314
|
+
if t is Pow:
|
|
315
|
+
b, e = _rebuild(n.base, picks, c), _rebuild(n.exp, picks, c)
|
|
316
|
+
return n if b is n.base and e is n.exp else Pow(b, e, n.d)
|
|
317
|
+
if t is Sqrt:
|
|
318
|
+
a = _rebuild(n.arg, picks, c)
|
|
319
|
+
return n if a is n.arg else Sqrt(a, n.d)
|
|
320
|
+
if t is Root:
|
|
321
|
+
i, a = _rebuild(n.idx, picks, c), _rebuild(n.arg, picks, c)
|
|
322
|
+
return n if i is n.idx and a is n.arg else Root(i, a, n.d)
|
|
323
|
+
if t is Neg:
|
|
324
|
+
a = _rebuild(n.arg, picks, c)
|
|
325
|
+
return n if a is n.arg else make_neg(a, n.d)
|
|
326
|
+
if t is Eq:
|
|
327
|
+
l, r = _rebuild(n.lhs, picks, c), _rebuild(n.rhs, picks, c)
|
|
328
|
+
return n if l is n.lhs and r is n.rhs else Eq(l, r)
|
|
329
|
+
return n
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _find_pi(n, out):
|
|
333
|
+
if type(n) is Pi:
|
|
334
|
+
out.append(n)
|
|
335
|
+
return
|
|
336
|
+
for k in kids(n):
|
|
337
|
+
_find_pi(k, out)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def subst_pi(tree, c):
|
|
341
|
+
"""Replace every pi by its numeric value (one step)."""
|
|
342
|
+
found = []
|
|
343
|
+
_find_pi(tree, found)
|
|
344
|
+
if not found:
|
|
345
|
+
return tree
|
|
346
|
+
return _rebuild(tree, {id(x) for x in found}, c)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def run(tree, c, emit):
|
|
350
|
+
"""Reduce `tree` step by step; call emit(new_tree) after each step."""
|
|
351
|
+
for _ in range(1_000_000):
|
|
352
|
+
cands = []
|
|
353
|
+
_full(tree, cands, c)
|
|
354
|
+
if not cands:
|
|
355
|
+
_partial(tree, cands, c)
|
|
356
|
+
if not cands:
|
|
357
|
+
return tree
|
|
358
|
+
if any(x.d > 0 for x in cands): # parentheses first
|
|
359
|
+
cands = [x for x in cands if x.d > 0]
|
|
360
|
+
tree = _rebuild(tree, {id(x) for x in cands}, c)
|
|
361
|
+
emit(tree)
|
|
362
|
+
raise error(c.lang, "too_big")
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# ------------------------------------------------------------ approximations
|
|
366
|
+
_Q = Decimal(1).scaleb(-10)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _ap(n):
|
|
370
|
+
t = type(n)
|
|
371
|
+
if t is Num:
|
|
372
|
+
return Decimal(n.v.numerator) / Decimal(n.v.denominator)
|
|
373
|
+
if t is Sum:
|
|
374
|
+
tot = Decimal(0)
|
|
375
|
+
for s, x in n.terms:
|
|
376
|
+
tot += _ap(x) if s > 0 else -_ap(x)
|
|
377
|
+
return tot
|
|
378
|
+
if t is Prod:
|
|
379
|
+
acc = _ap(n.facs[0][1])
|
|
380
|
+
for op, x in n.facs[1:]:
|
|
381
|
+
acc = acc * _ap(x) if op == "*" else acc / _ap(x)
|
|
382
|
+
return acc
|
|
383
|
+
if t is Pow:
|
|
384
|
+
return _ap(n.base) ** _ap(n.exp)
|
|
385
|
+
if t is Sqrt:
|
|
386
|
+
return _ap(n.arg).sqrt()
|
|
387
|
+
if t is Root:
|
|
388
|
+
k, x = _ap(n.idx), _ap(n.arg)
|
|
389
|
+
if x < 0 and k % 2 == 1:
|
|
390
|
+
return -((-x) ** (1 / k))
|
|
391
|
+
return x ** (1 / k)
|
|
392
|
+
if t is Neg:
|
|
393
|
+
return -_ap(n.arg)
|
|
394
|
+
raise TypeError(t)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def approx(n, c):
|
|
398
|
+
try:
|
|
399
|
+
with localcontext() as ctx:
|
|
400
|
+
ctx.prec = 40
|
|
401
|
+
return _ap(n)
|
|
402
|
+
except ArithmeticError:
|
|
403
|
+
raise error(c.lang, "unsupported", what="approximation") from None
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def dec_str(d):
|
|
407
|
+
try:
|
|
408
|
+
s = format(d.quantize(_Q), "f")
|
|
409
|
+
except InvalidOperation:
|
|
410
|
+
return format(d, ".15g")
|
|
411
|
+
if "." in s:
|
|
412
|
+
s = s.rstrip("0").rstrip(".")
|
|
413
|
+
return "0" if s in ("", "-0") else s
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
"bad_index": "Chỉ số của căn phải là số nguyên dương.",
|
|
24
|
+
"multi_var": "Chỉ hỗ trợ một biến, nhưng tìm thấy: {names}.",
|
|
25
|
+
"unsupported": "Chưa hỗ trợ dạng biểu thức này ({what}).",
|
|
26
|
+
"degree": "Chỉ giải được phương trình bậc 1 và bậc 2.",
|
|
27
|
+
"nested": "Biểu thức lồng nhau quá sâu.",
|
|
28
|
+
"has_var": "Biểu thức chứa biến nên không tính ra một số được.",
|
|
29
|
+
"no_solution": "Phương trình vô nghiệm.",
|
|
30
|
+
"infinite": "Phương trình có vô số nghiệm.",
|
|
31
|
+
"delta_neg": "Δ < 0 ⇒ phương trình vô nghiệm (trong tập số thực).",
|
|
32
|
+
"delta_zero": "Δ = 0 ⇒ phương trình có nghiệm kép.",
|
|
33
|
+
"delta_pos": "Δ > 0 ⇒ phương trình có hai nghiệm phân biệt.",
|
|
34
|
+
"true": "Đúng.",
|
|
35
|
+
"false": "Sai.",
|
|
36
|
+
},
|
|
37
|
+
"en": {
|
|
38
|
+
"empty": "Empty expression.",
|
|
39
|
+
"too_long": "Expression too long (max {n} characters).",
|
|
40
|
+
"bad_char": "Invalid character {c!r} at position {pos}.",
|
|
41
|
+
"unexpected": "Unexpected {tok!r} at position {pos}.",
|
|
42
|
+
"unexpected_end": "Unexpected end of expression.",
|
|
43
|
+
"missing_rparen": "Missing closing parenthesis.",
|
|
44
|
+
"multi_eq": "Only one '=' is allowed.",
|
|
45
|
+
"div_zero": "Division by zero.",
|
|
46
|
+
"undefined": "0^0 is undefined.",
|
|
47
|
+
"too_big": "Number too large to compute.",
|
|
48
|
+
"sqrt_neg": "Square root of a negative number.",
|
|
49
|
+
"int_exp": "Only integer exponents are supported.",
|
|
50
|
+
"bad_index": "The root index must be a positive integer.",
|
|
51
|
+
"multi_var": "Only one variable is supported, found: {names}.",
|
|
52
|
+
"unsupported": "Unsupported expression ({what}).",
|
|
53
|
+
"degree": "Only linear and quadratic equations can be solved.",
|
|
54
|
+
"nested": "Expression is nested too deeply.",
|
|
55
|
+
"has_var": "Expression contains a variable and has no numeric value.",
|
|
56
|
+
"no_solution": "No solution.",
|
|
57
|
+
"infinite": "Infinitely many solutions.",
|
|
58
|
+
"delta_neg": "Δ < 0 ⇒ no real solutions.",
|
|
59
|
+
"delta_zero": "Δ = 0 ⇒ one repeated root.",
|
|
60
|
+
"delta_pos": "Δ > 0 ⇒ two distinct roots.",
|
|
61
|
+
"true": "True.",
|
|
62
|
+
"false": "False.",
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
LANGS = tuple(_MESSAGES)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def msg(lang: str, key: str, **kw) -> str:
|
|
70
|
+
return _MESSAGES[lang][key].format(**kw)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def error(lang: str, key: str, **kw) -> MathStepError:
|
|
74
|
+
return MathStepError(msg(lang, key, **kw))
|