/** * پارسرِ کوچکِ عبارتِ ریاضی برای نمودارِ دستیار (`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 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; }