src/utils/moneyUtils.ts
Rama main · réplica de lectura del repositorio original.
// 金额输入工具:数字键盘按键序列 → 合法「金额表达式」字符串,并安全求值
// 支持 + - * / 四则运算(随手记式),规则:
// - 单个操作数:整数最多 7 位、小数最多 2 位;不能以 0 开头(0. 除外)
// - 不允许以运算符开头、不允许连续运算符(后者替换前者)、末尾小数点后接运算符时替换该点
// - 整串长度上限,避免溢出显示
export type PadKey =
'1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '0' | '.' | 'backspace' | '+' | '-' | '*' | '/';
const MAX_EXPR_LEN = 30; // 表达式整体长度上限
function isOperator(ch: string): boolean {
return ch === '+' || ch === '-' || ch === '*' || ch === '/';
}
// 取「当前操作数」:最后一个运算符之后的片段
function currentSegment(s: string): string {
for (let i = s.length - 1; i >= 0; i--) {
if (isOperator(s[i])) return s.slice(i + 1);
}
return s;
}
export function appendKey(current: string, key: string): string {
if (key === 'backspace') {
return current.slice(0, -1);
}
// 运算符
if (isOperator(key)) {
if (current === '') return current; // 不能以运算符开头
const last = current[current.length - 1];
if (isOperator(last)) return current.slice(0, -1) + key; // 连续运算符 → 替换
if (last === '.') return current.slice(0, -1) + key; // 末尾裸小数点 → 替换
if (current.length >= MAX_EXPR_LEN) return current;
return current + key;
}
// 小数点
if (key === '.') {
const seg = currentSegment(current);
if (seg.includes('.')) return current;
if (current === '' || seg === '') return current + '0.'; // 空 / 运算符后直接点 → 0.
return current + '.';
}
// 数字
const seg = currentSegment(current);
if (seg.includes('.')) {
const decimals = seg.split('.')[1] ?? '';
if (decimals.length >= 2) return current; // 单个操作数最多两位小数
return current + key;
}
if (seg === '0') return current.slice(0, -1) + key; // 前导 0 被替换
if (seg.length >= 7) return current; // 单个操作数整数最多 7 位
if (current.length >= MAX_EXPR_LEN) return current;
return current + key;
}
// 安全求值:不依赖 eval。按 * / 优先于 + - 计算,容错尾部运算符、除零(视为 0)。
export function evaluateAmount(expr: string): number {
const tokens = expr.match(/\d+(?:\.\d+)?|[+\-*/]/g);
if (!tokens || tokens.length === 0) return 0;
const values: number[] = [];
const ops: string[] = [];
const prec = (o: string): number => (o === '+' || o === '-' ? 1 : 2);
const apply = (): boolean => {
const o = ops[ops.length - 1];
if (!o) return false;
const b = values[values.length - 1];
const a = values[values.length - 2];
if (a === undefined || b === undefined) return false;
ops.pop();
values.length -= 2;
let r: number;
if (o === '+') r = a + b;
else if (o === '-') r = a - b;
else if (o === '*') r = a * b;
else r = b === 0 ? NaN : a / b; // 除零 → NaN,末尾归零
values.push(r);
return true;
};
for (const t of tokens) {
if (isOperator(t)) {
while (ops.length && prec(ops[ops.length - 1]) >= prec(t)) {
if (!apply()) break;
}
ops.push(t);
} else {
const n = Number(t);
if (Number.isFinite(n)) values.push(n);
}
}
while (ops.length) {
if (!apply()) break;
}
const result = values.length ? values[values.length - 1] : 0;
if (!Number.isFinite(result)) return 0;
return Math.round(result * 100) / 100;
}
// 是否含运算符(用于决定是否显示「= 结果」预览)
export function hasOperator(expr: string): boolean {
return /[+\-*/]/.test(expr);
}
export function toAmount(value: string): number {
return evaluateAmount(value);
}
export function isValidAmount(value: string): boolean {
return evaluateAmount(value) > 0;
}