Ctrl K
ComunidadGuías…

src/utils/moneyUtils.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// 金额输入工具:数字键盘按键序列 → 合法「金额表达式」字符串,并安全求值
// 支持 + - * / 四则运算(随手记式),规则:
// - 单个操作数:整数最多 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;
}