176 lines
4.4 KiB
TypeScript
176 lines
4.4 KiB
TypeScript
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<unknown>) => {
|
|
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 = <T>(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
|
|
}
|
|
}
|