/* ------------------------------------------------------------------ * * 安全四則運算求值。 * * 模型輸出的 expression 屬於不可信輸入,因此: * - 先用正則白色清單擋掉任何運算子以外的字元 * - 再用遞迴下降解析器求值 * 全程不用 Function() / eval() / new Function,沒有代碼執行面。 * ------------------------------------------------------------------ */ const CALC_RE = /^[\d\s+\-*/().%]+$/ /** 把表達式轉成計算結果字串;任何異常都轉成錯誤訊息回傳,不丟例外。 */ export function safeCalc(expression) { if (typeof expression !== 'string' || !expression.trim()) { return '計算錯誤:表達式為空' } if (!CALC_RE.test(expression)) { return `計算錯誤:只支援數字與 + - * / ( ) . %(收到「${expression}」)` } try { const value = evaluate(expression.replace(/\s+/g, '')) if (!Number.isFinite(value)) return `計算錯誤:結果無效(${expression})` // 消掉浮點誤差:0.1 + 0.2 應顯示 0.3 return `計算結果:${Math.round(value * 1e10) / 1e10}` } catch (e) { return `計算錯誤:${expression}(${e.message})` } } /** 遞迴下降解析器:+ - 最外層,* / % 次之,數字/括號最內層。 */ function evaluate(expr) { let pos = 0 const peek = () => expr[pos] const take = () => expr[pos++] function parseExpr() { let left = parseTerm() while (peek() === '+' || peek() === '-') { const op = take() const right = parseTerm() left = op === '+' ? left + right : left - right } return left } function parseTerm() { let left = parseFactor() while (peek() === '*' || peek() === '/' || peek() === '%') { const op = take() const right = parseFactor() left = op === '*' ? left * right : op === '/' ? left / right : left % right } return left } function parseFactor() { if (peek() === '-') { take(); return -parseFactor() } if (peek() === '+') { take(); return parseFactor() } if (peek() === '(') { take() const value = parseExpr() if (peek() !== ')') throw new Error('括號不配對') take() return value } const start = pos while (pos < expr.length && /[\d.]/.test(expr[pos])) pos++ if (start === pos) throw new Error('預期數字') const value = Number(expr.slice(start, pos)) if (Number.isNaN(value)) throw new Error('數字格式錯誤') return value } const result = parseExpr() if (pos !== expr.length) throw new Error('表達式有多餘字元') return result }