File size: 2,632 Bytes
e5faeaf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | /* ------------------------------------------------------------------ *
* 安全四則運算求值。
*
* 模型輸出的 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
}
|