- src/diagram: plane (constructed points, graphs with computed roots, vertex, extrema, tangent, asymptotes, piecewise limits, regions, rotations and reflections, line from ax + by = c), number line with intervals and stacked fractions, solids with hidden edges, bar/line/pie charts, sign tables, Venn, arrow mappings, probability trees, fraction bars and mixed numbers, grids, groups, clock. Pure-math text is bidi-overridden: Persian digits used to turn 3 × 4 = 12 into 12 = 4 × 3 even with dir=ltr. - MathText locks math runs inside Persian sentences in chat bubbles and figure captions, so -1 no longer shows as 1-. - Family screen: a student sees only the invite-code form; the parent form is behind a link. - Dev-only diagram gallery at ?gallery (not in the production build). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
180 lines
7.5 KiB
TypeScript
180 lines
7.5 KiB
TypeScript
/**
|
||
* پارسرِ کوچکِ عبارتِ ریاضی برای نمودارِ دستیار (`x^2-2*x`، `sin(x)+1`).
|
||
*
|
||
* عمداً eval یا new Function نیست: عبارت از مدل میآید و مدل متنِ کاربر را
|
||
* خوانده است. فقط عدد، x، pi، e، + − * / ^، پرانتز و چند تابعِ شناختهشده
|
||
* پذیرفته میشود؛ هر چیزِ دیگر خطا است و نمودار کشیده نمیشود. ضربِ ضمنی
|
||
* (2x، 3(x+1)، x sin(x)) هم پشتیبانی میشود چون مدل گاهی مینویسدش.
|
||
*/
|
||
type Node =
|
||
| { k: 'num'; v: number }
|
||
| { k: 'x' }
|
||
| { k: 'neg'; a: Node }
|
||
| { k: 'bin'; op: '+' | '-' | '*' | '/' | '^'; a: Node; b: Node }
|
||
| { k: 'fn'; f: string; a: Node };
|
||
|
||
const FUNCS: Record<string, (v: number) => number> = {
|
||
sin: (v) => Math.sin(v), cos: (v) => Math.cos(v), tan: (v) => Math.tan(v),
|
||
sqrt: (v) => Math.sqrt(v), abs: (v) => Math.abs(v), log: (v) => Math.log10(v), ln: (v) => Math.log(v),
|
||
};
|
||
|
||
function tokenize(src: string): string[] {
|
||
const s = src.replace(/\s+/g, '').replace(/\*\*/g, '^').replace(/[−–]/g, '-').replace(/[×·]/g, '*').replace(/÷/g, '/').replace(/²/g, '^2').replace(/³/g, '^3').replace(/π/g, 'pi');
|
||
const out: string[] = [];
|
||
let i = 0;
|
||
while (i < s.length) {
|
||
const rest = s.slice(i);
|
||
const num = /^(\d+\.?\d*|\.\d+)/.exec(rest);
|
||
if (num) { out.push(num[0]); i += num[0].length; continue; }
|
||
const word = /^[a-z]+/.exec(rest);
|
||
if (word) {
|
||
const w = word[0];
|
||
if (w === 'x' || w === 'pi' || w === 'e' || w in FUNCS) { out.push(w); i += w.length; continue; }
|
||
// «xsin» یا «pix»: نامِ چسبیده را تکهتکه کن
|
||
const known = ['sqrt', 'sin', 'cos', 'tan', 'abs', 'log', 'ln', 'pi', 'x', 'e'].find((k) => w.startsWith(k));
|
||
if (!known) throw new Error(`نامِ ناشناخته: ${w}`);
|
||
out.push(known); i += known.length; continue;
|
||
}
|
||
if ('+-*/^()'.includes(s[i])) { out.push(s[i]); i++; continue; }
|
||
throw new Error(`نویسهی ناشناخته: ${s[i]}`);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
export function parseExpr(src: string): (x: number) => number {
|
||
if (src.length > 120) throw new Error('عبارت بلند است');
|
||
const t = tokenize(src);
|
||
let p = 0;
|
||
const peek = () => t[p];
|
||
const eat = (tok?: string) => {
|
||
const v = t[p++];
|
||
if (tok !== undefined && v !== tok) throw new Error(`انتظارِ ${tok}`);
|
||
return v;
|
||
};
|
||
const startsAtom = (tok: string | undefined) => tok !== undefined && (/^[\d.]/.test(tok) || tok === 'x' || tok === 'pi' || tok === 'e' || tok === '(' || tok in FUNCS);
|
||
|
||
function expr(): Node {
|
||
let a = term();
|
||
while (peek() === '+' || peek() === '-') { const op = eat() as '+' | '-'; a = { k: 'bin', op, a, b: term() }; }
|
||
return a;
|
||
}
|
||
function term(): Node {
|
||
let a = unary();
|
||
for (;;) {
|
||
if (peek() === '*' || peek() === '/') { const op = eat() as '*' | '/'; a = { k: 'bin', op, a, b: unary() }; }
|
||
else if (startsAtom(peek())) a = { k: 'bin', op: '*', a, b: power() }; // ضربِ ضمنی
|
||
else return a;
|
||
}
|
||
}
|
||
function unary(): Node {
|
||
if (peek() === '-') { eat(); return { k: 'neg', a: unary() }; }
|
||
if (peek() === '+') { eat(); return unary(); }
|
||
return power();
|
||
}
|
||
function power(): Node {
|
||
const base = atom();
|
||
if (peek() === '^') { eat(); return { k: 'bin', op: '^', a: base, b: unary() }; }
|
||
return base;
|
||
}
|
||
function atom(): Node {
|
||
const tok = eat();
|
||
if (tok === undefined) throw new Error('عبارت ناتمام است');
|
||
if (/^[\d.]/.test(tok)) return { k: 'num', v: Number(tok) };
|
||
if (tok === 'x') return { k: 'x' };
|
||
if (tok === 'pi') return { k: 'num', v: Math.PI };
|
||
if (tok === 'e') return { k: 'num', v: Math.E };
|
||
if (tok in FUNCS) {
|
||
if (peek() === '(') { eat('('); const a = expr(); eat(')'); return { k: 'fn', f: tok, a }; }
|
||
return { k: 'fn', f: tok, a: power() };
|
||
}
|
||
if (tok === '(') { const a = expr(); eat(')'); return a; }
|
||
throw new Error(`غیرمنتظره: ${tok}`);
|
||
}
|
||
|
||
const tree = expr();
|
||
if (p !== t.length) throw new Error('عبارت اضافه دارد');
|
||
|
||
const ev = (n: Node, x: number): number => {
|
||
switch (n.k) {
|
||
case 'num': return n.v;
|
||
case 'x': return x;
|
||
case 'neg': return -ev(n.a, x);
|
||
case 'fn': return FUNCS[n.f](ev(n.a, x));
|
||
case 'bin': {
|
||
const a = ev(n.a, x), b = ev(n.b, x);
|
||
return n.op === '+' ? a + b : n.op === '-' ? a - b : n.op === '*' ? a * b : n.op === '/' ? a / b : Math.pow(a, b);
|
||
}
|
||
}
|
||
};
|
||
return (x) => ev(tree, x);
|
||
}
|
||
|
||
/* ─── تحلیلِ عددی: ریشه، اکسترمم، شیب ───────────────────────────────────────
|
||
* همه با دو برابر کردنِ دقت (bisection) تا ۱e-10؛ برای نمایش به دو رقمِ اعشار گرد
|
||
* میشوند. ریشهی مضاعف (مثلِ x² در صفر) علامت عوض نمیکند، پس جدا از روی
|
||
* اکسترممی که مقدارش صفر است پیدا میشود.
|
||
*/
|
||
|
||
type F = (x: number) => number;
|
||
|
||
function bisect(g: F, a: number, b: number): number {
|
||
let ga = g(a);
|
||
for (let i = 0; i < 80; i++) {
|
||
const m = (a + b) / 2, gm = g(m);
|
||
if (!Number.isFinite(gm)) return m;
|
||
if (Math.sign(gm) === Math.sign(ga)) { a = m; ga = gm; } else b = m;
|
||
if (b - a < 1e-11) break;
|
||
}
|
||
return (a + b) / 2;
|
||
}
|
||
|
||
/** مشتقِ عددی (تفاضلِ مرکزی). */
|
||
export function derivative(f: F, x: number): number {
|
||
const h = 1e-5 * Math.max(1, Math.abs(x));
|
||
return (f(x + h) - f(x - h)) / (2 * h);
|
||
}
|
||
|
||
const snap = (x: number) => {
|
||
const r = Math.round(x);
|
||
if (Math.abs(x - r) < 1e-7) return r;
|
||
const r2 = Math.round(x * 1e6) / 1e6;
|
||
return Math.abs(x - r2) < 1e-9 ? r2 : x;
|
||
};
|
||
|
||
export function extrema(f: F, from: number, to: number): { x: number; y: number; kind: 'max' | 'min' }[] {
|
||
const n = 600, out: { x: number; y: number; kind: 'max' | 'min' }[] = [];
|
||
const d = (x: number) => derivative(f, x);
|
||
let px = from, pd = d(px);
|
||
for (let i = 1; i <= n; i++) {
|
||
const x = from + ((to - from) * i) / n, dx = d(x);
|
||
if (Number.isFinite(pd) && Number.isFinite(dx) && pd !== 0 && Math.sign(dx) !== Math.sign(pd)) {
|
||
const xe = snap(bisect(d, px, x));
|
||
const y = f(xe);
|
||
// لبهی مجانب (1/x) علامتِ مشتق را بیاکسترمم عوض نمیکند: تابع باید پیوسته باشد.
|
||
if (Number.isFinite(y) && Math.abs(f(px) - f(x)) < 1e3 * (Math.abs(y) + 1)) out.push({ x: xe, y: snap(y), kind: pd > 0 ? 'max' : 'min' });
|
||
}
|
||
px = x; pd = dx;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
export function roots(f: F, from: number, to: number): number[] {
|
||
const n = 800, out: number[] = [];
|
||
let px = from, py = f(px);
|
||
if (Math.abs(py) < 1e-12) out.push(from);
|
||
for (let i = 1; i <= n; i++) {
|
||
const x = from + ((to - from) * i) / n, y = f(x);
|
||
if (Math.abs(y) < 1e-12) out.push(x);
|
||
else if (Number.isFinite(py) && Number.isFinite(y) && py !== 0 && Math.sign(y) !== Math.sign(py)) {
|
||
const r = bisect(f, px, x);
|
||
// پرشِ علامت بدونِ گذر از صفر (مجانب) ریشه نیست.
|
||
if (Math.abs(f(r)) < 1e-6) out.push(r);
|
||
}
|
||
px = x; py = y;
|
||
}
|
||
for (const e of extrema(f, from, to)) if (Math.abs(e.y) < 1e-9) out.push(e.x);
|
||
const uniq: number[] = [];
|
||
for (const r of out.map(snap).sort((a, b) => a - b)) if (!uniq.length || Math.abs(r - uniq[uniq.length - 1]) > 1e-6) uniq.push(r);
|
||
return uniq;
|
||
}
|