/** 进位方式:向下取整、向上取整、四舍五入 */ export type FormattedNumberRounding = 'floor' | 'ceil' | 'round' /** 仅保留数字、至多一个小数点、可选前导负号 */ 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): number { const maxInt = Math.pow(10, integerDigits) - 1 if (decimalPlaces <= 0) return maxInt return maxInt + (1 - Math.pow(10, -decimalPlaces)) } /** 按整数位数上限约束绝对值 */ export function clampByIntegerDigits( value: number, integerDigits: number, decimalPlaces: number ): number { const max = maxAbsValue(integerDigits, decimalPlaces) const sign = value < 0 ? -1 : 1 const abs = Math.abs(value) if (abs <= max) return value return sign * max } export function roundToDecimals( value: number, decimalPlaces: number, rounding: FormattedNumberRounding ): number { if (decimalPlaces <= 0) { switch (rounding) { case 'floor': return Math.floor(value) case 'ceil': return Math.ceil(value) default: return Math.round(value) } } const factor = Math.pow(10, decimalPlaces) const x = value * factor let y: number switch (rounding) { case 'floor': y = Math.floor(x) break case 'ceil': y = Math.ceil(x) break default: y = Math.round(x) } return y / factor } /** * 解析 → 按整数位夹紧 → 按小数位与进位方式舍入 → 再夹紧(防止舍入后越界) */ export function normalizeNumericValue( value: number, integerDigits: number, decimalPlaces: number, rounding: FormattedNumberRounding ): number { let v = clampByIntegerDigits(value, integerDigits, decimalPlaces) v = roundToDecimals(v, decimalPlaces, rounding) v = clampByIntegerDigits(v, integerDigits, decimalPlaces) return v } /** * 固定小数位数字符串(无千分位),用于表单存值以保留配置的小数位数(number 无法保留尾随 0)。 */ export function toFixedDecimalString(value: number, decimalPlaces: number): string { if (Number.isNaN(value) || !Number.isFinite(value)) return '' return value.toFixed(Math.max(0, decimalPlaces)) } /** 千分位格式化展示(整数部分),小数部分固定 m 位 */ export function formatWithThousands(value: number, decimalPlaces: number): string { if (Number.isNaN(value) || !Number.isFinite(value)) return '' 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: number, decimalPlaces: number): string { if (Number.isNaN(value) || !Number.isFinite(value)) return '' let s = value.toFixed(Math.max(0, decimalPlaces)) s = s.replace(/(\.\d*?)0+$/, '$1') s = s.replace(/\.$/, '') return s }