import Decimal from 'decimal.js' type MaybeNumber = number | null | undefined type DecimalInput = Decimal.Value export const toDecimal = (value: DecimalInput) => new Decimal(value) export const roundTo = (value: DecimalInput, decimalPlaces = 2) => new Decimal(value).toDecimalPlaces(decimalPlaces, Decimal.ROUND_HALF_UP).toNumber() const isFiniteNumber = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value) const sumFiniteValues = (values: Iterable) => { let total = new Decimal(0) for (const value of values) { if (!isFiniteNumber(value)) continue total = total.plus(value) } return total.toNumber() } export const addNumbers = (...values: MaybeNumber[]) => sumFiniteValues(values) export const sumByNumber = (list: T[], pick: (item: T) => MaybeNumber) => { let total = new Decimal(0) for (const item of list) { const value = pick(item) if (!isFiniteNumber(value)) continue total = total.plus(value) } return total.toNumber() } export const decimalAggSum = (params: { values?: unknown[] }) => { const values = params.values || [] let hasFinite = false for (const value of values) { if (!isFiniteNumber(value)) continue hasFinite = true break } if (!hasFinite) return null return sumFiniteValues(values) } class DecimalExpressionParser { private readonly source: string private index = 0 constructor(source: string) { this.source = source } parse(): Decimal | null { const value = this.parseExpression() this.skipWhitespace() if (!value || this.index !== this.source.length) return null return value } private parseExpression(): Decimal | null { let value = this.parseTerm() if (!value) return null while (true) { this.skipWhitespace() const operator = this.peek() if (operator !== '+' && operator !== '-') return value this.index += 1 const right = this.parseTerm() if (!right) return null value = operator === '+' ? value.plus(right) : value.minus(right) } } private parseTerm(): Decimal | null { let value = this.parseFactor() if (!value) return null while (true) { this.skipWhitespace() const operator = this.peek() if (operator !== '*' && operator !== '/') return value this.index += 1 const right = this.parseFactor() if (!right) return null if (operator === '/') { if (right.isZero()) return null value = value.div(right) continue } value = value.mul(right) } } private parseFactor(): Decimal | null { this.skipWhitespace() const current = this.peek() if (current === '+') { this.index += 1 return this.parseFactor() } if (current === '-') { this.index += 1 const value = this.parseFactor() return value ? value.neg() : null } if (current === '(') { this.index += 1 const value = this.parseExpression() this.skipWhitespace() if (!value || this.peek() !== ')') return null this.index += 1 return value } return this.parseNumber() } private parseNumber(): Decimal | null { this.skipWhitespace() const start = this.index let hasDigit = false let hasDot = false while (this.index < this.source.length) { const char = this.source[this.index] if (char >= '0' && char <= '9') { hasDigit = true this.index += 1 continue } if (char === '.' && !hasDot) { hasDot = true this.index += 1 continue } break } if (!hasDigit) { this.index = start return null } const literal = this.source.slice(start, this.index) try { return new Decimal(literal) } catch { return null } } private skipWhitespace() { while (this.index < this.source.length && /\s/.test(this.source[this.index])) { this.index += 1 } } private peek() { return this.source[this.index] } } // 支持 + - * / () 的高精度表达式计算,用于数字输入框和表格 valueParser。 export const evaluateDecimalExpression = (value: string): number | null => { const trimmed = String(value || '').trim() if (!trimmed) return null try { const parsed = new DecimalExpressionParser(trimmed).parse() return parsed ? parsed.toNumber() : null } catch { return null } }