import Big from 'big.js' /** 进位方式:向下取整、向上取整、四舍五入 */ export type FormattedNumberRounding = 'floor' | 'ceil' | 'round' /** Big.js 舍入模式映射 */ function toRoundingMode(rounding: FormattedNumberRounding): Big.RoundingMode { if (rounding === 'floor') return 0 // RoundDown if (rounding === 'ceil') return 3 // RoundUp return 1 // RoundHalfUp (四舍五入) } /** * 将数字字符串安全解析为 Big 实例。 * 自动去除末尾小数点(如 "12." → Big(12)),解析失败返回 null。 */ export function toBig(s: string): Big | null { if (!s || s === '-') return null const normalized = s.endsWith('.') ? s.slice(0, -1) : s if (!normalized || normalized === '-') return null try { return Big(normalized) } catch { return null } } /** 仅保留数字、至多一个小数点、可选前导负号 */ export function sanitizeNumericInput(raw: string): string { const t = raw.trim() if (!t) return '' let i = 0 let res = '' if (t[0] === '-') { res = '-' i = 1 } let dot = false for (; i < t.length; i++) { const c = t[i] if (c >= '0' && c <= '9') { res += c continue } if (c === '.' && !dot) { dot = true if (res === '' || res === '-') res += '0' res += '.' } } return res } export function stripNumberGrouping(s: string): string { return s.replace(/,/g, '').trim() } /** * 对已清洗的数字串按整数位、小数位截断输入长度(inputLimit 为 true 时使用)。 * 整数部分至多 integerDigits 位,小数部分至多 decimalPlaces 位;保留末尾仅小数点(如 `12.`)以便继续输入。 */ export function applyNumericInputDigitLimits( sanitized: string, integerDigits: number, decimalPlaces: number ): string { if (!sanitized || sanitized === '-') return sanitized const neg = sanitized[0] === '-' let body = neg ? sanitized.slice(1) : sanitized const dotIdx = body.indexOf('.') if (dotIdx === -1) { const maxInt = Math.max(0, integerDigits) body = maxInt > 0 ? body.slice(0, maxInt) : body return neg ? '-' + body : body } let intPart = body.slice(0, dotIdx) if (integerDigits > 0) { intPart = intPart.slice(0, integerDigits) } const afterDot = body.slice(dotIdx + 1) const endsWithDot = afterDot === '' && body.endsWith('.') if (decimalPlaces <= 0) { return neg ? '-' + intPart : intPart } if (endsWithDot) { return (neg ? '-' : '') + intPart + '.' } const decPart = afterDot.slice(0, decimalPlaces) return (neg ? '-' : '') + intPart + '.' + decPart } /** 按整数位数上限计算最大绝对值 */ function maxAbsValue(integerDigits: number, decimalPlaces: number): Big { const maxInt = Big(10).pow(integerDigits).minus(1) if (decimalPlaces <= 0) return maxInt return maxInt.plus(Big(1).minus(Big(10).pow(-decimalPlaces))) } /** 按整数位数上限约束绝对值 */ export function clampByIntegerDigits( value: Big, integerDigits: number, decimalPlaces: number ): Big { const max = maxAbsValue(integerDigits, decimalPlaces) if (value.abs().gt(max)) { return value.lt(0) ? max.times(-1) : max } return value } /** 按小数位与进位方式舍入 */ export function roundToDecimals( value: Big, decimalPlaces: number, rounding: FormattedNumberRounding ): Big { return value.round(decimalPlaces, toRoundingMode(rounding)) } /** * 解析 → 按整数位夹紧 → 按小数位与进位方式舍入 → 再夹紧(防止舍入后越界) */ export function normalizeNumericValue( value: Big, integerDigits: number, decimalPlaces: number, rounding: FormattedNumberRounding ): Big { let v = clampByIntegerDigits(value, integerDigits, decimalPlaces) v = roundToDecimals(v, decimalPlaces, rounding) v = clampByIntegerDigits(v, integerDigits, decimalPlaces) return v } /** * 固定小数位数字符串(无千分位),用于表单存值以保留配置的小数位数。 */ export function toFixedDecimalString(value: Big, decimalPlaces: number): string { return value.toFixed(Math.max(0, decimalPlaces)) } /** 千分位格式化展示(整数部分),小数部分固定 m 位 */ export function formatWithThousands(value: Big, decimalPlaces: number): string { const fixed = value.toFixed(Math.max(0, decimalPlaces)) const neg = fixed.startsWith('-') const body = neg ? fixed.slice(1) : fixed const [intPart, decPart] = body.split('.') const withSep = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',') if (decimalPlaces <= 0 || decPart === undefined) return (neg ? '-' : '') + withSep return (neg ? '-' : '') + withSep + '.' + decPart } /** 编辑态:无千分位,去掉多余尾部 0(在 m 位精度内) */ export function numberToEditString(value: Big, decimalPlaces: number): string { let s = value.toFixed(Math.max(0, decimalPlaces)) s = s.replace(/(\.\d*?)0+$/, '$1') s = s.replace(/\.$/, '') return s }