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