Ethosoft-Math 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.
- ethosoft_math/__init__.py +30 -0
- ethosoft_math/__main__.py +3 -0
- ethosoft_math/cli.py +56 -0
- ethosoft_math/converter.py +1418 -0
- ethosoft_math/protocol.py +858 -0
- ethosoft_math-0.1.0.dist-info/METADATA +72 -0
- ethosoft_math-0.1.0.dist-info/RECORD +10 -0
- ethosoft_math-0.1.0.dist-info/WHEEL +5 -0
- ethosoft_math-0.1.0.dist-info/entry_points.txt +2 -0
- ethosoft_math-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1418 @@
|
|
|
1
|
+
"""Cozum metinlerindeki hesaplari MathTool cagrilarina cevirir (v2).
|
|
2
|
+
|
|
3
|
+
Ilke: sadece DOGRULANABILEN hesaplar donusturulur; dogrulanamayan her sey oldugu gibi kalir.
|
|
4
|
+
Kapsam: sayisal eslitlikler, \\boxed cevaplar, yaklasik (≈) degerler, kelimeyle islemler,
|
|
5
|
+
birimler, yuzde, sembolik ozdeslik (expand/factor/simplify), denklem cozme (solve),
|
|
6
|
+
fonksiyon tanimlama (define) ve $...$ icinde tek basina duran sayisal ifadeler.
|
|
7
|
+
"""
|
|
8
|
+
import bisect
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
from decimal import Decimal
|
|
12
|
+
from fractions import Fraction
|
|
13
|
+
|
|
14
|
+
import sympy
|
|
15
|
+
|
|
16
|
+
from .protocol import (CALL_OPEN, CALL_CLOSE, RES_OPEN, RES_CLOSE, ToolError, fmt_frac,
|
|
17
|
+
to_text, run, execute, parse_call, _round_half_up, _sym)
|
|
18
|
+
|
|
19
|
+
# ------------------------------------------------------------------ tokenizer
|
|
20
|
+
_TOK = re.compile(r"""
|
|
21
|
+
(?P<NUM>(?<![\d.])(?:\d{1,3}(?:(?:,\\!|\{,\}|,)\d{3})+(?!\d)|\d+)(?:\.\d+)?|(?<![\d.])\.\d+)
|
|
22
|
+
|(?P<CMD>\\[A-Za-z]+)
|
|
23
|
+
|(?P<ESC>\\[%$,;!:\s_])
|
|
24
|
+
|(?P<WORD>[A-Za-z]+)
|
|
25
|
+
|(?P<SP>\s+)
|
|
26
|
+
|(?P<CH>.)
|
|
27
|
+
""", re.X | re.S)
|
|
28
|
+
|
|
29
|
+
_OPMAP = {"×": "*", "·": "*", "⋅": "*", "∗": "*", "÷": "/", "−": "-", "–": "-", "—": "-"}
|
|
30
|
+
_CMD_OP = {"\\times": "*", "\\cdot": "*", "\\div": "/", "\\ast": "*"}
|
|
31
|
+
_FUNC_WORDS = {"sqrt", "sin", "cos", "tan", "log", "ln", "exp", "abs",
|
|
32
|
+
"asin", "acos", "atan", "gcd", "lcm"}
|
|
33
|
+
_CURRENCY = {"Rs", "INR", "USD", "EUR", "Re", "Rupees", "Dollars"}
|
|
34
|
+
_BINOM = ("\\binom", "\\dbinom", "\\tbinom")
|
|
35
|
+
_CMD_FUNCS = {"\\sin": "sin", "\\cos": "cos", "\\tan": "tan", "\\cot": "cot", "\\sec": "sec",
|
|
36
|
+
"\\csc": "csc", "\\log": "log", "\\ln": "ln", "\\exp": "exp",
|
|
37
|
+
"\\arcsin": "asin", "\\arccos": "acos", "\\arctan": "atan"}
|
|
38
|
+
_CMD_ALLOWED = set(_CMD_OP) | set(_CMD_FUNCS) | {"\\frac", "\\dfrac", "\\tfrac", "\\sqrt", "\\pi",
|
|
39
|
+
"\\circ", "\\boxed", "\\binom", "\\dbinom",
|
|
40
|
+
"\\tbinom", "\\gcd", "\\lcm", "\\sum", "\\prod"}
|
|
41
|
+
_STOP = set("""of and is are was were be been to the an by in on at for with so then thus that which
|
|
42
|
+
if as or we it has have had total each every per from into than this these those there here
|
|
43
|
+
not no yes can will would should could must may might do does did its their his her our your
|
|
44
|
+
what when where who how why also only just more less most least about over under between
|
|
45
|
+
after before again once both all any some such other same new old first second third""".split())
|
|
46
|
+
_NL_SYMS = set("xyznkpqwtr") | (set("ABCDEFGHJKLMNOPQRSTUVWXYZ"))
|
|
47
|
+
_UNIT_TEXT = {"\\text", "\\mbox", "\\mathrm", "\\textbf", "\\mathbf"}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def tokenize(s):
|
|
51
|
+
out = []
|
|
52
|
+
for m in _TOK.finditer(s):
|
|
53
|
+
k, v = m.lastgroup, m.group()
|
|
54
|
+
st, en = m.start(), m.end()
|
|
55
|
+
if k == "NUM":
|
|
56
|
+
out.append(("NUM", v, st, en))
|
|
57
|
+
elif k == "CMD":
|
|
58
|
+
if v in _CMD_OP:
|
|
59
|
+
out.append(("OP", _CMD_OP[v], st, en))
|
|
60
|
+
elif v in ("\\left", "\\right", "\\displaystyle", "\\quad", "\\qquad"):
|
|
61
|
+
out.append(("SP", " ", st, en))
|
|
62
|
+
elif v in ("\\cdots", "\\ldots", "\\dots", "\\cdot\\cdot\\cdot"):
|
|
63
|
+
out.append(("OTHER", "⋯", st, en))
|
|
64
|
+
elif v == "\\approx":
|
|
65
|
+
out.append(("OTHER", "≈", st, en))
|
|
66
|
+
elif v in ("\\le", "\\leq", "\\leqslant"):
|
|
67
|
+
out.append(("OTHER", "≤", st, en))
|
|
68
|
+
elif v in ("\\ge", "\\geq", "\\geqslant"):
|
|
69
|
+
out.append(("OTHER", "≥", st, en))
|
|
70
|
+
elif v == "\\lt":
|
|
71
|
+
out.append(("OTHER", "<", st, en))
|
|
72
|
+
elif v == "\\gt":
|
|
73
|
+
out.append(("OTHER", ">", st, en))
|
|
74
|
+
else:
|
|
75
|
+
out.append(("CMD", v, st, en))
|
|
76
|
+
elif k == "ESC":
|
|
77
|
+
c = v[1]
|
|
78
|
+
if c == "%":
|
|
79
|
+
out.append(("OP", "%", st, en))
|
|
80
|
+
elif c == "$":
|
|
81
|
+
out.append(("DOLLAR", "$", st, en))
|
|
82
|
+
elif c == "_":
|
|
83
|
+
out.append(("OTHER", "_", st, en))
|
|
84
|
+
else:
|
|
85
|
+
out.append(("SP", " ", st, en))
|
|
86
|
+
elif k == "WORD":
|
|
87
|
+
out.append(("ID", v, st, en))
|
|
88
|
+
elif k == "SP":
|
|
89
|
+
out.append(("SP", "\n" if "\n" in v else " ", st, en))
|
|
90
|
+
else:
|
|
91
|
+
c = _OPMAP.get(v, v)
|
|
92
|
+
if c == "$":
|
|
93
|
+
out.append(("DOLLAR", c, st, en))
|
|
94
|
+
elif c == "√":
|
|
95
|
+
out.append(("SQRT", c, st, en))
|
|
96
|
+
elif c == "°":
|
|
97
|
+
out.append(("DEG", c, st, en))
|
|
98
|
+
elif c in "²³":
|
|
99
|
+
out.append(("UNIT", c, st, en))
|
|
100
|
+
elif c in "+-*/^()!{}[]%":
|
|
101
|
+
out.append(("OP", c, st, en))
|
|
102
|
+
else:
|
|
103
|
+
out.append(("OTHER", c, st, en))
|
|
104
|
+
merged, q = [], 0
|
|
105
|
+
while q < len(out):
|
|
106
|
+
a = out[q]
|
|
107
|
+
if a[0] == "OTHER" and a[1] in "<>" and q + 1 < len(out) and out[q + 1][:2] == ("OTHER", "=") \
|
|
108
|
+
and out[q + 1][2] == a[3]:
|
|
109
|
+
merged.append(("OTHER", "≤" if a[1] == "<" else "≥", a[2], out[q + 1][3]))
|
|
110
|
+
q += 2
|
|
111
|
+
continue
|
|
112
|
+
merged.append(a)
|
|
113
|
+
q += 1
|
|
114
|
+
out = merged
|
|
115
|
+
# \text{...} -> UNIT (\mathrm{gcd} gibi fonksiyon adlari ID kalir)
|
|
116
|
+
res, i, n = [], 0, len(out)
|
|
117
|
+
while i < n:
|
|
118
|
+
t = out[i]
|
|
119
|
+
if t[0] == "CMD" and t[1] in _UNIT_TEXT and i + 1 < n and out[i + 1][:2] == ("OP", "{"):
|
|
120
|
+
j = i + 2
|
|
121
|
+
while j < n and out[j][:2] != ("OP", "}"):
|
|
122
|
+
j += 1
|
|
123
|
+
if j < n:
|
|
124
|
+
inner = [x for x in out[i + 2:j] if x[0] != "SP"]
|
|
125
|
+
if len(inner) == 1 and inner[0][0] == "ID" and inner[0][1] in _FUNC_WORDS:
|
|
126
|
+
res.append(("ID", inner[0][1], t[2], out[j][3]))
|
|
127
|
+
else:
|
|
128
|
+
res.append(("UNIT", "", t[2], out[j][3]))
|
|
129
|
+
i = j + 1
|
|
130
|
+
continue
|
|
131
|
+
res.append(t)
|
|
132
|
+
i += 1
|
|
133
|
+
return res
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# --------------------------------------------------------------------- parser
|
|
137
|
+
class _Fail(Exception):
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _mk(name, *args):
|
|
142
|
+
return ("call", name, list(args))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def is_call(n):
|
|
146
|
+
return isinstance(n, tuple) and n[0] == "call"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def is_sym(n):
|
|
150
|
+
return isinstance(n, tuple) and n[0] == "sym"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def has_sym(n):
|
|
154
|
+
if is_sym(n):
|
|
155
|
+
return True
|
|
156
|
+
if is_call(n):
|
|
157
|
+
return any(has_sym(a) for a in n[2])
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def count_ops(n):
|
|
162
|
+
if is_call(n):
|
|
163
|
+
return 1 + sum(count_ops(a) for a in n[2])
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def literal_like(n):
|
|
168
|
+
"""Kendisi bir 'sayi yazimi' olan ifade: 3, -3, 3/4, sqrt(3), 2*sqrt(3), pi, 2*pi ..."""
|
|
169
|
+
if isinstance(n, Fraction):
|
|
170
|
+
return True
|
|
171
|
+
if not is_call(n):
|
|
172
|
+
return False
|
|
173
|
+
name, args = n[1], n[2]
|
|
174
|
+
if name == "pi":
|
|
175
|
+
return True
|
|
176
|
+
if name == "neg":
|
|
177
|
+
return literal_like(args[0])
|
|
178
|
+
if name == "div":
|
|
179
|
+
a, b = args
|
|
180
|
+
if isinstance(a, Fraction) and isinstance(b, Fraction):
|
|
181
|
+
import math as _m
|
|
182
|
+
return (a.denominator == 1 and b.denominator == 1 and b > 1
|
|
183
|
+
and _m.gcd(abs(a.numerator), b.numerator) == 1)
|
|
184
|
+
return all(literal_like(x) for x in args)
|
|
185
|
+
if name == "sqrt":
|
|
186
|
+
return isinstance(args[0], Fraction)
|
|
187
|
+
if name == "mul" and len(args) == 2:
|
|
188
|
+
return isinstance(args[0], Fraction) and is_call(args[1]) and args[1][1] in ("sqrt", "pi") \
|
|
189
|
+
and literal_like(args[1])
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class _Parser:
|
|
194
|
+
def __init__(self, toks, allow_syms=False, defined=()):
|
|
195
|
+
self.t, self.nl = [], []
|
|
196
|
+
pending = False
|
|
197
|
+
for k, v, *_ in toks:
|
|
198
|
+
if k == "SP":
|
|
199
|
+
pending = pending or v == "\n"
|
|
200
|
+
continue
|
|
201
|
+
self.t.append((k, v))
|
|
202
|
+
self.nl.append(pending)
|
|
203
|
+
pending = False
|
|
204
|
+
self.i = 0
|
|
205
|
+
self.end_nonunit = 0
|
|
206
|
+
self.allow_syms = allow_syms
|
|
207
|
+
self.defined = set(defined)
|
|
208
|
+
self.boxed = False
|
|
209
|
+
|
|
210
|
+
def sym_ok(self, v):
|
|
211
|
+
if len(v) == 1:
|
|
212
|
+
return v.isalpha() and (self.allow_syms or v in _NL_SYMS)
|
|
213
|
+
return 2 <= len(v) <= 3 and v.isalpha() and v.isupper() and v not in _CURRENCY
|
|
214
|
+
|
|
215
|
+
def peek(self, off=0):
|
|
216
|
+
j = self.i + off
|
|
217
|
+
return self.t[j] if j < len(self.t) else (None, None)
|
|
218
|
+
|
|
219
|
+
def eat(self, kind=None, val=None):
|
|
220
|
+
k, v = self.peek()
|
|
221
|
+
if k is None or (kind and k != kind) or (val is not None and v != val):
|
|
222
|
+
raise _Fail
|
|
223
|
+
self.i += 1
|
|
224
|
+
return v
|
|
225
|
+
|
|
226
|
+
def at(self, kind, val=None):
|
|
227
|
+
k, v = self.peek()
|
|
228
|
+
return k == kind and (val is None or v == val)
|
|
229
|
+
|
|
230
|
+
def _is_unit(self):
|
|
231
|
+
if self.i < len(self.nl) and self.nl[self.i]:
|
|
232
|
+
return False
|
|
233
|
+
k, v = self.peek()
|
|
234
|
+
if k == "UNIT":
|
|
235
|
+
return True
|
|
236
|
+
if k == "ID" and v.islower() and v not in _STOP and v not in _FUNC_WORDS and v != "pi" \
|
|
237
|
+
and len(v) <= 14 and not self.sym_ok(v):
|
|
238
|
+
nk, nv = self.peek(1)
|
|
239
|
+
return not (nk == "OP" and nv == "(")
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
def _skip_units(self):
|
|
243
|
+
c = 0
|
|
244
|
+
while c < 3 and self._is_unit():
|
|
245
|
+
self.i += 1
|
|
246
|
+
c += 1
|
|
247
|
+
if self.peek() == ("ID", "per") and self.peek(1)[0] == "ID":
|
|
248
|
+
self.i += 2
|
|
249
|
+
# km/h, slices/pack gibi bolme isaretli birimler
|
|
250
|
+
while self.peek() == ("OP", "/") and self.peek(1)[0] in ("ID", "UNIT") and \
|
|
251
|
+
self.peek(1)[0] == "ID" and self.peek(1)[1].islower() and \
|
|
252
|
+
self.peek(1)[1] not in _STOP and self.peek(2) != ("OP", "("):
|
|
253
|
+
self.i += 2
|
|
254
|
+
while self.peek()[0] == "UNIT":
|
|
255
|
+
self.i += 1
|
|
256
|
+
|
|
257
|
+
def expr(self):
|
|
258
|
+
acc = self.term()
|
|
259
|
+
pure_add = False
|
|
260
|
+
while self.at("OP", "+") or self.at("OP", "-"):
|
|
261
|
+
if self.at("OP", "+") and self.peek(1) == ("OTHER", "⋯") and self.peek(2) == ("OP", "+"):
|
|
262
|
+
terms = acc[2] if (is_call(acc) and acc[1] == "add" and pure_add) else None
|
|
263
|
+
if terms is None or len(terms) < 2 or not all(isinstance(x, Fraction) for x in terms):
|
|
264
|
+
raise _Fail
|
|
265
|
+
d = terms[1] - terms[0]
|
|
266
|
+
if d == 0 or any(terms[i + 1] - terms[i] != d for i in range(len(terms) - 1)):
|
|
267
|
+
raise _Fail
|
|
268
|
+
self.eat(); self.eat(); self.eat()
|
|
269
|
+
last = self.term()
|
|
270
|
+
if not isinstance(last, Fraction):
|
|
271
|
+
raise _Fail
|
|
272
|
+
cnt = (last - terms[0]) / d + 1
|
|
273
|
+
if cnt.denominator != 1 or cnt < len(terms) + 1 or cnt > 100000:
|
|
274
|
+
raise _Fail
|
|
275
|
+
acc = _mk("arith_sum", terms[0], d, cnt)
|
|
276
|
+
pure_add = False
|
|
277
|
+
continue
|
|
278
|
+
op = self.eat()
|
|
279
|
+
rhs = self.term()
|
|
280
|
+
if op == "+":
|
|
281
|
+
if pure_add:
|
|
282
|
+
acc[2].append(rhs)
|
|
283
|
+
else:
|
|
284
|
+
acc = _mk("add", acc, rhs)
|
|
285
|
+
pure_add = True
|
|
286
|
+
else:
|
|
287
|
+
acc = _mk("sub", acc, rhs)
|
|
288
|
+
pure_add = False
|
|
289
|
+
return acc
|
|
290
|
+
|
|
291
|
+
def _starts_primary(self):
|
|
292
|
+
if self.i < len(self.nl) and self.nl[self.i]:
|
|
293
|
+
return False
|
|
294
|
+
k, v = self.peek()
|
|
295
|
+
if k == "OP" and v in ("(", "{"):
|
|
296
|
+
return True
|
|
297
|
+
if k == "CMD" and (v in ("\\frac", "\\dfrac", "\\tfrac", "\\sqrt", "\\pi") or v in _CMD_FUNCS):
|
|
298
|
+
return True
|
|
299
|
+
if k == "SQRT":
|
|
300
|
+
return True
|
|
301
|
+
if k == "ID" and (v == "pi" or v in _FUNC_WORDS):
|
|
302
|
+
return True
|
|
303
|
+
if k == "ID" and self.sym_ok(v):
|
|
304
|
+
return True
|
|
305
|
+
return False
|
|
306
|
+
|
|
307
|
+
def term(self):
|
|
308
|
+
acc = self.unary()
|
|
309
|
+
pure_mul = False
|
|
310
|
+
while True:
|
|
311
|
+
if self.at("OP", "*"):
|
|
312
|
+
self.eat()
|
|
313
|
+
rhs = self.unary()
|
|
314
|
+
if pure_mul:
|
|
315
|
+
acc[2].append(rhs)
|
|
316
|
+
else:
|
|
317
|
+
acc = _mk("mul", acc, rhs)
|
|
318
|
+
pure_mul = True
|
|
319
|
+
elif self.at("OP", "/"):
|
|
320
|
+
self.eat()
|
|
321
|
+
acc = _mk("div", acc, self.unary())
|
|
322
|
+
pure_mul = False
|
|
323
|
+
elif self._starts_primary():
|
|
324
|
+
rhs = self.unary()
|
|
325
|
+
if pure_mul:
|
|
326
|
+
acc[2].append(rhs)
|
|
327
|
+
else:
|
|
328
|
+
acc = _mk("mul", acc, rhs)
|
|
329
|
+
pure_mul = True
|
|
330
|
+
else:
|
|
331
|
+
break
|
|
332
|
+
return acc
|
|
333
|
+
|
|
334
|
+
def unary(self):
|
|
335
|
+
if self.at("OP", "-"):
|
|
336
|
+
self.eat()
|
|
337
|
+
v = self.unary()
|
|
338
|
+
return -v if isinstance(v, Fraction) else _mk("neg", v)
|
|
339
|
+
if self.at("OP", "+"):
|
|
340
|
+
self.eat()
|
|
341
|
+
return self.unary()
|
|
342
|
+
return self.power()
|
|
343
|
+
|
|
344
|
+
def power(self):
|
|
345
|
+
base = self.postfix()
|
|
346
|
+
if self.at("OP", "^"):
|
|
347
|
+
self.eat()
|
|
348
|
+
if self.at("CMD", "\\circ") or (self.at("OP", "{") and self.peek(1) == ("CMD", "\\circ")):
|
|
349
|
+
if self.at("OP", "{"):
|
|
350
|
+
self.eat(); self.eat(); self.eat("OP", "}")
|
|
351
|
+
else:
|
|
352
|
+
self.eat()
|
|
353
|
+
return _mk("deg", base)
|
|
354
|
+
return _mk("pow", base, self.exponent())
|
|
355
|
+
return base
|
|
356
|
+
|
|
357
|
+
def exponent(self):
|
|
358
|
+
if self.at("OP", "{"):
|
|
359
|
+
self.eat()
|
|
360
|
+
e = self.expr()
|
|
361
|
+
self.eat("OP", "}")
|
|
362
|
+
return e
|
|
363
|
+
if self.at("OP", "-"):
|
|
364
|
+
self.eat()
|
|
365
|
+
v = self.exponent()
|
|
366
|
+
return -v if isinstance(v, Fraction) else _mk("neg", v)
|
|
367
|
+
base = self.postfix(units=False)
|
|
368
|
+
if self.at("OP", "^"):
|
|
369
|
+
self.eat()
|
|
370
|
+
return _mk("pow", base, self.exponent())
|
|
371
|
+
return base
|
|
372
|
+
|
|
373
|
+
def postfix(self, units=True):
|
|
374
|
+
node = self.primary()
|
|
375
|
+
while True:
|
|
376
|
+
if self.at("OP", "!"):
|
|
377
|
+
self.eat()
|
|
378
|
+
node = _mk("fact", node)
|
|
379
|
+
elif self.at("DEG"):
|
|
380
|
+
self.eat()
|
|
381
|
+
node = _mk("deg", node)
|
|
382
|
+
elif self.at("OP", "%"):
|
|
383
|
+
self.eat()
|
|
384
|
+
if self.at("OP", "*"):
|
|
385
|
+
node = _mk("div", node, Fraction(100))
|
|
386
|
+
else:
|
|
387
|
+
break
|
|
388
|
+
self.end_nonunit = self.i
|
|
389
|
+
if units:
|
|
390
|
+
self._skip_units()
|
|
391
|
+
return node
|
|
392
|
+
|
|
393
|
+
def _group(self, open_, close):
|
|
394
|
+
self.eat("OP", open_)
|
|
395
|
+
e = self.expr()
|
|
396
|
+
self.eat("OP", close)
|
|
397
|
+
return e
|
|
398
|
+
|
|
399
|
+
def primary(self):
|
|
400
|
+
k, v = self.peek()
|
|
401
|
+
if k == "DOLLAR":
|
|
402
|
+
self.eat()
|
|
403
|
+
return self.primary()
|
|
404
|
+
if k == "CMD" and v in ("\\sum", "\\prod"):
|
|
405
|
+
self.eat()
|
|
406
|
+
self.eat("OTHER", "_")
|
|
407
|
+
self.eat("OP", "{")
|
|
408
|
+
var = self.eat("ID")
|
|
409
|
+
if len(var) != 1:
|
|
410
|
+
raise _Fail
|
|
411
|
+
self.eat("OTHER", "≔")
|
|
412
|
+
lo = self.expr()
|
|
413
|
+
self.eat("OP", "}")
|
|
414
|
+
self.eat("OP", "^")
|
|
415
|
+
hi = self._group("{", "}") if self.at("OP", "{") else self.primary()
|
|
416
|
+
body = self.term()
|
|
417
|
+
if not (isinstance(lo, Fraction) and isinstance(hi, Fraction)):
|
|
418
|
+
raise _Fail
|
|
419
|
+
return _mk("sum_range" if v == "\\sum" else "prod_range", infix(body), var, lo, hi)
|
|
420
|
+
if k == "NUM" and v.isdigit() and self.peek(1) == ("OTHER", "_"):
|
|
421
|
+
if self.peek(2)[0] == "NUM":
|
|
422
|
+
bt, adv = self.peek(2)[1], 3
|
|
423
|
+
elif self.peek(2) == ("OP", "{") and self.peek(3)[0] == "NUM" and self.peek(4) == ("OP", "}"):
|
|
424
|
+
bt, adv = self.peek(3)[1], 5
|
|
425
|
+
else:
|
|
426
|
+
bt, adv = None, 0
|
|
427
|
+
if bt is not None and bt.isdigit() and 2 <= int(bt) <= 16:
|
|
428
|
+
try:
|
|
429
|
+
int(v, int(bt))
|
|
430
|
+
except ValueError:
|
|
431
|
+
raise _Fail
|
|
432
|
+
self.i += adv
|
|
433
|
+
return _mk("from_base", v, Fraction(int(bt)))
|
|
434
|
+
if k == "NUM":
|
|
435
|
+
if (v.isdigit() and self.peek(1)[0] == "NUM" and self.peek(1)[1].isdigit()
|
|
436
|
+
and self.peek(2) == ("OP", "/") and self.peek(3)[0] == "NUM" and self.peek(3)[1].isdigit()
|
|
437
|
+
and not self.nl[self.i + 1] and int(self.peek(1)[1]) < int(self.peek(3)[1])):
|
|
438
|
+
w = Fraction(int(v))
|
|
439
|
+
a = Fraction(int(self.peek(1)[1]))
|
|
440
|
+
b = Fraction(int(self.peek(3)[1]))
|
|
441
|
+
self.i += 4
|
|
442
|
+
return _mk("add", w, _mk("div", a, b))
|
|
443
|
+
self.eat()
|
|
444
|
+
return Fraction(Decimal(v.replace(",", "").replace("\\!", "").replace("{", "").replace("}", "")))
|
|
445
|
+
if k == "OP" and v == "(":
|
|
446
|
+
return self._group("(", ")")
|
|
447
|
+
if k == "OP" and v == "{":
|
|
448
|
+
return self._group("{", "}")
|
|
449
|
+
if k == "OTHER" and v == "|":
|
|
450
|
+
self.eat()
|
|
451
|
+
e = self.expr()
|
|
452
|
+
self.eat("OTHER", "|")
|
|
453
|
+
return _mk("abs", e)
|
|
454
|
+
if k == "CMD" and v == "\\boxed":
|
|
455
|
+
self.eat()
|
|
456
|
+
e = self._group("{", "}")
|
|
457
|
+
self.boxed = True
|
|
458
|
+
return e
|
|
459
|
+
if k == "CMD" and v in ("\\frac", "\\dfrac", "\\tfrac"):
|
|
460
|
+
self.eat()
|
|
461
|
+
if self.at("NUM"):
|
|
462
|
+
d = self.eat()
|
|
463
|
+
if len(d) >= 2 and d.isdigit():
|
|
464
|
+
return _mk("div", Fraction(int(d[0])), Fraction(int(d[1:])))
|
|
465
|
+
a = Fraction(Decimal(d))
|
|
466
|
+
return _mk("div", a, self.primary())
|
|
467
|
+
a = self._group("{", "}")
|
|
468
|
+
b = self._group("{", "}")
|
|
469
|
+
return _mk("div", a, b)
|
|
470
|
+
if k == "CMD" and v in _BINOM:
|
|
471
|
+
self.eat()
|
|
472
|
+
a = self._group("{", "}")
|
|
473
|
+
b = self._group("{", "}")
|
|
474
|
+
return _mk("comb", a, b)
|
|
475
|
+
if (k == "ID" and v in ("gcd", "lcm")) or (k == "CMD" and v in ("\\gcd", "\\lcm")):
|
|
476
|
+
name = v.lstrip("\\")
|
|
477
|
+
self.eat()
|
|
478
|
+
self.eat("OP", "(")
|
|
479
|
+
args = [self.expr()]
|
|
480
|
+
while self.at("OTHER", ","):
|
|
481
|
+
self.eat()
|
|
482
|
+
args.append(self.expr())
|
|
483
|
+
self.eat("OP", ")")
|
|
484
|
+
if len(args) < 2:
|
|
485
|
+
raise _Fail
|
|
486
|
+
return _mk(name, *args)
|
|
487
|
+
if k == "CMD" and v == "\\sqrt":
|
|
488
|
+
self.eat()
|
|
489
|
+
if self.at("OP", "["):
|
|
490
|
+
self.eat()
|
|
491
|
+
n = self.expr()
|
|
492
|
+
self.eat("OP", "]")
|
|
493
|
+
a = self._group("{", "}")
|
|
494
|
+
return _mk("root", n, a)
|
|
495
|
+
if self.at("OP", "{"):
|
|
496
|
+
return _mk("sqrt", self._group("{", "}"))
|
|
497
|
+
return _mk("sqrt", self.primary())
|
|
498
|
+
if k == "SQRT":
|
|
499
|
+
self.eat()
|
|
500
|
+
if self.at("OP", "("):
|
|
501
|
+
return _mk("sqrt", self._group("(", ")"))
|
|
502
|
+
if self.at("NUM"):
|
|
503
|
+
return _mk("sqrt", self.primary())
|
|
504
|
+
raise _Fail
|
|
505
|
+
if (k == "CMD" and v == "\\pi") or (k == "ID" and v == "pi"):
|
|
506
|
+
self.eat()
|
|
507
|
+
return _mk("pi")
|
|
508
|
+
fname = None
|
|
509
|
+
if k == "CMD" and v in _CMD_FUNCS:
|
|
510
|
+
fname = _CMD_FUNCS[v]
|
|
511
|
+
elif k == "ID" and v in _FUNC_WORDS:
|
|
512
|
+
fname = v
|
|
513
|
+
if fname == "log" and self.peek(1) == ("OTHER", "_"):
|
|
514
|
+
self.eat(); self.eat()
|
|
515
|
+
if self.at("OP", "{"):
|
|
516
|
+
base = self._group("{", "}")
|
|
517
|
+
else:
|
|
518
|
+
base = self.primary()
|
|
519
|
+
if self.at("OP", "("):
|
|
520
|
+
arg = self._group("(", ")")
|
|
521
|
+
elif self.at("OP", "{"):
|
|
522
|
+
arg = self._group("{", "}")
|
|
523
|
+
else:
|
|
524
|
+
arg = self._arg_noexp()
|
|
525
|
+
return _mk("log", arg, base)
|
|
526
|
+
if fname:
|
|
527
|
+
self.eat()
|
|
528
|
+
if self.at("OP", "("):
|
|
529
|
+
arg = self._group("(", ")")
|
|
530
|
+
elif self.at("OP", "{"):
|
|
531
|
+
arg = self._group("{", "}")
|
|
532
|
+
else:
|
|
533
|
+
arg = self._arg_noexp()
|
|
534
|
+
return _mk(fname, arg)
|
|
535
|
+
if k == "ID" and v in _CURRENCY:
|
|
536
|
+
self.eat()
|
|
537
|
+
if self.at("OTHER", "."):
|
|
538
|
+
self.eat()
|
|
539
|
+
return self.primary()
|
|
540
|
+
if k == "ID" and (len(v) == 1 or self.sym_ok(v)) and v.isalpha():
|
|
541
|
+
if v in self.defined and self.peek(1) == ("OP", "("):
|
|
542
|
+
self.eat()
|
|
543
|
+
a = self._group("(", ")")
|
|
544
|
+
return _mk(v, a)
|
|
545
|
+
if self.sym_ok(v):
|
|
546
|
+
self.eat()
|
|
547
|
+
return ("sym", v + self._subscript())
|
|
548
|
+
raise _Fail
|
|
549
|
+
|
|
550
|
+
def _subscript(self):
|
|
551
|
+
if not self.at("OTHER", "_"):
|
|
552
|
+
return ""
|
|
553
|
+
self.eat()
|
|
554
|
+
if self.at("OP", "{"):
|
|
555
|
+
self.eat()
|
|
556
|
+
parts = []
|
|
557
|
+
while not self.at("OP", "}"):
|
|
558
|
+
k, v = self.peek()
|
|
559
|
+
if k is None or len(parts) > 6:
|
|
560
|
+
raise _Fail
|
|
561
|
+
self.i += 1
|
|
562
|
+
parts.append({"+": "p", "-": "m", "*": "t"}.get(v, v) if k == "OP" else v)
|
|
563
|
+
self.eat("OP", "}")
|
|
564
|
+
return "_" + re.sub(r"\W", "", "".join(parts))
|
|
565
|
+
k, v = self.peek()
|
|
566
|
+
if k in ("NUM", "ID"):
|
|
567
|
+
self.i += 1
|
|
568
|
+
return "_" + re.sub(r"\W", "", v)
|
|
569
|
+
raise _Fail
|
|
570
|
+
|
|
571
|
+
def _arg_noexp(self):
|
|
572
|
+
node = self.primary()
|
|
573
|
+
if self.at("DEG"):
|
|
574
|
+
self.eat()
|
|
575
|
+
return _mk("deg", node)
|
|
576
|
+
if self.at("OP", "^") and (self.peek(1) == ("CMD", "\\circ") or
|
|
577
|
+
(self.peek(1) == ("OP", "{") and self.peek(2) == ("CMD", "\\circ"))):
|
|
578
|
+
self.eat()
|
|
579
|
+
if self.at("OP", "{"):
|
|
580
|
+
self.eat(); self.eat(); self.eat("OP", "}")
|
|
581
|
+
else:
|
|
582
|
+
self.eat()
|
|
583
|
+
return _mk("deg", node)
|
|
584
|
+
return node
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def parse_full(toks, allow_syms=False, defined=()):
|
|
588
|
+
p = _Parser(toks, allow_syms, defined)
|
|
589
|
+
try:
|
|
590
|
+
n = p.expr()
|
|
591
|
+
except (_Fail, IndexError):
|
|
592
|
+
return None
|
|
593
|
+
return n if p.i == len(p.t) else None
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def parse_prefix(toks, allow_syms=False, defined=()):
|
|
597
|
+
"""RHS: bir ifade ayristirir. (node, tuketilen_token_sayisi(birimsiz, SP dahil), boxed)"""
|
|
598
|
+
nonsp = [i for i, t in enumerate(toks) if t[0] != "SP"]
|
|
599
|
+
p = _Parser(toks, allow_syms, defined)
|
|
600
|
+
try:
|
|
601
|
+
n = p.expr()
|
|
602
|
+
except (_Fail, IndexError):
|
|
603
|
+
return None, 0, False
|
|
604
|
+
if p.end_nonunit == 0:
|
|
605
|
+
return None, 0, False
|
|
606
|
+
return n, nonsp[p.end_nonunit - 1] + 1, p.boxed
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
# --------------------------------------------------------- cikti bicimleyiciler
|
|
610
|
+
def render(n):
|
|
611
|
+
if isinstance(n, Fraction):
|
|
612
|
+
return fmt_frac(n)
|
|
613
|
+
if isinstance(n, str):
|
|
614
|
+
return json.dumps(n)
|
|
615
|
+
_, name, args = n
|
|
616
|
+
return f"{name}({', '.join(render(a) for a in args)})"
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
_PREC = {"add": 1, "sub": 1, "mul": 2, "div": 2, "neg": 2.5, "pow": 3}
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def infix(n, parent=0):
|
|
623
|
+
"""Sembolik arguman icin sympy'nin okuyabilecegi duz metin."""
|
|
624
|
+
if isinstance(n, Fraction):
|
|
625
|
+
if n.denominator == 1:
|
|
626
|
+
return str(n.numerator) if n >= 0 or parent < 2 else f"({n.numerator})"
|
|
627
|
+
return f"({n.numerator}/{n.denominator})"
|
|
628
|
+
if is_sym(n):
|
|
629
|
+
return n[1]
|
|
630
|
+
_, name, a = n
|
|
631
|
+
if name in ("add", "mul"):
|
|
632
|
+
op = " + " if name == "add" else "*"
|
|
633
|
+
s = op.join(infix(x, _PREC[name]) for x in a)
|
|
634
|
+
return f"({s})" if parent > _PREC[name] else s
|
|
635
|
+
if name == "sub":
|
|
636
|
+
s = f"{infix(a[0], 1)} - {infix(a[1], 2)}"
|
|
637
|
+
return f"({s})" if parent > 1 else s
|
|
638
|
+
if name == "div":
|
|
639
|
+
s = f"{infix(a[0], 2)}/{infix(a[1], 3)}"
|
|
640
|
+
return f"({s})" if parent > 2 else s
|
|
641
|
+
if name == "neg":
|
|
642
|
+
s = f"-{infix(a[0], 3)}"
|
|
643
|
+
return f"({s})" if parent > 2 else s
|
|
644
|
+
if name == "pow":
|
|
645
|
+
return f"{infix(a[0], 4)}^{infix(a[1], 4)}" if parent <= 3 else f"({infix(a[0], 4)}^{infix(a[1], 4)})"
|
|
646
|
+
if name == "fact":
|
|
647
|
+
return f"factorial({infix(a[0])})"
|
|
648
|
+
if name == "pi":
|
|
649
|
+
return "pi"
|
|
650
|
+
if name == "deg":
|
|
651
|
+
return f"({infix(a[0], 2)})*pi/180"
|
|
652
|
+
return f"{name}({', '.join(infix(x) for x in a)})"
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def to_sympy(n):
|
|
656
|
+
if isinstance(n, Fraction):
|
|
657
|
+
return sympy.Rational(n.numerator, n.denominator)
|
|
658
|
+
if is_sym(n):
|
|
659
|
+
return sympy.Symbol(n[1])
|
|
660
|
+
_, name, a = n
|
|
661
|
+
if name == "from_base":
|
|
662
|
+
return sympy.Integer(int(a[0], int(a[1])))
|
|
663
|
+
if name in ("sum_range", "prod_range"):
|
|
664
|
+
return _back_sym(run(n))
|
|
665
|
+
v = [to_sympy(x) for x in a]
|
|
666
|
+
def _pw():
|
|
667
|
+
if v[1].is_number and (abs(v[1]) > 64 or (v[1].is_Rational and v[1].q > 8)):
|
|
668
|
+
raise ToolError("us cok buyuk")
|
|
669
|
+
if v[1].free_symbols and (v[0].free_symbols or v[1].count_ops() > 6):
|
|
670
|
+
raise ToolError("sembolik us")
|
|
671
|
+
return v[0] ** v[1]
|
|
672
|
+
|
|
673
|
+
m = {"add": lambda: sympy.Add(*v), "sub": lambda: v[0] - v[1], "mul": lambda: sympy.Mul(*v),
|
|
674
|
+
"div": lambda: v[0] / v[1], "pow": _pw, "neg": lambda: -v[0],
|
|
675
|
+
"fact": lambda: sympy.factorial(v[0]), "sqrt": lambda: sympy.sqrt(v[0]),
|
|
676
|
+
"root": lambda: sympy.root(v[1], v[0]), "pi": lambda: sympy.pi,
|
|
677
|
+
"deg": lambda: v[0] * sympy.pi / 180,
|
|
678
|
+
"arith_sum": lambda: v[2] * (2 * v[0] + (v[2] - 1) * v[1]) / 2, "ln": lambda: sympy.log(v[0]),
|
|
679
|
+
"log": lambda: sympy.log(v[0], v[1] if len(v) > 1 else 10), "exp": lambda: sympy.exp(v[0]), "abs": lambda: sympy.Abs(v[0])}
|
|
680
|
+
for f in ("sin", "cos", "tan", "cot", "sec", "csc", "asin", "acos", "atan"):
|
|
681
|
+
m[f] = (lambda f=f: getattr(sympy, f)(v[0]))
|
|
682
|
+
if name not in m:
|
|
683
|
+
raise ToolError(f"to_sympy: {name}")
|
|
684
|
+
return m[name]()
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _back_sym(x):
|
|
688
|
+
return sympy.Rational(x.numerator, x.denominator) if isinstance(x, Fraction) else x
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _finite_ends(sol):
|
|
692
|
+
out = []
|
|
693
|
+
if isinstance(sol, sympy.Union):
|
|
694
|
+
for a in sol.args:
|
|
695
|
+
out += _finite_ends(a)
|
|
696
|
+
elif isinstance(sol, sympy.Interval):
|
|
697
|
+
for e in (sol.start, sol.end):
|
|
698
|
+
if e.is_Rational:
|
|
699
|
+
out.append(Fraction(int(e.p), int(e.q)))
|
|
700
|
+
elif isinstance(sol, sympy.FiniteSet):
|
|
701
|
+
for e in sol.args:
|
|
702
|
+
if e.is_Rational:
|
|
703
|
+
out.append(Fraction(int(e.p), int(e.q)))
|
|
704
|
+
return out
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _same(a, b):
|
|
708
|
+
try:
|
|
709
|
+
if isinstance(a, Fraction):
|
|
710
|
+
a = sympy.Rational(a.numerator, a.denominator)
|
|
711
|
+
if isinstance(b, Fraction):
|
|
712
|
+
b = sympy.Rational(b.numerator, b.denominator)
|
|
713
|
+
d = a - b
|
|
714
|
+
if d == 0:
|
|
715
|
+
return True
|
|
716
|
+
if not d.free_symbols:
|
|
717
|
+
return abs(sympy.N(d, 40)) < sympy.Float("1e-30")
|
|
718
|
+
if d.count_ops() > 40:
|
|
719
|
+
return False
|
|
720
|
+
d = sympy.expand(d)
|
|
721
|
+
if d == 0:
|
|
722
|
+
return True
|
|
723
|
+
if d.has(sympy.sin, sympy.cos, sympy.tan, sympy.exp, sympy.log, sympy.factorial,
|
|
724
|
+
sympy.Abs, sympy.floor, sympy.ceiling):
|
|
725
|
+
return False
|
|
726
|
+
return sympy.cancel(d) == 0
|
|
727
|
+
except Exception:
|
|
728
|
+
return False
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _tag(call_str, result_str, prefix=""):
|
|
732
|
+
return f"{prefix}{CALL_OPEN}{call_str}{CALL_CLOSE}{RES_OPEN}{result_str}{RES_CLOSE}"
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
# ----------------------------------------------------------- on isleme (uzunluk koruyan)
|
|
736
|
+
_GSM_RE = re.compile(r"<<([^<>=]+)=([^<>]+)>>")
|
|
737
|
+
_LATEX_HINT = re.compile(r"\\(?:frac|dfrac|sqrt|times|cdot|boxed|text|left|right|div|pi|ldots|cdots|"
|
|
738
|
+
r"le|ge|neq|pm|sin|cos|tan|log|ln|binom|mathrm|begin|circ|approx|"
|
|
739
|
+
r"Rightarrow|implies)\b|\^\{|\\\(|\\\[")
|
|
740
|
+
_REGION = re.compile(r"\$\$(.+?)\$\$|\$([^$\n]+?)\$|\\\((.+?)\\\)|\\\[(.+?)\\\]", re.S)
|
|
741
|
+
_ALLOWED_OPS = set("+-*/^(){}!%")
|
|
742
|
+
_X_MULT = re.compile(r"(?<=[\d)])(\s*|\s+(?:[A-Za-z]+\s+){1,2})[xX](\s*)(?=[\d($])")
|
|
743
|
+
_CODE_HINT = re.compile(r"^\s*(print\(|def |import |from \w+ import |for .+ in |return )", re.M)
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _pad(rep):
|
|
747
|
+
return lambda m: rep + " " * (len(m.group(0)) - len(rep))
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
_WORD_SUBS = [
|
|
751
|
+
(re.compile(r"(?<=[\d)}])\s+divided by\s+(?=[\d(\\$-])", re.I), _pad("/")),
|
|
752
|
+
(re.compile(r"(?<=[\d)}])\s+(?:multiplied by|times)\s+(?=[\d(\\$-])", re.I), _pad("*")),
|
|
753
|
+
(re.compile(r"(?<=[\d)}])\s+plus\s+(?=[\d(\\$-])", re.I), _pad("+")),
|
|
754
|
+
(re.compile(r"(?<=[\d)}])\s+minus\s+(?=[\d(\\$-])", re.I), _pad("-")),
|
|
755
|
+
(re.compile(r"(?<=[\d)}])[\s,]*(?:which equals|which is equal to|is equal to|equals|equal to|gives)\s+"
|
|
756
|
+
r"(?=[\d(\\$-])", re.I), _pad("=")),
|
|
757
|
+
(re.compile(r"(?<=%)\s+of\s+(?=[\d(\\$])", re.I), _pad("*")),
|
|
758
|
+
]
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
_ASY = re.compile(r"\[asy\].*?\[/asy\]", re.S)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
_IS_EQ = re.compile(r"(\d[\d.,]*\s*[+\-*/]\s*\d[\d.,]*(?:\s*[+\-*/]\s*\d[\d.,]*)*)(\s+is\s+)(?=[\d$])")
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _is_to_eq(m):
|
|
768
|
+
sp = m.group(2)
|
|
769
|
+
return m.group(1) + " " + "=" + " " * (len(sp) - 2)
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
_SUM_HEAD = re.compile(r"(\\(?:sum|prod|lim)\s*_\s*\{[^{}=]*)=")
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def _make_scan(text):
|
|
776
|
+
text = _ASY.sub(lambda m: re.sub(r"[^\n]", " ", m.group(0)), text)
|
|
777
|
+
s = _X_MULT.sub(lambda m: m.group(1) + "*" + m.group(2), text)
|
|
778
|
+
s = s.replace("&", " ")
|
|
779
|
+
s = _SUM_HEAD.sub(lambda m: m.group(1) + "≔", s)
|
|
780
|
+
s = s.replace("...", "⋯ ").replace("…", "⋯")
|
|
781
|
+
for rx, rep in _WORD_SUBS:
|
|
782
|
+
s = rx.sub(rep, s)
|
|
783
|
+
s = _IS_EQ.sub(_is_to_eq, s)
|
|
784
|
+
return s
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _dollar_math(text):
|
|
788
|
+
for m in re.finditer(r"\$([^$\n]{1,200}?)\$", text):
|
|
789
|
+
inner = re.sub(r"\\[A-Za-z]+", "", m.group(1))
|
|
790
|
+
if re.search(r"[A-Za-z]{2,}", inner):
|
|
791
|
+
continue
|
|
792
|
+
if re.search(r"(?<![A-Za-z])[A-Za-z](?![A-Za-z])", inner) and re.search(r"[=^_(]", inner):
|
|
793
|
+
return True
|
|
794
|
+
return False
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
_DBG = []
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def _to_frac(v):
|
|
801
|
+
r = sympy.Rational(str(sympy.N(v, 40)))
|
|
802
|
+
return Fraction(int(r.p), int(r.q))
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def _new_stats():
|
|
806
|
+
return dict(found=0, converted=0, mismatch=0)
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def _fix_gsm_annotations(text, st):
|
|
810
|
+
out, pos = [], 0
|
|
811
|
+
for m in _GSM_RE.finditer(text):
|
|
812
|
+
expr, res = m.group(1).strip(), m.group(2).strip()
|
|
813
|
+
pre = text[pos:m.start()]
|
|
814
|
+
post_start = m.end()
|
|
815
|
+
node = parse_full(tokenize(expr))
|
|
816
|
+
ok, val = False, None
|
|
817
|
+
if node is not None and is_call(node):
|
|
818
|
+
rnode = parse_full(tokenize(res))
|
|
819
|
+
try:
|
|
820
|
+
val = run(node)
|
|
821
|
+
ok = rnode is not None and _same(val, run(rnode))
|
|
822
|
+
except Exception:
|
|
823
|
+
ok = False
|
|
824
|
+
st["found"] += 1
|
|
825
|
+
if ok:
|
|
826
|
+
pre2 = pre
|
|
827
|
+
body = pre.rstrip()
|
|
828
|
+
if body.endswith("="):
|
|
829
|
+
before = body[:-1].rstrip()
|
|
830
|
+
nrm = lambda z: re.sub(r"[\s$]", "", z)
|
|
831
|
+
for k in range(max(1, len(expr) - 3), len(expr) + 8):
|
|
832
|
+
if k <= len(before) and nrm(before[-k:]) == nrm(expr):
|
|
833
|
+
pre2 = before[:-k]
|
|
834
|
+
break
|
|
835
|
+
out.append(pre2)
|
|
836
|
+
rest = text[post_start:]
|
|
837
|
+
dup = re.match(re.escape(res) + r"(?![\d.])", rest)
|
|
838
|
+
out.append(_tag(render(node), to_text(val)))
|
|
839
|
+
st["converted"] += 1
|
|
840
|
+
pos = post_start + (dup.end() if dup else 0)
|
|
841
|
+
else:
|
|
842
|
+
st["mismatch"] += 1
|
|
843
|
+
out.append(pre)
|
|
844
|
+
pos = post_start
|
|
845
|
+
out.append(text[pos:])
|
|
846
|
+
return "".join(out)
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
_ENV_REGION = re.compile(r"\\begin\{(align|equation|gather|eqnarray|aligned|array)(\*?)\}(.+?)"
|
|
850
|
+
r"\\end\{\1\2\}", re.S)
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def _regions(text):
|
|
854
|
+
regs = []
|
|
855
|
+
for m in _ENV_REGION.finditer(text):
|
|
856
|
+
regs.append((m.start(), m.end(), m.start(3), m.end(3), text[m.start():m.start(3)],
|
|
857
|
+
text[m.end(3):m.end()]))
|
|
858
|
+
taken = [(r[0], r[1]) for r in regs]
|
|
859
|
+
for m in _REGION.finditer(text):
|
|
860
|
+
if any(a <= m.start() and m.end() <= b for a, b in taken):
|
|
861
|
+
continue
|
|
862
|
+
g = next(i for i in range(1, 5) if m.group(i) is not None)
|
|
863
|
+
regs.append((m.start(), m.end(), m.start(g), m.end(g), text[m.start():m.start(g)],
|
|
864
|
+
text[m.end(g):m.end()]))
|
|
865
|
+
regs.sort(key=lambda r: r[0])
|
|
866
|
+
return regs
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def _nums_after(text, pos):
|
|
870
|
+
vals = set()
|
|
871
|
+
for m in re.finditer(r"(?<![\d.])\d+(?:\.\d+)?", text[pos:]):
|
|
872
|
+
try:
|
|
873
|
+
vals.add(Fraction(Decimal(m.group(0))))
|
|
874
|
+
vals.add(-Fraction(Decimal(m.group(0))))
|
|
875
|
+
except Exception:
|
|
876
|
+
pass
|
|
877
|
+
for m in re.finditer(r"\\[dt]?frac\{(-?\d+)\}\{(\d+)\}", text[pos:]):
|
|
878
|
+
if int(m.group(2)):
|
|
879
|
+
f = Fraction(int(m.group(1)), int(m.group(2)))
|
|
880
|
+
vals.add(f)
|
|
881
|
+
vals.add(-f)
|
|
882
|
+
return vals
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def convert_text(text: str, aggressive: bool = False):
|
|
886
|
+
st = _new_stats()
|
|
887
|
+
if _CODE_HINT.search(_ASY.sub("", text)):
|
|
888
|
+
return text, st
|
|
889
|
+
if "<<" in text:
|
|
890
|
+
text = _fix_gsm_annotations(text, st)
|
|
891
|
+
latex_mode = bool(_LATEX_HINT.search(text)) or _dollar_math(text)
|
|
892
|
+
regions = _regions(text) if latex_mode else []
|
|
893
|
+
starts = [r[0] for r in regions]
|
|
894
|
+
|
|
895
|
+
def region_at(cs, ce):
|
|
896
|
+
k = bisect.bisect_right(starts, cs) - 1
|
|
897
|
+
if k >= 0 and regions[k][0] <= cs and ce <= regions[k][1]:
|
|
898
|
+
return k
|
|
899
|
+
return None
|
|
900
|
+
|
|
901
|
+
scan = _make_scan(text)
|
|
902
|
+
toks = tokenize(scan)
|
|
903
|
+
n = len(toks)
|
|
904
|
+
env = {} # tanimli fonksiyonlar: ad -> (vars, sympy_body)
|
|
905
|
+
edits = [] # (cs, ce, call_str, result_str, prefix, boxed_result)
|
|
906
|
+
solve_cands = [] # (cs, ce, node_l, node_r, after_pos)
|
|
907
|
+
last_end = 0
|
|
908
|
+
|
|
909
|
+
def allowed_left(t, pos_in_region):
|
|
910
|
+
k, v = t[0], t[1]
|
|
911
|
+
if k in ("NUM", "SP", "SQRT", "DEG", "UNIT"):
|
|
912
|
+
return True
|
|
913
|
+
if k == "OP":
|
|
914
|
+
return v in _ALLOWED_OPS
|
|
915
|
+
if k == "OTHER":
|
|
916
|
+
return v in (",", "|", "_", "⋯", "≔")
|
|
917
|
+
if k == "DOLLAR":
|
|
918
|
+
return not latex_mode
|
|
919
|
+
if k == "CMD":
|
|
920
|
+
return v in _CMD_ALLOWED
|
|
921
|
+
if k == "ID":
|
|
922
|
+
if v in _FUNC_WORDS or v == "pi" or v in _CURRENCY:
|
|
923
|
+
return True
|
|
924
|
+
if len(v) == 1 and v.isalpha() and ((latex_mode and pos_in_region) or v in _NL_SYMS):
|
|
925
|
+
return True
|
|
926
|
+
if 2 <= len(v) <= 3 and v.isupper() and v.isalpha():
|
|
927
|
+
return True
|
|
928
|
+
if v.islower() and v not in _STOP and len(v) <= 14:
|
|
929
|
+
return True # birim kelimesi olabilir
|
|
930
|
+
return False
|
|
931
|
+
return False
|
|
932
|
+
|
|
933
|
+
for i, t in enumerate(toks):
|
|
934
|
+
if t[0] != "OTHER" or t[1] not in ("=", "≈", "<", ">", "≤", "≥"):
|
|
935
|
+
continue
|
|
936
|
+
approx = t[1] == "≈"
|
|
937
|
+
rel = t[1] in ("<", ">", "≤", "≥")
|
|
938
|
+
if rel:
|
|
939
|
+
prv = toks[i - 1] if i > 0 else None
|
|
940
|
+
nx = toks[i + 1] if i + 1 < n else None
|
|
941
|
+
if (prv and prv[0] in ("OP", "OTHER") and prv[1] in ("-", "=", "<", ">")) or \
|
|
942
|
+
(nx and nx[0] == "OTHER" and nx[1] in ("<", ">")):
|
|
943
|
+
continue
|
|
944
|
+
if i + 1 < n and toks[i + 1][0] == "OTHER" and toks[i + 1][1] == "=":
|
|
945
|
+
continue
|
|
946
|
+
if i > 0 and toks[i - 1][0] == "OTHER" and toks[i - 1][1] in "=<>!≠":
|
|
947
|
+
continue
|
|
948
|
+
in_reg = region_at(t[2], t[3]) is not None
|
|
949
|
+
allow_syms = latex_mode and in_reg
|
|
950
|
+
j = i + 1
|
|
951
|
+
while j < n and toks[j][0] == "SP":
|
|
952
|
+
j += 1
|
|
953
|
+
if j >= n:
|
|
954
|
+
continue
|
|
955
|
+
rhs, used, boxed = parse_prefix(toks[j:], allow_syms, env.keys())
|
|
956
|
+
if rhs is None:
|
|
957
|
+
continue
|
|
958
|
+
r_end = j + used
|
|
959
|
+
nxt = r_end
|
|
960
|
+
while nxt < n and toks[nxt][0] == "SP":
|
|
961
|
+
nxt += 1
|
|
962
|
+
if nxt < n:
|
|
963
|
+
k2, v2 = toks[nxt][0], toks[nxt][1]
|
|
964
|
+
gap = scan[toks[r_end - 1][3]:toks[nxt][2]]
|
|
965
|
+
if "\n" not in gap and ((k2 == "OP" and v2 in "^(!") or k2 in ("NUM", "DEG", "SQRT") or
|
|
966
|
+
(k2 == "CMD" and v2 in _CMD_ALLOWED and v2 not in ("\\circ", "\\boxed"))):
|
|
967
|
+
continue
|
|
968
|
+
chain_next = nxt < n and toks[nxt][0] == "OTHER" and toks[nxt][1] == "="
|
|
969
|
+
# sol taraf
|
|
970
|
+
L = i
|
|
971
|
+
while L - 1 >= 0 and allowed_left(toks[L - 1], region_at(toks[L - 1][2], toks[L - 1][3]) is not None):
|
|
972
|
+
L -= 1
|
|
973
|
+
while L < i and toks[L][0] == "SP":
|
|
974
|
+
L += 1
|
|
975
|
+
found = None
|
|
976
|
+
for s in range(L, min(i, L + 60)):
|
|
977
|
+
if toks[s][0] == "SP":
|
|
978
|
+
continue
|
|
979
|
+
lhs_toks = toks[s:i]
|
|
980
|
+
# f(x) = ... tanimi
|
|
981
|
+
nonsp = [x for x in lhs_toks if x[0] != "SP"]
|
|
982
|
+
if (allow_syms and len(nonsp) >= 4 and nonsp[0][0] == "ID" and len(nonsp[0][1]) == 1
|
|
983
|
+
and nonsp[1][:2] == ("OP", "(") and nonsp[-1][:2] == ("OP", ")")
|
|
984
|
+
and all(x[0] == "ID" and len(x[1]) == 1 or x[:2] == ("OTHER", ",") for x in nonsp[2:-1])
|
|
985
|
+
and not (s > 0 and toks[s - 1][0] == "ID")):
|
|
986
|
+
found = (s, ("define", nonsp[0][1], [x[1] for x in nonsp[2:-1] if x[0] == "ID"]))
|
|
987
|
+
break
|
|
988
|
+
node = parse_full(lhs_toks, allow_syms, env.keys())
|
|
989
|
+
if node is None or isinstance(node, Fraction) or is_sym(node):
|
|
990
|
+
continue
|
|
991
|
+
q = s - 1
|
|
992
|
+
while q >= 0 and toks[q][0] == "SP":
|
|
993
|
+
q -= 1
|
|
994
|
+
if q >= 0:
|
|
995
|
+
kq, vq = toks[q][0], toks[q][1]
|
|
996
|
+
adjacent = (q == s - 1)
|
|
997
|
+
fk, fv = toks[s][0], toks[s][1]
|
|
998
|
+
if kq == "ID" and (fk == "OP" and fv in "+-*/^" or adjacent):
|
|
999
|
+
continue
|
|
1000
|
+
if kq == "OP" and vq in (")", "}", "^", "_") and fk == "OP":
|
|
1001
|
+
continue
|
|
1002
|
+
if kq == "OTHER" and vq == "_":
|
|
1003
|
+
continue
|
|
1004
|
+
if adjacent and kq in ("NUM", "ID"):
|
|
1005
|
+
continue
|
|
1006
|
+
if kq == "NUM" and fk == "NUM":
|
|
1007
|
+
continue
|
|
1008
|
+
found = (s, node)
|
|
1009
|
+
break
|
|
1010
|
+
if found is None and rel:
|
|
1011
|
+
continue
|
|
1012
|
+
if found is None:
|
|
1013
|
+
# "Sayi = 45 - 21" gibi: sol taraf etiket/degisken, sag taraf sayisal islem
|
|
1014
|
+
if approx or boxed or has_sym(rhs) or not is_call(rhs) or literal_like(rhs):
|
|
1015
|
+
continue
|
|
1016
|
+
if nxt < n and toks[nxt][0] == "OTHER" and toks[nxt][1] in ("=", "≈"):
|
|
1017
|
+
continue
|
|
1018
|
+
cs = toks[j][2]
|
|
1019
|
+
ce = toks[r_end - 1][3]
|
|
1020
|
+
if cs < last_end:
|
|
1021
|
+
continue
|
|
1022
|
+
try:
|
|
1023
|
+
v_ = run(rhs, env)
|
|
1024
|
+
except Exception:
|
|
1025
|
+
continue
|
|
1026
|
+
if isinstance(v_, str):
|
|
1027
|
+
continue
|
|
1028
|
+
st["found"] += 1
|
|
1029
|
+
edits.append((cs, ce, render(rhs), to_text(v_), "$" if toks[j][0] == "DOLLAR" and not latex_mode
|
|
1030
|
+
else "", None))
|
|
1031
|
+
last_end = ce
|
|
1032
|
+
st["converted"] += 1
|
|
1033
|
+
continue
|
|
1034
|
+
s, lhs = found
|
|
1035
|
+
cs = toks[s][2]
|
|
1036
|
+
ce = toks[r_end - 1][3]
|
|
1037
|
+
if cs < last_end:
|
|
1038
|
+
continue
|
|
1039
|
+
st["found"] += 1
|
|
1040
|
+
|
|
1041
|
+
# ---- fonksiyon tanimi
|
|
1042
|
+
if isinstance(lhs, tuple) and lhs and lhs[0] == "define":
|
|
1043
|
+
if not has_sym(rhs) or boxed:
|
|
1044
|
+
continue
|
|
1045
|
+
try:
|
|
1046
|
+
body = infix(rhs)
|
|
1047
|
+
vars_ = lhs[2]
|
|
1048
|
+
env[lhs[1]] = (vars_, _sym(body))
|
|
1049
|
+
edits.append((cs, ce, f'define("{lhs[1]}", "{", ".join(vars_)}", "{body}")',
|
|
1050
|
+
f"{lhs[1]}({', '.join(vars_)}) = {body}", "", None))
|
|
1051
|
+
last_end = ce
|
|
1052
|
+
st["converted"] += 1
|
|
1053
|
+
except Exception:
|
|
1054
|
+
pass
|
|
1055
|
+
continue
|
|
1056
|
+
|
|
1057
|
+
sym_l, sym_r = has_sym(lhs), has_sym(rhs)
|
|
1058
|
+
call_str = res_str = None
|
|
1059
|
+
if rel and not (sym_l or sym_r):
|
|
1060
|
+
continue
|
|
1061
|
+
try:
|
|
1062
|
+
if not sym_l and not sym_r:
|
|
1063
|
+
lv = run(lhs, env)
|
|
1064
|
+
rv = run(rhs, env)
|
|
1065
|
+
if approx:
|
|
1066
|
+
if not (isinstance(rhs, Fraction) and isinstance(lv, (Fraction,)) or True):
|
|
1067
|
+
continue
|
|
1068
|
+
rtxt = text[toks[j][2]:toks[r_end - 1][3]]
|
|
1069
|
+
mm = re.search(r"\.(\d+)", rtxt)
|
|
1070
|
+
nd = len(mm.group(1)) if mm else 0
|
|
1071
|
+
if not isinstance(rhs, Fraction):
|
|
1072
|
+
continue
|
|
1073
|
+
lvf = lv if isinstance(lv, Fraction) else Fraction(float(sympy.N(lv, 30)))
|
|
1074
|
+
if _round_half_up(lvf, nd) != rhs:
|
|
1075
|
+
st["mismatch"] += 1
|
|
1076
|
+
continue
|
|
1077
|
+
call_str = f"round({render(lhs)}, {nd})"
|
|
1078
|
+
res_str = to_text(rhs)
|
|
1079
|
+
else:
|
|
1080
|
+
if not _same(lv, rv):
|
|
1081
|
+
rtxt = text[toks[j][2]:toks[r_end - 1][3]]
|
|
1082
|
+
mm = re.search(r"\.(\d+)", rtxt)
|
|
1083
|
+
if isinstance(rhs, Fraction) and mm and not boxed or (isinstance(rhs, Fraction) and mm):
|
|
1084
|
+
nd = len(mm.group(1))
|
|
1085
|
+
try:
|
|
1086
|
+
lvf = lv if isinstance(lv, Fraction) else _to_frac(lv)
|
|
1087
|
+
if _round_half_up(lvf, nd) == rhs and is_call(lhs):
|
|
1088
|
+
edits.append((cs, ce, f"round({render(lhs)}, {nd})", to_text(rhs), "",
|
|
1089
|
+
to_text(rv) if boxed else None))
|
|
1090
|
+
last_end = ce
|
|
1091
|
+
st["converted"] += 1
|
|
1092
|
+
continue
|
|
1093
|
+
except Exception:
|
|
1094
|
+
pass
|
|
1095
|
+
if isinstance(rhs, Fraction) and rhs.denominator == 1 and is_call(lhs):
|
|
1096
|
+
try:
|
|
1097
|
+
import math as _m
|
|
1098
|
+
lvf = lv if isinstance(lv, Fraction) else _to_frac(lv)
|
|
1099
|
+
if lvf.denominator != 1 and abs(lvf - rhs) < 1:
|
|
1100
|
+
if _round_half_up(lvf, 0) == rhs:
|
|
1101
|
+
call_i = f"round({render(lhs)}, 0)"
|
|
1102
|
+
elif Fraction(_m.floor(lvf)) == rhs:
|
|
1103
|
+
call_i = f"floor({render(lhs)})"
|
|
1104
|
+
else:
|
|
1105
|
+
call_i = None
|
|
1106
|
+
if call_i:
|
|
1107
|
+
edits.append((cs, ce, call_i, to_text(rhs), "",
|
|
1108
|
+
to_text(rv) if boxed else None))
|
|
1109
|
+
last_end = ce
|
|
1110
|
+
st["converted"] += 1
|
|
1111
|
+
continue
|
|
1112
|
+
except Exception:
|
|
1113
|
+
pass
|
|
1114
|
+
st["mismatch"] += 1
|
|
1115
|
+
continue
|
|
1116
|
+
if not literal_like(rhs) and not boxed:
|
|
1117
|
+
# iki ifade de islem iceriyor: esitligi araca dogrulat
|
|
1118
|
+
if not is_call(rhs) or count_ops(rhs) >= count_ops(lhs) + 3 or chain_next:
|
|
1119
|
+
continue
|
|
1120
|
+
call_str = f'equal("{infix(lhs)}", "{infix(rhs)}")'
|
|
1121
|
+
res_str = "true"
|
|
1122
|
+
else:
|
|
1123
|
+
call_str = render(lhs)
|
|
1124
|
+
res_str = to_text(lv)
|
|
1125
|
+
else:
|
|
1126
|
+
if rel:
|
|
1127
|
+
L_, R_ = to_sympy(lhs), to_sympy(rhs)
|
|
1128
|
+
fs = (L_ - R_).free_symbols
|
|
1129
|
+
if len(fs) != 1 or boxed:
|
|
1130
|
+
continue
|
|
1131
|
+
var = next(iter(fs))
|
|
1132
|
+
diff_ = sympy.together(L_ - R_)
|
|
1133
|
+
if diff_.count_ops() > 50:
|
|
1134
|
+
continue
|
|
1135
|
+
if sympy.Poly(sympy.fraction(diff_)[0], var).degree() > 4:
|
|
1136
|
+
continue
|
|
1137
|
+
relcls = {"<": sympy.Lt, ">": sympy.Gt, "≤": sympy.Le, "≥": sympy.Ge}[t[1]]
|
|
1138
|
+
sol = sympy.solve_univariate_inequality(relcls(L_, R_), var, relational=False)
|
|
1139
|
+
ends = _finite_ends(sol)
|
|
1140
|
+
seen = _nums_after(text, ce)
|
|
1141
|
+
if not ends or not any(e_ in seen for e_ in ends):
|
|
1142
|
+
continue
|
|
1143
|
+
opstr = {"<": "<", ">": ">", "≤": "<=", "≥": ">="}[t[1]]
|
|
1144
|
+
call_ = f'solve("{infix(lhs)} {opstr} {infix(rhs)}", "{var}")'
|
|
1145
|
+
res_ = execute(call_)
|
|
1146
|
+
edits.append((cs, ce, call_, res_, "", None))
|
|
1147
|
+
last_end = ce
|
|
1148
|
+
st["converted"] += 1
|
|
1149
|
+
continue
|
|
1150
|
+
if approx or isinstance(lhs, Fraction):
|
|
1151
|
+
continue
|
|
1152
|
+
L_, R_ = to_sympy(lhs), to_sympy(rhs)
|
|
1153
|
+
if _same(L_, R_):
|
|
1154
|
+
# ozdeslik: LHS'i en uygun fonksiyonla yeniden yaz, RHS ile ayni olmali
|
|
1155
|
+
if sym_l and not sym_r:
|
|
1156
|
+
continue
|
|
1157
|
+
lhs_s = infix(lhs)
|
|
1158
|
+
for fn, f in (("expand", sympy.expand), ("factor", sympy.factor),
|
|
1159
|
+
("simplify", sympy.simplify)):
|
|
1160
|
+
cand = f(L_)
|
|
1161
|
+
if cand != L_ and _same(cand, R_):
|
|
1162
|
+
call_str = f'{fn}("{lhs_s}")'
|
|
1163
|
+
res_str = to_text(cand)
|
|
1164
|
+
break
|
|
1165
|
+
if call_str is None:
|
|
1166
|
+
if (not is_call(rhs) and not has_sym(rhs)) or chain_next:
|
|
1167
|
+
continue
|
|
1168
|
+
call_str = f'equal("{lhs_s}", "{infix(rhs)}")'
|
|
1169
|
+
res_str = "true"
|
|
1170
|
+
else:
|
|
1171
|
+
# denklem: solve adayi
|
|
1172
|
+
fs = (L_ - R_).free_symbols
|
|
1173
|
+
if len(fs) == 1 and (is_call(lhs) or is_call(rhs)) and not boxed:
|
|
1174
|
+
solve_cands.append((cs, ce, lhs, rhs, ce, next(iter(fs))))
|
|
1175
|
+
continue
|
|
1176
|
+
except Exception:
|
|
1177
|
+
continue
|
|
1178
|
+
edits.append((cs, ce, call_str, res_str, "", to_text(rv) if boxed and not sym_l and not sym_r
|
|
1179
|
+
and not approx else None))
|
|
1180
|
+
# $5 * 3 = $15 bicimi (para birimi)
|
|
1181
|
+
if not latex_mode and toks[s][0] == "DOLLAR":
|
|
1182
|
+
edits[-1] = edits[-1][:4] + ("$",) + edits[-1][5:]
|
|
1183
|
+
last_end = ce
|
|
1184
|
+
st["converted"] += 1
|
|
1185
|
+
|
|
1186
|
+
# ---- denklem cozme (solve): sonucu metinde sonradan gecen kokler
|
|
1187
|
+
taken = [(e[0], e[1]) for e in edits]
|
|
1188
|
+
for cs, ce, lhs, rhs, after, var in solve_cands:
|
|
1189
|
+
if any(a < ce and cs < b for a, b in taken):
|
|
1190
|
+
continue
|
|
1191
|
+
try:
|
|
1192
|
+
le, re_ = to_sympy(lhs), to_sympy(rhs)
|
|
1193
|
+
diff_ = sympy.together(le - re_)
|
|
1194
|
+
if diff_.count_ops() > 50:
|
|
1195
|
+
continue
|
|
1196
|
+
numer = sympy.fraction(diff_)[0]
|
|
1197
|
+
if sympy.Poly(numer, var).degree() > 4:
|
|
1198
|
+
continue
|
|
1199
|
+
sols = sympy.solve(sympy.Eq(le, re_), var)
|
|
1200
|
+
except Exception:
|
|
1201
|
+
continue
|
|
1202
|
+
if not sols or len(sols) > 4 or not all(getattr(s_, "is_real", False) for s_ in sols):
|
|
1203
|
+
continue
|
|
1204
|
+
seen = _nums_after(text, after)
|
|
1205
|
+
rat = [Fraction(int(s_.p), int(s_.q)) for s_ in sols if s_.is_Rational]
|
|
1206
|
+
if not rat or not any(r in seen for r in rat):
|
|
1207
|
+
continue
|
|
1208
|
+
sols = sorted(sols, key=lambda z: float(z))
|
|
1209
|
+
call = f'solve("{infix(lhs)} = {infix(rhs)}", "{var}")'
|
|
1210
|
+
try:
|
|
1211
|
+
res = execute(call)
|
|
1212
|
+
except Exception:
|
|
1213
|
+
continue
|
|
1214
|
+
edits.append((cs, ce, call, res, "", None))
|
|
1215
|
+
taken.append((cs, ce))
|
|
1216
|
+
st["converted"] += 1
|
|
1217
|
+
|
|
1218
|
+
# ---- agresif mod: "=" olmayan, yalnizca sayilardan olusan islem ifadeleri
|
|
1219
|
+
if aggressive:
|
|
1220
|
+
occupied = [(e[0], e[1]) for e in edits]
|
|
1221
|
+
k = 0
|
|
1222
|
+
while k < n:
|
|
1223
|
+
tk = toks[k]
|
|
1224
|
+
is_start = tk[0] == "NUM" or (tk[0] == "OP" and tk[1] == "(")
|
|
1225
|
+
if not is_start or any(a < tk[3] and tk[2] < b for a, b in occupied):
|
|
1226
|
+
k += 1
|
|
1227
|
+
continue
|
|
1228
|
+
if k > 0 and toks[k - 1][0] in ("ID", "NUM") or (tk[2] > 0 and scan[tk[2] - 1] in "_."):
|
|
1229
|
+
k += 1
|
|
1230
|
+
continue
|
|
1231
|
+
j = k
|
|
1232
|
+
while j < n:
|
|
1233
|
+
tj = toks[j]
|
|
1234
|
+
if tj[0] in ("NUM", "DEG", "SQRT"):
|
|
1235
|
+
j += 1
|
|
1236
|
+
elif tj[0] == "SP":
|
|
1237
|
+
if "\n" in scan[tj[2]:tj[3]]:
|
|
1238
|
+
break
|
|
1239
|
+
j += 1
|
|
1240
|
+
elif tj[0] == "OP" and tj[1] in "+-*/^()!":
|
|
1241
|
+
j += 1
|
|
1242
|
+
else:
|
|
1243
|
+
break
|
|
1244
|
+
end, done = j, False
|
|
1245
|
+
while end - k >= 3:
|
|
1246
|
+
while end > k and (toks[end - 1][0] == "SP" or
|
|
1247
|
+
(toks[end - 1][0] == "OP" and toks[end - 1][1] in "+-*/^(")):
|
|
1248
|
+
end -= 1
|
|
1249
|
+
if end - k < 3:
|
|
1250
|
+
break
|
|
1251
|
+
node = parse_full(toks[k:end], False, env.keys())
|
|
1252
|
+
if node is not None and is_call(node) and not literal_like(node) and count_ops(node) >= 1:
|
|
1253
|
+
cs_, ce_ = toks[k][2], toks[end - 1][3]
|
|
1254
|
+
seg = text[cs_:ce_]
|
|
1255
|
+
pk = k - 1
|
|
1256
|
+
while pk >= 0 and toks[pk][0] == "SP" and "\n" not in scan[toks[pk][2]:toks[pk][3]]:
|
|
1257
|
+
pk -= 1
|
|
1258
|
+
nk = end
|
|
1259
|
+
while nk < n and toks[nk][0] == "SP" and "\n" not in scan[toks[nk][2]:toks[nk][3]]:
|
|
1260
|
+
nk += 1
|
|
1261
|
+
prev_t = toks[pk] if pk >= 0 else None
|
|
1262
|
+
next_t = toks[nk] if nk < n else None
|
|
1263
|
+
|
|
1264
|
+
def _ok_neighbor(t_):
|
|
1265
|
+
if t_ is None or t_[0] in ("SP", "DOLLAR", "UNIT"):
|
|
1266
|
+
return True
|
|
1267
|
+
if t_[0] == "OTHER":
|
|
1268
|
+
return t_[1] in ",;:.=≈?!"
|
|
1269
|
+
if t_[0] == "ID":
|
|
1270
|
+
return t_[1].islower() and len(t_[1]) > 1
|
|
1271
|
+
if t_[0] == "OP":
|
|
1272
|
+
return t_[1] in "%" and False
|
|
1273
|
+
return False
|
|
1274
|
+
|
|
1275
|
+
bad = (
|
|
1276
|
+
re.fullmatch(r"\d{1,4}\s*[-/]\s*\d{1,4}(\s*[-/]\s*\d{1,4})?", seg) is not None
|
|
1277
|
+
and " " not in seg and not latex_mode
|
|
1278
|
+
) or any(a < ce_ and cs_ < b for a, b in occupied) \
|
|
1279
|
+
or region_at(cs_, ce_) is not None \
|
|
1280
|
+
or not _ok_neighbor(prev_t) or not _ok_neighbor(next_t)
|
|
1281
|
+
if not bad:
|
|
1282
|
+
try:
|
|
1283
|
+
v_ = run(node, env)
|
|
1284
|
+
except Exception:
|
|
1285
|
+
v_ = None
|
|
1286
|
+
if v_ is not None and not isinstance(v_, str) and len(to_text(v_)) <= 30:
|
|
1287
|
+
edits.append((cs_, ce_, render(node), to_text(v_), "", None))
|
|
1288
|
+
occupied.append((cs_, ce_))
|
|
1289
|
+
st["converted"] += 1
|
|
1290
|
+
done = True
|
|
1291
|
+
break
|
|
1292
|
+
end -= 1
|
|
1293
|
+
k = end if done else k + 1
|
|
1294
|
+
|
|
1295
|
+
# ---- $...$ icinde tek basina duran sayisal ifade
|
|
1296
|
+
for (a, b, ia, ib, op, cl) in regions:
|
|
1297
|
+
if any(x < b and a < y for x, y in taken):
|
|
1298
|
+
continue
|
|
1299
|
+
inner = text[ia:ib]
|
|
1300
|
+
if "=" in inner or not re.search(r"\d", inner):
|
|
1301
|
+
continue
|
|
1302
|
+
tk = tokenize(_make_scan(inner))
|
|
1303
|
+
node = parse_full(tk, False, env.keys())
|
|
1304
|
+
if node is None or not is_call(node) or literal_like(node) or count_ops(node) < 1:
|
|
1305
|
+
continue
|
|
1306
|
+
try:
|
|
1307
|
+
v = run(node, env)
|
|
1308
|
+
except Exception:
|
|
1309
|
+
continue
|
|
1310
|
+
if isinstance(v, str):
|
|
1311
|
+
continue
|
|
1312
|
+
edits.append((a, b, render(node), to_text(v), "", "REGION"))
|
|
1313
|
+
taken.append((a, b))
|
|
1314
|
+
st["converted"] += 1
|
|
1315
|
+
|
|
1316
|
+
if not edits:
|
|
1317
|
+
return text, st
|
|
1318
|
+
# son guvenlik: ciktidaki her cagri yeniden calistirilir, tutmayanlar geri alinir
|
|
1319
|
+
for _ in range(4):
|
|
1320
|
+
out = _apply(text, edits, regions, region_at)
|
|
1321
|
+
env2, failed = {}, set()
|
|
1322
|
+
for m in _TAG_RE.finditer(out):
|
|
1323
|
+
try:
|
|
1324
|
+
if to_text(run(parse_call(m.group(1)), env2)) != m.group(2):
|
|
1325
|
+
failed.add(m.group(1))
|
|
1326
|
+
except Exception:
|
|
1327
|
+
failed.add(m.group(1))
|
|
1328
|
+
if not failed:
|
|
1329
|
+
break
|
|
1330
|
+
edits = [e for e in edits if e[2] not in failed]
|
|
1331
|
+
st["converted"] = max(0, len(edits))
|
|
1332
|
+
if not edits:
|
|
1333
|
+
return text, st
|
|
1334
|
+
st["converted"] = len(edits)
|
|
1335
|
+
return out, st
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
def _wrap(content, op, cl):
|
|
1339
|
+
return f"{op}{content}{cl}" if content.strip() else ""
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
def _apply(text, edits, regions, region_at):
|
|
1343
|
+
edits = sorted(edits, key=lambda e: e[0])
|
|
1344
|
+
by_region, free = {}, []
|
|
1345
|
+
for e in edits:
|
|
1346
|
+
r = region_at(e[0], e[1])
|
|
1347
|
+
if r is not None and e[5] != "REGION":
|
|
1348
|
+
by_region.setdefault(r, []).append(e)
|
|
1349
|
+
else:
|
|
1350
|
+
free.append(e)
|
|
1351
|
+
# tam bolge degistirmeleri (REGION) ve serbest duzenlemeler
|
|
1352
|
+
out_edits = []
|
|
1353
|
+
for e in free:
|
|
1354
|
+
cs, ce, call, res, prefix, boxed = e
|
|
1355
|
+
if boxed == "REGION":
|
|
1356
|
+
out_edits.append((cs, ce, _tag(call, res)))
|
|
1357
|
+
else:
|
|
1358
|
+
tail = f"\\boxed{{{boxed}}}" if boxed else ""
|
|
1359
|
+
out_edits.append((cs, ce, _tag(call, res, prefix) + tail))
|
|
1360
|
+
for r, es in by_region.items():
|
|
1361
|
+
a, b, ia, ib, op, cl = regions[r]
|
|
1362
|
+
parts, pos = [], ia
|
|
1363
|
+
for cs, ce, call, res, prefix, boxed in es:
|
|
1364
|
+
parts.append(_wrap(text[pos:cs], op, cl))
|
|
1365
|
+
parts.append(_tag(call, res))
|
|
1366
|
+
if boxed:
|
|
1367
|
+
parts.append(f"{op}\\boxed{{{boxed}}}{cl}")
|
|
1368
|
+
pos = ce
|
|
1369
|
+
parts.append(_wrap(text[pos:ib], op, cl))
|
|
1370
|
+
out_edits.append((a, b, "".join(parts)))
|
|
1371
|
+
out_edits.sort(key=lambda e: e[0])
|
|
1372
|
+
out, pos = [], 0
|
|
1373
|
+
for cs, ce, new in out_edits:
|
|
1374
|
+
if cs < pos:
|
|
1375
|
+
continue
|
|
1376
|
+
out.append(text[pos:cs])
|
|
1377
|
+
out.append(new)
|
|
1378
|
+
pos = ce
|
|
1379
|
+
out.append(text[pos:])
|
|
1380
|
+
return "".join(out)
|
|
1381
|
+
|
|
1382
|
+
|
|
1383
|
+
_TAG_RE = re.compile(re.escape(CALL_OPEN) + r"(.+?)" + re.escape(CALL_CLOSE) + re.escape(RES_OPEN) +
|
|
1384
|
+
r"(.*?)" + re.escape(RES_CLOSE), re.S)
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
def verify_converted(text: str):
|
|
1388
|
+
"""Her cagriyi sirayla yeniden calistirir (define ortami korunur); (toplam, hatali)."""
|
|
1389
|
+
total = bad = 0
|
|
1390
|
+
env = {}
|
|
1391
|
+
for m in _TAG_RE.finditer(text):
|
|
1392
|
+
total += 1
|
|
1393
|
+
try:
|
|
1394
|
+
if to_text(run(parse_call(m.group(1)), env)) != m.group(2):
|
|
1395
|
+
bad += 1
|
|
1396
|
+
except Exception:
|
|
1397
|
+
bad += 1
|
|
1398
|
+
return total, bad
|
|
1399
|
+
|
|
1400
|
+
|
|
1401
|
+
if __name__ == "__main__":
|
|
1402
|
+
import sys
|
|
1403
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
1404
|
+
tests = [
|
|
1405
|
+
"Natalia sold 48/2 = <<48/2=24>>24 clips in May. In total 48+24 = <<48+24=72>>72 clips. #### 72",
|
|
1406
|
+
"She earns $5 * 3 = $15 per day and 15 * 7 = $105 per week.",
|
|
1407
|
+
"We have $3 + 4 = 7$ and $\\frac{3}{4} \\times 8 = 6$. Also $\\sin 30^\\circ = \\frac{1}{2}$.",
|
|
1408
|
+
"Jungkook collected 6 divided by 3, which equals 2. Then 12 cm = 12 * 10 mm = 120 mm.",
|
|
1409
|
+
"So $x=\\frac{70}{8}=\\boxed{8.75}$. Also $1/3 \\approx 0.33$ and $100 \\cdot 2^{10}$ is large.",
|
|
1410
|
+
"Then $(x+1)^2 = x^2 + 2x + 1$. Solving $24 + 0.25s + \\frac{2}{3}s = s$ we get $s = \\boxed{288}$.",
|
|
1411
|
+
"Let $f(x) = 2x + 1$. Then $f(3) = 7$ and $20\\%$ of 50 is 10; 20% of 50 = 10.",
|
|
1412
|
+
]
|
|
1413
|
+
for t in tests:
|
|
1414
|
+
o, st = convert_text(t)
|
|
1415
|
+
print("IN :", t)
|
|
1416
|
+
print("OUT:", o)
|
|
1417
|
+
print("ST :", st, "verify:", verify_converted(o))
|
|
1418
|
+
print()
|