项目文件夹

文件
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

253 行
6.8 KiB
TypeScript

/**
* 安全表达式求值器 — 递归下降解析器
* 替代 eval(),支持完整的数学运算
*
* 支持: +, -, *, /, ^, !, sin, cos, tan, ln, log, sqrt, π, e, 括号
* 精度: 12 位有效数字 + 5 位保护位 (与 AOSP MAX_DIGITS=12, ROUNDING_DIGITS=5 一致)
*/
import { normalize } from './tokenizer';
import { EVAL_CONFIG } from '../constants';
import { strings } from '../res/strings';
export type EvalResult =
| { kind: 'success'; value: string }
| { kind: 'error'; message: string }
| { kind: 'empty' };
/**
* 求值入口
*/
export function evaluate(formula: string): EvalResult {
const expr = normalize(formula).trim();
if (!expr) return { kind: 'empty' };
try {
const parser = new Parser(expr);
const result = parser.parseExpression();
if (parser.pos < parser.input.length) {
return { kind: 'error', message: strings.error_syntax };
}
if (!isFinite(result)) {
return { kind: 'error', message: result !== result ? strings.error_nan : strings.error_divide_by_zero };
}
return { kind: 'success', value: formatResult(result) };
} catch {
return { kind: 'error', message: strings.error_syntax };
}
}
/** 格式化结果:12 位有效数字 */
function formatResult(n: number): string {
if (n === 0) return '0';
const maxDigits = EVAL_CONFIG.maxDigits;
// 用 toPrecision 限制有效数字
let s = Number(n.toPrecision(maxDigits)).toString();
// 去掉末尾多余的零
if (s.includes('.')) {
s = s.replace(/\.?0+$/, '');
}
return s;
}
// ==================== 递归下降解析器 ====================
class Parser {
input: string;
pos: number;
constructor(input: string) {
this.input = input;
this.pos = 0;
}
peek(): string {
this.skipSpaces();
return this.input[this.pos] || '';
}
consume(expected?: string): string {
this.skipSpaces();
const ch = this.input[this.pos];
if (expected && ch !== expected) {
throw new Error(`Expected '${expected}' at pos ${this.pos}`);
}
this.pos++;
return ch;
}
skipSpaces() {
while (this.pos < this.input.length && this.input[this.pos] === ' ') {
this.pos++;
}
}
// expression = term (('+' | '-') term)*
parseExpression(): number {
let result = this.parseTerm();
while (this.peek() === '+' || this.peek() === '-') {
const op = this.consume();
const right = this.parseTerm();
result = op === '+' ? result + right : result - right;
}
return result;
}
// term = power (('*' | '/') power)*
parseTerm(): number {
let result = this.parsePower();
while (this.peek() === '*' || this.peek() === '/') {
const op = this.consume();
const right = this.parsePower();
result = op === '*' ? result * right : result / right;
}
return result;
}
// power = unary ('^' power)? (右结合)
parsePower(): number {
const base = this.parseUnary();
if (this.peek() === '^') {
this.consume();
const exp = this.parsePower(); // 右结合
return Math.pow(base, exp);
}
return base;
}
// unary = '-' unary | postfix
parseUnary(): number {
if (this.peek() === '-') {
this.consume();
return -this.parseUnary();
}
return this.parsePostfix();
}
// postfix = primary ('!')*
parsePostfix(): number {
let result = this.parsePrimary();
while (this.peek() === '!') {
this.consume();
result = factorial(result);
}
return result;
}
// primary = number | '(' expression ')' | function '(' expression ')' | constant
parsePrimary(): number {
const ch = this.peek();
// 括号
if (ch === '(') {
this.consume('(');
const result = this.parseExpression();
if (this.peek() === ')') this.consume(')');
return result;
}
// 函数
const funcNames = ['sin', 'cos', 'tan', 'ln', 'log', 'sqrt'];
for (const fn of funcNames) {
if (this.input.substring(this.pos).startsWith(fn)) {
this.pos += fn.length;
// 期望 '('
if (this.peek() === '(') {
this.consume('(');
const arg = this.parseExpression();
if (this.peek() === ')') this.consume(')');
return applyFunction(fn, arg);
}
// 没有括号也尝试解析
const arg = this.parsePrimary();
return applyFunction(fn, arg);
}
}
// 常量 π
if (this.input.substring(this.pos).startsWith('π')) {
this.pos += 1;
return Math.PI;
}
// 常量 e (注意不要匹配到科学记数法的 e)
if (ch === 'e' && !isDigitAt(this.input, this.pos - 1)) {
const nextCh = this.input[this.pos + 1];
// 如果 e 后面不是数字也不是 +/-(科学记数法),视为常量
if (nextCh === undefined || (nextCh !== '+' && nextCh !== '-' && !(nextCh >= '0' && nextCh <= '9'))) {
this.pos++;
return Math.E;
}
}
// 数字
return this.parseNumber();
}
parseNumber(): number {
this.skipSpaces();
const start = this.pos;
// 整数部分
while (this.pos < this.input.length && this.input[this.pos] >= '0' && this.input[this.pos] <= '9') {
this.pos++;
}
// 小数部分
if (this.pos < this.input.length && this.input[this.pos] === '.') {
this.pos++;
while (this.pos < this.input.length && this.input[this.pos] >= '0' && this.input[this.pos] <= '9') {
this.pos++;
}
}
// 科学记数法
if (this.pos < this.input.length && (this.input[this.pos] === 'e' || this.input[this.pos] === 'E')) {
this.pos++;
if (this.pos < this.input.length && (this.input[this.pos] === '+' || this.input[this.pos] === '-')) {
this.pos++;
}
while (this.pos < this.input.length && this.input[this.pos] >= '0' && this.input[this.pos] <= '9') {
this.pos++;
}
}
if (this.pos === start) {
throw new Error(`Unexpected character at pos ${this.pos}: '${this.input[this.pos]}'`);
}
return parseFloat(this.input.substring(start, this.pos));
}
}
function isDigitAt(s: string, pos: number): boolean {
if (pos < 0 || pos >= s.length) return false;
return s[pos] >= '0' && s[pos] <= '9';
}
function applyFunction(name: string, arg: number): number {
switch (name) {
case 'sin': return Math.sin(arg * Math.PI / 180); // AOSP 用度数
case 'cos': return Math.cos(arg * Math.PI / 180);
case 'tan': return Math.tan(arg * Math.PI / 180);
case 'ln': return Math.log(arg);
case 'log': return Math.log10(arg);
case 'sqrt': return Math.sqrt(arg);
default: throw new Error(`Unknown function: ${name}`);
}
}
function factorial(n: number): number {
if (n < 0 || !Number.isInteger(n)) return NaN;
if (n > 170) return Infinity;
let result = 1;
for (let i = 2; i <= n; i++) {
result *= i;
}
return result;
}