diff --git a/src/views/casio-fx580/engine.ts b/src/views/casio-fx580/engine.ts new file mode 100644 index 00000000..83f40f8d --- /dev/null +++ b/src/views/casio-fx580/engine.ts @@ -0,0 +1,436 @@ +/** + * Calculator Engine + * Evaluates AST, handles modes, memory, advanced features + */ +import { Parser, type ASTNode } from './parser' + +export type AngleMode = 'DEG' | 'RAD' | 'GRAD' + +export interface CalcMemory { + A: number + B: number + C: number + D: number + X: number + Y: number + M: number + Ans: number +} + +export interface ComplexNum { + re: number + im: number +} + +// ── Engine ──────────────────────────────────────────────────────────────── +export class CalcEngine { + angleMode: AngleMode = 'DEG' + memory: CalcMemory = { A: 0, B: 0, C: 0, D: 0, X: 0, Y: 0, M: 0, Ans: 0 } + + // ── Angle helpers ────────────────────────────────────────────────────── + toRad(x: number): number { + if (this.angleMode === 'RAD') return x + if (this.angleMode === 'GRAD') return (x * Math.PI) / 200 + return (x * Math.PI) / 180 + } + fromRad(x: number): number { + if (this.angleMode === 'RAD') return x + if (this.angleMode === 'GRAD') return (x * 200) / Math.PI + return (x * 180) / Math.PI + } + + // ── Evaluate expression string ───────────────────────────────────────── + eval(expr: string): number { + if (!expr.trim()) throw new Error('Empty expression') + const parser = new Parser(this.preprocess(expr)) + const ast = parser.parse() + return this.evalNode(ast) + } + + preprocess(expr: string): string { + return expr + .replace(/×10\^/g, 'E') + .replace(/×/g, '*') + .replace(/÷/g, '/') + .replace(/−/g, '-') + .replace(/²/g, '^2') + .replace(/³/g, '^3') + .replace(/√\(/g, 'sqrt(') + .replace(/∛\(/g, 'cbrt(') + .replace(/π/g, 'π') + .replace(/ℯ/g, 'e') + } + + private evalNode(node: ASTNode): number { + switch (node.kind) { + case 'num': + return node.value + case 'var': { + const k = node.name.toUpperCase() as keyof CalcMemory + if (k in this.memory) return this.memory[k] + return 0 + } + case 'unary': { + const v = this.evalNode(node.arg) + if (node.op === '-') return -v + if (node.op === '%') return v / 100 + return v + } + case 'factorial': { + const n = Math.round(this.evalNode(node.arg)) + if (n < 0 || n > 69) throw new Error('Math ERROR') + let r = 1 + for (let i = 2; i <= n; i++) r *= i + return r + } + case 'binop': { + const l = this.evalNode(node.left) + const r = this.evalNode(node.right) + switch (node.op) { + case '+': + return l + r + case '-': + return l - r + case '*': + return l * r + case '/': + if (r === 0) throw new Error('Math ERROR') + return l / r + case '^': + return Math.pow(l, r) + case 'E': + return l * Math.pow(10, r) + default: + throw new Error('Unknown op') + } + } + case 'call': + return this.evalFn( + node.fn, + node.args.map((a) => this.evalNode(a)), + ) + } + } + + private evalFn(fn: string, args: number[]): number { + const a = args[0] ?? 0 + const b = args[1] ?? 0 + switch (fn) { + // trig + case 'sin': + return Math.sin(this.toRad(a)) + case 'cos': + return Math.cos(this.toRad(a)) + case 'tan': + return Math.tan(this.toRad(a)) + case 'sin⁻¹': + case 'asin': + return this.fromRad(Math.asin(a)) + case 'cos⁻¹': + case 'acos': + return this.fromRad(Math.acos(a)) + case 'tan⁻¹': + case 'atan': + return this.fromRad(Math.atan(a)) + // hyperbolic + case 'sinh': + return Math.sinh(a) + case 'cosh': + return Math.cosh(a) + case 'tanh': + return Math.tanh(a) + case 'sinh⁻¹': + case 'asinh': + return Math.asinh(a) + case 'cosh⁻¹': + case 'acosh': + return Math.acosh(a) + case 'tanh⁻¹': + case 'atanh': + return Math.atanh(a) + // log + case 'log': + return b !== 0 ? Math.log(b) / Math.log(a) : Math.log10(a) + case 'log10': + return Math.log10(a) + case 'ln': + return Math.log(a) + // root + case 'sqrt': + case '√': + return Math.sqrt(a) + case 'cbrt': + case '∛': + return Math.cbrt(a) + case 'nthroot': + return Math.pow(b, 1 / a) + // misc + case 'abs': + case 'Abs': + return Math.abs(a) + case 'int': + return Math.trunc(a) + case 'frac': + return a - Math.trunc(a) + case 'round': + return Math.round(a) + case 'floor': + return Math.floor(a) + case 'ceil': + return Math.ceil(a) + case 'sign': + return Math.sign(a) + case 'exp': + return Math.exp(a) + // combinations + case 'npr': { + const n = Math.round(a), + r = Math.round(b) + return this.perm(n, r) + } + case 'ncr': { + const n = Math.round(a), + r = Math.round(b) + return this.comb(n, r) + } + case 'gcd': + return this.gcd(Math.round(a), Math.round(b)) + case 'lcm': + return Math.abs(a * b) / this.gcd(Math.round(a), Math.round(b)) + case 'mod': + return a % b + case 'random': + return Math.random() + case 'ranint': + return Math.floor(a + Math.random() * (b - a + 1)) + case 'max': + return Math.max(...args) + case 'min': + return Math.min(...args) + case 'sum': + return args.reduce((s, x) => s + x, 0) + default: + throw new Error(`Unknown function: ${fn}`) + } + } + + private perm(n: number, r: number): number { + if (r > n || r < 0) throw new Error('Math ERROR') + let result = 1 + for (let i = 0; i < r; i++) result *= n - i + return result + } + private comb(n: number, r: number): number { + if (r > n || r < 0) throw new Error('Math ERROR') + r = Math.min(r, n - r) + let num = 1, + den = 1 + for (let i = 0; i < r; i++) { + num *= n - i + den *= i + 1 + } + return num / den + } + private gcd(a: number, b: number): number { + a = Math.abs(a) + b = Math.abs(b) + while (b) { + const t = b + b = a % b + a = t + } + return a + } + + // ── Derivative (numerical) ───────────────────────────────────────────── + derivative(expr: string, x: number, h = 1e-8): number { + const f = (v: number) => { + this.memory.X = v + return this.eval(expr) + } + return (f(x + h) - f(x - h)) / (2 * h) + } + + // ── Definite integral (Simpson's rule) ──────────────────────────────── + integrate(expr: string, a: number, b: number, n = 1000): number { + if (n % 2 !== 0) n++ + const h = (b - a) / n + const f = (x: number) => { + this.memory.X = x + return this.eval(expr) + } + let s = f(a) + f(b) + for (let i = 1; i < n; i++) s += f(a + i * h) * (i % 2 === 0 ? 2 : 4) + return (s * h) / 3 + } + + // ── Solve quadratic ax²+bx+c=0 ──────────────────────────────────────── + solveQuadratic(a: number, b: number, c: number): ComplexNum[] { + const disc = b * b - 4 * a * c + if (disc >= 0) { + return [ + { re: (-b + Math.sqrt(disc)) / (2 * a), im: 0 }, + { re: (-b - Math.sqrt(disc)) / (2 * a), im: 0 }, + ] + } + return [ + { re: -b / (2 * a), im: Math.sqrt(-disc) / (2 * a) }, + { re: -b / (2 * a), im: -Math.sqrt(-disc) / (2 * a) }, + ] + } + + // ── Solve cubic ax³+bx²+cx+d=0 (Cardano) ───────────────────────────── + solveCubic(a: number, b: number, c: number, d: number): number[] { + // Normalize + b /= a + c /= a + d /= a + const p = c - (b * b) / 3 + const q = (2 * b * b * b) / 27 - (b * c) / 3 + d + const D = (q * q) / 4 + (p * p * p) / 27 + if (D > 0) { + const u = Math.cbrt(-q / 2 + Math.sqrt(D)) + const v = Math.cbrt(-q / 2 - Math.sqrt(D)) + return [u + v - b / 3] + } else if (D === 0) { + const u = Math.cbrt(-q / 2) + return [2 * u - b / 3, -u - b / 3] + } else { + const r = Math.sqrt((-p * p * p) / 27) + const theta = Math.acos(-q / (2 * r)) + const m = 2 * Math.cbrt(r) + return [ + m * Math.cos(theta / 3) - b / 3, + m * Math.cos((theta + 2 * Math.PI) / 3) - b / 3, + m * Math.cos((theta + 4 * Math.PI) / 3) - b / 3, + ] + } + } + + // ── Solve 2×2 linear system ────────────────────────────────────────── + solve2x2( + a1: number, + b1: number, + c1: number, + a2: number, + b2: number, + c2: number, + ): [number, number] { + const det = a1 * b2 - a2 * b1 + if (Math.abs(det) < 1e-12) throw new Error('No unique solution') + return [(c1 * b2 - c2 * b1) / det, (a1 * c2 - a2 * c1) / det] + } + + // ── Solve 3×3 linear system ────────────────────────────────────────── + solve3x3( + a1: number, + b1: number, + c1: number, + d1: number, + a2: number, + b2: number, + c2: number, + d2: number, + a3: number, + b3: number, + c3: number, + d3: number, + ): [number, number, number] { + const det = a1 * (b2 * c3 - b3 * c2) - b1 * (a2 * c3 - a3 * c2) + c1 * (a2 * b3 - a3 * b2) + if (Math.abs(det) < 1e-12) throw new Error('No unique solution') + const x = (d1 * (b2 * c3 - b3 * c2) - b1 * (d2 * c3 - d3 * c2) + c1 * (d2 * b3 - d3 * b2)) / det + const y = (a1 * (d2 * c3 - d3 * c2) - d1 * (a2 * c3 - a3 * c2) + c1 * (a2 * d3 - a3 * d2)) / det + const z = (a1 * (b2 * d3 - b3 * d2) - b1 * (a2 * d3 - a3 * d2) + d1 * (a2 * b3 - a3 * b2)) / det + return [x, y, z] + } + + // ── Matrix ops (2×2) ──────────────────────────────────────────────── + matDet2(m: number[][]): number { + return (m[0]![0] ?? 0) * (m[1]![1] ?? 0) - (m[0]![1] ?? 0) * (m[1]![0] ?? 0) + } + matInv2(m: number[][]): number[][] { + const det = this.matDet2(m) + if (Math.abs(det) < 1e-12) throw new Error('Singular matrix') + return [ + [(m[1]![1] ?? 0) / det, -(m[0]![1] ?? 0) / det], + [-(m[1]![0] ?? 0) / det, (m[0]![0] ?? 0) / det], + ] + } + matMul(a: number[][], b: number[][]): number[][] { + const rows = a.length, + cols = b[0]!.length, + inner = b.length + return Array.from({ length: rows }, (_, i) => + Array.from({ length: cols }, (_, j) => + Array.from({ length: inner }, (_, k) => (a[i]![k] ?? 0) * (b[k]![j] ?? 0)).reduce( + (s, v) => s + v, + 0, + ), + ), + ) + } + + // ── Complex arithmetic ─────────────────────────────────────────────── + complexAdd(a: ComplexNum, b: ComplexNum): ComplexNum { + return { re: a.re + b.re, im: a.im + b.im } + } + complexMul(a: ComplexNum, b: ComplexNum): ComplexNum { + return { re: a.re * b.re - a.im * b.im, im: a.re * b.im + a.im * b.re } + } + complexAbs(a: ComplexNum): number { + return Math.sqrt(a.re * a.re + a.im * a.im) + } + complexArg(a: ComplexNum): number { + return this.fromRad(Math.atan2(a.im, a.re)) + } + + // ── Table generation ──────────────────────────────────────────────── + generateTable( + expr: string, + start: number, + end: number, + step: number, + ): { x: number; y: number }[] { + const rows: { x: number; y: number }[] = [] + for (let x = start; x <= end + step * 0.001; x += step) { + this.memory.X = x + try { + rows.push({ x, y: this.eval(expr) }) + } catch { + rows.push({ x, y: NaN }) + } + } + return rows + } + + // ── Format output ─────────────────────────────────────────────────── + format(val: number, fracMode = false): string { + if (isNaN(val)) return 'Math ERROR' + if (!isFinite(val)) return val > 0 ? '+∞' : '-∞' + if (fracMode) { + const frac = this.toFraction(val) + if (frac) return frac + } + if (Number.isInteger(val) && Math.abs(val) < 1e12) return val.toString() + if (Math.abs(val) >= 1e10 || (Math.abs(val) < 1e-9 && val !== 0)) { + return val + .toExponential(9) + .replace(/\.?0+(e)/, '$1') + .replace('e+', '×10^') + .replace('e-', '×10^-') + } + return parseFloat(val.toPrecision(10)).toString() + } + + toFraction(x: number): string | null { + const MAX_DENOM = 1000 + for (let d = 1; d <= MAX_DENOM; d++) { + const n = Math.round(x * d) + if (Math.abs(n / d - x) < 1e-9 && d > 1) { + const g = this.gcd(Math.abs(n), d) + return `${n / g}/${d / g}` + } + } + return null + } +} diff --git a/src/views/casio-fx580/index.vue b/src/views/casio-fx580/index.vue new file mode 100644 index 00000000..ad796fad --- /dev/null +++ b/src/views/casio-fx580/index.vue @@ -0,0 +1,1489 @@ + + + + + diff --git a/src/views/casio-fx580/meta.ts b/src/views/casio-fx580/meta.ts new file mode 100644 index 00000000..9e0b2568 --- /dev/null +++ b/src/views/casio-fx580/meta.ts @@ -0,0 +1,12 @@ +import type { PageMeta } from '@/types/page' + +const meta: PageMeta = { + name: 'Casio fx-580VN X', + description: + 'Máy tính khoa học Casio fx-580VN X mô phỏng — Natural Display, đầy đủ chức năng COMP, STAT, TABLE, BASE-N, EQUA, MATRIX, VECTOR, CALC, SOLVE, INEQ, PRGM, RECUR và nhiều hơn nữa.', + author: 'Eintes-steinla', + facebook: 'https://www.facebook.com/entes.steinla?locale=vi_VN', + category: 'learn', +} + +export default meta diff --git a/src/views/casio-fx580/parser.ts b/src/views/casio-fx580/parser.ts new file mode 100644 index 00000000..e4bf5918 --- /dev/null +++ b/src/views/casio-fx580/parser.ts @@ -0,0 +1,256 @@ +/** + * Math Expression Parser & Evaluator + * Shunting-yard algorithm — no eval() + */ + +export type TokenType = + | 'NUMBER' + | 'IDENT' + | 'OP' + | 'LPAREN' + | 'RPAREN' + | 'COMMA' + | 'FACTORIAL' + | 'PERCENT' + | 'EOF' + +export interface Token { + type: TokenType + value: string +} + +// ── Tokenizer ───────────────────────────────────────────────────────────── +export function tokenize(input: string): Token[] { + const tokens: Token[] = [] + let i = 0 + const src = input.trim() + + while (i < src.length) { + const ch = src[i]! + + // whitespace + if (/\s/.test(ch)) { + i++ + continue + } + + // number (including scientific) + if ( + /[\d.]/.test(ch) || + (ch === '-' && tokens.length === 0) || + (ch === '-' && ['OP', 'LPAREN', 'COMMA'].includes(tokens[tokens.length - 1]?.type ?? '')) + ) { + let num = '' + if (ch === '-') { + num = '-' + i++ + } + while (i < src.length && /[\d.]/.test(src[i]!)) { + num += src[i++] + } + if (i < src.length && src[i] === 'E') { + num += 'E' + i++ + if (i < src.length && (src[i] === '+' || src[i] === '-')) num += src[i++] + while (i < src.length && /\d/.test(src[i]!)) num += src[i++] + } + tokens.push({ type: 'NUMBER', value: num }) + continue + } + + // identifier / function + if (/[a-zA-Zπℯ∞]/.test(ch)) { + let id = '' + while (i < src.length && /[a-zA-Z0-9πℯ⁻¹_]/.test(src[i]!)) id += src[i++] + // handle superscript inverse like sin⁻¹ + if (i < src.length && src.slice(i, i + 2) === '⁻¹') { + id += '⁻¹' + i += 2 + } + tokens.push({ type: 'IDENT', value: id }) + continue + } + + // operators + if (['+', '-', '*', '×', '÷', '/', '^', '='].includes(ch)) { + tokens.push({ type: 'OP', value: ch === '×' ? '*' : ch === '÷' ? '/' : ch }) + i++ + continue + } + if (ch === '(') { + tokens.push({ type: 'LPAREN', value: '(' }) + i++ + continue + } + if (ch === ')') { + tokens.push({ type: 'RPAREN', value: ')' }) + i++ + continue + } + if (ch === ',') { + tokens.push({ type: 'COMMA', value: ',' }) + i++ + continue + } + if (ch === '!') { + tokens.push({ type: 'FACTORIAL', value: '!' }) + i++ + continue + } + if (ch === '%') { + tokens.push({ type: 'PERCENT', value: '%' }) + i++ + continue + } + + // ×10^ notation + if (src.slice(i, i + 4) === '×10^') { + tokens.push({ type: 'OP', value: 'E' }) + i += 4 + continue + } + + i++ // skip unknown + } + + tokens.push({ type: 'EOF', value: '' }) + return tokens +} + +// ── Operator table ───────────────────────────────────────────────────────── +const OPS: Record = { + '+': { prec: 1, right: false }, + '-': { prec: 1, right: false }, + '*': { prec: 2, right: false }, + '/': { prec: 2, right: false }, + '^': { prec: 4, right: true }, + E: { prec: 5, right: false }, +} + +// ── AST nodes ────────────────────────────────────────────────────────────── +export type ASTNode = + | { kind: 'num'; value: number } + | { kind: 'var'; name: string } + | { kind: 'binop'; op: string; left: ASTNode; right: ASTNode } + | { kind: 'unary'; op: string; arg: ASTNode } + | { kind: 'call'; fn: string; args: ASTNode[] } + | { kind: 'factorial'; arg: ASTNode } + +// ── Recursive descent parser ─────────────────────────────────────────────── +export class Parser { + private tokens: Token[] + private pos = 0 + + constructor(input: string) { + this.tokens = tokenize(input) + } + + private peek(): Token { + return this.tokens[this.pos] ?? { type: 'EOF', value: '' } + } + private eat(): Token { + return this.tokens[this.pos++] ?? { type: 'EOF', value: '' } + } + + parse(): ASTNode { + const node = this.parseExpr(0) + return node + } + + private parseExpr(minPrec: number): ASTNode { + let left = this.parseUnary() + + while (true) { + const tok = this.peek() + if (tok.type === 'PERCENT') { + this.eat() + left = { kind: 'unary', op: '%', arg: left } + continue + } + if (tok.type === 'FACTORIAL') { + this.eat() + left = { kind: 'factorial', arg: left } + continue + } + if (tok.type !== 'OP') break + const op = tok.value + const info = OPS[op] + if (!info || info.prec < minPrec) break + this.eat() + const right = this.parseExpr(info.right ? info.prec : info.prec + 1) + // implicit multiply before paren: 2(3) → but handled in unary + left = { kind: 'binop', op, left, right } + } + + // implicit multiply: number/ident followed by ( or ident + const next = this.peek() + if (left && (next.type === 'LPAREN' || next.type === 'IDENT')) { + if (minPrec <= 2) { + const right = this.parseExpr(3) + left = { kind: 'binop', op: '*', left, right } + } + } + + return left + } + + private parseUnary(): ASTNode { + const tok = this.peek() + if (tok.type === 'OP' && tok.value === '-') { + this.eat() + return { kind: 'unary', op: '-', arg: this.parseUnary() } + } + if (tok.type === 'OP' && tok.value === '+') { + this.eat() + return this.parseUnary() + } + return this.parsePrimary() + } + + private parsePrimary(): ASTNode { + const tok = this.peek() + + if (tok.type === 'NUMBER') { + this.eat() + return { kind: 'num', value: parseFloat(tok.value) } + } + + if (tok.type === 'LPAREN') { + this.eat() + const node = this.parseExpr(0) + if (this.peek().type === 'RPAREN') this.eat() + return node + } + + if (tok.type === 'IDENT') { + this.eat() + const name = tok.value.toLowerCase() + + // constants + if (name === 'π' || name === 'pi') return { kind: 'num', value: Math.PI } + if (name === 'ℯ' || name === 'e') return { kind: 'num', value: Math.E } + if (name === 'ans') return { kind: 'var', name: 'ans' } + + // function call + if (this.peek().type === 'LPAREN') { + this.eat() + const args: ASTNode[] = [] + if (this.peek().type !== 'RPAREN') { + args.push(this.parseExpr(0)) + while (this.peek().type === 'COMMA') { + this.eat() + args.push(this.parseExpr(0)) + } + } + if (this.peek().type === 'RPAREN') this.eat() + return { kind: 'call', fn: name, args } + } + + // variable / memory + return { kind: 'var', name: tok.value } + } + + // fallback + return { kind: 'num', value: 0 } + } +} diff --git a/src/views/math-grapher/index.vue b/src/views/math-grapher/index.vue index 4c694af2..c1d49d6b 100644 --- a/src/views/math-grapher/index.vue +++ b/src/views/math-grapher/index.vue @@ -612,7 +612,7 @@ onUnmounted(() => {

Đồ Thị Toán Học

- + @@ -846,17 +846,6 @@ onUnmounted(() => { ↺ Reset view - - -
-

- sin(x) · - x^2 · - sqrt(x) -

-

x^2+y^2=1 — vòng tròn

-

(x^2+y^2-1)^3-x^2*y^3=0

-
diff --git a/src/views/math-grapher/meta.ts b/src/views/math-grapher/meta.ts index bd3be811..2351f8c1 100644 --- a/src/views/math-grapher/meta.ts +++ b/src/views/math-grapher/meta.ts @@ -2,8 +2,8 @@ import type { PageMeta } from '@/types/page' const meta: PageMeta = { name: 'Đồ Thị Toán Học', - description: 'Vẽ đồ thị hàm số toán học tương tự Desmos — hỗ trợ nhiều hàm cùng lúc, zoom, pan', - author: 'Deku', + description: 'Vẽ đồ thị hàm số toán học tương tự Desmos — hỗ trợ nhiều hàm cùng lúc, zoom', + author: 'Eintes-steinla', facebook: 'https://www.facebook.com/entes.steinla?locale=vi_VN', category: 'learn', }